TP2: Introduction aux réseaux de neurones et implémentation¶
Dans ce TP on s'interesse à l'implémentation des MLP (Multilayers perceptron) qui sont des réseaux feed forward (feed forward neural network). Nous allons voir comment les utiliser pour approcher des fonctions simples.
1) Approximation de fonction simples¶
a) 1er exemple¶
On va utiliser un MLP pour approcher la fonction sin
du TP1.
On se donner un jeu de donnée $(x_1, y_1),\dots,(x_n,y_n)$, où les $x_1,\dots,x_n$ correspondent à une grille uniforme de $[-\pi,\pi]$ (données d'entrée), et les $y_1,\dots,y_n$ (données de sortie) sont données par
$$ y_j = \sin(2x_j) + \varepsilon_j \qquad j\in\{1,\dots,n\}, $$
où les $\varepsilon_j$ sont des variables aléatoires i.i.d de loi $N(0,\sigma^2)$, et modélisent des erreurs de mesure.
L'idée est de minimiser le coût/risque quadratique
$$ R_n(\theta) = \sum_{j=1}^n (y_j - N_\theta(x_j))^2 $$
en $\theta$ pour determiner les coefficients $\theta$ du réseau. Notre réseau devra être capable de bien représenter les données.
Illustration des données¶
import numpy as np
from math import pi
import matplotlib.pyplot as plt
x = np.linspace(-pi, pi, 50) # données d'entrée
y0 = np.sin(2*x)
noise = 0.1 * np.random.randn(len(x))
y = y0 + noise
plt.plot(x, y, "o", label="data")
plt.plot(x, y0, lw=2, label="ground truth")
plt.xlabel("x", fontsize=18)
plt.legend(fontsize=12)
plt.show()
Mettons les données dans un format approprié
import torch
import torch.nn as nn
# les données sont sous une forme de matrice ligne.
x_data = torch.tensor(x, dtype=torch.float32).reshape(-1,1)
y_data = torch.tensor(y, dtype=torch.float32).reshape(-1,1)
Pour commencer, on va considérer un réseau à une couche cachée de 10 neuronnes de profondeur, avec tanh
comme fonction d'activation.
model = nn.Sequential(
nn.Linear(1,10),
nn.Tanh(),
nn.Linear(10,1)
)
Définnissons le learning rate, le nombre d'epoch pour l'entrainement, la fonction de coût (la loss), et l'algorithme d'optimisation.
learning_rate = 1e-3
n_epoch = 15000
# on cosidère une fonction de coût quadratique
loss_fct = torch.nn.MSELoss(reduction='sum')
# Algorithme d'optimisation: descente de gradient stochastique
optimizer = torch.optim.SGD(model.parameters(), lr=learning_rate)
# vecteur pour dessiner l'évolution de la loss
loss_plot = np.zeros(n_epoch)
# Entrainement du réseau
for t in range(n_epoch):
y_pred = model(x_data)
loss = loss_fct(y_pred, y_data)
loss_plot[t] = loss
if t%1000 == 0:
print("iteration=%s, loss=%s" % (t,loss.item()))
# On met à zéro les gradients avant de les calculer.
optimizer.zero_grad()
# On calcul les gradients
loss.backward()
# On met à jour les paramètres
optimizer.step()
print("iteration=%s, loss=%s" % (t,loss.item()))
iteration=0, loss=45.294952392578125 iteration=1000, loss=3.8436532020568848 iteration=2000, loss=3.091489553451538 iteration=3000, loss=2.5776941776275635 iteration=4000, loss=2.0562374591827393 iteration=5000, loss=1.5320309400558472 iteration=6000, loss=1.1966335773468018 iteration=7000, loss=0.9965047836303711 iteration=8000, loss=0.8490756750106812 iteration=9000, loss=0.7322078347206116 iteration=10000, loss=0.6381136775016785 iteration=11000, loss=0.5618182420730591 iteration=12000, loss=0.4995132088661194 iteration=13000, loss=0.45187807083129883 iteration=14000, loss=0.4318889379501343 iteration=14999, loss=0.42147117853164673
# Illustration de l'adéquation des prédiction du réseau avec les données
# et par rapport à la fonction x-> sin(2x)
plt.plot(x, y, "o", label="data")
plt.plot(x, y_pred.detach(), "o", label="predictions")
plt.plot(x, y0, lw=2, label="ground truth")
plt.xlabel("x", fontsize=18)
plt.legend(fontsize=12, loc='upper right')
plt.show()
# Illustration de l'évolution de la loss
plt.plot(loss_plot[10:], label="loss values")
plt.xlabel("nbre d'epoch", fontsize=18)
plt.show()
x = np.linspace(0, 20*pi, 50) # données d'entrée
y0 = np.sin(2*(x/10-pi))
y = y0 + noise
plt.plot(x, y, "o", label="data")
plt.plot(x, y0, lw=2, label="ground truth")
plt.xlabel("x")
plt.legend()
plt.show()
x_data = torch.tensor(x, dtype=torch.float32).reshape(-1,1)
y_data = torch.tensor(y, dtype=torch.float32).reshape(-1,1)
Reprennons la même configuration de réseau et d'entrainement que précédemment.
model = nn.Sequential(
nn.Linear(1,10),
nn.Tanh(),
nn.Linear(10,1)
)
learning_rate = 1e-3
n_epoch = 15000
loss_fct = torch.nn.MSELoss(reduction='sum')
# On pourra tester Adam à la place de SGD
optimizer = torch.optim.SGD(model.parameters(), lr=learning_rate)
loss_plot = np.zeros(n_epoch)
# Entrainement du réseau
for t in range(n_epoch):
y_pred = model(x_data)
loss = loss_fct(y_pred, y_data)
loss_plot[t] = loss
if t%1000 == 0:
print("iteration=%s, loss=%s" % (t,loss.item()))
# On met à zéro les gradients avant de les calculer.
optimizer.zero_grad()
# On calcul les gradients
loss.backward()
# On met à jour les paramètres
optimizer.step()
print("iteration=%s, loss=%s" % (t,loss.item()))
iteration=0, loss=0.708846926689148 iteration=1000, loss=17.060224533081055 iteration=2000, loss=17.052589416503906 iteration=3000, loss=17.04593849182129 iteration=4000, loss=17.042583465576172 iteration=5000, loss=17.040864944458008 iteration=6000, loss=17.039493560791016 iteration=7000, loss=17.038251876831055 iteration=8000, loss=17.037092208862305 iteration=9000, loss=17.035995483398438 iteration=10000, loss=17.034940719604492 iteration=11000, loss=17.03392791748047 iteration=12000, loss=17.032943725585938 iteration=13000, loss=17.0319881439209 iteration=14000, loss=17.031051635742188 iteration=14999, loss=17.030139923095703
plt.plot(x, y, "o", label="data")
plt.plot(x, y_pred.detach(), "o", label="predictions")
plt.plot(x, y0, lw=2, label="ground truth")
plt.xlabel("x", fontsize=18)
plt.legend(fontsize=12, loc='upper right')
plt.show()
Oups! Ca marche moins bien! Pourquoi?¶
Ici, la différence entre les valeurs de $x_j$ affecte les amplitudes des points des neuronnes et rend plus difficile son entrainement.
On peut voir qu'Adam marche mieux que SGD pour entrainer le réseau. Par contre, ca reste moins facile que quand les données d'entrées ont des valeurs contenues comme précédemment dans $[-\pi,\pi]$.
Il faut donc penser à normaliser ces données.
c) 3ème exemple: On reprend le 1er exemple, mais on considère l'intervalle $[-1,1]$ pour les $x$ et la fonction $x\mapsto \sin(2\pi x)$ sur cette intervalle. On pourra essayer avec un réseau à une couche cachée de 5 neurones.¶
x = np.linspace(-1, 1, 50)
y0 = np.sin(2*pi*x)
y = y0 + noise
x_data = torch.tensor(x, dtype=torch.float32).reshape(-1,1)
y_data = torch.tensor(y, dtype=torch.float32).reshape(-1,1)
learning_rate = 1e-3
n_epoch = 15000
model = nn.Sequential(
nn.Linear(1,5),
nn.Tanh(),
nn.Linear(5,1)
)
loss_fct = torch.nn.MSELoss(reduction='sum')
optimizer = torch.optim.SGD(model.parameters(), lr=learning_rate)
loss_plot = np.zeros(n_epoch)
for t in range(n_epoch):
y_pred = model(x_data)
loss = loss_fct(y_pred, y_data)
loss_plot[t] = loss
if t%1000 == 0:
print("iteration=%s, loss=%s" % (t,loss.item()))
# On met à zéro les gradients avant de les calculer.
optimizer.zero_grad()
# On calcul les gradients
loss.backward()
# On met à jour les paramètres
optimizer.step()
print("iteration=%s, loss=%s" % (t,loss.item()))
iteration=0, loss=53.54950714111328 iteration=1000, loss=21.623794555664062 iteration=2000, loss=21.588838577270508 iteration=3000, loss=21.561233520507812 iteration=4000, loss=21.52503204345703 iteration=5000, loss=21.45871353149414 iteration=6000, loss=21.33314323425293 iteration=7000, loss=21.158235549926758 iteration=8000, loss=20.979969024658203 iteration=9000, loss=20.71510887145996 iteration=10000, loss=19.85545539855957 iteration=11000, loss=7.089118003845215 iteration=12000, loss=2.8711769580841064 iteration=13000, loss=2.5627188682556152 iteration=14000, loss=2.2384681701660156 iteration=14999, loss=1.7420483827590942
plt.plot(x, y, "o", label="data")
plt.plot(x, y_pred.detach(), "o", label="predictions")
plt.plot(x, y0, lw=2, label="ground truth")
plt.xlabel("x", fontsize=18)
plt.legend(fontsize=12)
plt.show()
plt.plot(loss_plot[10:], label="loss values")
plt.xlabel("nbre d'epoch", fontsize=18)
plt.show()
On peut voir que l'entrainement n'a pas été si simple, mais cela reste mieux que sur l'intervalle $[0,20\pi]$, et ce même avec un réseau plus petit.
d) 4ème exemple: Que se passe-t-il si on considère l'intervalle $[-0.1,0.1]$ pour la fonction $x\mapsto \sin(20\pi x)$?¶
x = np.linspace(-0.1,0.1,50)
y0 = np.sin(20*pi*x)
y = y0 + noise
x_data = torch.tensor(x, dtype=torch.float32).reshape(-1,1)
y_data = torch.tensor(y, dtype=torch.float32).reshape(-1,1)
model = nn.Sequential(
nn.Linear(1,10),
nn.Tanh(),
nn.Linear(10,1)
)
learning_rate = 1e-3
n_epoch = 15000
loss_fct = torch.nn.MSELoss(reduction='sum')
optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)
for t in range(n_epoch):
y_pred = model(x_data)
loss = loss_fct(y_pred, y_data)
if t%1000 == 0:
print("iteration=%s, loss=%s" % (t,loss.item()))
# On met à zéro les gradients avant de les calculer.
optimizer.zero_grad()
# On calcul les gradients
loss.backward()
# On met à jour les paramètres
optimizer.step()
print("iteration=%s, loss=%s" % (t,loss.item()))
iteration=0, loss=25.61466407775879 iteration=1000, loss=21.553333282470703 iteration=2000, loss=21.552804946899414 iteration=3000, loss=21.551851272583008 iteration=4000, loss=21.55023765563965 iteration=5000, loss=21.547744750976562 iteration=6000, loss=21.54418182373047 iteration=7000, loss=21.53826904296875 iteration=8000, loss=21.524202346801758 iteration=9000, loss=21.487756729125977 iteration=10000, loss=21.370471954345703 iteration=11000, loss=20.53572654724121 iteration=12000, loss=18.813823699951172 iteration=13000, loss=17.79069709777832 iteration=14000, loss=16.94384765625 iteration=14999, loss=15.827191352844238
plt.plot(x, y, "o", label="data")
plt.plot(x, y_pred.detach(), "o", label="predictions")
plt.plot(x, y0, lw=2, label="ground truth")
plt.xlabel("x", fontsize=18)
plt.legend(fontsize=12)
plt.show()
Encore une fois, ca marche moins bien.
x = np.linspace(-1, 1, 200)
y0 = np.sin(10*pi*x)
noise = 0.1 * np.random.randn(len(x))
y = y0 + noise
x_data = torch.tensor(x, dtype=torch.float32).reshape(-1,1)
y_data = torch.tensor(y, dtype=torch.float32).reshape(-1,1)
learning_rate = 1e-3
n_epoch = 15000
model = nn.Sequential(
nn.Linear(1,16),
nn.Tanh(),
nn.Linear(16,32),
nn.Tanh(),
nn.Linear(32,16),
nn.Tanh(),
nn.Linear(16,1)
)
loss_fct = torch.nn.MSELoss(reduction='sum')
optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)
for t in range(n_epoch):
y_pred = model(x_data)
loss = loss_fct(y_pred, y_data)
if t%1000 == 0:
print("iteration=%s, loss=%s" % (t,loss.item()))
# On met à zéro les gradients avant de les calculer.
optimizer.zero_grad()
# On calcul les gradients
loss.backward()
# On met à jour les paramètres
optimizer.step()
print("iteration=%s, loss=%s" % (t,loss.item()))
iteration=0, loss=105.27815246582031 iteration=1000, loss=101.01466369628906 iteration=2000, loss=99.89808654785156 iteration=3000, loss=97.90980529785156 iteration=4000, loss=74.51273345947266 iteration=5000, loss=43.20464324951172 iteration=6000, loss=19.956443786621094 iteration=7000, loss=2.0992343425750732 iteration=8000, loss=2.263878345489502 iteration=9000, loss=1.901151418685913 iteration=10000, loss=1.8677173852920532 iteration=11000, loss=1.8439821004867554 iteration=12000, loss=1.8264466524124146 iteration=13000, loss=1.8127532005310059 iteration=14000, loss=1.8026986122131348 iteration=14999, loss=1.8797768354415894
plt.plot(x, y, "o", label="data")
plt.plot(x, y_pred.detach(), "o", label="predictions")
plt.plot(x, y0, lw=2, label="ground truth")
plt.xlabel("x", fontsize=18)
plt.legend(fontsize=12, loc='upper right')
plt.show()
En jouant sur la taille du réseau, on voit qu'il est plus ou moins facile de capturer la haute fréquence. En augmentant la taille du réseau on le rend plus expressif, et donc plus à même de capturer la haute fréquence.
x = np.linspace(-1,1,40)
y0 = np.sin(pi*x)
noise = 0.2 * np.random.randn(len(x))
y = y0 + noise
x_data = torch.tensor(x, dtype=torch.float32).reshape(-1,1)
y_data = torch.tensor(y, dtype=torch.float32).reshape(-1,1)
learning_rate = 1e-3
n_epoch = 5000
Remarque: On peut jouer sur le nombre d'epoch pour eviter le sur apprentissage.¶
model1 = nn.Sequential(
nn.Linear(1,8),
nn.Tanh(),
nn.Linear(8,1)
)
model2 = nn.Sequential(
nn.Linear(1,32),
nn.Tanh(),
nn.Linear(32,128),
nn.Tanh(),
nn.Linear(128,256),
nn.Tanh(),
nn.Linear(256,128),
nn.Tanh(),
nn.Linear(128,32),
nn.Tanh(),
nn.Linear(32,1)
)
loss_fct = torch.nn.MSELoss(reduction='sum')
optimizer1 = torch.optim.Adam(model1.parameters(), lr=learning_rate)
optimizer2 = torch.optim.Adam(model2.parameters(), lr=learning_rate)
for t in range(n_epoch):
y_pred1 = model1(x_data)
loss = loss_fct(y_pred1, y_data)
if t%1000 == 0:
print("iteration=%s, loss=%s" % (t,loss.item()))
# On met à zéro les gradients avant de les calculer.
optimizer1.zero_grad()
# On calcul les gradients
loss.backward()
# On met à jour les paramètres
optimizer1.step()
print("iteration=%s, loss=%s" % (t,loss.item()))
iteration=0, loss=28.613441467285156 iteration=1000, loss=7.465003967285156 iteration=2000, loss=3.9898436069488525 iteration=3000, loss=1.684462547302246 iteration=4000, loss=1.3863723278045654 iteration=4999, loss=1.3202569484710693
for t in range(n_epoch):
y_pred2 = model2(x_data)
loss = loss_fct(y_pred2, y_data)
loss_plot[t] = loss
if t%1000 == 0:
print("iteration=%s, loss=%s" % (t,loss.item()))
# On met à zéro les gradients avant de les calculer.
optimizer2.zero_grad()
# On calcul les gradients
loss.backward()
# On met à jour les paramètres
optimizer2.step()
print("iteration=%s, loss=%s" % (t,loss.item()))
iteration=0, loss=19.408687591552734 iteration=1000, loss=0.273794025182724 iteration=2000, loss=0.035442329943180084 iteration=3000, loss=0.00011954765068367124 iteration=4000, loss=0.019675984978675842 iteration=4999, loss=6.75494156894274e-05
plt.plot(x, y, "o", label="data")
plt.plot(x, y_pred1.detach(), label="model 1")
plt.plot(x, y_pred2.detach(), label="model 2")
plt.plot(x, y0, lw=2, label="ground truth")
plt.xlabel("x", fontsize=18)
plt.legend(fontsize=12)
plt.show()
Jouer avec les différent parametres (architecture du réseau, learning rate, algorithme d'optimisation, niveau de bruit dans les données et taille des données) et voir comment cela se comporte.¶
Essayer avec d'autres fonctions que sin
, et aussi à deux variables ($(x,y)\mapsto \sin(2xy)$ par exemple).¶
2) Implémentation du MLP¶
Ecrire un modèle sous la forme suivante n'est pas forcement très pratique pour tester de manière automatique différentes architecture de reseau:
model = nn.Sequential(
nn.Linear(1,8),
nn.Tanh(),
nn.Linear(8,1)
)
Comme dans la correction du TP1, on peut implémenter un réseau à l'aide d'une sous-classe.
class MLP(nn.Module):
def __init__(self):
super(MLP).__init__()
self.layer = nn.Sequential(
nn.Linear(1,8),
nn.Tanh(),
nn.Linear(8,1)
)
def forward(self,x):
return self.layer(x)
Cette écriture n'apporte rien par rapport à la précédente, mais on peut donner des arguments à une classe, comme l'architecture du raiseau et les fonctions d'activation.
class MLP(nn.Module):
def __init__(self, layers, activations):
"""
Parameters:
- layers: List of integer, the number of neurons in each layer (including input and output layers).
- activations: List of the activation functions for each layer.
The length of activations should be len(layers) - 2 (one for each hidden layer).
"""
super(MLP, self).__init__()
if len(activations) != len(layers) - 2:
raise ValueError("The number of activation functions must match the number of hidden layers.")
# Build the network
self.layers = nn.Sequential()
for i in range(len(layers) - 1):
self.layers.add_module(f"linear_{i}", nn.Linear(layers[i], layers[i + 1]))
if i < len(activations): # Apply activation to all but the last layer
self.layers.add_module(f"activation_{i}", activations[i])
def forward(self, x):
return self.layers(x)
Utilisons cette classe sur l'exemple e) précédent avec $x\mapsto \sin(10\pi x)$.
layers = [1, 32, 64, 32, 1]
activations = [nn.Tanh(), nn.Tanh(), nn.Tanh()]
model = MLP(layers, activations)
génération des données¶
x = np.linspace(-1, 1, 200)
y0 = np.sin(10*pi*x)
noise = 0.2 * np.random.randn(len(x))
y = y0 + noise
x_data = torch.tensor(x, dtype=torch.float32).reshape(-1,1)
y_data = torch.tensor(y, dtype=torch.float32).reshape(-1,1)
learning_rate = 1e-3
n_epoch = 15000
loss_fct = torch.nn.MSELoss(reduction='sum')
optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)
for t in range(n_epoch):
y_pred = model(x_data)
loss = loss_fct(y_pred, y_data)
if t%1000 == 0:
print("iteration=%s, loss=%s" % (t,loss.item()))
# On met à zéro les gradients avant de les calculer.
optimizer.zero_grad()
# On calcul les gradients
loss.backward()
# On met à jour les paramètres
optimizer.step()
print("iteration=%s, loss=%s" % (t,loss.item()))
iteration=0, loss=107.62730407714844 iteration=1000, loss=102.43451690673828 iteration=2000, loss=75.59066772460938 iteration=3000, loss=50.018104553222656 iteration=4000, loss=41.405494689941406 iteration=5000, loss=32.99094009399414 iteration=6000, loss=29.72494888305664 iteration=7000, loss=27.3754825592041 iteration=8000, loss=23.03763198852539 iteration=9000, loss=16.297521591186523 iteration=10000, loss=6.425240993499756 iteration=11000, loss=5.968374252319336 iteration=12000, loss=5.7656354904174805 iteration=13000, loss=5.539782524108887 iteration=14000, loss=5.236714839935303 iteration=14999, loss=4.771312236785889
plt.plot(x, y, "o", label="données")
plt.plot(x, y_pred.detach(), label="prédictions")
plt.plot(x, y0, lw=2, label="ground truth")
plt.xlabel("x", fontsize=18)
plt.legend(fontsize=12)
plt.show()