Tenseur
Dans ce chapitre, nous nous plaçons dans la configuration où tous les tenseurs sont stockés en mémoire RAM. Des informations spécifiques au GPU sont disponibles en fin de page.
Création
torch.tensor
La fonction torch.tensor(..) permet de créer un tenseur à partir d’une liste Python ou d’un tableau NumPy. Les valeurs contenues dans le tenseur sont copiées depuis les données fournies. Le tenseur et l’objet original sont indépendants : modifier les valeurs de l’un n’impacte pas l’autre.
Il faut faire attention au type, des règles implicites de conversion s’appliquent :
Contexte |
Type des données du tenseur |
|---|---|
t = torch.tensor(Liste Python d’entiers) |
int64 pour les entiers |
t = torch.tensor(Liste Python de flottants) |
float32 pour les flottants |
t = torch.tensor(Tableau NumPy) |
dtype du tableau NumPy |
Pour optimiser l’occupation mémoire, on peut utiliser la fonction torch.from_numpy(a) qui lie les données du nouveau tenseur aux données d’origine contenues dans le tableau NumPy. Cette approche évite ainsi une recopie des données mais elle lie les deux objets : modifier l’un modifie l’autre.
Spécifier le type
Lorsque l’on écrit a = np.array([1.0, 2.0]), NumPy crée par défaut un tableau de flottants en float64. Par conséquent, l’appel à torch.tensor(a) produira un tenseur de type torch.float64 ce qui n’est pas optimal. En effet, le type float64 est polluant. Par exemple, il suffit d’un seul tenseur en float64 pour produire par combinaison avec les autres tenseurs des résultats en float64.
Il est donc préférable de spécifier explicitement les types des données des tenseurs afin d’éviter de mauvaises surprises :
t = torch.tensor([1.0, 2.0], dtype = torch.float32) |
En cas de doute, il est possible de vérifier le type sous-jacent du tenseur :
print(t.dtype) |
Fonctions
Voici différentes fonctions permettant d’initialiser un tenseur en PyTorch. Pour chaque fonction, la dimension du tenseur est donnée à partir d’un tuple. Vous remarquerez la forte similarité avec NumPy.
Code |
dtype |
Description |
Remarques |
|---|---|---|---|
torch.zeros((2, 2)) |
float32 |
Initialisation avec des 0 |
tensor( [ [0., 0.], [0., 0.] ] ) |
torch.ones((2, 2)) |
float32 |
Initialisation avec des 1 |
— |
torch.full((2, 2), 7) |
float32 |
Initialisation avec une valeur constante |
— |
torch.empty((2, 2)) |
float32 |
Non initialisé |
Valeurs indéfinies |
torch.rand((2, 2)) |
float32 |
Aléatoire - loi uniforme continue [0, 1[ |
|
torch.randn((2, 2)) |
float32 |
Aléatoire - loi normale N(0,1) |
|
torch.randint(0, 10, (2, 2)) |
int64 |
Entiers aléatoires - loi uniforme discrète |
Borne supérieure exclue |
Il existe aussi des fonctions pour créer des séquences :
torch.arange(0, 10, 2) |
int64 |
Séquence 0, 2, 4, … |
Tenseur 1D |
torch.linspace(0, 1, steps=5) |
float32 |
Séquence régulière, espacement constant |
Tenseur 1D, bornes incluses |
Tenseur scalaire
Un tenseur scalaire désigne un tenseur de dimension 0. Voici différentes syntaxes pour le créer :
torch.tensor(3.14) |
Syntaxe valide |
torch.scalar_tensor(3.14) |
Syntaxe plus explicite |
torch.scalar_tensor(x) |
Accepte les variables |
Un tenseur scalaire (0-D) représente un nombre (un scalaire). Il peut être utilisé pour multiplier (ou additionner) un tenseur par une valeur.
Autograd
Terminologie
Dans le cadre des réseaux de neurones, nous manipulons deux types de tenseurs :
Les tenseurs dont le gradient n’est pas requis, correspondant par exemple aux images en entrée
Les tenseurs créés avec requires_grad=True, comme les poids du réseau et les tenseurs intermédiaires impliqués dans le calcul de la Loss
Considérons un tenseur de poids et un tenseur image. D’un point de vue purement technique, ces deux tenseurs stockent des valeurs numériques. Cette similarité peut prêter à confusion car leurs rôles dans le réseau sont eux très différents :
Les images restent inchangées tout au long de l’apprentissage
À l’inverse, les valeurs des poids évoluent par application de la méthode du gradient
Pour indiquer à PyTorch qu’un tenseur contient des valeurs dont le gradient doit être connu, il est nécessaire de spécifier le paramètre requires_grad=True lors de sa création.
Graphe de calcul
La librairie Autograd interne à PyTorch est capable de construire un graphe de calcul utilisé pour calculer les gradients des tenseurs dont le paramètre requires_grad est actif.
Prenons un exemple :
Voici une manière de coder cette expression étape par étape :
I = torch.tensor([5,7])
W = torch.tensor([3.,11.],requires_grad=True)
a = I[0] * W[0]
b = 8 * W[1]
c = a + b
Loss = c + I[1]
Grâce aux informations suivantes :
print(Loss.grad_fn) # AddBackward0
print(c.grad_fn) # AddBackward0
print(b.grad_fn) # MulBackward0
print(a.grad_fn) # MulBackward0
On peut déduire le graphe de calcul généré par Autograd :
I[0] ────────┐
├─ (*) ─> a = I[0]·W[0] ──┐
W[0] ────────┘ │
├─ (+) ─> c = a + b ──┐
W[1] ────────┐ │ │
├─ (*) ─> b = 8·W[1] ─────┘ ├─ (+) ─> Loss
8 ──────────┘ │
│
I[1] ────────────────────────────────────────────────────────┘
Ainsi, la librairie Autograd conserve pour chaque noeud les informations nécessaires à lé rétropopagation.
Expression complexe
La librairie Autograd est suffisamment puissante pour générer automatiquement un graphe à partir d’une expression complexe. Pour exhiber la structure du graphe sous-jacent, nous mettons en place une fonction récursive traverse() :
import torch
I = torch.tensor([5,7])
W = torch.tensor([3.,11.],requires_grad=True)
Loss = I[0]*W[0] + W[1]*8 + I[1]
def traverse(fn, depth=0):
if fn is None : return
print(" " * depth + type(fn).__name__)
for parent, _ in fn.next_functions:
traverse(parent, depth + 1)
traverse(Loss.grad_fn)
Ainsi, nous pouvons examiner le graphe généré automatiquement par Autograd :
AddBackward0 # Loss = c + 7
AddBackward0 # I[0]·W[0] + 8·W[1]
MulBackward0 # I[0]·W[0]
SelectBackward0 # W[0]
AccumulateGrad # gradient à accumuler dans W.grad
MulBackward0 # 8 * W[1]
SelectBackward0 # W[1]
AccumulateGrad # gradient à accumuler dans W.grad
Gradient
L’appel de Loss.backward() déclenche le mécanisme de rétropropagation. Après cela, il est possible de connaître le gradient de W en lisant l’attribut W.grad :
Loss.backward()
print(W.grad)
>> tensor([5., 8.])
Pour rappel, la fonction Loss s’exprime ainsi :
Ce qui après application des formules de dérivation donne une réponse identique à la rétropopagation :
Récupérer des résultats
Conversion vers Python
Si votre tenseur ne contient qu’une valeur, il est possible d’utiliser la fonction item() pour l’extraire directement vers Python.
x = torch.tensor(3.14)
x.item() # 3.14 (float Python)
Conversion vers Numpy
Pour afficher des résultats, vous aurez peut-être besoin de convertir un tenseur en tableau NumPy. Voici la syntaxe à choisir suivant le contexte :
Action |
Commentaire |
|---|---|
t.detach().numpy() |
Uniquement si accès en lecture seule |
t.detach().numpy().copy() |
Isolation totale |
PyTorch demande d’utiliser la fonction detach() pour retourner un tenseur-vue déconnecté d’Autograd. La fonction numpy() retourne un tableau NumPy correspondant le plus souvent à une vue. Ceci signifie que le tableau Numpy peut rester lié au tenseur d’origine sauf si vous appliquez une copie.
GPU
Transfert vers le GPU
Les syntaxes suivantes permettent de transférer un tenseur vers la mémoire GPU :
tgpu = t.to(« cuda ») |
tgpu = t.cuda() |
Pour éviter d’écrire deux versions du code, une pour CPU et une pour GPU, on utilise cette approche :
import torch
target_device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
t = t.to(target_device)
Ainsi, on transfère le tenseur t vers la mémoire GPU s’il est présent.
Création dans le GPU
S’il n’y a pas de données à transférer, il est possible de créer directement un tenseur sur le GPU. 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…
Récupération depuis le GPU
Pour récupérer des résultats présents sur le GPU, il est nécessaire :
de détacher le tenseur du graphe de calcul en utilisant la fonction detach()
de transférer ses données vers le CPU en appelant la méthode cpu()
On utilise donc généralement les syntaxes suivantes :
Action |
Commentaire |
|---|---|
a = t.detach().cpu().numpy() |
lecture seule |
a = t.detach().cpu().numpy().copy() |
isolation totale |