Différenciation automatique PyTorch Autograd
L’entraînement de l’apprentissage profond est essentiellement un processus de calcul répété des gradients et de mise à jour des paramètres.
Dériver manuellement les gradients pour chaque couche est fastidieux et source d’erreurs. Le moteurAutogradde PyTorch (différenciation automatique) a été créé pour résoudre ce problème—il peutcalculer automatiquement les gradients de n’importe quel graphe de calcul, ce qui vous permet de vous concentrer sur la conception du modèle plutôt que sur la dérivation du calcul.
Concepts fondamentaux
1. Qu’est-ce que la différenciation automatique
La différenciation automatique n’est ni une différenciation numérique (méthode des différences finies), ni une différenciation symbolique (dérivation algébrique), mais plutôtenregistrer le processus de calcul et appliquer la règle de la chaîne en sens inverse étape par étapepour calculer précisément les dérivées.
Autograd de PyTorch utilise une approche degraphe de calcul dynamique(Define-by-Run) : chaque passe avant construit un graphe orienté acyclique (DAG) en temps réel, en enregistrant chaque opération ainsi que ses entrées et sorties ; lors de la rétropropagation, il parcourt le graphe en sens inverse et calcule le gradient de chaque nœud dans l'ordre.
2. Attribut requires_grad
L'attribut du Tensorrequires_gradcontrôle si les gradients doivent être suivis pour ce tenseur :
Exemple
# Créer un tenseur qui nécessite le suivi du gradient (requires_grad=False par défaut)
x = torch.tensor(3.0, requires_grad=True)
print(x) # tensor(3., requires_grad=True)
print(x.requires_grad) # True
# Vous pouvez également le modifier après sa création
y = torch.tensor(2.0)
print(y.requires_grad) # False
y.requires_grad_(True) # Modification en place (notez le tiret bas final)
print(y.requires_grad) # True
# Les résultats des opérations impliquant des tenseurs avec requires_grad=True héritent automatiquement de requires_grad=True
z = x * y
print(z.requires_grad) # True
3. grad_fn et graphe de calcul
Chaque tenseur produit par une opération enregistre ungrad_fn, qui pointe vers le nœud d’opération qui l’a créé. C’est le « squelette » du graphe de calcul :
Exemple
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>
# Retracer la chaîne d’opérations qui a créé z
print(z.grad_fn.next_functions)
# ((<PowBackward0 object>, 0), (<MulBackward0 object>, 0))
# Vous pouvez voir que z est composé d’une opération de puissance et d’une opération de multiplication
backward() Rétropropagation
1. Appel de backward() pour une sortie scalaire
Appel.backward()sur le scalaire final (valeur de perte), et Autograd calculera automatiquement les gradients de tous les nœuds feuilles en remontant le graphe de calcul, en stockant les résultats dans chaque tenseur, dans son.gradattribut :
Exemple
x = torch.tensor(2.0, requires_grad=True)
y = torch.tensor(3.0, requires_grad=True)
# Passe avant : z = x² + 3y
z = x ** 2 + y * 3
# Passe arrière : calculer automatiquement dz/dx et dz/dy
z.backward()
# Afficher les gradients
print(x.grad) # tensor(4.) ← dz/dx = 2x = 2×2 = 4
print(y.grad) # tensor(3.) ← dz/dy = 3
# Vérification mathématique :
# z = x² + 3y
# dz/dx = 2x = 2×2 = 4 ✓
# dz/dy = 3 ✓
2. Problème d'accumulation avec des appels multiples à backward()
Les gradients d'Autograd sontaccumulés, et non écrasés. Chaque fois quebackward()est appelée, les gradients seront ajoutés aux.gradvaleurs existantes. C'est le piège le plus courant dans les boucles d'entraînement :
Exemple
x = torch.tensor(2.0, requires_grad=True)
# Première rétropropagation
loss = x ** 2
loss.backward()
print(x.grad) # tensor(4.) ← dL/dx = 2x = 4
# Deuxième rétropropagation (sans remise à zéro !)
loss = x ** 2
loss.backward()
print(x.grad) # tensor(8.) ← Accumulé ! Pas 4, mais 4+4=8
# ✅ Approche correcte : remettre les gradients à zéro avant chaque rétropropagation
x.grad.zero_() # Remise à zéro sur place (notez le tiret bas final)
loss = x ** 2
loss.backward()
print(x.grad) # tensor(4.) ← Correct
Lors de l'entraînement d'un réseau de neurones, avant chaque
backward()calloptimizer.zero_grad()remettre les gradients à zéro, sinon les gradients continueront de s'accumuler et provoqueront des mises à jour incorrectes des paramètres.
3. Appel de backward(gradient) pour une sortie non scalaire
Si la sortie est un vecteur ou une matrice plutôt qu'un scalaire,backward()vous devez passer ungradientparamètre de même forme que la sortie (c.-à-d. le « gradient amont »), ce qui revient essentiellement à calculer le produit vecteur-jacobien (VJP) :
Exemple
x = torch.tensor([1.0, 2.0, 3.0], requires_grad=True)
# Passe avant : y est un vecteur
y = x ** 2 # y = [1, 4, 9]
# Pour une sortie non scalaire, vous devez passer le paramètre gradient (de même forme que y)
# Le gradient peut être compris comme « le gradient de la perte par rapport à y »
y.backward(gradient=torch.ones_like(y)) # Supposons que le gradient amont soit uniquement composé de 1
print(x.grad)
# tensor([2., 4., 6.]) ← dy/dx = 2x, calculé élément par élément
# Si le gradient amont n'est pas uniquement composé de 1 (p. ex., pondéré)
x.grad.zero_()
y.backward(gradient=torch.tensor([1.0, 0.5, 2.0])) # Poids différents
# Calcul réel : x.grad = 2x * gradient = [2×1, 4×0.5, 6×2]
print(x.grad)
# tensor([2., 2., 12.])
# Approche plus courante : utiliser d'abord sum/mean pour convertir en un scalaire, puis appeler backward()
x.grad.zero_()
loss = (x ** 2).sum() # Agréger le vecteur en un scalaire
loss.backward()
print(x.grad)
# tensor([2., 4., 6.]) ← Équivalent à la première approche
torch.no_grad() Arrêter le suivi des gradients
Lors de l'inférence du modèle (prédiction), il n'est pas nécessaire de calculer les gradients. En utilisant,torch.no_grad()on peut éviter la construction du graphe de calcul, ce qui économise considérablement de la mémoire et du calcul :
Exemple
x = torch.tensor(3.0, requires_grad=True)
# Dans un contexte no_grad, aucune opération ne suivra les gradients
with torch.no_grad():
y = x ** 2
print(y.requires_grad) # False ← Ne suit plus les gradients
print(y.grad_fn) # None ← Aucun nœud de graphe de calcul
# Après avoir quitté le contexte no_grad, le suivi normal reprend
z = x ** 2
print(z.requires_grad) # True
# Utilisation courante : envelopper tout le processus d'inférence lors de l'évaluation du modèle
model = torch.nn.Linear(10, 1)
inputs = torch.randn(32, 10)
with torch.no_grad():
outputs = model(inputs) # Aucun graphe de calcul n'est construit ; vitesse plus rapide et utilisation mémoire réduite
@torch.no_grad() Syntaxe du décorateur
Vous pouvez également utiliser la forme décorateur, qui convient pour marquer une fonction d'inférence entière comme sans gradient :
Exemple
import torch.nn as nn
model = nn.Linear(10, 1)
@torch.no_grad()
def predict(model, x):
"""Fonction d'inférence, aucun calcul de gradient nécessaire"""
return model(x)
x = torch.randn(5, 10)
output = predict(model, x)
print(output.requires_grad) # False
detach() Séparation du graphe de calcul
.detach()Renvoie un nouveau tenseur qui partage les données avec le tenseur d'origine mais ne suit pas les gradients. Couramment utilisé dans les scénarios suivants :
| Scénario | Description |
|---|---|
| Conversion de résultats intermédiaires en tableaux numpy | numpy ne prend pas en charge les tenseurs avec gradients ; vous devez d'abord appeler detach() |
| Enregistrement de la perte d'entraînement (journaux) | Éviter de conserver l'intégralité du graphe de calcul pour éviter les fuites de mémoire |
| Geler la propagation des gradients pour une partie du réseau | Scénarios tels que l'entraînement de GAN et l'apprentissage par transfert |
Exemple
x = torch.tensor([1.0, 2.0, 3.0], requires_grad=True)
y = x ** 2 + x * 3 # y possède grad_fn
# Le tenseur après detach partage des données avec y mais est détaché du graphe de calcul
y_detached = y.detach()
print(y_detached.requires_grad) # False
print(y_detached.grad_fn) # None
# ✅ Convertir en numpy (les tenseurs avec gradients ne peuvent pas être convertis directement)
# y.numpy() # ❌ Erreur : RuntimeError
y_detached.numpy() # ✅ Normal
# ✅ Lors de l'enregistrement des valeurs de perte, utilisez detach (pour éviter de retenir le graphe de calcul et de consommer de la mémoire)
losses = []
for i in range(3):
loss = (x ** 2).sum()
losses.append(loss.detach().item()) # .item() convertit un tenseur scalaire en un float Python
loss.backward()
x.grad.zero_()
print(losses) # [14.0, 14.0, 14.0]
retain_graph Conservation du graphe de calcul
Par défaut,backward()après l'exécution, le graphe de calcul seraautomatiquement libéré(pour économiser de la mémoire). Si vous devez rétropropager plusieurs fois sur le même graphe de calcul (par exemple, dans certains entraînements de GAN), vous devez passerretain_graph=True:
Exemple
x = torch.tensor(2.0, requires_grad=True)
y = x ** 3 # y = x³
# Première rétropropagation (conserver le graphe de calcul)
y.backward(retain_graph=True)
print(x.grad) # tensor(12.) ← dy/dx = 3x² = 3×4 = 12
# Deuxième rétropropagation (le graphe de calcul existe toujours)
x.grad.zero_()
y.backward(retain_graph=True)
print(x.grad) # tensor(12.) ← Même résultat
# Pas besoin de conserver lors de la dernière passe
x.grad.zero_()
y.backward() # Après cela, le graphe de calcul est libéré
print(x.grad) # tensor(12.)
# Tenter à nouveau backward lèvera une erreur (le graphe de calcul est déjà libéré)
# y.backward() # ❌ RuntimeError: Tentative de rétropropagation dans le graphe une deuxième fois
Utilisation inutile
retain_graph=Trueentraînera une croissance continue de la mémoire, car le graphe de calcul ne peut pas être libéré. Ne l'utilisez que lorsque plusieurs rétropropagations sont réellement nécessaires.
Application des gradients dans l'entraînement des réseaux de neurones
Voici un exemple complet d'implémentation manuelle de la descente de gradient avec Autograd, démontrant le flux de travail complet d'Autograd dans un entraînement réel :
Exemple
# Construire les données d'entraînement : y = 2x + 1 avec du bruit
torch.manual_seed(42)
X = torch.randn(100, 1)
y_true = 2 * X + 1 + 0.1 * torch.randn(100, 1)
# Initialiser les paramètres du modèle (nécessite le suivi des gradients)
w = torch.zeros(1, requires_grad=True) # Poids
b = torch.zeros(1, requires_grad=True) # Biais
lr = 0.1 # Taux d'apprentissage
epochs = 50 # Nombre d'époques d'entraînement
for epoch in range(epochs):
# 1. Propagation avant : calculer les prédictions
y_pred = X * w + b
# 2. Calculer la perte (erreur quadratique moyenne)
loss = ((y_pred - y_true) ** 2).mean()
# 3. Rétropropagation : calculer automatiquement d(loss)/dw et d(loss)/db
loss.backward()
# 4. Mettre à jour manuellement les paramètres (encapsuler avec no_grad pour éviter que les opérations de mise à jour soient suivies dans le graphe de calcul)
with torch.no_grad():
w -= lr * w.grad
b -= lr * b.grad
# 5. Réinitialiser les gradients (doivent être réinitialisés avant la prochaine rétropropagation)
w.grad.zero_()
b.grad.zero_()
if (epoch + 1) % 10 == 0:
print(f"Époque {epoch+1:3d} | Perte : {loss.item():.4f} | w={w.item():.3f}, b={b.item():.3f}")
print(f"\nEntraînement terminé : w ≈ {w.item():.3f} (valeur réelle 2.0), b ≈ {b.item():.3f} (valeur réelle 1.0))
Le résultat d'exécution du code ci-dessus est similaire à ce qui suit :
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)
Tableau de référence rapide des API courantes
| API | Fonction | Scénarios courants |
|---|---|---|
tensor.requires_grad_(True) |
Activer le suivi des gradients en place | Activer Autograd pour un tenseur existant |
loss.backward() |
Rétropropagation, calculer les gradients de tous les nœuds feuilles | Chaque itération dans la boucle d'entraînement |
tensor.grad |
Accéder à la valeur du gradient d'un tenseur | Afficher ou mettre à jour manuellement les paramètres |
tensor.grad.zero_() |
Effacer les gradients en place | Doivent être effacés avant chaque backward() |
torch.no_grad() |
Gestionnaire de contexte, désactiver le suivi des gradients | Phase d'inférence, mises à jour manuelles des paramètres |
tensor.detach() |
Retourner un nouveau tenseur détaché du graphe de calcul | Convertir en numpy, enregistrer les journaux, geler les gradients |
tensor.item() |
Convertir un tenseur scalaire en nombre Python | Afficher les valeurs de perte, enregistrer les métriques |
loss.backward(retain_graph=True) |
Conserver le graphe de calcul, autoriser plusieurs rétropropagations | Scénarios tels que l'entraînement de GAN nécessitant plusieurs rétropropagations |
Questions courantes et précautions
1. Différence entre les nœuds feuilles et les nœuds non feuilles
Seulsles nœuds feuilles(c'est-à-dire les tenseurs directement créés par l'utilisateur, et non le résultat d'opérations) ont leurs gradients enregistrés dans.grad. Les nœuds non feuilles produits par des opérations intermédiaires ne conservent pas les gradients par défaut (pour économiser de la mémoire). Si vous devez consulter le gradient d'un nœud intermédiaire, appelez.retain_grad():
Exemple
x = torch.tensor(2.0, requires_grad=True) # Nœud feuille
# Nœud intermédiaire (nœud non feuille)
y = x ** 2 # y est un nœud intermédiaire
y.retain_grad() # Déclarer explicitement la conservation du gradient de y
z = y * 3 # z est la sortie finale
z.backward()
print(x.grad) # tensor(12.) ← Nœud feuille, sauvegardé normalement
print(y.grad) # tensor(3.) ← Sauvegardé grâce à retain_grad()
# Nœuds intermédiaires sans retain_grad(), .grad est None
2. Les opérations en place peuvent casser le graphe de calcul
Effectuer des opérations en place sur des tenseurs qui nécessitent le suivi du gradient (p. ex.+=、.add_()) peuvent empêcher Autograd de rétropropager correctement, il faut donc les éviter autant que possible :
Exemple
x = torch.tensor([1.0, 2.0], requires_grad=True)
# Dangereux : opération en place sur un nœud feuille
# x += 1 # Peut lever une erreur : une Variable feuille qui nécessite grad a été utilisée dans une opération en place
# Sûr : utiliser une opération non en place
y = x + 1 # Créer un nouveau tenseur, ne pas modifier x
# Il est sans danger d'effectuer des opérations en place dans un contexte no_grad (p. ex., mises à jour de paramètres)
with torch.no_grad():
x += 0.01 # Utilisé pour les mises à jour manuelles des paramètres, c'est sans danger
3. Seuls les tenseurs à virgule flottante prennent en charge les gradients
Les types entiers (par ex.torch.int64) les tenseurs ne prennent pas en chargerequires_grad=True, seuls les types à virgule flottante (float32、float64、float16) peuvent participer à la différenciation automatique.