Pour commencer, nous introduisons les tableaux de dimension $n$ également appelé tenseur (tensor en anglais). Si vous avez travaillé avec NumPy, le package de calcul scientifique le plus utilisé en Python, cette partie vous sera familière. Quel que soit le framework que vous utilisez, sa classe tensor (ndarray dans MXNet, Tensor dans PyTorch et TensorFlow) est similaire à celle de NumPy appelée ndarray avec quelques caractéristiques particulières. Premièrement, le GPU est bien supporté pour accélérer le calcul alors que NumPy ne prend en charge que les calculs effectués par le CPU. Deuxièmement, la classe tensor supporte la différenciation automatique. Ces propriétés rendent la classe tensorielle adaptée à l'apprentissage profond. Tout au long de ce notebook, lorsque tenseurs, nous faisons référence à des instances de la classe tensor, sauf indication contraire.
Pour commencer, nous importons torch. Notez que bien qu'elle soit appelée PyTorch, nous devons importer torch au lieu de pytorch.
import torch
Un tenseur représente un tableau (éventuellement multidimensionnel) de valeurs numériques. Avec un seul axe (une dimension), un tenseur est appelé un vecteur. Avec deux axes, un tenseur est appelé matrice. Avec $k > 2$ axes, nous abandonnons les noms spécialisés et désignons simplement l'objet comme un tenseur d'ordre $k$. PyTorch fournit une variété de fonctions pour créer de nouveaux tenseurs pré-remplis avec des valeurs numériques. Par exemple, en utilisant $arange(n)$, nous pouvons créer un vecteur de valeurs uniformément espacées, commençant à $0$ (inclus) et finissant à $n$ (non inclus). Par défaut, la taille de l'intervalle est de $1$. Sauf indication contraire, les nouveaux tenseurs sont stockés en mémoire principale et prédisposés pour calcul par CPU.
x = torch.arange(12, dtype=torch.float32)
x
x.shape
## le nombre d'éléments du tensor
x.numel()
## alternatives : x.reshape(-1, 4) ou x.reshape(3, -1)
X = x.reshape(3, 4)
X
torch.zeros((2, 3, 4))
torch.ones((2, 3, 4))
torch.randn(3, 4)
torch.tensor([[2, 1, 4, 3], [1, 2, 3, 4], [4, 3, 2, 1]])
x = torch.tensor([1.0, 2, 4, 8])
y = torch.tensor([2, 2, 2, 2])
x + y, x - y, x * y, x / y, x ** y # ** correspond au calcul de puissance
torch.exp(x)
X = torch.arange(12, dtype=torch.float32).reshape((3,4))
Y = torch.tensor([[2.0, 1, 4, 3], [1, 2, 3, 4], [4, 3, 2, 1]])
torch.cat((X, Y), dim=0), torch.cat((X, Y), dim=1)
X == Y
X.sum()
a = torch.arange(3).reshape((3, 1))
b = torch.arange(2).reshape((1, 2))
a, b
a + b
X[-1], X[1:3]
X[1, 2] = 9
X
X[0:2, :] = 12
X
L'exécution d'opérations peut entraîner l'allocation d'une nouvelle mémoire pour accueillir les résultats. Par exemple, si nous écrivons $Y = X + Y$, nous déréférencerons le tenseur vers lequel $Y$ pointait et pointerons $Y$ vers la nouvelle la mémoire nouvellement allouée. Dans l'exemple suivant, nous le démontrons avec la fonction id() de Python qui nous donne l'adresse exacte de l'objet référencé en mémoire. Après avoir exécuté $Y = Y + X$, nous constaterons que id(Y) pointe vers un emplacement différent. Cela est dû au fait que Python évalue d'abord $Y + X$, allouant une nouvelle mémoire pour le résultat, puis fait pointer $Y$ vers ce nouvel emplacement en mémoire.
before = id(Y)
Y = Y + X
id(Y) == before
Heureusement, il est facile d'effectuer des opérations in-place.
Z = torch.zeros_like(Y)
print('id(Z):', id(Z))
Z[:] = X + Y
print('id(Z):', id(Z))
A = X.numpy()
B = torch.from_numpy(A)
type(A), type(B)
a = torch.tensor([3.5])
a, a.item(), float(a), int(a)
## opérations scalaires
x = torch.tensor(3.0)
y = torch.tensor(2.0)
x + y, x * y, x / y, x**y
## vecteur
x = torch.arange(4)
x
x[3], len(x), x.shape
A = torch.arange(20).reshape(5, 4)
A
A.T
B = torch.tensor([[1, 2, 3], [2, 0, 4], [3, 4, 5]])
B
B==B.T
X = torch.arange(24).reshape(2, 3, 4)
X
### propriétés de base
A = torch.arange(20, dtype=torch.float32).reshape(5, 4)
B = A.clone() # Assign a copy of `A` to `B` by allocating new memory
A, A + B
A*B
a = 2
X = torch.arange(24).reshape(2, 3, 4)
a + X, (a * X).shape
x = torch.arange(4, dtype=torch.float32)
x, x.sum()
A.shape, A.sum()
A_sum_axis0 = A.sum(axis=0)
A_sum_axis0, A_sum_axis0.shape
A_sum_axis1 = A.sum(axis=1)
A_sum_axis1, A_sum_axis1.shape
A.sum(axis=[0, 1]) # Same as `A.sum()`
A.mean(), A.sum() / A.numel()
import torch
x = torch.arange(4.0)
x
Avant même de calculer le gradient de $y$ par rapport à $x$ , nous aurons besoin d'un endroit pour le stocker. Il est important de ne pas allouer une nouvelle mémoire à chaque fois que nous prenons une dérivée par rapport à un paramètre, car nous mettrons souvent à jour les mêmes paramètres des milliers ou des millions de fois et nous pourrions rapidement manquer de mémoire. Notez que le gradient d'une fonction à valeur scalaire par rapport à un vecteur $x$ est lui-même à valeur vectorielle et a la même forme que $x$.
x.requires_grad_(True) # même instruction que `x = torch.arange(4.0, requires_grad=True)`
x.grad # La valeur par défaut est un None
Calculons maintenant $y$
y = 2 * torch.dot(x, x)
y
Comme $x$ est un vecteur de longueur 4, un produit scalaire de $x$ par $x$ est effectué, ce qui donne la sortie scalaire que nous attribuons à $y$. Ensuite, nous pouvons calculer automatiquement le gradient de $y$ par rapport à chaque composante de $x$ en appelant la fonction de rétropropagation et en affichant le gradient.
y.backward()
x.grad
Le gradient de la fonction $y = 2 x^\prime x$ par rapport à $x$ devrait être $4x$. Vérifions rapidement que notre gradient souhaité a été calculé correctement.
x.grad == 4 * x
Calculons maintenant une autre fonction de $x$
# On doit remettre le gradient à 0
x.grad.zero_()
y = x.sum()
y.backward()
x.grad
Techniquement, lorsque $y$ n'est pas un scalaire, l'interprétation la plus naturelle de la dérivation d'un vecteur $y$ par rapport à un vecteur $x$ est une matrice. Pour des $y$ et $x$ d'ordre supérieur et de dimension supérieure, le résultat de la dérivation pourrait être un tenseur d'ordre supérieur.
# Invoking `backward` on a non-scalar requires passing in a `gradient` argument
# which specifies the gradient of the differentiated function w.r.t `self`.
# In our case, we simply want to sum the partial derivatives, so passing
# in a gradient of ones is appropriate
x.grad.zero_()
y = x * x
# y.backward(torch.ones(len(x))) equivalent to the below
y.sum().backward()
x.grad