Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Autograd und Berechnungsgraph

Heinrich-Heine-Universität Düsseldorf

Im Kapitel „Backpropagation“ haben wir Gradienten von Hand rückwärts durch ein Netz gerechnet. PyTorch tut genau dasselbe – automatisch, für beliebige Architekturen, und mit demselben Muster: Zwischenergebnisse im Vorwärtsdurchlauf speichern, Kettenregel im Rückwärtsdurchlauf. Dieses Kapitel schaut hinter die Kulissen von loss.backward().

Ein Beispiel im Kleinen

Ein Blick darauf, wie PyTorch Backpropagation tatsächlich macht. Wir bauen ein sehr kleines „Netzwerk“:

import torch

x = torch.tensor(3.0)                       # Input (Konstante)
w = torch.tensor(2.0, requires_grad=True)   # Gewicht: "Beobachte mich!"
b = torch.tensor(1.0, requires_grad=True)   # Bias
z = w * x     # z = 6
y = z + b     # y = 7
L = y ** 2    # Loss = 49

Der Berechnungsgraph (ein DAG, directed acyclic graph) entsteht erst beim Ausführen: Jeder Tensor, der aus einer Operation mit einem requires_grad-Tensor entstanden ist, trägt eine grad_fn, und man kann den Graphen von hinten nach vorn ablaufen:

print(L.grad_fn)                                    # <PowBackward0>
print(L.grad_fn.next_functions[0][0])               # <AddBackward0>
print(L.grad_fn.next_functions[0][0].next_functions)
# ((<MulBackward0>, 0), (<AccumulateGrad>, 0))  -- z, und das Blatt b

Die grad_fn weiß, wie man die Operation ableitet, und hält Referenzen auf die saved tensors, die gespeicherten Zwischenergebnisse – genau die zkz_k aus dem Kapitel „Backpropagation“. AccumulateGrad sind die Blätter des Graphen, die Parameter ww und bb; die topologische Sortierung ist implizit.

L.backward() ist der rekursive Aufruf der Kettenregel, initial mit dLdL=1\frac{dL}{dL} = 1:

dLdw=dLdy⋅dydz⋅dzdw=2y⋅1⋅x=14⋅3=42,dLdb=2y⋅1=14.\frac{dL}{dw} = \frac{dL}{dy} \cdot \frac{dy}{dz} \cdot \frac{dz}{dw} = 2y \cdot 1 \cdot x = 14 \cdot 3 = 42, \qquad \frac{dL}{db} = 2y \cdot 1 = 14.
L.backward()
print(w.grad, b.grad)   # tensor(42.), tensor(14.)

Danach steht das Ergebnis in w.grad und b.grad. Ein Schritt Gradientenabstieg von Hand, das ist alles, was optimizer.step() tut:

lr = 0.01
with torch.no_grad():        # sonst würde der Update selbst Teil des Graphen
    w -= lr * w.grad         # 2.0 - 0.01 * 42 = 1.58
    b -= lr * b.grad         # 1.0 - 0.01 * 14 = 0.86
    w.grad.zero_()           # sonst wird beim nächsten backward() akkumuliert
    b.grad.zero_()

Der Optimizer-Schritt muss unter torch.no_grad() passieren, damit der Update selbst nicht Teil des Graphen wird, und die Gradienten müssen danach mit zero_() zurückgesetzt werden – sonst akkumuliert PyTorch sie beim nächsten backward(). Das ist der Grund für optimizer.zero_grad() in jeder Trainingsschleife.

Freezing

Setzt man w.requires_grad = False, so hat z = w * x keine grad_fn mehr, der Graph ist an dieser Stelle abgeschnitten:

w.requires_grad = False
z = w * x
print(z.grad_fn)             # None  -- Blatt abgeschnitten
print((z + b).grad_fn)       # <AddBackward0>  -- b wird weiter beobachtet

Ein Optimizer verändert ww nicht mehr, bb aber schon. So friert man beim Transfer Learning Schichten eines vortrainierten Modells ein – und spart dabei Speicher, weil für eingefrorene Parameter keine Gradienten gehalten werden.

Geteilte Gewichte: Backprop durch eine Faltung

Wie funktioniert Backprop, wenn Gewichte geteilt werden wie bei einem Faltungslayer? Der Kernel [0.5] wird über den Input [1, 2, 3] geschoben, oi=xi⋅ko_i = x_i \cdot k, und der Loss ist o1+o2+o3o_1 + o_2 + o_3:

x = torch.tensor([1.0, 2.0, 3.0])
k = torch.tensor([0.5], requires_grad=True)
o1, o2, o3 = x[0] * k, x[1] * k, x[2] * k   # von Hand, wie nn.Conv1d
loss = o1 + o2 + o3
loss.backward()
print(k.grad)                # tensor([6.])

Der Gradient nach kk ist die Summe der Beiträge aller Positionen, dLdk=x1+x2+x3=6\frac{dL}{dk} = x_1 + x_2 + x_3 = 6: Ein geteilter Parameter bekommt die Gradienten aller seiner Verwendungen aufsummiert – im Graphen führen mehrere Kanten zu demselben Blatt.

Übung

  1. Ein Modell hat drei Layer. Vor dem Training wird für Layer 1 und 2 requires_grad = False gesetzt. Welche Tensoren bekommen im Backward Pass einen .grad, welche nicht – und wo wird trotzdem noch gerechnet? Tipp: Zeichnen Sie den Graphen und verfolgen Sie die Pfeile rückwärts, von der Loss-Funktion zu den Blättern. Fragen Sie sich bei jedem Layer: Muss der Gradient hier überhaupt noch hindurch, damit weiter vorne etwas ankommt?

  2. Ein Gewicht kk wird an drei Stellen im Graphen benutzt (geteilte Gewichte, wie bei einer Faltung). Wie viele Beiträge hat ∂L∂k\frac{\partial L}{\partial k}, und wie werden sie verrechnet?

Antwortprobe: Bei (1) spart das Einfrieren Speicher, aber nicht jede Rechnung.