Warum diese Seite jetzt wichtig ist

Viele Einsteiger verwechseln lineare Regression, Training und Regularisierung. Hier trennen wir die Schritte klar: Loss definieren, Optimierer auswählen, Parameter iterativ verbessern, Ergebnis auf neue Daten denken.

Codepfad
Dauer: 60-120 Minuten
Python: 3.11.3
Libraries: numpy, matplotlib
Ziel: Training nachvollziehbar erklären können

1) Recap: Was bedeutet "Modell trainieren"?

Für lineare Regression suchen wir Parameter, die den Fehler minimieren. Geschlossen geht das über die Normal Equation, iterativ über Gradient Descent.

Sprachbrücke zur Praxis:
In Python startet `model.fit(X_train, y_train)` den Trainingsprozess. Unter der Haube wird je nach Modell/Implementierung ein passender Solver genutzt.
Code
# optionales Setup
python3.11 -m venv .venv
source .venv/bin/activate
pip install numpy matplotlib
Mini-Check: Warum Train/Test-Split vor dem finalen Bewerten?

Damit du nicht nur Trainingsanpassung misst, sondern Generalisierung auf ungesehenen Daten.

2) Gradient Descent für eine kleine Funktion

Wir starten mit einer 1D-Loss-Funktion. So wird sichtbar, wie Lernrate und Gradientenrichtung zusammenwirken.

Code
import numpy as np

# synthetische Loss-Funktion
# J(w) = 0.1*(w-2.5)^4 + 0.95*(w-2.5)^2 + 4

def loss(w):
    d = w - 2.5
    return 0.1 * d**4 + 0.95 * d**2 + 4

def grad(w):
    d = w - 2.5
    return 0.4 * d**3 + 1.9 * d

w = 7.0
alpha = 0.25
history = [w]

for _ in range(25):
    w = w - alpha * grad(w)
    history.append(w)

print("final w:", round(w, 4))
print("final loss:", round(loss(w), 4))
Faustregel: Zu kleine Lernrate = stabil, aber langsam. Zu große Lernrate = Risiko von Überschwingen/Oszillation.
Mini-Check: Warum laufen wir entgegen dem Gradienten?

Der Gradient zeigt Richtung stärkster Zunahme der Loss. Für Minimierung gehen wir in die entgegengesetzte Richtung.

3) Loss-Verbesserung über Iterationen visualisieren

Jetzt machen wir den Lernverlauf sichtbar. Das hilft didaktisch enorm, um Konvergenz zu erklären.

Code
import numpy as np
import matplotlib.pyplot as plt

# Standalone-Block: baut den Verlauf lokal neu auf
def loss(w):
    d = w - 2.5
    return 0.1 * d**4 + 0.95 * d**2 + 4

def grad(w):
    d = w - 2.5
    return 0.4 * d**3 + 1.9 * d

w = 7.0
alpha = 0.25
history = [w]

for _ in range(25):
    w = w - alpha * grad(w)
    history.append(w)

loss_history = [loss(wi) for wi in history]

plt.figure(figsize=(7, 4))
plt.plot(loss_history, marker="o")
plt.xlabel("Iteration")
plt.ylabel("Loss J(w)")
plt.title("Gradient Descent: Loss sinkt über Iterationen")
plt.grid(alpha=0.3)
plt.show()
Didaktischer Punkt:
Wenn die Kurve sauber fällt und dann abflacht, verstehen Teilnehmende sofort "Training konvergiert" ohne tiefere Analysis.
Mini-Check: Was deutet auf zu hohe Lernrate hin?

Starkes Auf-und-Ab in der Loss-Kurve oder sogar steigende Loss statt stabiler Abnahme.

4) Batch vs Mini-Batch vs Stochastic GD (linear regression)

Hier sehen wir dieselbe Grundidee auf einem kleinen Regressionsproblem. Unterschiedlich ist nur, wie viele Beobachtungen pro Update eingehen.

Code
import numpy as np

# synthetische Daten: y ~= 3 + 2*x + noise
rng = np.random.default_rng(42)
X = rng.uniform(-2, 2, size=200)
noise = rng.normal(0, 0.7, size=200)
y = 3 + 2 * X + noise

# lineares Modell: y_hat = b0 + b1*x

def mse(b0, b1, x_batch, y_batch):
    y_hat = b0 + b1 * x_batch
    return np.mean((y_batch - y_hat) ** 2)

def gradients(b0, b1, x_batch, y_batch):
    y_hat = b0 + b1 * x_batch
    err = y_hat - y_batch
    db0 = 2 * np.mean(err)
    db1 = 2 * np.mean(err * x_batch)
    return db0, db1

def train_gd(mode="batch", lr=0.05, epochs=60, batch_size=32):
    b0, b1 = 0.0, 0.0
    losses = []

    n = len(X)
    for _ in range(epochs):
        if mode == "batch":
            idx_sets = [np.arange(n)]
        elif mode == "mini":
            perm = rng.permutation(n)
            idx_sets = [perm[i:i+batch_size] for i in range(0, n, batch_size)]
        else:  # stochastic
            perm = rng.permutation(n)
            idx_sets = [[i] for i in perm]

        for idx in idx_sets:
            xb, yb = X[idx], y[idx]
            db0, db1 = gradients(b0, b1, xb, yb)
            b0 -= lr * db0
            b1 -= lr * db1

        losses.append(mse(b0, b1, X, y))

    return b0, b1, losses

for mode in ["batch", "mini", "stochastic"]:
    b0, b1, losses = train_gd(mode=mode)
    print(mode, "-> b0:", round(b0, 3), "b1:", round(b1, 3), "final loss:", round(losses[-1], 3))
Typische Trade-offs: Batch ist stabiler, SGD reagiert schneller pro Update, Mini-Batch liegt oft dazwischen.
Mini-Check: Warum ist SGD oft "rauschiger"?

Weil jedes Update nur auf einer einzelnen Beobachtung basiert und damit stärker von Zufallsschwankungen beeinflusst wird.

5) Momentum einführen

Momentum führt Trägheit ein: statt nur den aktuellen Gradient zu nutzen, wird ein Anteil früherer Richtungen mitgenommen.

Code
import numpy as np

# Standalone-Block: eigenes Mini-Dataset + Hilfsfunktionen
rng = np.random.default_rng(42)
X = rng.uniform(-2, 2, size=200)
noise = rng.normal(0, 0.7, size=200)
y = 3 + 2 * X + noise

def mse(b0, b1, x_batch, y_batch):
    y_hat = b0 + b1 * x_batch
    return np.mean((y_batch - y_hat) ** 2)

def gradients(b0, b1, x_batch, y_batch):
    y_hat = b0 + b1 * x_batch
    err = y_hat - y_batch
    db0 = 2 * np.mean(err)
    db1 = 2 * np.mean(err * x_batch)
    return db0, db1

def train_with_momentum(mode="mini", lr=0.05, beta=0.9, epochs=60, batch_size=32):
    b0, b1 = 0.0, 0.0
    v0, v1 = 0.0, 0.0
    losses = []
    n = len(X)

    for _ in range(epochs):
        if mode == "batch":
            idx_sets = [np.arange(n)]
        elif mode == "mini":
            perm = rng.permutation(n)
            idx_sets = [perm[i:i+batch_size] for i in range(0, n, batch_size)]
        else:
            perm = rng.permutation(n)
            idx_sets = [[i] for i in perm]

        for idx in idx_sets:
            xb, yb = X[idx], y[idx]
            db0, db1 = gradients(b0, b1, xb, yb)
            v0 = beta * v0 + db0
            v1 = beta * v1 + db1
            b0 -= lr * v0
            b1 -= lr * v1

        losses.append(mse(b0, b1, X, y))

    return b0, b1, losses

b0_m, b1_m, losses_m = train_with_momentum(mode="mini", beta=0.9)
print("with momentum ->", round(b0_m, 3), round(b1_m, 3), round(losses_m[-1], 3))
Interpretation:
Momentum kann bei verrauschten Gradienten den Lernpfad glätten und Konvergenz beschleunigen. Zu hoch eingestellt kann es aber auch überschießen.
Mini-Check: In welchem Fall hilft Momentum besonders?

Wenn Updates stark schwanken (z. B. Mini-Batch/SGD) und der Lernpfad sonst zickzackartig verläuft.

6) Wie erkenne ich, ob ein Modell GD "under the hood" nutzt?

Nicht jedes Modell in scikit-learn trainiert mit Gradient Descent. Prüfe immer die Solver-Dokumentation des konkreten Estimators.

Code
# Beispiel: unterschiedliche Solver-Strategien
# LinearRegression() nutzt i. d. R. keine klassische iterative GD-Schleife.
# SGDRegressor() trainiert explizit stochastisch iterativ.

from sklearn.linear_model import LinearRegression, SGDRegressor

print(LinearRegression())
print(SGDRegressor(max_iter=1000, tol=1e-3, random_state=42))
Praxisregel: Wenn du Iterationen, Lernrate, Batchgröße oder Momentum direkt einstellst, bist du typischerweise in einem GD-nahen Trainingssetting.
Mini-Check: Wann würdest du GD bevorzugen?

Bei großen Datenmengen, vielen Parametern oder wenn eine geschlossene Lösung unpraktisch wird und iterative Optimierung besser skaliert.

End-of-Day Fragen (Lehrplan)

  1. Was ist der Unterschied zwischen Batch, Mini-Batch und Stochastic GD?
  2. Wie beeinflusst die Lernrate die Konvergenz?
  3. Woran erkennst du, ob ein Modell GD unter der Haube nutzt?
  4. Wann ist GD sinnvoller als eine geschlossene Lösung?
  5. Was ist das Ziel von Gradient Descent in einem Satz?