Tensors et Opérations en PyTorch : Maîtriser les Fondations

tl;dr: Tensors = type données PyTorch (arrays NumPy + GPU + autograd). Création : zeros, ones, randn, from_numpy. Opérations : +, *, matmul. Broadcasting = ops entre shapes différentes. Autograd : requires_grad=True, .backward() calcule gradients auto. Essentiel réseaux neurones. Support CUDA (GPU) natif.

Dans l’article précédent, nous avons installé PyTorch et créé nos premiers tensors. Maintenant, nous allons plonger en profondeur dans les tensors et leurs opérations, car ce sont les briques fondamentales de tout ce que vous ferez avec PyTorch. Comprendre les tensors est essentiel pour construire des réseaux de neurones efficaces.

Qu’est-ce qu’un Tensor ?

Un tensor est une structure de données multidimensionnelle qui peut contenir des nombres. C’est la généralisation des scalaires (0D), vecteurs (1D), matrices (2D) à N dimensions.

import torch

# Scalaire (0D tensor)
scalar = torch.tensor(42)
print(f"Scalaire : {scalar}, shape : {scalar.shape}")  # shape : torch.Size([])

# Vecteur (1D tensor)
vector = torch.tensor([1, 2, 3, 4])
print(f"Vecteur : {vector}, shape : {vector.shape}")  # shape : torch.Size([4])

# Matrice (2D tensor)
matrix = torch.tensor([[1, 2], [3, 4], [5, 6]])
print(f"Matrice :\n{matrix}\nshape : {matrix.shape}")  # shape : torch.Size([3, 2])

# Tensor 3D (par exemple : batch d'images)
tensor_3d = torch.randn(32, 3, 224, 224)  # 32 images RGB de 224x224
print(f"Tensor 3D shape : {tensor_3d.shape}")  # torch.Size([32, 3, 224, 224])
💡 Analogie avec NumPy : Si vous connaissez NumPy, les tensors PyTorch sont très similaires aux ndarray. La principale différence : les tensors PyTorch peuvent être sur GPU et supportent l’autograd pour le calcul de gradients automatique.

Visualisation des différentes dimensions de tensors

Création de Tensors

PyTorch offre de nombreuses méthodes pour créer des tensors selon vos besoins.

Méthodes de création directe

import torch

# À partir d'une liste Python
t1 = torch.tensor([1, 2, 3])
print(f"torch.tensor() : {t1}")

# Spécifier le dtype
t2 = torch.tensor([1, 2, 3], dtype=torch.float32)
print(f"Avec dtype float32 : {t2}")

# Tensor 2D
t3 = torch.tensor([[1, 2], [3, 4], [5, 6]])
print(f"Tensor 2D :\n{t3}")

Tensors remplis de valeurs spécifiques

# Tensor de zéros
zeros = torch.zeros(3, 4)  # 3 lignes, 4 colonnes
print(f"Zeros shape {zeros.shape} :\n{zeros}")

# Tensor de uns
ones = torch.ones(2, 3)
print(f"Ones shape {ones.shape} :\n{ones}")

# Tensor rempli d'une valeur spécifique
sevens = torch.full((3, 3), 7)
print(f"Filled with 7 :\n{sevens}")

# Tensor identité
identity = torch.eye(4)  # Matrice identité 4x4
print(f"Identity matrix :\n{identity}")

Tensors aléatoires

# Distribution normale (moyenne=0, std=1)
normal = torch.randn(3, 3)
print(f"Normal distribution :\n{normal}")

# Distribution uniforme [0, 1)
uniform = torch.rand(2, 4)
print(f"Uniform [0, 1) :\n{uniform}")

# Entiers aléatoires
random_ints = torch.randint(0, 10, (3, 3))  # Entre 0 et 10
print(f"Random integers [0, 10) :\n{random_ints}")

Séquences et ranges

# Range (comme Python range)
r1 = torch.arange(0, 10)  # 0 à 9
print(f"arange(0, 10) : {r1}")

# Range avec step
r2 = torch.arange(0, 10, 2)  # 0, 2, 4, 6, 8
print(f"arange(0, 10, 2) : {r2}")

# Linspace (N valeurs équidistantes)
lin = torch.linspace(0, 1, 5)  # 5 valeurs de 0 à 1
print(f"linspace(0, 1, 5) : {lin}")

# Logspace (échelle logarithmique)
log = torch.logspace(0, 3, 4)  # 10^0, 10^1, 10^2, 10^3
print(f"logspace(0, 3, 4) : {log}")

Créer à partir de tensors existants

x = torch.tensor([[1, 2], [3, 4]])

# Même shape, rempli de zéros
zeros_like = torch.zeros_like(x)
print(f"zeros_like :\n{zeros_like}")

# Même shape, rempli de uns
ones_like = torch.ones_like(x)
print(f"ones_like :\n{ones_like}")

# Même shape, valeurs aléatoires
rand_like = torch.rand_like(x, dtype=torch.float32)
print(f"rand_like :\n{rand_like}")

Conversion depuis NumPy

import numpy as np

# NumPy array → PyTorch tensor
np_array = np.array([[1, 2], [3, 4]])
torch_tensor = torch.from_numpy(np_array)
print(f"From NumPy :\n{torch_tensor}")

# PyTorch tensor → NumPy array
back_to_numpy = torch_tensor.numpy()
print(f"Back to NumPy :\n{back_to_numpy}")
⚠️ Warning
Attention : torch.from_numpy() partage la mémoire avec l’array NumPy d’origine. Modifier l’un modifie l’autre ! Utilisez .clone() pour créer une copie indépendante : torch.from_numpy(np_array).clone()

Attributs des tensors

Chaque tensor PyTorch possède plusieurs attributs importants :

x = torch.randn(3, 4, 5)

# Shape (dimensions)
print(f"Shape : {x.shape}")           # torch.Size([3, 4, 5])
print(f"Size : {x.size()}")           # Identique à shape
print(f"Size dim 0 : {x.size(0)}")    # Taille de la dimension 0 : 3

# Nombre total d'éléments
print(f"Nombre d'éléments : {x.numel()}")  # 3 × 4 × 5 = 60

# Nombre de dimensions
print(f"Dimensions : {x.ndim}")       # 3

# Type de données
print(f"Dtype : {x.dtype}")           # torch.float32

# Device (CPU ou GPU)
print(f"Device : {x.device}")         # cpu ou cuda:0

# Gradient activé ?
print(f"Requires grad : {x.requires_grad}")  # False par défaut

Types de données (dtype)

PyTorch supporte plusieurs types de données :

# Entiers
int8 = torch.tensor([1, 2, 3], dtype=torch.int8)      # 8-bit
int16 = torch.tensor([1, 2, 3], dtype=torch.int16)    # 16-bit
int32 = torch.tensor([1, 2, 3], dtype=torch.int32)    # 32-bit (default)
int64 = torch.tensor([1, 2, 3], dtype=torch.int64)    # 64-bit (long)

# Flottants
float16 = torch.randn(3, dtype=torch.float16)  # Half precision
float32 = torch.randn(3, dtype=torch.float32)  # Single precision (default)
float64 = torch.randn(3, dtype=torch.float64)  # Double precision

# Booléens
bool_tensor = torch.tensor([True, False, True], dtype=torch.bool)

# Conversion de type
x = torch.randn(3)
x_int = x.int()        # Convertir en int32
x_long = x.long()      # Convertir en int64
x_float = x.float()    # Convertir en float32
x_double = x.double()  # Convertir en float64
🔎 Tip
Performance : Par défaut, PyTorch utilise float32 pour les calculs. Pour économiser mémoire GPU, vous pouvez utiliser float16 (half precision) avec une perte de précision minime. Pour plus de précision (calculs scientifiques), utilisez float64.

Opérations mathématiques

PyTorch supporte toutes les opérations mathématiques classiques.

Opérations élément par élément

a = torch.tensor([1.0, 2.0, 3.0])
b = torch.tensor([4.0, 5.0, 6.0])

# Addition
c = a + b           # ou torch.add(a, b)
print(f"Addition : {c}")  # [5., 7., 9.]

# Soustraction
d = a - b           # ou torch.sub(a, b)
print(f"Soustraction : {d}")  # [-3., -3., -3.]

# Multiplication
e = a * b           # ou torch.mul(a, b)
print(f"Multiplication : {e}")  # [4., 10., 18.]

# Division
f = a / b           # ou torch.div(a, b)
print(f"Division : {f}")  # [0.25, 0.4, 0.5]

# Puissance
g = a ** 2          # ou torch.pow(a, 2)
print(f"Puissance : {g}")  # [1., 4., 9.]

# Racine carrée
h = torch.sqrt(a)
print(f"Sqrt : {h}")  # [1., 1.414, 1.732]

Opérations matricielles

# Matrices 2D
A = torch.tensor([[1.0, 2.0], [3.0, 4.0]])
B = torch.tensor([[5.0, 6.0], [7.0, 8.0]])

# Multiplication matricielle (dot product)
C = A @ B              # ou torch.matmul(A, B) ou torch.mm(A, B)
print(f"Matmul :\n{C}")
# [[19., 22.],
#  [43., 50.]]

# Transpose
A_T = A.T              # ou A.transpose(0, 1)
print(f"Transpose :\n{A_T}")

# Produit scalaire (dot product pour vecteurs 1D)
v1 = torch.tensor([1.0, 2.0, 3.0])
v2 = torch.tensor([4.0, 5.0, 6.0])
dot = torch.dot(v1, v2)
print(f"Dot product : {dot}")  # 1*4 + 2*5 + 3*6 = 32

# Batch matrix multiplication (3D)
batch1 = torch.randn(10, 3, 4)  # 10 matrices de 3x4
batch2 = torch.randn(10, 4, 5)  # 10 matrices de 4x5
result = torch.bmm(batch1, batch2)  # 10 matrices de 3x5
print(f"Batch matmul shape : {result.shape}")  # torch.Size([10, 3, 5])

Opérations de réduction

x = torch.tensor([[1.0, 2.0, 3.0],
                  [4.0, 5.0, 6.0]])

# Somme de tous les éléments
total = x.sum()
print(f"Somme totale : {total}")  # 21.0

# Somme par dimension
sum_dim0 = x.sum(dim=0)  # Somme sur les lignes → [5., 7., 9.]
sum_dim1 = x.sum(dim=1)  # Somme sur les colonnes → [6., 15.]
print(f"Sum dim=0 : {sum_dim0}")
print(f"Sum dim=1 : {sum_dim1}")

# Moyenne
mean_all = x.mean()
mean_dim0 = x.mean(dim=0)
print(f"Moyenne totale : {mean_all}")  # 3.5
print(f"Moyenne dim=0 : {mean_dim0}")  # [2.5, 3.5, 4.5]

# Maximum et minimum
max_val = x.max()
min_val = x.min()
print(f"Max : {max_val}, Min : {min_val}")  # 6.0, 1.0

# Max/Min avec indices
max_val, max_idx = x.max(dim=1)
print(f"Max par ligne : {max_val}")  # [3., 6.]
print(f"Indices : {max_idx}")        # [2, 2]

# Produit de tous les éléments
prod = x.prod()
print(f"Produit : {prod}")  # 720.0

# Norme (L2)
norm = torch.norm(x)
print(f"Norme L2 : {norm}")  # √(1² + 2² + ... + 6²) = 9.539

Opérations in-place

Les opérations se terminant par _ modifient le tensor en place (économie mémoire) :

x = torch.tensor([1.0, 2.0, 3.0])

# Opération normale (crée un nouveau tensor)
y = x.add(5)
print(f"x après add : {x}")  # [1., 2., 3.] (inchangé)
print(f"y : {y}")             # [6., 7., 8.]

# Opération in-place (modifie x)
x.add_(5)
print(f"x après add_ : {x}")  # [6., 7., 8.] (modifié)

# Autres exemples
x.mul_(2)      # x = x * 2
x.div_(4)      # x = x / 4
x.sqrt_()      # x = sqrt(x)
⚠️ Warning
Attention avec in-place : Les opérations in-place (_) économisent de la mémoire mais peuvent causer des problèmes avec autograd. Évitez-les sur des tensors qui ont requires_grad=True durant la backpropagation !

Broadcasting : Opérations entre shapes différentes

Le broadcasting permet d’effectuer des opérations entre tensors de shapes différentes en étendant automatiquement les dimensions.

Règles de broadcasting

PyTorch suit ces règles (similaires à NumPy) :

  1. Aligner les shapes à droite
  2. Les dimensions de taille 1 sont étendues pour correspondre à l’autre dimension
  3. Si une dimension est manquante, elle est considérée comme 1
# Exemple 1 : Vecteur + Scalaire
a = torch.tensor([1, 2, 3])    # shape: (3,)
b = torch.tensor(10)            # shape: ()
c = a + b                       # Broadcasting: b devient [10, 10, 10]
print(c)  # [11, 12, 13]

# Exemple 2 : Matrice + Vecteur
a = torch.randn(3, 4)           # shape: (3, 4)
b = torch.randn(4)              # shape: (4,)
c = a + b                       # Broadcasting: b devient (1, 4) puis (3, 4)
print(f"Matrice + Vecteur shape : {c.shape}")  # (3, 4)

# Exemple 3 : Broadcasting complexe
a = torch.randn(8, 1, 6, 1)     # shape: (8, 1, 6, 1)
b = torch.randn(7, 1, 5)        # shape: (7, 1, 5)
c = a + b                       # Result shape: (8, 7, 6, 5)
print(f"Broadcasting complexe : {c.shape}")

Exemples pratiques de broadcasting

# Normaliser un batch d'images
batch_images = torch.randn(32, 3, 224, 224)  # 32 images RGB
mean = torch.tensor([0.485, 0.456, 0.406]).view(1, 3, 1, 1)
std = torch.tensor([0.229, 0.224, 0.225]).view(1, 3, 1, 1)

normalized = (batch_images - mean) / std
print(f"Images normalisées shape : {normalized.shape}")  # (32, 3, 224, 224)

# Ajouter un biais à chaque colonne
matrix = torch.randn(100, 10)    # 100 exemples, 10 features
bias = torch.randn(10)            # Un biais par feature
result = matrix + bias            # Broadcasting automatique
print(f"Matrice + biais shape : {result.shape}")  # (100, 10)
💡 Broadcasting = efficacité : Le broadcasting évite de créer des copies de données en mémoire. PyTorch étend virtuellement les dimensions, ce qui est beaucoup plus rapide et économe en mémoire que de dupliquer manuellement les données.

Indexing et slicing

PyTorch utilise une syntaxe d’indexing similaire à NumPy.

Indexing de base

x = torch.arange(0, 24).reshape(4, 6)  # Matrice 4x6
print(f"Matrice :\n{x}")

# Accéder à un élément
print(f"x[0, 0] = {x[0, 0]}")      # Premier élément : 0
print(f"x[2, 3] = {x[2, 3]}")      # Ligne 2, colonne 3 : 15

# Accéder à une ligne entière
print(f"Première ligne : {x[0]}")   # [0, 1, 2, 3, 4, 5]
print(f"Dernière ligne : {x[-1]}")  # [18, 19, 20, 21, 22, 23]

# Accéder à une colonne
print(f"Première colonne : {x[:, 0]}")  # [0, 6, 12, 18]

Slicing avancé

x = torch.arange(0, 24).reshape(4, 6)

# Sous-matrice
sub = x[1:3, 2:5]  # Lignes 1-2, colonnes 2-4
print(f"Sous-matrice :\n{sub}")
# [[ 8,  9, 10],
#  [14, 15, 16]]

# Avec step
every_other = x[::2, ::2]  # Une ligne sur 2, une colonne sur 2
print(f"Every other :\n{every_other}")

# Slicing négatif
last_two_rows = x[-2:, :]  # 2 dernières lignes
print(f"2 dernières lignes :\n{last_two_rows}")

Boolean masking

x = torch.randn(4, 4)
print(f"Matrice originale :\n{x}")

# Créer un masque booléen
mask = x > 0
print(f"Masque (x > 0) :\n{mask}")

# Sélectionner les éléments positifs
positives = x[mask]
print(f"Éléments positifs : {positives}")

# Modifier les éléments selon masque
x[x < 0] = 0  # Mettre tous les négatifs à zéro (ReLU!)
print(f"Après ReLU :\n{x}")

# Masque combiné
mask_combined = (x > 0.5) & (x < 1.0)
selected = x[mask_combined]
print(f"Éléments entre 0.5 et 1.0 : {selected}")

Indexing avancé

x = torch.arange(0, 24).reshape(4, 6)

# Sélectionner des lignes spécifiques
rows = torch.tensor([0, 2, 3])
selected_rows = x[rows]
print(f"Lignes 0, 2, 3 :\n{selected_rows}")

# Indexing multidimensionnel
row_indices = torch.tensor([0, 1, 2])
col_indices = torch.tensor([1, 2, 3])
elements = x[row_indices, col_indices]  # (0,1), (1,2), (2,3)
print(f"Éléments sélectionnés : {elements}")

# where (comme np.where)
condition = x > 10
result = torch.where(condition, x, torch.tensor(0))
print(f"Where (>10) :\n{result}")  # Valeurs >10, sinon 0

Reshaping et manipulation de dimensions

Reshape et view

x = torch.arange(0, 12)
print(f"Original : {x.shape}")  # torch.Size([12])

# Reshape en matrice 3x4
reshaped = x.reshape(3, 4)
print(f"Reshaped 3x4 :\n{reshaped}")

# View (similaire mais avec contraintes mémoire)
viewed = x.view(2, 6)
print(f"Viewed 2x6 :\n{viewed}")

# -1 pour dimension automatique
auto = x.reshape(3, -1)  # 3 lignes, colonnes automatique
print(f"Auto reshape (3, -1) : {auto.shape}")  # (3, 4)
🔎 Tip
view() vs reshape() : view() nécessite que le tensor soit contigü en mémoire et retourne une vue (pas de copie). reshape() fonctionne toujours mais peut faire une copie si nécessaire. Préférez reshape() sauf si vous voulez garantir aucune copie.

Squeeze et unsqueeze

# Unsqueeze : Ajouter une dimension
x = torch.tensor([1, 2, 3])  # shape: (3,)
x_unsqueezed = x.unsqueeze(0)  # shape: (1, 3)
print(f"Unsqueeze dim 0 : {x_unsqueezed.shape}")

x_unsqueezed = x.unsqueeze(1)  # shape: (3, 1)
print(f"Unsqueeze dim 1 : {x_unsqueezed.shape}")

# Squeeze : Retirer les dimensions de taille 1
x = torch.randn(1, 3, 1, 5)  # shape: (1, 3, 1, 5)
squeezed = x.squeeze()  # Retire toutes les dims de taille 1
print(f"Squeezed : {squeezed.shape}")  # (3, 5)

# Squeeze une dimension spécifique
squeezed_0 = x.squeeze(0)  # Retire dim 0
print(f"Squeeze dim 0 : {squeezed_0.shape}")  # (3, 1, 5)

Transpose et permute

# Transpose : Échanger 2 dimensions
x = torch.randn(3, 4, 5)
transposed = x.transpose(0, 1)  # Échanger dims 0 et 1
print(f"Transpose (0, 1) : {transposed.shape}")  # (4, 3, 5)

# T : Transpose pour 2D
matrix = torch.randn(3, 4)
matrix_T = matrix.T  # ou matrix.transpose(0, 1)
print(f"Matrix transpose : {matrix_T.shape}")  # (4, 3)

# Permute : Réorganiser toutes les dimensions
x = torch.randn(2, 3, 4, 5)  # (batch, channels, height, width)
permuted = x.permute(0, 2, 3, 1)  # (batch, height, width, channels)
print(f"Permuted : {permuted.shape}")  # (2, 4, 5, 3)

Flatten et concatenation

# Flatten : Aplatir en 1D
x = torch.randn(3, 4, 5)
flat = x.flatten()
print(f"Flattened : {flat.shape}")  # (60,) = 3*4*5

# Flatten à partir d'une dimension
flat_partial = x.flatten(start_dim=1)
print(f"Flatten from dim 1 : {flat_partial.shape}")  # (3, 20)

# Concatenation
a = torch.randn(2, 3)
b = torch.randn(2, 3)
c_dim0 = torch.cat([a, b], dim=0)  # Concat sur lignes
c_dim1 = torch.cat([a, b], dim=1)  # Concat sur colonnes
print(f"Cat dim=0 : {c_dim0.shape}")  # (4, 3)
print(f"Cat dim=1 : {c_dim1.shape}")  # (2, 6)

# Stack : Ajoute une nouvelle dimension
stacked = torch.stack([a, b], dim=0)
print(f"Stacked : {stacked.shape}")  # (2, 2, 3)

# Split : Diviser un tensor
x = torch.randn(10, 5)
chunks = torch.split(x, 3, dim=0)  # Diviser en chunks de taille 3
print(f"Nombre de chunks : {len(chunks)}")  # 4 chunks (3+3+3+1)

Autograd : Différentiation automatique en profondeur

L’autograd est le cœur de PyTorch. Il calcule automatiquement les gradients pour la backpropagation.

Activer le tracking des gradients

# Créer un tensor avec gradient tracking
x = torch.tensor([2.0, 3.0], requires_grad=True)
print(f"x : {x}")
print(f"requires_grad : {x.requires_grad}")  # True

# Opérations sont trackées
y = x ** 2 + 2 * x + 1
print(f"y : {y}")
print(f"y.requires_grad : {y.requires_grad}")  # True (hérité)

# Calculer les gradients
y.backward(torch.tensor([1.0, 1.0]))  # Gradient de y par rapport à x
print(f"x.grad : {x.grad}")  # dy/dx = 2x + 2 = [6., 8.]

Exemple complet : Régression linéaire manuelle

import torch

# Données : y = 3x + 2 + bruit
x_data = torch.randn(100, 1)
y_data = 3 * x_data + 2 + torch.randn(100, 1) * 0.1

# Paramètres à apprendre
w = torch.randn(1, 1, requires_grad=True)
b = torch.randn(1, requires_grad=True)

# Hyperparamètres
learning_rate = 0.01
epochs = 100

for epoch in range(epochs):
    # Forward pass
    y_pred = x_data @ w + b  # Prédiction

    # Loss (Mean Squared Error)
    loss = ((y_pred - y_data) ** 2).mean()

    # Backward pass
    loss.backward()  # Calcule les gradients

    # Update des paramètres (gradient descent)
    with torch.no_grad():  # Désactiver autograd temporairement
        w -= learning_rate * w.grad
        b -= learning_rate * b.grad

        # Reset gradients
        w.grad.zero_()
        b.grad.zero_()

    if (epoch + 1) % 10 == 0:
        print(f"Epoch {epoch+1}, Loss: {loss.item():.4f}, w: {w.item():.2f}, b: {b.item():.2f}")

print(f"\nParamètres finaux : w = {w.item():.2f}, b = {b.item():.2f}")
print(f"Valeurs attendues : w = 3.00, b = 2.00")

Contrôler autograd

# Désactiver temporairement autograd
x = torch.randn(3, requires_grad=True)

with torch.no_grad():
    y = x * 2  # Cette opération n'est pas trackée
    print(f"y.requires_grad : {y.requires_grad}")  # False

# Detach : Créer une copie sans gradient
x = torch.randn(3, requires_grad=True)
y = x.detach()  # y est une copie sans gradient tracking
print(f"x.requires_grad : {x.requires_grad}")  # True
print(f"y.requires_grad : {y.requires_grad}")  # False

# Activer/désactiver globally (pour inférence)
torch.set_grad_enabled(False)  # Désactiver
# ... inférence ...
torch.set_grad_enabled(True)   # Réactiver
💡 Performance en inférence : Utilisez torch.no_grad() ou @torch.inference_mode() pendant l’inférence (prédiction). Cela désactive autograd, économise mémoire et accélère les calculs de 20-30%.

Graphe de calcul

Autograd construit un graphe de calcul dynamique (DAG - Directed Acyclic Graph) :

x = torch.tensor([2.0], requires_grad=True)
y = x ** 2        # y = x²
z = 2 * y + 3     # z = 2x² + 3

# Le graphe : x → y → z
# Chaque opération est un nœud

z.backward()  # Calcule dz/dx via la règle de la chaîne

print(f"dz/dx = {x.grad}")  # dz/dx = dz/dy * dy/dx = 2 * 2x = 4x = 8.0

Accumuler les gradients

x = torch.tensor([1.0, 2.0, 3.0], requires_grad=True)

# Premier backward
y1 = (x ** 2).sum()
y1.backward()
print(f"Gradient après 1er backward : {x.grad}")  # [2., 4., 6.]

# Deuxième backward (gradients s'accumulent!)
y2 = (x ** 3).sum()
y2.backward()
print(f"Gradient après 2ème backward : {x.grad}")  # [2.+3., 4.+12., 6.+27.]

# Pour éviter l'accumulation, reset les gradients
x.grad.zero_()
print(f"Après zero_() : {x.grad}")  # [0., 0., 0.]
⚠️ Warning
Toujours reset les gradients : Entre chaque itération d’entraînement, appelez optimizer.zero_grad() ou tensor.grad.zero_(). Sinon, les gradients s’accumulent et faussent l’entraînement !

Exercices pratiques

Maintenant à vous de pratiquer ! Essayez ces exercices :

Normalisation min-max

Implémentez la normalisation min-max pour ramener un tensor dans [0, 1] :

def min_max_normalize(x):
    """
    Normalise x dans [0, 1] : (x - min) / (max - min)
    """
    # Votre code ici
    pass

# Test
x = torch.tensor([1.0, 5.0, 3.0, 8.0, 2.0])
normalized = min_max_normalize(x)
print(normalized)  # Devrait être [0.0000, 0.5714, 0.2857, 1.0000, 0.1429]

Softmax from scratch

Implémentez la fonction softmax avec broadcasting :

def softmax(x):
    """
    Calcule softmax : exp(x) / sum(exp(x))
    x: Tensor de shape (batch_size, num_classes)
    Returns: Probabilities (même shape)
    """
    # Votre code ici (attention à la stabilité numérique!)
    pass

# Test
logits = torch.tensor([[1.0, 2.0, 3.0],
                       [1.0, 3.0, 2.0]])
probs = softmax(logits)
print(probs)
print(probs.sum(dim=1))  # Devrait être [1., 1.]

Matrice de covariance

Calculez la matrice de covariance d’un dataset :

def covariance_matrix(X):
    """
    Calcule la matrice de covariance de X
    X: Tensor de shape (n_samples, n_features)
    Returns: Matrice de covariance (n_features, n_features)
    """
    # Votre code ici
    pass

# Test
X = torch.randn(100, 5)  # 100 exemples, 5 features
cov = covariance_matrix(X)
print(f"Covariance shape : {cov.shape}")  # (5, 5)

Conclusion

Dans cet article, vous avez maîtrisé les fondations de PyTorch :

  • ✅ Création de tensors (zeros, ones, randn, from_numpy)
  • ✅ Attributs des tensors (shape, dtype, device)
  • ✅ Opérations mathématiques (élément par élément, matricielles, réductions)
  • ✅ Broadcasting pour opérations entre shapes différentes
  • ✅ Indexing et slicing avancés (masking, where)
  • ✅ Reshaping (reshape, view, squeeze, unsqueeze, transpose)
  • ✅ Autograd et différentiation automatique
  • ✅ Graphe de calcul et accumulation de gradients

Prochaine étape : Dans le prochain article, nous allons utiliser ces connaissances pour construire notre premier réseau de neurones avec nn.Module. Vous apprendrez à empiler des layers, définir des architectures, et comprendre les fonctions d’activation.

Pour aller plus loin :

🔎 Tip
Exercice final : Créez un notebook et reproduisez tous les exemples. Modifiez les dimensions, essayez d’autres opérations, et expérimentez avec autograd sur différentes fonctions mathématiques. La pratique est la clé pour maîtriser les tensors !