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

import torch

# 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

import torch

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

import torch

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

import torch

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 jederbackward()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

import torch

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

import torch

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
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

import torch

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

import torch

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() # &#x274c; RuntimeError: Trying to backward through the graph a second time

Unnötige Verwendungretain_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

import torch

# 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

import torch

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

import torch

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.

Weitere Erweiterungen