21  Gradient Descent

21.1 Motivace: Jak sestoupit do údolí?

Představte si, že stojíte na kopci v husté mlze a chcete se dostat do údolí. Nevidíte daleko, ale můžete cítit sklon půdy pod nohama. Nejrozumnější strategie? Jděte směrem, kde to nejvíce klesá.

Přesně toto dělá gradient descent (sestup po gradientu):

  1. Spočítej gradient (směr největšího růstu)
  2. Udělej krok opačným směrem (směr největšího poklesu)
  3. Opakuj, dokud nedosáhneš minima
import numpy as np
import matplotlib.pyplot as plt

# Vizualizace principu
def loss_function(x, y):
    return (x - 2)**2 + (y - 1)**2

x = np.linspace(-1, 5, 100)
y = np.linspace(-2, 4, 100)
X, Y = np.meshgrid(x, y)
Z = loss_function(X, Y)

# Simulace gradient descent
path = [(4, 3)]
lr = 0.2
for _ in range(15):
    x_curr, y_curr = path[-1]
    # Gradient
    grad_x = 2 * (x_curr - 2)
    grad_y = 2 * (y_curr - 1)
    # Update
    x_new = x_curr - lr * grad_x
    y_new = y_curr - lr * grad_y
    path.append((x_new, y_new))

path = np.array(path)

plt.figure(figsize=(10, 8))
contour = plt.contour(X, Y, Z, levels=15, cmap='viridis')
plt.clabel(contour, inline=True, fontsize=8)

# Cesta
plt.plot(path[:, 0], path[:, 1], 'ro-', markersize=8, lw=2, label='Gradient descent')
plt.scatter([path[0, 0]], [path[0, 1]], color='green', s=150, zorder=5, label='Start')
plt.scatter([2], [1], color='red', s=150, marker='*', zorder=5, label='Minimum')

# Šipky gradientu
for i in range(0, len(path)-1, 3):
    x_curr, y_curr = path[i]
    grad_x = 2 * (x_curr - 2)
    grad_y = 2 * (y_curr - 1)
    plt.arrow(x_curr, y_curr, -0.3*grad_x, -0.3*grad_y,
              head_width=0.15, head_length=0.1, fc='blue', ec='blue', alpha=0.7)

plt.xlabel('x')
plt.ylabel('y')
plt.title('Gradient Descent: Sledujeme záporný gradient')
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()

21.2 Algoritmus Gradient Descent

PoznámkaAlgoritmus: Gradient Descent

Vstup: Počáteční bod \(\mathbf{w}_0\), learning rate \(\eta\), počet iterací \(T\)

Opakuj pro \(t = 0, 1, \ldots, T-1\): \[\mathbf{w}_{t+1} = \mathbf{w}_t - \eta \nabla f(\mathbf{w}_t)\]

Výstup: \(\mathbf{w}_T\)

Klíčové komponenty:

  • \(\nabla f(\mathbf{w})\) = gradient loss funkce
  • \(\eta\) = learning rate (krok učení)
  • \(-\nabla f\) = směr největšího poklesu
import numpy as np

def gradient_descent(grad_f, w0, learning_rate=0.1, n_iterations=100):
    """
    Základní gradient descent.

    Args:
        grad_f: Funkce počítající gradient
        w0: Počáteční parametry
        learning_rate: Velikost kroku
        n_iterations: Počet iterací

    Returns:
        Historie parametrů
    """
    w = np.array(w0, dtype=float)
    history = [w.copy()]

    for _ in range(n_iterations):
        grad = grad_f(w)
        w = w - learning_rate * grad
        history.append(w.copy())

    return np.array(history)

# Příklad: f(w) = (w[0] - 3)^2 + (w[1] + 1)^2
def grad_f(w):
    return np.array([2 * (w[0] - 3), 2 * (w[1] + 1)])

history = gradient_descent(grad_f, w0=[0, 0], learning_rate=0.1, n_iterations=50)

print(f"Start: {history[0]}")
print(f"Po 10 iteracích: {history[10]}")
print(f"Po 50 iteracích: {history[-1]}")
print(f"Optimum: [3, -1]")
Start: [0. 0.]
Po 10 iteracích: [ 2.67787745 -0.89262582]
Po 50 iteracích: [ 2.99995718 -0.99998573]
Optimum: [3, -1]

21.3 Learning Rate: Klíčový hyperparametr

Learning rate (\(\eta\)) je nejdůležitější hyperparametr gradient descent:

  • Příliš malý → pomalá konvergence
  • Příliš velký → oscilace nebo divergence
  • Správný → rychlá a stabilní konvergence
import numpy as np
import matplotlib.pyplot as plt

def f(x):
    return (x - 2)**2

def grad_f(x):
    return 2 * (x - 2)

fig, axes = plt.subplots(2, 3, figsize=(15, 10))

learning_rates = [0.01, 0.1, 0.5, 0.9, 1.0, 1.1]

for ax, lr in zip(axes.flatten(), learning_rates):
    x = 5.0
    path = [x]

    for _ in range(30):
        x = x - lr * grad_f(x)
        path.append(x)
        if abs(x) > 100:
            break

    path = np.array(path)

    # Funkce
    x_range = np.linspace(-2, 8, 100)
    ax.plot(x_range, f(x_range), 'b-', lw=2)

    # Path (pokud neexplodovala)
    if np.max(np.abs(path)) < 50:
        ax.plot(path, f(path), 'ro-', markersize=5)
        final_loss = f(path[-1])
        ax.set_title(f'η = {lr}\nFinální loss: {final_loss:.4f}')
    else:
        ax.set_title(f'η = {lr}\nDIVERGUJE!')
        ax.set_ylim(-5, 50)

    ax.set_xlabel('x')
    ax.set_ylabel('f(x)')
    ax.grid(True, alpha=0.3)
    ax.axhline(y=0, color='gray', linestyle='--', alpha=0.5)

plt.tight_layout()
plt.show()

21.3.1 Teoretický pohled na learning rate

Pro konvexní funkce s Lipschitzovsky spojitým gradientem existuje optimální learning rate:

# Konvergence jako funkce learning rate
import numpy as np
import matplotlib.pyplot as plt

def analyze_convergence(lr, n_steps=50, x0=5.0):
    x = x0
    losses = [f(x)]
    for _ in range(n_steps):
        x = x - lr * grad_f(x)
        losses.append(f(x))
    return losses

lrs = np.linspace(0.01, 1.05, 50)
final_losses = []

for lr in lrs:
    try:
        losses = analyze_convergence(lr)
        if np.isnan(losses[-1]) or losses[-1] > 1000:
            final_losses.append(np.nan)
        else:
            final_losses.append(losses[-1])
    except:
        final_losses.append(np.nan)

plt.figure(figsize=(10, 5))
plt.semilogy(lrs, final_losses, 'b.-')
plt.axvline(x=1.0, color='red', linestyle='--', label='Hranice stability (η=1)')
plt.xlabel('Learning rate η')
plt.ylabel('Finální loss (log scale)')
plt.title('Vliv learning rate na konvergenci')
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()

print("Pro f(x) = (x-2)² je Lipschitzova konstanta L = 2")
print("Teoreticky: η < 2/L = 1 pro konvergenci")

Pro f(x) = (x-2)² je Lipschitzova konstanta L = 2
Teoreticky: η < 2/L = 1 pro konvergenci

21.4 Varianty Gradient Descent

21.4.1 1. Batch Gradient Descent

Používá celý dataset pro výpočet gradientu:

\[\nabla f(\mathbf{w}) = \frac{1}{N} \sum_{i=1}^N \nabla f_i(\mathbf{w})\]

import numpy as np

np.random.seed(42)

# Generování dat pro lineární regresi
N = 100
X = np.random.randn(N, 1)
y = 2 * X.squeeze() + 1 + 0.5 * np.random.randn(N)

def compute_gradient_batch(w, b, X, y):
    """Gradient na celém datasetu."""
    predictions = X.squeeze() * w + b
    errors = predictions - y
    dw = 2 * np.mean(errors * X.squeeze())
    db = 2 * np.mean(errors)
    return dw, db

def mse_loss(w, b, X, y):
    predictions = X.squeeze() * w + b
    return np.mean((predictions - y)**2)

# Batch gradient descent
w, b = 0.0, 0.0
lr = 0.1
losses_batch = []

for _ in range(100):
    losses_batch.append(mse_loss(w, b, X, y))
    dw, db = compute_gradient_batch(w, b, X, y)
    w -= lr * dw
    b -= lr * db

print(f"Batch GD - Naučené parametry: w = {w:.4f}, b = {b:.4f}")
print(f"Skutečné parametry: w = 2.0, b = 1.0")
Batch GD - Naučené parametry: w = 1.9284, b = 1.0037
Skutečné parametry: w = 2.0, b = 1.0

21.4.2 2. Stochastic Gradient Descent (SGD)

Používá jeden vzorek pro výpočet gradientu:

# Stochastic gradient descent
import numpy as np

w, b = 0.0, 0.0
lr = 0.01
losses_sgd = []
X_flat = X.squeeze()  # Převod na 1D pole

for epoch in range(100):
    epoch_loss = mse_loss(w, b, X, y)
    losses_sgd.append(epoch_loss)

    # Náhodné pořadí vzorků
    indices = np.random.permutation(N)

    for i in indices:
        xi = X_flat[i]
        yi = y[i]
        prediction = xi * w + b
        error = prediction - yi

        # Gradient z jednoho vzorku
        dw = 2 * error * xi
        db = 2 * error

        w -= lr * dw
        b -= lr * db

print(f"SGD - Naučené parametry: w = {w:.4f}, b = {b:.4f}")
SGD - Naučené parametry: w = 1.9395, b = 0.9511

21.4.3 3. Mini-batch Gradient Descent

Kompromis - používá malou skupinu vzorků (batch):

# Mini-batch gradient descent
import numpy as np
import matplotlib.pyplot as plt

w, b = 0.0, 0.0
lr = 0.05
batch_size = 16
losses_minibatch = []

for epoch in range(100):
    epoch_loss = mse_loss(w, b, X, y)
    losses_minibatch.append(epoch_loss)

    # Náhodné pořadí
    indices = np.random.permutation(N)

    for start in range(0, N, batch_size):
        batch_indices = indices[start:start + batch_size]
        X_batch = X[batch_indices]
        y_batch = y[batch_indices]

        dw, db = compute_gradient_batch(w, b, X_batch, y_batch)
        w -= lr * dw
        b -= lr * db

print(f"Mini-batch GD - Naučené parametry: w = {w:.4f}, b = {b:.4f}")

# Porovnání
plt.figure(figsize=(12, 5))
plt.plot(losses_batch, 'b-', label='Batch GD', lw=2)
plt.plot(losses_sgd, 'r-', label='SGD', alpha=0.7)
plt.plot(losses_minibatch, 'g-', label='Mini-batch GD', lw=2)
plt.xlabel('Epocha')
plt.ylabel('Loss')
plt.title('Porovnání variant Gradient Descent')
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()
Mini-batch GD - Naučené parametry: w = 1.9265, b = 1.0569

21.4.4 Porovnání variant

import matplotlib.pyplot as plt

fig, axes = plt.subplots(1, 3, figsize=(15, 4))

varianta = ['Batch GD', 'SGD', 'Mini-batch GD']
vyhody = [
    '• Stabilní konvergence\n• Přesný gradient\n• Předvídatelné chování',
    '• Rychlé iterace\n• Může uniknout lok. minimům\n• Malá paměť',
    '• Kompromis\n• Paralelizace na GPU\n• Prakticky nejpoužívanější'
]
nevyhody = [
    '• Pomalé pro velká data\n• Vysoká paměť\n• Může uvíznout v lok. min.',
    '• Šumný gradient\n• Nestabilní konvergence\n• Těžší ladění',
    '• Výběr batch size\n• Stále nějaký šum',
]

for ax, var, vyh, nev in zip(axes, varianta, vyhody, nevyhody):
    ax.text(0.5, 0.9, var, transform=ax.transAxes, fontsize=16,
            fontweight='bold', ha='center', va='top')
    ax.text(0.5, 0.65, 'Výhody:', transform=ax.transAxes, fontsize=12,
            color='green', ha='center', va='top', fontweight='bold')
    ax.text(0.5, 0.55, vyh, transform=ax.transAxes, fontsize=10,
            ha='center', va='top')
    ax.text(0.5, 0.25, 'Nevýhody:', transform=ax.transAxes, fontsize=12,
            color='red', ha='center', va='top', fontweight='bold')
    ax.text(0.5, 0.15, nev, transform=ax.transAxes, fontsize=10,
            ha='center', va='top')
    ax.axis('off')

plt.tight_layout()
plt.show()

21.5 Problémy Gradient Descent

21.5.1 1. Volba learning rate

# Demonstrace problému s konstantním learning rate
import numpy as np
import matplotlib.pyplot as plt

def rosenbrock(x, y):
    """Rosenbrockova funkce - klasický test pro optimalizátory."""
    return (1 - x)**2 + 100 * (y - x**2)**2

def grad_rosenbrock(pos):
    x, y = pos
    dx = -2 * (1 - x) + 200 * (y - x**2) * (-2 * x)
    dy = 200 * (y - x**2)
    return np.array([dx, dy])

# Různé learning rates
lrs = [0.0001, 0.001, 0.005]
paths = []

for lr in lrs:
    pos = np.array([-1.0, 1.0])
    path = [pos.copy()]
    for _ in range(1000):
        grad = grad_rosenbrock(pos)
        pos = pos - lr * grad
        path.append(pos.copy())
    paths.append(np.array(path))

# Vizualizace
x = np.linspace(-2, 2, 100)
y = np.linspace(-1, 3, 100)
X, Y = np.meshgrid(x, y)
Z = rosenbrock(X, Y)

fig, axes = plt.subplots(1, 3, figsize=(15, 5))

for ax, path, lr in zip(axes, paths, lrs):
    ax.contour(X, Y, Z, levels=np.logspace(-1, 3, 20), cmap='viridis')
    ax.plot(path[:, 0], path[:, 1], 'r.-', markersize=2, alpha=0.7)
    ax.scatter([1], [1], color='green', s=100, marker='*', zorder=5)
    ax.set_title(f'lr = {lr}\nFinální: ({path[-1, 0]:.3f}, {path[-1, 1]:.3f})')
    ax.set_xlabel('x')
    ax.set_ylabel('y')
    ax.set_xlim(-2, 2)
    ax.set_ylim(-1, 3)

plt.tight_layout()
plt.show()

21.5.2 2. Špatně podmíněné problémy

Když má loss různou “strmost” v různých směrech:

import numpy as np
import matplotlib.pyplot as plt

def ill_conditioned(x, y):
    """Špatně podmíněná funkce."""
    return x**2 + 50 * y**2

def grad_ill_conditioned(pos):
    return np.array([2 * pos[0], 100 * pos[1]])

# Gradient descent
pos = np.array([5.0, 1.0])
lr = 0.01
path = [pos.copy()]

for _ in range(100):
    grad = grad_ill_conditioned(pos)
    pos = pos - lr * grad
    path.append(pos.copy())

path = np.array(path)

# Vizualizace
x = np.linspace(-6, 6, 100)
y = np.linspace(-2, 2, 100)
X, Y = np.meshgrid(x, y)
Z = ill_conditioned(X, Y)

plt.figure(figsize=(12, 5))
plt.contour(X, Y, Z, levels=20, cmap='viridis')
plt.plot(path[:, 0], path[:, 1], 'ro-', markersize=4)
plt.scatter([0], [0], color='green', s=100, marker='*', zorder=5, label='Optimum')
plt.xlabel('x')
plt.ylabel('y')
plt.title('Špatně podmíněný problém: f(x,y) = x² + 50y²\nKonvergence je pomalá kvůli "cik-cak" trajektorii')
plt.legend()
plt.axis('equal')
plt.grid(True, alpha=0.3)
plt.show()

21.5.3 3. Lokální minima a sedlové body

import numpy as np
import matplotlib.pyplot as plt

def multi_minima(x, y):
    return np.sin(x) * np.cos(y) + 0.1 * (x**2 + y**2)

def grad_multi_minima(pos):
    x, y = pos
    dx = np.cos(x) * np.cos(y) + 0.2 * x
    dy = -np.sin(x) * np.sin(y) + 0.2 * y
    return np.array([dx, dy])

# Různé počáteční body
starts = [(-2, 2), (2, -2), (0, 3), (-3, 0)]
paths = []

for start in starts:
    pos = np.array(start, dtype=float)
    path = [pos.copy()]
    for _ in range(100):
        grad = grad_multi_minima(pos)
        pos = pos - 0.5 * grad
        path.append(pos.copy())
    paths.append(np.array(path))

# Vizualizace
x = np.linspace(-4, 4, 100)
y = np.linspace(-4, 4, 100)
X, Y = np.meshgrid(x, y)
Z = multi_minima(X, Y)

plt.figure(figsize=(10, 8))
plt.contourf(X, Y, Z, levels=30, cmap='viridis', alpha=0.8)
plt.colorbar(label='f(x, y)')

colors = ['red', 'blue', 'orange', 'purple']
for path, color, start in zip(paths, colors, starts):
    plt.plot(path[:, 0], path[:, 1], 'o-', color=color, markersize=3,
             label=f'Start: {start}')

plt.xlabel('x')
plt.ylabel('y')
plt.title('Různé počáteční body → různá lokální minima')
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()

21.6 Gradient Descent v PyTorch

PyTorch automatizuje výpočet gradientů a aktualizaci parametrů:

import matplotlib.pyplot as plt

import torch
import torch.nn as nn
import torch.optim as optim

# Data
torch.manual_seed(42)
X = torch.randn(100, 1)
y = 2 * X + 1 + 0.3 * torch.randn(100, 1)

# Model
model = nn.Linear(1, 1)
criterion = nn.MSELoss()

# Různé optimizátory
optimizers = {
    'SGD': optim.SGD(model.parameters(), lr=0.1),
}

# Reset modelu
model.weight.data.fill_(0)
model.bias.data.fill_(0)

losses = []

for epoch in range(100):
    # Forward pass
    predictions = model(X)
    loss = criterion(predictions, y)
    losses.append(loss.item())

    # Backward pass
    optimizers['SGD'].zero_grad()  # Vynulování gradientů
    loss.backward()                 # Výpočet gradientů
    optimizers['SGD'].step()        # Aktualizace parametrů

print(f"Naučené parametry:")
print(f"  w = {model.weight.item():.4f} (skutečné: 2.0)")
print(f"  b = {model.bias.item():.4f} (skutečné: 1.0)")

plt.figure(figsize=(10, 4))
plt.plot(losses, 'b-', lw=2)
plt.xlabel('Epocha')
plt.ylabel('MSE Loss')
plt.title('Trénink v PyTorch')
plt.grid(True, alpha=0.3)
plt.show()
Naučené parametry:
  w = 2.0035 (skutečné: 2.0)
  b = 1.0107 (skutečné: 1.0)

21.6.1 Kompletní trénovací smyčka

import matplotlib.pyplot as plt

import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader, TensorDataset

# Větší dataset
torch.manual_seed(42)
N = 1000
X = torch.randn(N, 2)
y = (X[:, 0] + 2 * X[:, 1] + 0.5 * X[:, 0] * X[:, 1] + 0.3 * torch.randn(N)).unsqueeze(1)

# DataLoader pro mini-batch
dataset = TensorDataset(X, y)
dataloader = DataLoader(dataset, batch_size=32, shuffle=True)

# Model
class SimpleNet(nn.Module):
    def __init__(self):
        super().__init__()
        self.layers = nn.Sequential(
            nn.Linear(2, 16),
            nn.ReLU(),
            nn.Linear(16, 1)
        )

    def forward(self, x):
        return self.layers(x)

model = SimpleNet()
criterion = nn.MSELoss()
optimizer = optim.SGD(model.parameters(), lr=0.01)

# Trénink
epoch_losses = []

for epoch in range(50):
    epoch_loss = 0
    for X_batch, y_batch in dataloader:
        # Forward
        predictions = model(X_batch)
        loss = criterion(predictions, y_batch)

        # Backward
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

        epoch_loss += loss.item()

    epoch_losses.append(epoch_loss / len(dataloader))

plt.figure(figsize=(10, 4))
plt.plot(epoch_losses, 'b-', lw=2)
plt.xlabel('Epocha')
plt.ylabel('Průměrná loss')
plt.title('Mini-batch SGD v PyTorch')
plt.grid(True, alpha=0.3)
plt.show()

print(f"Finální loss: {epoch_losses[-1]:.4f}")

Finální loss: 0.1082

21.7 Řešené příklady

21.7.1 Příklad 1: Implementace SGD od nuly

Zadání: Implementujte SGD pro logistickou regresi.

Řešení:

import numpy as np
import matplotlib.pyplot as plt

np.random.seed(42)

# Binární klasifikační data
N = 200
X = np.random.randn(N, 2)
y = (X[:, 0] + X[:, 1] > 0).astype(float)

def sigmoid(z):
    return 1 / (1 + np.exp(-np.clip(z, -500, 500)))

def predict(X, w, b):
    return sigmoid(X @ w + b)

def cross_entropy_loss(X, y, w, b):
    probs = predict(X, w, b)
    probs = np.clip(probs, 1e-10, 1 - 1e-10)
    return -np.mean(y * np.log(probs) + (1 - y) * np.log(1 - probs))

def compute_gradients(X, y, w, b):
    probs = predict(X, w, b)
    errors = probs - y
    dw = X.T @ errors / len(y)
    db = np.mean(errors)
    return dw, db

# SGD trénink
w = np.zeros(2)
b = 0.0
lr = 0.5
losses = []

for epoch in range(100):
    losses.append(cross_entropy_loss(X, y, w, b))

    # Náhodné pořadí
    for i in np.random.permutation(N):
        xi = X[i:i+1]
        yi = y[i:i+1]
        dw, db = compute_gradients(xi, yi, w, b)
        w -= lr * dw.flatten()
        b -= lr * db

# Výsledky
accuracy = np.mean((predict(X, w, b) > 0.5) == y)
print(f"Naučené váhy: w = {w}")
print(f"Naučený bias: b = {b:.4f}")
print(f"Přesnost: {accuracy:.2%}")

# Vizualizace
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))

# Data a rozhodovací hranice
ax1.scatter(X[y==0, 0], X[y==0, 1], c='blue', alpha=0.5, label='Třída 0')
ax1.scatter(X[y==1, 0], X[y==1, 1], c='red', alpha=0.5, label='Třída 1')

x_line = np.linspace(-3, 3, 100)
y_line = -(w[0] * x_line + b) / w[1]
ax1.plot(x_line, y_line, 'g-', lw=2, label='Rozhodovací hranice')
ax1.set_xlabel('x₁')
ax1.set_ylabel('x₂')
ax1.set_title('Logistická regrese')
ax1.legend()
ax1.set_xlim(-3, 3)
ax1.set_ylim(-3, 3)
ax1.grid(True, alpha=0.3)

# Loss
ax2.plot(losses, 'b-', lw=2)
ax2.set_xlabel('Epocha')
ax2.set_ylabel('Cross-Entropy Loss')
ax2.set_title('Konvergence')
ax2.grid(True, alpha=0.3)

plt.tight_layout()
plt.show()
Naučené váhy: w = [19.6202958  18.20770383]
Naučený bias: b = -0.0073
Přesnost: 100.00%

21.7.2 Příklad 2: Vliv batch size

Zadání: Porovnejte konvergenci pro různé batch sizes.

Řešení:

import numpy as np
import matplotlib.pyplot as plt

np.random.seed(42)

# Data
N = 500
X = np.random.randn(N, 1)
y = 3 * X.squeeze() + 2 + 0.5 * np.random.randn(N)

def train_with_batch_size(batch_size, n_epochs=50):
    w, b = 0.0, 0.0
    lr = 0.1

    losses = []
    for epoch in range(n_epochs):
        loss = np.mean((X.squeeze() * w + b - y)**2)
        losses.append(loss)

        indices = np.random.permutation(N)
        for start in range(0, N, batch_size):
            batch_idx = indices[start:start + batch_size]
            X_batch = X[batch_idx].squeeze()
            y_batch = y[batch_idx]

            pred = X_batch * w + b
            error = pred - y_batch

            dw = 2 * np.mean(error * X_batch)
            db = 2 * np.mean(error)

            w -= lr * dw
            b -= lr * db

    return losses, w, b

batch_sizes = [1, 8, 32, 128, 500]
results = {}

for bs in batch_sizes:
    losses, w, b = train_with_batch_size(bs)
    results[bs] = losses

# Vizualizace
plt.figure(figsize=(12, 5))
for bs, losses in results.items():
    label = f'Batch size = {bs}' + (' (SGD)' if bs == 1 else ' (Full batch)' if bs == 500 else '')
    plt.plot(losses, label=label, lw=2)

plt.xlabel('Epocha')
plt.ylabel('MSE Loss')
plt.title('Vliv batch size na konvergenci')
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()

21.7.3 Příklad 3: Learning rate scheduling

Zadání: Implementujte decay learning rate.

Řešení:

import numpy as np
import matplotlib.pyplot as plt

np.random.seed(42)

# Data
X = np.random.randn(100, 1)
y = 2 * X.squeeze() + 1 + 0.3 * np.random.randn(100)

def train_with_schedule(schedule_type='constant', initial_lr=0.5, n_epochs=100):
    w, b = 0.0, 0.0
    losses = []
    lrs = []

    for epoch in range(n_epochs):
        # Výpočet learning rate podle schedule
        if schedule_type == 'constant':
            lr = initial_lr
        elif schedule_type == 'step':
            lr = initial_lr * (0.5 ** (epoch // 30))
        elif schedule_type == 'exponential':
            lr = initial_lr * (0.95 ** epoch)
        elif schedule_type == 'cosine':
            lr = initial_lr * 0.5 * (1 + np.cos(np.pi * epoch / n_epochs))

        lrs.append(lr)

        loss = np.mean((X.squeeze() * w + b - y)**2)
        losses.append(loss)

        pred = X.squeeze() * w + b
        error = pred - y
        dw = 2 * np.mean(error * X.squeeze())
        db = 2 * np.mean(error)

        w -= lr * dw
        b -= lr * db

    return losses, lrs

schedules = ['constant', 'step', 'exponential', 'cosine']
results = {s: train_with_schedule(s) for s in schedules}

fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 5))

for schedule in schedules:
    losses, lrs = results[schedule]
    ax1.plot(losses, label=schedule, lw=2)
    ax2.plot(lrs, label=schedule, lw=2)

ax1.set_xlabel('Epocha')
ax1.set_ylabel('Loss')
ax1.set_title('Konvergence s různými LR schedules')
ax1.legend()
ax1.grid(True, alpha=0.3)

ax2.set_xlabel('Epocha')
ax2.set_ylabel('Learning rate')
ax2.set_title('Learning rate schedules')
ax2.legend()
ax2.grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

21.7.4 Příklad 4: Gradient clipping

Zadání: Implementujte gradient clipping pro stabilizaci tréninku.

Řešení:

import numpy as np
import matplotlib.pyplot as plt

def clip_gradient(grad, max_norm=1.0):
    """Ořízne gradient, pokud jeho norma překročí max_norm."""
    norm = np.linalg.norm(grad)
    if norm > max_norm:
        grad = grad * max_norm / norm
    return grad

# Demonstrace na "explodujícím" problému
def steep_function(x):
    return x**4

def grad_steep(x):
    return 4 * x**3

x_no_clip = 5.0
x_with_clip = 5.0
lr = 0.01

history_no_clip = [x_no_clip]
history_with_clip = [x_with_clip]

for _ in range(50):
    # Bez clippingu
    grad = grad_steep(x_no_clip)
    if abs(x_no_clip) < 1000:  # Ochrana proti explozi
        x_no_clip = x_no_clip - lr * grad
    history_no_clip.append(x_no_clip)

    # S clippingem
    grad = grad_steep(x_with_clip)
    grad = clip_gradient(np.array([grad]), max_norm=10.0)[0]
    x_with_clip = x_with_clip - lr * grad
    history_with_clip.append(x_with_clip)

plt.figure(figsize=(10, 5))
plt.plot(history_no_clip[:30], 'r-', label='Bez clippingu', lw=2)
plt.plot(history_with_clip, 'g-', label='S gradient clipping', lw=2)
plt.xlabel('Iterace')
plt.ylabel('x')
plt.title('Gradient Clipping stabilizuje trénink')
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()

21.8 Cvičení

PoznámkaCvičení 1: Implementace

Implementujte mini-batch gradient descent od nuly pro kvadratickou regresi \(y = ax^2 + bx + c\).

PoznámkaCvičení 2: Learning rate finder

Implementujte “learning rate finder” - trénujte model s exponenciálně rostoucím learning rate a sledujte, kde loss začne růst. Toto je dobrý odhad maximálního použitelného learning rate.

PoznámkaCvičení 3: Warmup

Implementujte “warmup” strategii - začněte s malým learning rate a postupně ho zvyšujte během prvních epoch.

PoznámkaCvičení 4: Porovnání

Porovnejte konvergenci SGD na Rosenbrockově funkci pro různé počáteční body. Kolik z nich konverguje k globálnímu minimu (1, 1)?

PoznámkaCvičení 5: PyTorch

Natrénujte jednoduchou neuronovou síť v PyTorch na datasetu MNIST (nebo jiném) pomocí SGD. Experimentujte s learning rate a batch size.

21.9 Shrnutí

TipCo jsme se naučili
  1. Gradient descent iterativně sleduje záporný gradient k minimu
  2. Learning rate je kritický - příliš malý = pomalé, příliš velký = nestabilní
  3. Batch GD používá celý dataset - stabilní, ale pomalé
  4. SGD používá jeden vzorek - rychlé, ale šumné
  5. Mini-batch je praktický kompromis - nejpoužívanější varianta
  6. Problémy: špatně podmíněné funkce, lokální minima, volba hyperparametrů
  7. PyTorch automatizuje gradienty a nabízí různé optimizátory
DůležitéKlíčové vzorce
  • Update pravidlo: \(\mathbf{w}_{t+1} = \mathbf{w}_t - \eta \nabla f(\mathbf{w}_t)\)
  • Batch gradient: \(\nabla f = \frac{1}{N}\sum_{i=1}^N \nabla f_i\)
  • SGD gradient: \(\nabla f \approx \nabla f_i\) pro náhodné \(i\)
  • Mini-batch: \(\nabla f \approx \frac{1}{B}\sum_{i \in \text{batch}} \nabla f_i\)

V další kapitole se podíváme na pokročilé optimalizátory jako Momentum, RMSprop a Adam, které řeší mnoho problémů základního gradient descent.