Fondamentaux de PyTorch : Tenseurs, Autograd et Accélération GPU

  1. Concept et initialisation des tenseurs

En apprentissage profond, les données sont généralement représentées sous forme de tenseurs, c'est-à-dire des tableaux multidimensionnels capables de stocker des vecteurs, des matrices ou des structures de rang supérieur.

Instanciation de tenseurs

PyTorch offre plusieurs méthodes pour construire des tenseurs selon le besoin :

  • torch.randn() : génère des valeurs suivant une distribution normale standard.
  • torch.zeros() / torch.ones() : initialise un tenseur rempli de zéros ou d'uns.
  • torch.tensor() : convertit directement une structure Python (liste, tuple) en tenseur.
  • Méthodes _like : créent un nouveau tenseur en copiant la forme et le dispositif d'un tenseur existant, comme new_ones() ou randn_like().
import torch

# Création aléatoire
A = torch.randn(5, 4)
print(A)

# Initialisation à zéro avec type entier 64 bits
B = torch.zeros(5, 4, dtype=torch.int64)
print(B)

# À partir de données brutes
C = torch.tensor([7.2, 4.0])
print(C)

# Copie de forme et de dispositif avec nouvelle valeur
D = A.new_full((5, 4), 1.0, dtype=torch.float64)
print(D)

# Recodage aléatoire en préservant la géométrie
E = torch.randn_like(D, dtype=torch.float32)
print(E.shape, E.size())
  1. Manipulations et transformations

Opérations arithmétiques

Les calculs élémentaires peuvent s'effectuer via les opérateurs standards, les fonctions dédiées, ou les méthodes in-place qui modifient le tenseur original sans allouer de nouvelle mémoire.

F = torch.randn(5, 4)
print(A + F)               # Opérateur binaire
print(torch.add(A, F))     # Fonction statique
F.add_(A)                  # Modification in-place
print(F)

Indexation et partage mémoire

Extrait une sous-partie d'un tenseur. Attention : les vues partagent la mémoire sous-jacente avec le tenseur source. Toute modification d'une vue impacte l'original. Utilisez .clone() pour isoler les données.

source = torch.randn(4, 3)
colonne_1 = source[:, 1]   # Deuxième colonne
print(colonne_1)

ligne_0 = source[0, :].clone()
ligne_0 += 2.5
print(source[0, :])  # Reste inchangé grâce à clone()

Redimensionnement

La méthode view() retourne une vue avec une nouevlle forme sans copier les données. L'argument -1 indique à PyTorch de déduire automatiquement la taille de cette dimension à partir du nombre total d'éléments.

mat = torch.randn(4, 4)
vect = mat.view(16)
mat_2d = mat.view(-1, 8)
print(mat.shape, vect.shape, mat_2d.shape)

Contrairement à reshape(), view() garantit une vue contiguë en mémoire, mais exige que le tenseur soit d'abord rendu contigu si nécessaire via .contiguous().

Extraction scalaire

Si un tenseur ne contient qu'un seul élément, la méthode .item() convertit sa valeur en un nombre Python natif (int ou float).

scal = torch.randn(1)
print(type(scal.item()))  # <class 'float'>
  1. Diffusion automatique (Broadcasting)

Lors d'opérations élémentaires entre tenseurs de formes incompatibles, PyTorch applique le broadcastign : il étend virtuellement les dimensions de taille 1 pour aligner les formes avant le calcul.

row_vec = torch.arange(1, 3).view(1, 2)
col_vec = torch.arange(1, 4).view(3, 1)
print(row_vec + col_vec)
# Résultat : matrice 3x2 où chaque ligne de row_vec est ajoutée à chaque colonne de col_vec
  1. Différentiation automatique

Le moteur autograd de PyTorch construit dynamiquement un graphe computationnel durant la passe forward. Ce graphe mémorise les opérateurs appliqués aux tenseurs marqués pour le suivi des gradients, permettant une rétropropagation efficace sans dérivées manuelles.

Attributs fondamentaux d'un tenseur

  • data : contenu numérique brut.
  • requires_grad : booléen activant le traçage du graphe.
  • grad : accumulateur du gradient calculé lors de backward().
  • grad_fn : référence à la fonction d'opérateur responsable de la création du tenseur (null pour les nœuds feuilles).
  • is_leaf : indique si le tenseur a été créé par l'utilisateur ou par un opérateur intermédiaire.
  1. Pratique de la rétropropagation

Pour activer le calcul des dérivées, initialisez le tenseur d'entrée avec requires_grad=True. PyTorch déduit automatiquement la propagation de cette propriété aux tenseurs dérivés.

u = torch.ones(2, 3, requires_grad=True)
v = u * 2.0 + 1.0
print(v.requires_grad)  # True
print(v.grad_fn)        # <AddBackward0>

Exécution de backward()

La rétropropagation s'active via .backward() sur un tenseur scalaire représentant la perte ou le critère d'optimisation.

x_in = torch.tensor([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]], requires_grad=True)
y_op = x_in.pow(2)
z_op = torch.mean(y_op)

z_op.backward()
print(x_in.grad)

Note : les tenseurs doivent être de type flottant pour supporter les gradients. Le gradient accumulé dans x_in.grad correspond à la dérivée partielle de z_op par rapport à x_in.

Gestion des sorties non scalaires

Si le tenseur terminal est une matrice ou un vecteur, backward() nécessite un tenseur de poids grad_tensors de même forme pour calculer le produit scalaire pondéré avant la descente.

a = torch.ones(3, requires_grad=True)
b = a + 5.0
# b.backward() lèverait une erreur
w = torch.tensor([1.0, 2.0, 0.5])
b.backward(w)
print(a.grad)  # tensor([1., 2., 0.5])

Conservation du graphe

Par défaut, le graphe computationnel est libéré après backward() pour économiser la RAM. Pour exécuter plusieurs passes ou accéder aux intermédiaires, spécifiez retain_graph=True.

loss1 = torch.sum(x_in)
loss1.backward(retain_graph=True)
loss2 = torch.mean(x_in ** 2)
loss2.backward()  # Opération valide sans erreur de graphe détruit
  1. Déploiement sur GPU et parallélisation

L'accélération matérielle repose sur les cartes graphiques via l'API CUDA. Le transfert de modèles ou de tenseurs vers un GPU s'effectue avec .to('cuda') ou .cuda().

Configuration multi-GPU

Dans un environnement disposant de plusieurs processeurs graphiques, il est impératif de restreindre la visibilité des périphériques pour éviter les conflits mémoire. Cela se fait via la variable d'environnement système :

import os
os.environ["CUDA_VISIBLE_DEVICES"] = "1,2"

Cette instruction expose uniquement les GPU d'indices physiques 1 et 2 au processus Python, qui seront renumérotés virtuellement 0 et 1.

Parallélisme de données

PyTorch implémente le parallélisme de données en dupliquant le modèle sur chaque GPU disponible. Les lots d'entraînement sont fractionnés, traités indépendamment, puis les gradients sont agrégés avant la mise à jour des poids. Cette stratégie préserve l'architecture du réseau tout en exploitant la bande passante mémoire distribuée.

Étiquettes: PyTorch tensor autograd CUDA deep-learning

Publié le 2 septembre à 14h34