Tenseur + TD1 + TD2
PyTorch permet d’exécuter des opérations complexes sur des tableaux multidimensionnels appelés tenseurs ceci en une seule instruction. Cette syntaxe permet d’éviter l’écriture de boucles ce qui rend la lecture du code pour compacte. Les tenseurs sont l’héritage direct des tableaux multidimensionnels introduits en calcul scientifique notamment par la librairie NumPy. Ainsi, une grande partie de la syntaxe de PyTorch est héritée de celle de NumPy. Comme nous allons travailler 95% du temps avec des tenseurs, nous choisissons de ne plus présenter Numpy et d’introduire directement les tenseurs Pytorch.
Convention
Dans la suite, nous supposerons que nous avons importé la librairie PyTorch ainsi :
import torch
Les tenseurs PyTorch
Les tenseurs PyTorch sont des structures de données implémentées en C++, stockant des valeurs de même type dans un bloc mémoire contigu, ce qui leur permet d’atteindre des performances élevées sur CPU et GPU. Ils constituent la structure de base de PyTorch.
Dans nos applications, nous devons stocker des données (images, sons, textes…). Par exemple, un lot de 50000 images en niveaux de gris de résolution 36x36 pourra être stocké dans un tenseur de dimensions 50000x36x36.
Les tenseurs sont très similaires aux tableaux NumPy, mais ils offrent en plus :
l’accélération GPU
le calcul automatique des gradients (autograd)
l’intégration directe avec les modèles de deep learning
Liens utiles :
Avertissement
Si vous ne précisez pas le type des éléments lors de la création d’un tenseur, PyTorch choisira un type par défaut (généralement torch.float32 ou torch.int64). En deep learning, il est recommandé de toujours préciser explicitement le type pour éviter toute ambiguïté et contrôler la consommation mémoire.
Fonctions de création
torch.zeros(size=(2,3), dtype=torch.int32) |
Crée un tenseur 2x3 de valeurs entières nulles |
torch.ones(size=(5,5), dtype=torch.int32) |
Crée un tenseur 5x5 d’entiers valant 1 |
torch.full(size=(3,3), fill_value=4) |
Crée un tenseur 3x3 initialisé avec la valeur 4 |
torch.arange(4) |
Crée le tenseur [0,1,2,3] |
torch.arange(0, 10, 2) |
Crée le tenseur 1D [0, 2, 4, 6, 8] |
torch.linspace(0, 1, steps=5) |
Séquence régulière : tensor([0.00, 0.25, 0.50, 0.75, 1.00]) |
torch.empty(size=(3,2), dtype=torch.float32) |
Crée un tenseur de flottants sans initialiser leurs valeurs |
T.clone() |
Duplique un tenseur T |
Fonctions de génération aléatoire
torch.randint(low=3, high=8, size=(2,4)) |
Tenseur 2x4 avec valeurs aléatoires parmi 3, 4, 5, 6 et 7 |
torch.rand(size=(3,2)) |
Tenseur 3x2 avec valeurs aléatoires dans [0,1[ suivant une distribution uniforme |
torch.normal(mean=0, std=1, size=(3,2)) |
Tenseur avec valeurs aléatoires suivant une loi normale(0,1) |
Conversion
Pour Tensor ↔ Python :
torch.tensor([1,2,3]) |
liste Python → tenseur |
T.tolist() |
tenseur → liste Python |
T.item() |
tenseur contenant 1 seule valeur → valeur Python |
Pour Tensor ↔ NumPy :
torch.from_numpy(A) |
NumPy → tenseur (partage mémoire) |
T.numpy() |
tenseur CPU → NumPy |
Conversion de type
x = x.to(torch.float32) |
Conversion vers float32 |
x = x.to(torch.int32) |
Conversion vers int32 |
Propriétés du tenseur
Pour un tenseur de taille (2, 4, 5), 3 correspond au nombre de dimensions et 4 à la taille de la dimension 1/l’axe 1.
T.size() ou T.shape |
Taille du tenseur : (2,4,5) |
T.dim() ou T.ndim |
Nombre de dimensions : 3 |
T.size(1) ou T.shape[1] |
Taille de la dimension 1/axe 1 : 4 |
T.numel() |
Nombre de valeurs dans T : 40 |
T.dtype |
Type des données de T |
Tenseur scalaire
T = torch.tensor(3.14) |
Crée un tenseur scalaire |
Un tenseur scalaire est un tenseur de dimension 0 représentant un nombre. Il est utilisé pour effectuer des opérations comme la multiplication ou l’addition d’un tenseur avec une valeur.
Exercice
Quelle propriété retourne le type des données ?
Quelle propriété retourne le nombre de dimensions d’un tenseur ?
Quelle fonction crée un tenseur initialisé avec des 0 ?
Quelle fonction génère des valeurs aléatoires uniformes à l’intérieur d’un intervalle ?
Quelle fonction crée un tenseur PyTorch depuis une liste Python ?
Quelle fonction convertit les éléments d’un tenseur vers un autre type ?
Pour un tenseur de dimensions (3,4,5), l’axe 2 a quelle taille ?
Quel argument permet de sélectionner le type des données ?
Quelle fonction transforme un tenseur en une liste Python ?
Quelle fonction crée un tenseur initialisé avec des 1 ?
Peut-on effectuer reshape((2,3)) sur T = torch.arange(4) ?
Pour un tenseur vecteur de taille 5, T[2] et T[-3] sont-ils équivalents ?
Indexation
Indices
Les tenseurs PyTorch peuvent être indexés :
Vecteur : T[i]
Matrice : T[i,j]
Volume : T[i,j,k]
L’ordre de lecture des éléments d’un tenseur suit le style du langage C : l’indice situé le plus à droite correspond à l’alignement en mémoire. Ainsi, l’élément qui suit T[0,0] est T[0,1], puis T[0,2], et ainsi de suite.
Note
Il est possible d’indexer en partant de la fin en utilisant des valeurs négatives. Ainsi le dernier élément a pour indice -1, l’avant-dernier -2 et ainsi de suite.
On utilise le même ordre pour l’indiçage :
T[axe 0, axe 1, axe 2] :
axe 0 avec 2 indices : 0 et 1
axe 1 avec 4 indices : 0, 1, 2 et 3
axe 2 avec 5 indices : 0, 1, 2, 3, et 4
Sous-tenseur
Si le nombre d’indices est inférieur au nombre de dimensions, alors l’indexation permet d’extraire un sous-tenseur :
T = torch.tensor([[1,2,3],[4,5,6]])
print(T[0]) # tensor([1,2,3])
print(T[1]) # tensor([4,5,6])
print(T[-2]) # tensor([1,2,3])
Avertissement
Rappelez-vous que l’indexation PyTorch s’effectue avec des virgules : T[0,1,2]. Il est possible d’écrire T[0][1][2], mais cela effectue plusieurs extractions successives, ce qui est moins efficace.
Changement de dimensions
Pour changer les dimensions d’un tenseur, les nouvelles dimensions doivent contenir le même nombre d’éléments. Avec T1 = torch.tensor([[1,2,3],[4,5,6]]) :
T = T1.reshape(3,2) |
Renvoie un nouveau tenseur avec les dimensions demandées |
Par conséquent, les éléments d’un tenseur réorganisé conservent le même ordre en mémoire que le tenseur original.
Un mot sur les GPU
Transfert vers le GPU
On peut transférer un tenseur entre la mémoire GPU et la mémoire du CPU :
tgpu = tcpu.to(« cuda ») |
tcpu = tgpu.to(« cpu ») |
Dans les exemples que vous croiserez, vous trouverez cette ligne pour détecter si un gpu est présent et ainsi envoyer les tenseurs vers la mémoire du GPU s’il existe :
import torch
target_device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
t = t.to(target_device)
Création dans le GPU
Il est possible de créer directement un tenseur sur le GPU en utilisant le paramètre device : Voici un exemple :
torch.zeros( (2,2), dtype=torch.float32, device=target_device) |
Cette facilité vaut pour les fonctions : zeros, ones, empty, rand, randn, full, arange, linspace…
Vue
Pour pouvoir travailler efficacement sur de grands tenseurs sans multiplier les copies en mémoire, PyTorch utilise le mécanisme des vues. Ainsi, certaines opérations essayent en priorité de retourner une vue, c’est à dire, un tenseur qui ne possède pas ses propres données mais qui les partage avec le tenseur d’origine. C’est par exemple la stratégie de la fonction reshape.
Lorsqu’une vue est retournée, toute modification de la vue modifie également le tenseur original :
x = torch.arange(4) # tensor([0, 1, 2, 3])
y = x.reshape(2,2) # tensor([[0, 1],[2, 3]])
y[0,0] = 9
print(x) # tensor([9, 1, 2, 3])
print(y) # tensor([[9, 1],[2, 3]])
Il faut connaître ce mécanisme pour éviter des effets inattendus. Si vous souhaitez créer une copie indépendante, vous devez utiliser la fonction clone() :
y = x.clone()
Les vues sont particulièrement importantes en deep learning, car elles permettent d’optimiser l’utilisation de la mémoire, notamment sur GPU.
La fonction reshape peut retourner une vue.
La fonction reshape crée toujours une copie.
Les vues permettent d’optimiser l’utilisation de la mémoire.
Le mécanisme des vues n’existe pas dans PyTorch.
Le paramètre dim
mean
La fonction mean() des tenseurs PyTorch calcule la moyenne des valeurs du tenseur. Cependant, grâce à l’argument dim, on peut effectuer le calcul dans une direction donnée.
Pour un tenseur T de dimension (2,4,3), si on écrit T.mean(dim=2), PyTorch retire cette dimension du résultat en effectuant la moyenne sur cet axe :
T.mean(dim=0) => shape : (4,3)
T.mean(dim=1) => shape : (2,3)
T.mean(dim=2) => shape : (2,4)
Pour un tenseur T de dimension (2,4,3,6,5), si on donne plusieurs dimensions :
T.mean(dim=(1,2)) => shape : (2,6,5)
T.mean(dim=(0,4)) => shape : (4,3,6)
Voici une interprétation graphique :
Le paramètre dim est utilisé par de nombreuses autres fonctions comme sum, max, min, std, var…
Il existe également le paramètre keepdim=True, qui permet de conserver le nombre de dimensions du tenseur original. Ainsi, pour un tenseur T de dimension (2,4,3,6,5), on obtient :
T.mean(dim=(0,4), keepdim=True) => shape = (1,4,3,6,1)
La fonction mean() fonctionne uniquement sur des tenseurs de type flottant.
max
La logique reste la même, cependant la fonction max doublée du paramètre dim retourne un tuple nommé :
values : les valeurs max
indices : les indices des max
Question 1
T = torch.tensor([ [1,2,3,4],
[5,6,7,8] ])
print(T.max(dim=1))
Quel est le résultat obtenu ?
Réponse A : [5, 6, 7, 8]
Réponse B : [4, 8]
Question 2
T = torch.tensor([ [0,1,2],
[3,4,5],
[6,7,8] ])
Que faut-il écrire pour obtenir : [2, 5, 8] ?
Réponse A : T.max(dim=0).values
Réponse B : T.max(dim=1).values
Question 3
T = torch.tensor([ [[11,12],[13,14]], [[21,22],[23,24]], [[31,32],[33,34]] ])
Que faut-il écrire pour obtenir [ [13,14], [23,24], [33,34] ] ?
Réponse A : T.max(dim=0).values
Réponse B : T.max(dim=1).values
Réponse C : T.max(dim=2).values
Que faut-il écrire pour obtenir [33, 34] avec le même tenseur T ?
Réponse A : T.max(dim=(0,1)).values
Réponse B : T.max(dim=(1,2)).values
Réponse C : T.max(dim=(0,2)).values
Plage d’indices - slicing
Il est possible d’utiliser la syntaxe start:stop ou start:stop:step avec les tenseurs PyTorch. Ce mécanisme, appelé slicing, permet d’éviter l’utilisation de boucles et facilite la lecture du code.
T[0:5] |
Indices de 0 à 4 |
T[0:8:2] |
Indices : 0, 2, 4, 6 |
T[:5] |
Jusqu’à l’indice 4 compris |
T[5:] |
De l’indice 5 jusqu’à la fin |
T[:] |
Tous les indices |
Avec des tenseurs 2 dimensions :
A = torch.tensor([
[10,11,12,13],
[20,21,22,23],
[30,31,32,33],
[40,41,42,43]
])
A[1:3,1:3] # sélection des A[i,j] avec 1≤i<3 et 1≤j<3
>> tensor([[21, 22],
[31, 32]])
A[1:3,1:3] = 99 # affectation des indices sélectionnés
>> tensor([[10, 11, 12, 13],
[20, 99, 99, 23],
[30, 99, 99, 33],
[40, 41, 42, 43]])
Ou encore :
A = torch.tensor([
[1,2,3,4],
[6,7,8,9]
])
A[:,:2] # toutes les lignes, tous les indices < 2
>> tensor([[1, 2],
[6, 7]])
Ou encore :
A = torch.tensor([
[10,11,12,13,14,15],
[20,21,22,23,24,25],
[30,31,32,33,34,35],
[40,41,42,43,44,45]
])
A[:, 0:6:2] = 99 # affectation des colonnes 0, 2, 4
>> tensor([[99, 11, 99, 13, 99, 15],
[99, 21, 99, 23, 99, 25],
[99, 31, 99, 33, 99, 35],
[99, 41, 99, 43, 99, 45]])
Ou encore :
A = torch.tensor([
[1,2,3,4],
[6,7,8,9]
])
A[1] # 2ème ligne
A[1,:] # idem
>> tensor([6, 7, 8, 9])
A[0,2:] # 1ère ligne, indices >= 2
>> tensor([3, 4])
A[1,:2] # 2ème ligne, indices < 2
>> tensor([6, 7])
Indexage avancé
Par listes d’index
Cette technique se déclenche lorsque l’indexation se fait à partir de listes (ou tenseurs d’indices). Elle permet de sélectionner les éléments \((x_i,y_i)\) d’un tenseur en transmettant les deux listes \((x_i)\) et \((y_i)\) :
A = torch.tensor([
[10,11,12,13,14,15],
[20,21,22,23,24,25],
[30,31,32,33,34,35],
[40,41,42,43,44,45]
])
A[[0,0,3,3],[0,5,0,5]] # extrait les valeurs aux positions [0,0], [0,5], [3,0] et [3,5]
>> tensor([10, 15, 40, 45])
A[[0,0,3,3],[0,5,0,5]] = 99 # affecte les valeurs aux positions [0,0], [0,5], [3,0] et [3,5]
>> tensor([[99, 11, 12, 13, 14, 99],
[20, 21, 22, 23, 24, 25],
[30, 31, 32, 33, 34, 35],
[99, 41, 42, 43, 44, 99]])
On peut coupler l’indexage avancé avec d’autres :
A = torch.tensor([
[10,11,12,13,14,15],
[20,21,22,23,24,25],
[30,31,32,33,34,35],
[40,41,42,43,44,45]
])
A[[1,2],:] # extrait la 2ème et la 3ème ligne du tenseur
>> tensor([[20, 21, 22, 23, 24, 25],
[30, 31, 32, 33, 34, 35]])
A[:,[1,2]] # extrait la 2ème et la 3ème colonne du tenseur
>> tensor([[11, 12],
[21, 22],
[31, 32],
[41, 42]])
Par booléens
Un tenseur A peut être indexé à l’aide d’un masque correspondant à un tenseur de booléens. Le masque doit être de même dimension que le tenseur A, il sert ainsi à indiquer quels éléments sélectionner (True) et lesquels ignorer (False). La syntaxe A[mask] extrait les éléments sélectionnés vers un tenseur vecteur 1D :
A = torch.arange(12).reshape(3,4)
mask = A > 5
vector = A[mask]
----- A -----
tensor([[ 0, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11]])
----- mask -----
tensor([[False, False, False, False],
[False, False, True, True],
[ True, True, True, True]])
----- vector -----
tensor([ 6, 7, 8, 9, 10, 11])
La syntaxe A[mask] = … applique l’affectation uniquement sur les éléments sélectionnés. On peut par exemple écrire :
A[A>4] = 0
----- A -----
tensor([[0, 1, 2, 3],
[4, 0, 0, 0],
[0, 0, 0, 0]]
Question 1
A = torch.tensor([ [0,1],[2,3],[4,5] ])
Ecrivez sous la forme A[x,y] l’indexation de A retournant la valeur 2 :
Question 2
A = torch.tensor( [ [ [0,1], [2,3], [4,5] ] ] )
Donnez l’indexation de A retournant la valeur 5 :
Question 3
A = torch.tensor([[0,1,2],[4,5,6],[7,8,9]])
print(A[[0,2,0,2],[0,0,2,2]])
Donnez les chiffres affichés séparés par des espaces :
Question 4
Pour un tenseur 1D, si A[-3] désigne la même cellule que A[3], quelle est la taille de ce tenseur ?
Broadcasting
Introduction
Les tenseurs PyTorch permettent des opérations qui seraient interdites dans l’algèbre matricielle classique, comme additionner un vecteur ligne à un vecteur colonne. Ce mécanisme s’appelle le broadcasting.
Si vous ajoutez un tenseur de taille \((4,1)\) avec un tenseur de taille \((1,3)\), tout se comporte comme si chaque tenseur était étendu automatiquement pour obtenir deux tenseurs de taille \((4,3)\). Le résultat est alors un tenseur de taille \((4,3)\).
Exemple :
import torch
A = torch.tensor([[0],[10],[20],[30]])
B = torch.tensor([0,1,2])
C = A + B
print(C)
>> tensor([[ 0, 1, 2],
[10, 11, 12],
[20, 21, 22],
[30, 31, 32]])
Règle du broadcasting
Notons \((a_0, \ldots, a_n)\) et \((b_0, \ldots, b_n)\) les dimensions des deux tenseurs.
Les dimensions sont compatibles si, pour chaque dimension \(i\) :
La taille du tenseur résultat est donnée par :
Taille des tenseurs |
Résultat |
|---|---|
(3,4) et (3,2) |
|
(3,4) et (1,4) |
|
(1,5) et (3,1) |
Cas général
Si les deux tenseurs n’ont pas le même nombre de dimensions, PyTorch ajoute automatiquement des dimensions de taille 1 à gauche du plus petit tenseur.
Exemple :
Le tenseur entouré en bleu est un tenseur de dimensions \((2,2)\), le vecteur colonne sur la gauche a pour dimensions \((3,1,1)\). Après broadcasting, le résultat est un tenseur de taille \((3,2,2)\).
Exemple :
Un tenseur de dimension \((2,2)\) combiné avec un tenseur de dimension \((3,1,1)\) produit un tenseur de dimension :
Exercice
\(a\) |
\(b\) |
\(s\) |
|---|---|---|
\((3,4)\) |
\((1)\) |
\((3,4)\) |
\((3,4)\) |
\((1,1)\) |
|
\((3,1)\) |
\((4)\) |
|
\((3,1)\) |
\((1,4)\) |
|
\((1,1,4)\) |
\((1,3,1)\) |
|
\((1,1,4)\) |
\((3,2,1)\) |
Tenseur |
Dimension |
|---|---|
\([ 1, 2 ]\) |
\((2)\) |
\([ [1, 2] ]\) |
|
\([ [1, 2], [3, 4], [5, 6] ]\) |
|
\([ [[1]] ]\) |
|
\([ [1], [1], [1]]\) |
|
\([ [[1]], [[1]], [[1]] ]\) |
A = torch.tensor([ [1, 2, 3],
[4, 5, 6] ])
B = torch.tensor([ [0],
[1] ])
print(A+B)
Donnez le résultat obtenu :
A : [ [1], [2], [3], [5], [6], [7] ]
B : [ [1, 2, 3], [4, 5, 6], [2, 3, 4], [5, 6, 7] ]
C : [ [1, 2, 3], [5, 6, 7] ]
D : [ [ [1, 2, 3], [4, 5, 6] ], [ [2, 3, 4], [5, 6, 7] ] ]
Réponse :
Avertissement
Le broadcasting est très puissant, car il évite la duplication réelle des données et optimise les performances.
Cependant, il peut masquer certaines erreurs si les dimensions ne sont pas celles attendues. Il est donc essentiel de toujours vérifier la propriété shape des tenseurs.
Pour finir
Quelques fonctions utiles que vous rencontrerez fréquemment avec les tenseurs PyTorch :
T.view(-1) ou T.reshape(-1) : retourne un tenseur 1D contenant tous les éléments de T (aplatissement, vue si possible)
T.flatten() : retourne un tenseur 1D contenant tous les éléments
T.transpose(dim0, dim1) : retourne le tenseur transposé selon deux axes
T.argmin() : retourne l’indice de la plus petite valeur
T.swapdims(dim0, dim1) : échange deux axes du tenseur (sans copie)
T.unsqueeze(dim) : ajoute une dimension de taille 1
T.squeeze() : retire les dimensions de taille 1
torch.cat(…) : concatène plusieurs tenseurs selon un axe existant
torch.stack(…) : empile plusieurs tenseurs en créant un nouvel axe
TD1 PyTorch Tensor
Vous devez effectuer ce TD et le faire valider par votre responsable de salle.
TD2 Slicing et images avec PyTorch
Vous devez effectuer ce TD et le faire valider par votre responsable de salle.