Tenseurs et différentiation automatique

Première leçon du cours Deep learning avec PyTorch, en accès libre : environ 15 minutes, 3 exercices et 3 questions de quiz. C'est la leçon de base du catalogue ; dans l'espace étudiant, elle est réécrite pour ton profil.

Objectifs

  • Créer et manipuler des tenseurs PyTorch (forme, type, appareil)
  • Comprendre le graphe de calcul et calculer des gradients avec autograd
  • Vérifier un gradient par différences finies

TenseurAutogradGradient

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 :

  1. .backward() s’appelle sur un scalaire (une perte).
  2. Les gradients s’accumulent dans .grad : sans remise à zéro, un deuxième backward() additionne les nouveaux gradients aux anciens. C’est pour cela que les boucles d’entraînement appellent optimizer.zero_grad().
  3. 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.

  1. CodeDifficulté 2Python dans le navigateur2 indices

    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 de f en x par 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 grad
  2. CodeDifficulté 2Python dans le navigateur2 indices

    Rétropropagation à la main

    Pour un modèle linéaire y_pred = w * x + b et la perte MSE L = mean((y_pred - y)^2), écrivez gradients_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
        pass
  3. TâcheDifficulté 12 indices

    Autograd dans votre environnement

    Dans votre environnement de travail, ouvrez un notebook ou un script Python et :

    1. affichez torch.__version__ et l’appareil disponible ;
    2. reproduisez l’exemple w * x + b de la leçon et affichez w.grad et b.grad ;
    3. appelez backward() une seconde fois en recalculant la perte, sans remise à zéro, et observez w.grad ;
    4. 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

Quiz de fin de leçon

  1. 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
  2. À 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
  3. 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

  1. Construire un réseau de neurones avec nn.Module15 min
  2. La boucle d'entraînement : perte, optimiseur, DataLoader20 min
  3. Généraliser : surapprentissage et régularisation18 min
  4. Réseaux convolutifs et transfer learning20 min

Deep learning avec PyTorch · Avancé · 3 h 58