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.

../_images/tbll.png

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.

../_images/array.png

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.

../_images/tbl1.png

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 :

../_images/tbl2.png

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.

../_images/vecligne.png

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\) :

\[a_i = b_i \quad \text{ou} \quad a_i = 1 \quad \text{ou} \quad b_i = 1\]

La taille du tenseur résultat est donnée par :

\[\boxed{s_i = \max(a_i,b_i)}\]

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.

../_images/ext.png

Exemple :

../_images/schema1.png

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 :

\[(3,2,2)\]

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

Le source du Notebook TD1

Vous devez effectuer ce TD et le faire valider par votre responsable de salle.

TD2 Slicing et images avec PyTorch

Le source du Notebook TD2

Vous devez effectuer ce TD et le faire valider par votre responsable de salle.