PyTorch repose sur deux briques : les tenseurs, des tableaux multidimensionnels proches de numpy, et autograd, un moteur qui calcule automatiquement les dérivées. Tout le deep learning s’appuie sur elles.
Créer et manipuler des tenseurs
import torch
x = torch.tensor([[1.0, 2.0], [3.0, 4.0]]) # à partir d'une liste
z = torch.zeros(3, 4) # forme (3, 4)
r = torch.randn(2, 3) # loi normale centrée réduite
print(x.shape, x.dtype) # torch.Size([2, 2]) torch.float32
y = x @ x.T # produit matriciel
m = x.mean(dim=0) # moyenne par colonne
v = x.reshape(4) # changement de forme (view si possible)
a = torch.from_numpy(np.ones(3)) # partage la mémoire avec numpy (import numpy as np)
Le broadcasting fonctionne comme dans numpy : x + torch.tensor([10.0, 20.0]) ajoute le vecteur à chaque ligne. Par défaut, les flottants sont en float32, le type standard en deep learning.
Choisir l’appareil
if torch.cuda.is_available():
device = "cuda"
elif torch.backends.mps.is_available(): # puces Apple
device = "mps"
else:
device = "cpu"
x = x.to(device)
Avec les versions récentes de PyTorch (ce cours utilise la 2.15), torch.accelerator donne la même chose sans nommer chaque matériel : device = torch.accelerator.current_accelerator() or "cpu".
Deux tenseurs doivent être sur le même appareil pour être combinés, sinon PyTorch lève une erreur explicite.
Autograd : le graphe de calcul
Quand un tenseur a requires_grad=True, PyTorch enregistre chaque opération qui l’utilise. Un appel à .backward() parcourt ce graphe à l’envers (rétropropagation) et calcule les dérivées :
w = torch.tensor(2.0, requires_grad=True)
b = torch.tensor(0.5, requires_grad=True)
x, y_vrai = torch.tensor(3.0), torch.tensor(7.0)
y_pred = w * x + b # 6.5
loss = (y_pred - y_vrai) ** 2 # 0.25
loss.backward() # calcule dloss/dw et dloss/db
print(w.grad) # 2 * (y_pred - y_vrai) * x = -3.0
print(b.grad) # 2 * (y_pred - y_vrai) = -1.0
Trois règles importantes :
.backward()s’appelle sur un scalaire (une perte).- Les gradients s’accumulent dans
.grad: sans remise à zéro, un deuxièmebackward()additionne les nouveaux gradients aux anciens. C’est pour cela que les boucles d’entraînement appellentoptimizer.zero_grad(). - Pour l’évaluation, on désactive le suivi, ce qui économise mémoire et temps :
with torch.no_grad(): # ou torch.inference_mode()
prediction = w * x + b
tensor.detach() renvoie une copie hors du graphe, et .item() extrait la valeur Python d’un tenseur scalaire (utile pour journaliser une perte).
Vérifier un gradient
Un gradient se vérifie par différences finies centrées : (f(x + h) - f(x - h)) / (2h) approche la dérivée pour un petit h. C’est ainsi que l’on teste une implémentation à la main, et c’est l’exercice de cette leçon en numpy.
import numpy as np
def f(x):
return np.sum(x ** 2)
x0, h = np.array([1.0, -2.0]), 1e-5
approx = [(f(x0 + h * e) - f(x0 - h * e)) / (2 * h) for e in np.eye(2)]
print(approx) # proche de 2 * x0 = [2, -4]
À retenir
Un tenseur a une forme, un type et un appareil. Autograd calcule les dérivées d’une perte scalaire par rapport aux paramètres ; on remet les gradients à zéro à chaque itération et on coupe le suivi en inférence.
Exercices
Les solutions et les tests restent sur le serveur. Dans l'espace étudiant, ton code est exécuté et corrigé, et le tuteur donne des indices avant la solution.
Gradient par différences finies
Écrivez
gradient_numerique(f, x, h=1e-5)qui renvoie un tableau numpy contenant l’approximation de chaque dérivée partielle defenxpar différences finies centrées.import numpy as np def gradient_numerique(f, x, h=1e-5): x = np.asarray(x, dtype=float) grad = np.zeros_like(x) # TODO : pour chaque coordonnée i, (f(x + h e_i) - f(x - h e_i)) / (2h) return gradRétropropagation à la main
Pour un modèle linéaire
y_pred = w * x + bet la perte MSEL = mean((y_pred - y)^2), écrivezgradients_mse(w, b, x, y)qui renvoie le tuple(dL/dw, dL/db)calculé analytiquement (c’est ce qu’autograd ferait pour vous).import numpy as np def gradients_mse(w, b, x, y): x, y = np.asarray(x, float), np.asarray(y, float) # TODO passAutograd dans votre environnement
Dans votre environnement de travail, ouvrez un notebook ou un script Python et :
- affichez
torch.__version__et l’appareil disponible ; - reproduisez l’exemple
w * x + bde la leçon et affichezw.gradetb.grad; - appelez
backward()une seconde fois en recalculant la perte, sans remise à zéro, et observezw.grad; - remettez les gradients à zéro avec
w.grad.zero_()et vérifiez.
- La version de PyTorch et l'appareil (cpu/cuda/mps) sont affichés
- w.grad vaut -3.0 et b.grad vaut -1.0 après le premier backward
- Après le second backward sans remise à zéro, w.grad vaut -6.0 (accumulation)
- Après zero_(), w.grad vaut 0
- affichez
Quiz de fin de leçon
Que se passe-t-il si l’on appelle loss.backward() deux fois (en recalculant loss) sans remise à zéro ?
- Les gradients sont remplacés
- Les gradients s'additionnent dans .grad
- PyTorch lève toujours une erreur
- Les poids sont mis à jour automatiquement
À quoi sert le bloc with torch.no_grad(): ?
- À accélérer l'entraînement
- À désactiver l'enregistrement du graphe de calcul, par exemple pour l'évaluation
- À remettre les gradients à zéro
- À passer le modèle sur GPU
Vous additionnez un tenseur sur ‘cuda’ et un tenseur sur ‘cpu’. Que se passe-t-il ?
- PyTorch copie automatiquement sur le GPU
- Une erreur indique que les tenseurs sont sur des appareils différents
- Le résultat est sur le CPU
- Le calcul est fait en float64
La suite du cours
- Construire un réseau de neurones avec nn.Module15 min
- La boucle d'entraînement : perte, optimiseur, DataLoader20 min
- Généraliser : surapprentissage et régularisation18 min
- Réseaux convolutifs et transfer learning20 min