Dans ce TP, vous allez implémenter un réseau de neurones de type Perceptron Multicouche en utilisant la bibliothèque PyTorch.
spyder &
La documentation de la bibliothèque PyTorch est ici. Il est également possible d'accéder à la documentation d'une fonction en tapant help(torch.nom_de_la_fonction)
dans le terminal python de Spyder (exemple : help(torch.matmul)
après avoir importé la biblitohèque PyTorch import torch
).
import torch
Un torch.tensor
est l'équivalent en PyTorch d'un numpy.array
en Numpy : il s'agit d'un tableau multidimensionnel.
#Exemple de création d'un tableau bidimensionnel
x = torch.tensor([[1, 2, 3],[4, 5 ,6], [7, 8 ,9], [10, 11, 12]])
print(x.shape)
La principale différence entre un numpy.array
et un torch.tensor
est le fait que le torch.tensor
permet l'utilisation de la fonctionnalité autograd (option requires_grad=True
).
#Exemple de création d'un tableau bidimensionnel en activant l'autograd
x = torch.tensor([[1., 2., 3.],[4., 5. ,6.], [7., 8. ,9.], [10., 11., 12.]],requires_grad=True)
print(x)
Lorsque cette fonctionnalité est activée pour un torch.tensor
, un graphe de calcul se crée et chaque opération faisant intervenir (directement ou indirectement) est ajoutée à ce graphe de calcul. Tout ce processus est transparent pour l'utilisateur. Ce processus de création d'un graphe de calcul correspond à l'étape de propagation avant vue en cours. Lorsque la méthode .backward()
d'une variable torch.tensor
scalaire du graphe de calcul, l'étape de rétropropagation s'exécute et calcule la dérivée . Le résultat est stockée dans le champs x.grad
.
#Exemple d'utilisation de l'autograd avec des scalaires
# Création des tenseurs scalaires
x = torch.tensor(1., requires_grad=True)
w = torch.tensor(2.)
b = torch.tensor(3.)
print(x.grad) # None
# Construction du graphe de calcul (propagation avant)
z = w*x
y = z+b # y = 2*x + 3
# Calcul du gradient (rétropropagation)
y.backward(torch.tensor(1.))
# Affichage du gradient
print(x.grad) # x.grad = 2
Dessiner le graphe de calcul de l'exemple précédent. Dans ce graphe, vous remarquerez qu'il y a trois feuilles : le tenseur , le tenseur et le tenseur . Pour obtenir et en plus de , il suffit d'utiliser l'option requires_grad=True
sur et .
#Exemple d'utilisation de l'autograd avec des scalaires
# Création des tenseurs scalaires
x = torch.tensor(4., requires_grad=True)
w = torch.tensor(2., requires_grad=True)
b = torch.tensor(3., requires_grad=True)
print(x.grad) # None
print(w.grad) # None
print(b.grad) # None
# Construction du graphe de calcul (propagation avant)
z = w*x
y = z+b
# Calcul des gradients (rétropropagation)
y.backward(torch.tensor(1.))
# Affichage des gradients
print(x.grad) # x.grad = 2.
print(w.grad) # w.grad = 4.
print(b.grad) # b.grad = 1.
Ce mécanisme d'autograd fonctionne de la même manière lorsque les feuilles sont des tableaux multimensionnels plutôt que des scalaires.
#Exemple d'utilisation de l'autograd avec des tenseurs 2D
# Création des tenseurs
X = torch.tensor([[1., 2., 3.],[4., 5. ,6.], [7., 8. ,9.], [10., 11., 12.]],requires_grad=True) #tableau 4x3
W = torch.tensor([[1., 2.],[4., 5.], [7., 8.]],requires_grad=True) #tableau 3x2
b = torch.tensor([4., 5.], requires_grad=True) #vecteur de taille 2
# Construction du graphe de calcul (propagation avant)
z1 = X.matmul(W)
z2 = z1 + b
y = z2.sum()
# Calcul des gradients (rétropropagation)
y.backward(torch.tensor(1.))
# Affichage des gradients
print(X.grad)
print(W.grad)
print(b.grad)
Dessiner (sur papier) le graphe de calcul de l'exemple précédent. Quelle devrait-être les tailles des variables X.grad
, W.grad
et b.grad
? Vérifier leur taille dans le code (attribut .shape
d'un tenseur).
Comme nous venons de la voir, la fonctionnalité autograd est une implémentation du théorème de dérivation d'une fonction composée. Pour s'en convaincre, prenons le cas de la composition de deux fonctions et . Nous allons comparer deux utilisations de l'autograd :
Cas 1) Calculer directement avec l'autograd
Cas 2) Calculer manuellement (variable dy_dZ
) et fournir ce gradient à l'autograd (Z.backward(dy_dZ)
) pour qu'il termine le calcule de .
Les gradients obtenus avec le cas 1 et le cas 2 doivent être parfaitement identiques.
# CAS 1
# Création des tenseurs
X = torch.tensor([[1., 2., 3.],[4., 5. ,6.], [7., 8. ,9.], [10., 11., 12.]],requires_grad=True) #tableau 4x3
W = torch.tensor([[1., 2.],[4., 5.], [7., 8.]],requires_grad=True) #tableau 3x2
# Construction du graphe de calcul (propagation avant)
Z = X.matmul(W)
y = Z.sum()
y.backward(torch.tensor(1.))
print(X.grad)
# CAS 2
# Création des tenseurs
X = torch.tensor([[1., 2., 3.],[4., 5. ,6.], [7., 8. ,9.], [10., 11., 12.]],requires_grad=True) #tableau 4x3
W = torch.tensor([[1., 2.],[4., 5.], [7., 8.]],requires_grad=True) #tableau 3x2
# Construction du graphe de calcul (propagation avant)
Z = X.matmul(W)
y = Z.sum()
dy_dZ = torch.ones(Z.shape) #dérivée de la fonction sum
Z.backward(dy_dZ)
print(X.grad)
Reprendre le code Numpy du TP intitulé TP MLP Numpy et vérifier que le code fonctionne. L'objectif de cette partie est simplement de remplacer les numpy.array
par des torch.tensor
et de remplacer les appels aux fonctions Numpy par des appels aux fonctions équivalentes en PyTorch.
Le code final ne doit plus faire aucun appel à la bibliothèque Numpy (il ne doit donc pas contenir la ligne import numpy
).
Ne pas utiliser la fonctionnalité autograd de PyTorch, ni le paquet torch.nn.
L'objectif de cette partie consiste à remplacer dans le code de la partie précédente l'implémentation manuelle de la rétropropagation par la fonctionnalité autograd. Ainsi la méthode def backward(self,dl_dO, O, X2, X1, X0)
de la classe class MLP
doit être supprimée, et l'appel à cette méthode remplacé par l'appel à la méthode .backward()
de l'autograd comme vu précédemment.
Après avoir fini cette étape, ou si vous êtes bloqués, vous pourrez comparer votre implémentation au code ci-après.
import matplotlib.pyplot as plt
import torch
torch.random.manual_seed(0)
#%% DEFINE AND PLOT DATA
def make_meshgrid(x, y, h=.02):
x_min, x_max = x.min() - 1, x.max() + 1
y_min, y_max = y.min() - 1, y.max() + 1
xx, yy = torch.meshgrid(torch.arange(x_min, x_max, h),torch.arange(y_min, y_max, h))
return xx, yy
style_per_class = ['xb', 'or', 'sg']
X = torch.tensor([[1.2, 2.3, -0.7, 3.2, -1.3],[-3.4, 2.8, 1.2, -0.4, -2.3]]).T
y = torch.tensor([0,0,1,1,2])
C = len(style_per_class)
N = X.shape[0]
xx, yy = make_meshgrid(X[:,0].ravel(), X[:,1].ravel(), h=0.1)
plt.figure(1)
ax = plt.subplot(111)
ax.set_xlim(xx.min(), xx.max())
ax.set_ylim(yy.min(), yy.max())
plt.grid(True)
for i in range(C):
x_c = X[(y==i).ravel(),:]
plt.plot(x_c[:,0],x_c[:,1],style_per_class[i])
plt.pause(0.1)
class MLP:
def __init__(self, H, beta, lr):
self.C = 3
self.D = 2
self.H = H
self.beta= beta
self.lr = lr
#parameters
self.W1 = ((2./self.D)*(2*(torch.rand(size=(self.D,self.H)))-0.5)).requires_grad_()
self.b1 = ((1./torch.sqrt(torch.tensor(self.D)))*(2*(torch.rand(size=(1,self.H)))-0.5)).requires_grad_()
self.W3 = ((2./self.H)*(2*(torch.rand(size=(self.H,self.C)))-0.5)).requires_grad_()
self.b3 = ((1./torch.sqrt(torch.tensor(self.H)))*(2*(torch.rand(size=(1,self.C)))-0.5)).requires_grad_()
#momentum
self.VW1 = torch.zeros((self.D,self.H))
self.Vb1 = torch.zeros((self.H))
self.VW3 = torch.zeros((self.H,self.C))
self.Vb3 = torch.zeros((self.C))
def forward(self,X):
X1 = X.matmul(self.W1) + self.b1 #NxH
X2 = torch.maximum(torch.tensor(0.),X1) #NxH
O = X2.matmul(self.W3) + self.b3 #NxC
return O
def update(self):
with torch.no_grad():
self.VW1 = self.beta*self.VW1 + (1.0-self.beta)*self.W1.grad.data
self.W1 -= self.lr*self.VW1
self.VW3 = self.beta*self.VW3 + (1.0-self.beta)*self.W3.grad.data
self.W3 -= self.lr*self.VW3
self.Vb1 = self.beta*self.Vb1 + (1.0-self.beta)*self.b1.grad.data
self.b1 -= self.lr*self.Vb1
self.Vb3 = self.beta*self.Vb3 + (1.0-self.beta)*self.b3.grad.data
self.b3 -= self.lr*self.Vb3
def zero_gradients(self):
self.W1.grad = None
self.b1.grad = None
self.W3.grad = None
self.b3.grad = None
def logsoftmax(x):
x_shift = x - torch.amax(x, axis=1, keepdims=True)
return x_shift - torch.log(torch.exp(x_shift).sum(axis=1, keepdims=True))
def softmax(x):
e_x = torch.exp(x - torch.amax(x, axis=1, keepdims=True))
return e_x / e_x.sum(axis=1, keepdims=True)
def multinoulliCrossEntropyLoss(O, y):
with torch.no_grad():
N = y.shape[0]
P = softmax(O.type(torch.float32))
log_p = logsoftmax(O.type(torch.float32))
a = log_p[torch.arange(N),y]
l = -a.sum()/N
dl_do = P
dl_do[torch.arange(N),y] -= 1
dl_do = dl_do/N
return (l, dl_do)
def plot_contours(ax, model, xx, yy, **params):
"""Plot the decision boundaries for a classifier.
Parameters
----------
ax: matplotlib axes object
W: weight matrix
b: bias vector
xx: meshgrid ndarray
yy: meshgrid ndarray
params: dictionary of params to pass to contourf, optional
"""
O = model.forward(torch.hstack((torch.atleast_2d(xx.ravel()).T, torch.atleast_2d(yy.ravel()).T)))
pred = torch.argmax(O, axis=1)
Z = pred.reshape(xx.shape)
out = ax.contourf(xx, yy, Z, **params)
return out
#%% HYPERPARAMETERS
H = 30
lr = 1e-2 #learning rate
beta = 0.9 #momentum parameter
n_epoch = 10000 #number of iterations
model = MLP(H,beta, lr)
for i in range(n_epoch):
#Forward Pass
O = model.forward(X)
#Compute Loss
[l, dl_dO] = multinoulliCrossEntropyLoss(O, y)
#Print Loss and Classif Accuracy
pred = torch.argmax(O, axis=1)
acc = (torch.argmax(O, axis=1) == y).type(torch.float32).sum()/N
print('Iter {} | Loss = {} | Training Accuracy = {}%'.format(i,l,acc*100))
#Backward Pass (Compute Gradient)
model.zero_gradients()
O.backward(dl_dO)
#Update Parameters
model.update()
if((i%10)==0):
#Plot decision boundary
ax.cla()
for i in range(C):
x_c = X[(y==i).ravel(),:]
plt.plot(x_c[:,0],x_c[:,1],style_per_class[i])
plot_contours(ax, model, xx, yy, cmap=plt.cm.coolwarm, alpha=0.8)
plt.pause(0.5)
torch.nn
¶En plus de la fonctionnalité autograd, la bibliothèque PyTorch contient de nombreuses implémentations de fonctions paramétriques qui permettent de construire une architecture beaucoup plus rapidement que ce que nous avons fait jusqu'à présent. Ces fonctions se trouvent dans le paquet torch.nn
.
import torch.nn as nn
import torch
linear = nn.Linear(3, 2)
print ('w: ', linear.weight)
print ('b: ', linear.bias)
x = torch.randn(10, 3)
pred = linear(x)
On remarque que cette fonctionnalité "cache" beaucoup de détails d'implémentation. Par exemple concernant la fonction nn.linear
, ses paramètres sont définis implicitement ainsi que la méthode d'initialisation de leurs valeurs.
En modifiant la classe class MLP
(de l'implémentation utilisant l'autograd) en faisant usage du paquet torch.nn
on obtient l'implémentation suivante.
class MLP(nn.Module):
def __init__(self, H, beta, lr):
super(MLP, self).__init__()
self.C = 3
self.D = 2
self.H = H
self.beta= beta
self.lr = lr
self.fc1 = nn.Linear(self.D, self.H)
self.relu = nn.ReLU()
self.fc2 = nn.Linear(self.H, self.C)
#momentum
self.VW1 = torch.zeros((self.H,self.D))
self.Vb1 = torch.zeros((self.H))
self.VW3 = torch.zeros((self.C,self.H))
self.Vb3 = torch.zeros((self.C))
def forward(self,X):
X1 = self.fc1(X) #NxH
X2 = self.relu(X1) #NxH
O = self.fc2(X2) #NxC
return O
def update(self):
with torch.no_grad():
self.VW1 = self.beta*self.VW1 + (1.0-self.beta)*self.fc1.weight.grad.data
self.fc1.weight -= self.lr*self.VW1
self.VW3 = self.beta*self.VW3 + (1.0-self.beta)*self.fc2.weight.grad.data
self.fc2.weight -= self.lr*self.VW3
self.Vb1 = self.beta*self.Vb1 + (1.0-self.beta)*self.fc1.bias.grad.data
self.fc1.bias -= self.lr*self.Vb1
self.Vb3 = self.beta*self.Vb3 + (1.0-self.beta)*self.fc2.bias.grad.data
self.fc2.bias -= self.lr*self.Vb3
def zero_gradients(self):
self.fc1.weight.grad = None
self.fc1.bias.grad = None
self.fc2.weight.grad = None
self.fc2.bias.grad = None
Parmi les fonctions disponibles, torch.nn
contient également les fonctions de coûts les plus communément utilisées. Ainsi la fonction multinoulliCrossEntropyLoss
peut être remplacée par son équivalent PyTorch nn.CrossEntropyLoss
. Ainsi le code complet prend la forme simplifiée suivante.
import matplotlib.pyplot as plt
import torch
import torch.nn as nn
torch.random.manual_seed(0)
#%% DEFINE AND PLOT DATA
def make_meshgrid(x, y, h=.02):
x_min, x_max = x.min() - 1, x.max() + 1
y_min, y_max = y.min() - 1, y.max() + 1
xx, yy = torch.meshgrid(torch.arange(x_min, x_max, h),torch.arange(y_min, y_max, h))
return xx, yy
style_per_class = ['xb', 'or', 'sg']
X = torch.tensor([[1.2, 2.3, -0.7, 3.2, -1.3],[-3.4, 2.8, 1.2, -0.4, -2.3]]).T
y = torch.tensor([0,0,1,1,2])
C = len(style_per_class)
N = X.shape[0]
xx, yy = make_meshgrid(X[:,0].ravel(), X[:,1].ravel(), h=0.1)
plt.figure(1)
ax = plt.subplot(111)
ax.set_xlim(xx.min(), xx.max())
ax.set_ylim(yy.min(), yy.max())
plt.grid(True)
for i in range(C):
x_c = X[(y==i).ravel(),:]
plt.plot(x_c[:,0],x_c[:,1],style_per_class[i])
plt.pause(0.1)
class MLP(nn.Module):
def __init__(self, H, beta, lr):
super(MLP, self).__init__()
self.C = 3
self.D = 2
self.H = H
self.beta= beta
self.lr = lr
self.fc1 = nn.Linear(self.D, self.H)
self.relu = nn.ReLU()
self.fc2 = nn.Linear(self.H, self.C)
#momentum
self.VW1 = torch.zeros((self.H,self.D))
self.Vb1 = torch.zeros((self.H))
self.VW3 = torch.zeros((self.C,self.H))
self.Vb3 = torch.zeros((self.C))
def forward(self,X):
X1 = self.fc1(X) #NxH
X2 = self.relu(X1) #NxH
O = self.fc2(X2) #NxC
return O
def update(self):
with torch.no_grad():
self.VW1 = self.beta*self.VW1 + (1.0-self.beta)*self.fc1.weight.grad.data
self.fc1.weight -= self.lr*self.VW1
self.VW3 = self.beta*self.VW3 + (1.0-self.beta)*self.fc2.weight.grad.data
self.fc2.weight -= self.lr*self.VW3
self.Vb1 = self.beta*self.Vb1 + (1.0-self.beta)*self.fc1.bias.grad.data
self.fc1.bias -= self.lr*self.Vb1
self.Vb3 = self.beta*self.Vb3 + (1.0-self.beta)*self.fc2.bias.grad.data
self.fc2.bias -= self.lr*self.Vb3
def zero_gradients(self):
self.fc1.weight.grad = None
self.fc1.bias.grad = None
self.fc2.weight.grad = None
self.fc2.bias.grad = None
def plot_contours(ax, model, xx, yy, **params):
"""Plot the decision boundaries for a classifier.
Parameters
----------
ax: matplotlib axes object
W: weight matrix
b: bias vector
xx: meshgrid ndarray
yy: meshgrid ndarray
params: dictionary of params to pass to contourf, optional
"""
O = model.forward(torch.hstack((torch.atleast_2d(xx.ravel()).T, torch.atleast_2d(yy.ravel()).T)))
pred = torch.argmax(O, axis=1)
Z = pred.reshape(xx.shape)
out = ax.contourf(xx, yy, Z, **params)
return out
#%% HYPERPARAMETERS
H = 30
lr = 1e-2 #learning rate
beta = 0.9 #momentum parameter
n_epoch = 10000 #number of iterations
model = MLP(H,beta, lr)
criterion = nn.CrossEntropyLoss()
for i in range(n_epoch):
#Forward Pass
O = model.forward(X)
#Compute Loss
l = criterion(O, y)
#Print Loss and Classif Accuracy
pred = torch.argmax(O, axis=1)
acc = (torch.argmax(O, axis=1) == y).type(torch.float32).sum()/N
print('Iter {} | Loss = {} | Training Accuracy = {}%'.format(i,l,acc*100))
#Backward Pass (Compute Gradient)
model.zero_gradients()
l.backward()
#Update Parameters
model.update()
if((i%10)==0):
#Plot decision boundary
ax.cla()
for i in range(C):
x_c = X[(y==i).ravel(),:]
plt.plot(x_c[:,0],x_c[:,1],style_per_class[i])
plot_contours(ax, model, xx, yy, cmap=plt.cm.coolwarm, alpha=0.8)
plt.pause(0.5)
torch.optim
¶Plusieurs algorithmes d'optimisation sont également disponibles dans le paquet torch.optim
.
Lire la page de la documentation concernant ce paquet https://pytorch.org/docs/stable/optim.html?highlight=torch%20optim#module-torch.optim.
Utiliser l'algorithme torch.optim.SGD
pour simplifier le code précédent.
Après avoir fini ce travail, vous pourrez comparer votre code au code suivant. Observer comme le code est beaucoup plus court par rapport au début du TP, mais un certain nombre de choses sont désormais cachées ou implicites (paramètres, initialisation des paramètres, etc.).
import matplotlib.pyplot as plt
import torch
import torch.nn as nn
torch.random.manual_seed(0)
#%% DEFINE AND PLOT DATA
def make_meshgrid(x, y, h=.02):
x_min, x_max = x.min() - 1, x.max() + 1
y_min, y_max = y.min() - 1, y.max() + 1
xx, yy = torch.meshgrid(torch.arange(x_min, x_max, h),torch.arange(y_min, y_max, h))
return xx, yy
style_per_class = ['xb', 'or', 'sg']
X = torch.tensor([[1.2, 2.3, -0.7, 3.2, -1.3],[-3.4, 2.8, 1.2, -0.4, -2.3]]).T
y = torch.tensor([0,0,1,1,2])
C = len(style_per_class)
N = X.shape[0]
xx, yy = make_meshgrid(X[:,0].ravel(), X[:,1].ravel(), h=0.1)
plt.figure(1)
ax = plt.subplot(111)
ax.set_xlim(xx.min(), xx.max())
ax.set_ylim(yy.min(), yy.max())
plt.grid(True)
for i in range(C):
x_c = X[(y==i).ravel(),:]
plt.plot(x_c[:,0],x_c[:,1],style_per_class[i])
plt.pause(0.1)
class MLP(nn.Module):
def __init__(self, H):
super(MLP, self).__init__()
self.C = 3
self.D = 2
self.H = H
self.fc1 = nn.Linear(self.D, self.H)
self.relu = nn.ReLU()
self.fc2 = nn.Linear(self.H, self.C)
def forward(self,X):
X1 = self.fc1(X) #NxH
X2 = self.relu(X1) #NxH
O = self.fc2(X2) #NxC
return O
def plot_contours(ax, model, xx, yy, **params):
"""Plot the decision boundaries for a classifier.
Parameters
----------
ax: matplotlib axes object
W: weight matrix
b: bias vector
xx: meshgrid ndarray
yy: meshgrid ndarray
params: dictionary of params to pass to contourf, optional
"""
O = model.forward(torch.hstack((torch.atleast_2d(xx.ravel()).T, torch.atleast_2d(yy.ravel()).T)))
pred = torch.argmax(O, axis=1)
Z = pred.reshape(xx.shape)
out = ax.contourf(xx, yy, Z, **params)
return out
#%% HYPERPARAMETERS
H = 30
lr = 1e-2 #learning rate
beta = 0.9 #momentum parameter
n_epoch = 10000 #number of iterations
model = MLP(H)
optimizer = torch.optim.SGD(model.parameters(), lr=lr, momentum=beta)
criterion = nn.CrossEntropyLoss()
for i in range(n_epoch):
#Forward Pass
O = model.forward(X)
#Compute Loss
l = criterion(O, y)
#Print Loss and Classif Accuracy
pred = torch.argmax(O, axis=1)
acc = (torch.argmax(O, axis=1) == y).type(torch.float32).sum()/N
print('Iter {} | Loss = {} | Training Accuracy = {}%'.format(i,l,acc*100))
#Backward Pass (Compute Gradient)
optimizer.zero_grad()
l.backward()
#Update Parameters
optimizer.step()
if((i%10)==0):
#Plot decision boundary
ax.cla()
for i in range(C):
x_c = X[(y==i).ravel(),:]
plt.plot(x_c[:,0],x_c[:,1],style_per_class[i])
plot_contours(ax, model, xx, yy, cmap=plt.cm.coolwarm, alpha=0.8)
plt.pause(0.5)