PyTorch Autograd Automatische Differenzierung
Das Training von Deep Learning ist im Wesentlichen ein Prozess, bei dem wiederholt Gradienten berechnet und Parameter aktualisiert werden.
Das manuelle Ableiten der Gradienten für jede Schicht ist mühsam und fehleranfällig. PyTorchsAutograd(automatische Differenzierung)-Engine wurde geschaffen, um dieses Problem zu lösen – sie kannautomatisch die Gradienten jedes Berechnungsgraphen berechnen, damit Sie sich auf das Modelldesign statt auf die Ableitungsrechnung konzentrieren können.
Kernkonzepte
1. Was ist Automatische Differenzierung
Automatische Differenzierung ist weder numerische Differenzierung (Finite-Differenzen-Methode) noch symbolische Differenzierung (algebraische Ableitung), sondern vielmehrdas Aufzeichnen des Berechnungsprozesses und das schrittweise Anwenden der Kettenregel in umgekehrter Reihenfolgeum Ableitungen präzise zu berechnen.
PyTorchs Autograd verwendet einendynamischen Berechnungsgraphen(Define-by-Run)-Ansatz: Jeder Vorwärtsdurchlauf konstruiert in Echtzeit einen gerichteten azyklischen Graphen (DAG), der jede Operation sowie ihre Ein- und Ausgaben aufzeichnet; während der Backpropagation wird der Graph in umgekehrter Reihenfolge durchlaufen und der Gradient jedes Knotens nacheinander berechnet.
2. requires_grad-Attribut
Das Attribut des Tensorsrequires_gradsteuert, ob Gradienten für diesen Tensor verfolgt werden müssen:
Beispiel
# Erstelle einen Tensor, der Gradientenverfolgung erfordert (Standard: requires_grad=False)
x = torch.tensor(3.0, requires_grad=True)
print(x) # tensor(3., requires_grad=True)
print(x.requires_grad) # True
# Du kannst es auch nach der Erstellung ändern
y = torch.tensor(2.0)
print(y.requires_grad) # False
y.requires_grad_(True) # In-Place-Änderung (beachte den abschließenden Unterstrich)
print(y.requires_grad) # True
# Ergebnisse von Operationen mit Tensoren, bei denen requires_grad=True gesetzt ist, erben automatisch requires_grad=True
z = x * y
print(z.requires_grad) # True
3. grad_fn und Berechnungsgraph
Jeder Tensor, der durch eine Operation erzeugt wird, zeichnet eine aufgrad_fn, die auf den Operationsknoten zeigt, der ihn erzeugt hat. Dies ist das "Skelett" des Berechnungsgraphen:
Beispiel
x = torch.tensor(2.0, requires_grad=True)
y = torch.tensor(3.0, requires_grad=True)
z = x ** 2 + y * 3 # z = x² + 3y
print(z) # tensor(13., grad_fn=<AddBackward0>)
print(z.grad_fn) # <AddBackward0 object>
# Verfolge die Kette der Operationen, die z erzeugt haben
print(z.grad_fn.next_functions)
# ((<PowBackward0 object>, 0), (<MulBackward0 object>, 0))
# Man kann sehen, dass z aus einer Potenzoperation und einer Multiplikationsoperation besteht
backward() Backpropagation
1. Aufruf von backward() für skalare Ausgabe
Aufruf.backward()auf den finalen Skalar (Loss-Wert), und Autograd berechnet automatisch die Gradienten aller Blattknoten in umgekehrter Reihenfolge entlang des Berechnungsgraphen und speichert die Ergebnisse im jeweiligen Tensor-.gradAttribut:
Beispiel
x = torch.tensor(2.0, requires_grad=True)
y = torch.tensor(3.0, requires_grad=True)
# Vorwärtsdurchlauf: z = x² + 3y
z = x ** 2 + y * 3
# Rückwärtsdurchlauf: dz/dx und dz/dy automatisch berechnen
z.backward()
# Gradienten anzeigen
print(x.grad) # tensor(4.) ← dz/dx = 2x = 2×2 = 4
print(y.grad) # tensor(3.) ← dz/dy = 3
# Mathematische Überprüfung:
# z = x² + 3y
# dz/dx = 2x = 2×2 = 4 ✓
# dz/dy = 3 ✓
2. Akkumulationsproblem bei mehreren backward()-Aufrufen
Autograd-Gradienten werdenakkumuliert, nicht überschrieben. Jedes Malbackward()aufgerufen wird, werden die Gradienten addiert zu.gradden vorhandenen Werten. Dies ist der häufigste Fallstrick in Trainingsschleifen:
Beispiel
x = torch.tensor(2.0, requires_grad=True)
# Erste Backpropagation
loss = x ** 2
loss.backward()
print(x.grad) # tensor(4.) ← dL/dx = 2x = 4
# Zweite Backpropagation (ohne Nullsetzen!)
loss = x ** 2
loss.backward()
print(x.grad) # tensor(8.) ← Akkumuliert! Nicht 4, sondern 4+4=8
# ✅ Richtiger Ansatz: Gradienten vor jeder Backpropagation auf null setzen
x.grad.zero_() # In-place nullen (beachte den abschließenden Unterstrich)
loss = x ** 2
loss.backward()
print(x.grad) # tensor(4.) ← Korrekt
Beim Training eines neuronalen Netzwerks, vor jeder
backward()calloptimizer.zero_grad()die Gradienten auf null zu setzen, andernfalls akkumulieren sich die Gradienten weiter und verursachen fehlerhafte Parameter-Updates.
3. Aufruf von backward(gradient) für nicht-skalare Ausgabe
Wenn die Ausgabe ein Vektor oder eine Matrix statt eines Skalars ist,backward()müssen Sie einengradientParameter mit derselben Form wie die Ausgabe übergeben (d. h. den „Upstream-Gradienten“), was im Wesentlichen die Berechnung des Vektor-Jacobian-Produkts (VJP) ist:
Beispiel
x = torch.tensor([1.0, 2.0, 3.0], requires_grad=True)
# Vorwärtspass: y ist ein Vektor
y = x ** 2 # y = [1, 4, 9]
# Für nicht-skalare Ausgabe müssen Sie den Gradient-Parameter übergeben (gleiche Form wie y)
# Der Gradient kann als „der Gradient des Verlusts in Bezug auf y“ verstanden werden
y.backward(gradient=torch.ones_like(y)) # Angenommen, der Upstream-Gradient ist überall 1
print(x.grad)
# tensor([2., 4., 6.]) ← dy/dx = 2x, elementweise berechnet
# Wenn der Upstream-Gradient nicht überall 1 ist (z. B. gewichtet)
x.grad.zero_()
y.backward(gradient=torch.tensor([1.0, 0.5, 2.0])) # Unterschiedliche Gewichte
# Tatsächliche Berechnung: x.grad = 2x * gradient = [2×1, 4×0.5, 6×2]
print(x.grad)
# tensor([2., 2., 12.])
# Üblicherer Ansatz: Zuerst sum/mean verwenden, um in einen Skalar umzuwandeln, dann backward() aufrufen
x.grad.zero_()
loss = (x ** 2).sum() # Aggregiere den Vektor zu einem Skalar
loss.backward()
print(x.grad)
# tensor([2., 4., 6.]) ← Entspricht dem ersten Ansatz
torch.no_grad() Stoppt die Gradientenverfolgung
Während der Modellinferenz (Vorhersage) müssen keine Gradienten berechnet werden. Durch die Verwendung vontorch.no_grad()kann die Konstruktion des Berechnungsgraphen übersprungen werden, was Speicher und Rechenaufwand erheblich spart:
Beispiel
x = torch.tensor(3.0, requires_grad=True)
# In einem no_grad-Kontext werden keine Operationen Gradienten verfolgen
with torch.no_grad():
y = x ** 2
print(y.requires_grad) # False ← Verfolgt keine Gradienten mehr
print(y.grad_fn) # None ← Kein Knoten im Berechnungsgraphen
# Nach dem Verlassen des no_grad-Kontexts wird die normale Verfolgung fortgesetzt
z = x ** 2
print(z.requires_grad) # True
# Häufige Verwendung: den gesamten Inferenzprozess während der Modellauswertung umschließen
model = torch.nn.Linear(10, 1)
inputs = torch.randn(32, 10)
with torch.no_grad():
outputs = model(inputs) # Es wird kein Berechnungsgraph aufgebaut; schnellere Ausführung und geringerer Speicherverbrauch
@torch.no_grad() Decorator-Syntax
Du kannst auch die Decorator-Form verwenden, die sich eignet, um eine gesamte Inferenzfunktion als gradientenfrei zu markieren:
Beispiel
import torch.nn as nn
model = nn.Linear(10, 1)
@torch.no_grad()
def predict(model, x):
"""Inferenzfunktion, keine Gradientenberechnung erforderlich"""
return model(x)
x = torch.randn(5, 10)
output = predict(model, x)
print(output.requires_grad) # False
detach() Vom Berechnungsgraphen trennen
.detach()Gibt einen neuen Tensor zurück, der Daten mit dem ursprünglichen Tensor teilt, aber keine Gradienten verfolgt. Wird häufig in den folgenden Szenarien verwendet:
| Szenario | Beschreibung |
|---|---|
| Umwandeln von Zwischenergebnissen in numpy-Arrays | numpy unterstützt keine Tensoren mit Gradienten; Sie müssen zuerst detach() aufrufen |
| Aufzeichnen des Trainingsverlusts (Logs) | Vermeiden, den gesamten Berechnungsgraphen zu behalten, um Speicherlecks zu verhindern |
| Einfrieren der Gradientenpropagation für einen Teil des Netzwerks | Szenarien wie GAN-Training und Transfer Learning |
Beispiel
x = torch.tensor([1.0, 2.0, 3.0], requires_grad=True)
y = x ** 2 + x * 3 # y hat grad_fn
# Der Tensor nach detach teilt Daten mit y, ist aber vom Berechnungsgraphen getrennt
y_detached = y.detach()
print(y_detached.requires_grad) # False
print(y_detached.grad_fn) # None
# ✅ In numpy umwandeln (Tensoren mit Gradienten können nicht direkt umgewandelt werden)
# y.numpy() # ❌ Fehler: RuntimeError
y_detached.numpy() # ✅ Normal
# ✅ Beim Aufzeichnen von Loss-Werten detach verwenden (um das Beibehalten des Berechnungsgraphen und Speicherverbrauch zu vermeiden)
losses = []
for i in range(3):
loss = (x ** 2).sum()
losses.append(loss.detach().item()) # .item() wandelt einen Skalar-Tensor in einen Python-Float um
loss.backward()
x.grad.zero_()
print(losses) # [14.0, 14.0, 14.0]
retain_graph Beibehalten des Berechnungsgraphen
Standardmäßig,backward()Nach der Ausführung wird der Berechnungsgraphautomatisch freigegeben(um Speicher zu sparen). Wenn Sie mehrfach auf demselben Berechnungsgraphen Backpropagation durchführen müssen (z. B. beim Training mancher GANs), müssen Sie übergebenretain_graph=True:
Beispiel
x = torch.tensor(2.0, requires_grad=True)
y = x ** 3 # y = x³
# Erstes backward (Berechnungsgraph beibehalten)
y.backward(retain_graph=True)
print(x.grad) # tensor(12.) ← dy/dx = 3x² = 3×4 = 12
# Zweites backward (Berechnungsgraph existiert noch)
x.grad.zero_()
y.backward(retain_graph=True)
print(x.grad) # tensor(12.) ← Gleiches Ergebnis
# Beim letzten Mal ist kein Beibehalten nötig
x.grad.zero_()
y.backward() # Danach wird der Berechnungsgraph freigegeben
print(x.grad) # tensor(12.)
# Ein erneuter backward-Aufruf führt zu einem Fehler (Berechnungsgraph bereits freigegeben)
# y.backward() # ❌ RuntimeError: Trying to backward through the graph a second time
Unnötige Verwendung
retain_graph=Trueführt dazu, dass der Speicher weiter wächst, weil der Berechnungsgraph nicht freigegeben werden kann. Verwende es nur, wenn tatsächlich mehrere backward-Durchläufe benötigt werden.
Anwendung von Gradienten beim Training neuronaler Netze
Das Folgende ist ein vollständiges Beispiel für die manuelle Implementierung des Gradientenabstiegs mit Autograd, das den vollständigen Workflow von Autograd im tatsächlichen Training demonstriert:
Beispiel
# Trainingsdaten erstellen: y = 2x + 1 plus Rauschen
torch.manual_seed(42)
X = torch.randn(100, 1)
y_true = 2 * X + 1 + 0.1 * torch.randn(100, 1)
# Modellparameter initialisieren (erfordert Gradientenverfolgung)
w = torch.zeros(1, requires_grad=True) # Gewicht
b = torch.zeros(1, requires_grad=True) # Bias
lr = 0.1 # Lernrate
epochs = 50 # Anzahl der Trainingsepochen
for epoch in range(epochs):
# 1. Vorwärtspropagierung: Vorhersagen berechnen
y_pred = X * w + b
# 2. Verlust berechnen (mittlerer quadratischer Fehler)
loss = ((y_pred - y_true) ** 2).mean()
# 3. Rückwärtspropagation: d(loss)/dw und d(loss)/db automatisch berechnen
loss.backward()
# 4. Parameter manuell aktualisieren (mit no_grad umschließen, damit Aktualisierungsoperationen nicht im Berechnungsgraphen verfolgt werden)
with torch.no_grad():
w -= lr * w.grad
b -= lr * b.grad
# 5. Gradienten zurücksetzen (müssen vor dem nächsten Backward-Pass zurückgesetzt werden)
w.grad.zero_()
b.grad.zero_()
if (epoch + 1) % 10 == 0:
print(f"Epoche {epoch+1:3d} | Verlust: {loss.item():.4f} | w={w.item():.3f}, b={b.item():.3f}")
print(f"\nTraining abgeschlossen: w ≈ {w.item():.3f} (wahrer Wert 2.0), b ≈ {b.item():.3f} (wahrer Wert 1.0))
Das Ausführungsergebnis des obigen Codes ähnelt dem Folgenden:
Epoch 10 | Loss: 0.1064 | w=1.587, b=0.805 Epoch 20 | Loss: 0.0281 | w=1.876, b=0.949 Epoch 30 | Loss: 0.0152 | w=1.954, b=0.983 Epoch 40 | Loss: 0.0128 | w=1.978, b=0.993 Epoch 50 | Loss: 0.0122 | w=1.987, b=0.997 训练完成:w ≈ 1.987(真实值 2.0),b ≈ 0.997(真实值 1.0)
Schnellreferenztabelle für gängige APIs
| API | Funktion | Häufige Szenarien |
|---|---|---|
tensor.requires_grad_(True) |
Gradientenverfolgung in-place aktivieren | Autograd für einen vorhandenen Tensor aktivieren |
loss.backward() |
Rückwärtspropagation, Gradienten aller Blattknoten berechnen | Jede Iteration in der Trainingsschleife |
tensor.grad |
Auf den Gradientenwert eines Tensors zugreifen | Parameter anzeigen oder manuell aktualisieren |
tensor.grad.zero_() |
Gradienten in-place zurücksetzen | Vor jedem backward() müssen die Gradienten zurückgesetzt werden |
torch.no_grad() |
Kontextmanager, Gradientenverfolgung deaktivieren | Inferenzphase, manuelle Parameteraktualisierungen |
tensor.detach() |
Gibt einen neuen Tensor zurück, der vom Berechnungsgraphen getrennt ist | In numpy konvertieren, Protokolleinträge, Gradienten einfrieren |
tensor.item() |
Konvertiere einen Skalar-Tensor in eine Python-Zahl | Verlustwerte ausgeben, Metriken aufzeichnen |
loss.backward(retain_graph=True) |
Den Berechnungsgraphen beibehalten, mehrere Backward-Pässe ermöglichen | Szenarien wie GAN-Training, die mehrere Backward-Pässe erfordern |
Häufige Fragen und Vorsichtsmaßnahmen
1. Unterschied zwischen Blattknoten und Nicht-Blattknoten
NurBlattknoten(d. h. Tensoren, die direkt vom Benutzer erstellt werden, nicht das Ergebnis von Operationen) haben ihre Gradienten gespeichert in.grad. Nicht-Blattknoten, die durch Zwischenoperationen erzeugt werden, behalten standardmäßig keine Gradienten (um Speicher zu sparen). Wenn Sie den Gradienten eines Zwischenknotens anzeigen möchten, rufen Sie auf.retain_grad():
Beispiel
x = torch.tensor(2.0, requires_grad=True) # Blattknoten
# Zwischenknoten (Nicht-Blattknoten)
y = x ** 2 # y ist ein Zwischenknoten
y.retain_grad() # Explizit deklarieren, um den Gradienten von y zu behalten
z = y * 3 # z ist die endgültige Ausgabe
z.backward()
print(x.grad) # tensor(12.) ← Blattknoten, normal gespeichert
print(y.grad) # tensor(3.) ← Gespeichert wegen retain_grad()
# Zwischenknoten ohne retain_grad(), .grad ist None
2. In-Place-Operationen können den Berechnungsgraphen beschädigen
Das Ausführen von In-Place-Operationen auf Tensoren, die Gradientenverfolgung erfordern (z. B.+=、.add_()) kann Autograd daran hindern, korrekt rückwärts zu propagieren, daher sollten sie möglichst vermieden werden:
Beispiel
x = torch.tensor([1.0, 2.0], requires_grad=True)
# Gefährlich: In-Place-Operation auf einem Blattknoten
# x += 1 # Kann einen Fehler auslösen: Eine Blatt-Variable, die grad erfordert, wurde in einer In-Place-Operation verwendet
# Sicher: Verwenden Sie eine Nicht-In-Place-Operation
y = x + 1 # Einen neuen Tensor erstellen, x nicht verändern
# Es ist sicher, In-Place-Operationen in einem no_grad-Kontext durchzuführen (z. B. Parameteraktualisierungen)
with torch.no_grad():
x += 0.01 # Wird für manuelle Parameteraktualisierungen verwendet, dies ist sicher
3. Nur Gleitkomma-Tensoren unterstützen Gradienten
Ganzzahlige Typen (z. B.torch.int64) Tensoren unterstützen nichtrequires_grad=True, nur Gleitkommatypen (float32、float64、float16) können an der automatischen Differenzierung teilnehmen.