Travaux pratiques - Introduction à l’apprentissage profond (deep learning)

L’objectif de cette première séance de travaux pratiques est de vous faire implémenter par vous même l’apprentissage de réseaux de neurones simples.

Cettre prise en main sera très formatrice pour utiliser des modèles plus évolués et comprendre le fonctionnement des libaries (comme Keras) où l’apprentissage est automatisé.

Nousa travaillerons avec la base de données image MNIST, constituée d’images de caractères manuscrits (60000 images en apprentissage, 10000 en test).

Voici le code permettant de récupérer les données :

from keras.datasets import mnist
# the data, shuffled and split between train and test sets
(X_train, y_train), (X_test, y_test) = mnist.load_data()
# Reshape each 28x28 image -> 784 dim. vector
X_train = X_train.reshape(60000, 784)
X_test = X_test.reshape(10000, 784)
X_train = X_train.astype('float32')
X_test = X_test.astype('float32')
# Normalization
X_train /= 255
X_test /= 255
print(X_train.shape[0], 'train samples')
print(X_test.shape[0], 'test samples')

Exercice 0 : visualisation de quelques images de la base

Nous commencerons par afficher les 200 premières images de la base d’apprentissage.
  • Écrire un script exo0.py qui récupère les données avec le code précédent.

  • Compléter exo0.py pour permettre l’affichage demandé en utilisant le code suivant :

import matplotlib as mpl
mpl.use('TKAgg')
import matplotlib.pyplot as plt

plt.figure(figsize=(7.195, 3.841), dpi=100)
for i in range(200):
    plt.subplot(10,20,i+1)
    plt.imshow(X_train[i,:].reshape([28,28]), cmap='gray')
    plt.axis('off')
plt.show()

Le script exo0.py doit produire l’affichage ci-dessous :

_images/exo0.png

Question :

Quel est l’espace dans lequel se trouvent les images ? Quelle est sa taille ?

Exercice 1 : Régression Logistique

Modèle de prédiction

Nous commencerons par créer un modèle de classification linéaire populaire, la régression logistique. Ce modèle correspond à un réseau de neurones à une seule couche, qui va projeter le vecteur d’entrée \(\mathbf{x_i}\) pour une image MNIST (taille \(28^2=784\)) avec un vecteur de de paramètres \(\mathbf{w_{c}}\) pour chaque classe (plus un biais \(b_c\)). Pour correspondre à la matrice des données de l’exercice précédent, on considère que chaque exemple \(\mathbf{x_i}\) est un vecteur ligne - taille (1,784).

En regroupant l’ensemble des jeux de paramètres \(\mathbf{w_{c}}\) pour les 10 classes dans une matrice \(\mathbf{W}\) (taille \(784\times 10\)) et les biais dans un vecteur \(\mathbf{b}\), on obtient un vecteur \(\mathbf{\hat{s_i}} =\mathbf{x_i} \mathbf{W} + \mathbf{b}\) de taille (1,10).

Une fonction d’activation de type soft-max sur \(\mathbf{\hat{y_i}} =\) softmax \((\mathbf{s_i})\) permet d’obtenir le vecteur de sortie prédit par le modèle \(\mathbf{\hat{y_i}}\) - de taille (1,10) - qui représente la probabilité a posteriori \(p(\mathbf{\hat{y_i}} | \mathbf{x_i})\) pour chacune des 10 classes :

(1)\[p(\hat{y_{c,i}} | \mathbf{x_i}) ) = \frac{e^{\langle \mathbf{x_i} ; \mathbf{w_{c}}\rangle + b_{c}}}{\sum\limits_{c'=1}^{10} e^{\langle \mathbf{x_i} ; \mathbf{w_{c'}}\rangle + b_{c'}}}\]

Le schéma ci-dessous illustre le modèle de régression logistique avec un réseau de neurones.

_images/LR.png

Quel est le nombre de paramètres du modèle ? Justifier le calcul.

Formulation du problème d’apprentissage

Afin d’entraîner le réseau de neurones, on va comparer, pour chaque exemple d’apprentissage, la sortie prédite \(\mathbf{\hat{y_i}}\) par le réseau (équation (1)) pour l’image \(\mathbf{x_i}\) avec la sortie réelle \(\mathbf{y_i^*}\) issue de la supervision qui correspond à la catégorie de l’image \(\mathbf{x_i}\): on utilisera en encodage de type one-hot pour \(\mathbf{y_i^*}\), i.e. :

(2)\[\begin{split}y_{c,i}^* = \begin{cases} 1 & \text{si c correspond à l'indice de la classe de } \mathbf{x_i} \\ 0 & \text{sinon} \end{cases}\end{split}\]

Pour mesurer l’erreur de prédiction, on utilisera une fonction de coût de type entropie croisée (cross-entropy) entre \(\mathbf{\hat{y_i}}\) et \(\mathbf{y_i^*}\) (l’entropie croisée est liée à la divergence de Kullback-Leiber, qui mesure une dissimilarité entre distributions de probabilités) : \(\mathcal{L}(\mathbf{\hat{y_i}}, \mathbf{y_i^*}) = -\sum\limits_{c=1}^{10} y_{c,i}^* log(\hat{y}_{c,i}) = - log(\hat{y}_{c^*,i})\), où \(c^*\) correspond à l’indice de la classe donnée par la supervision pour l’image \(\mathbf{x_i}\).

La fonction de coût finale consistera à moyenner l’entropie croisée sur l’ensemble de la base d’apprentissage \(\mathcal{D}\) constituée de \(N=60000\) images :

(3)\[\mathcal{L}_{\mathbf{W},\mathbf{b}}(\mathcal{D}) = - \frac{1}{N}\sum_{i=1}^{N} log(\hat{y}_{c^*,i})\]

Question :

La fonction de coût de (3) est-elle convexe par rapport aux paramètres \(\mathbf{W}\), \(\mathbf{b}\) du modèle ? Avec un pas de gradient bien choisi, peut-on assurer la convergence vers le minimum global de la solution ?

Optimisation du modèle

Afin d’optimiser les paramètres \(\mathbf{W}\) et \(\mathbf{b}\) du modèle de régression logistique par descente de gradient, on va utiliser la règle des dérivées chaînées (chain rule) :

\[\frac{\partial \mathcal{L}}{\partial \mathbf{W}} = \frac{1}{N}\sum_{i=1}^{N} \frac{\partial \mathcal{L}}{\partial \mathbf{\hat{y_i}}} \frac{\partial \mathbf{\hat{y_i}}}{\partial \mathbf{s_i}} \frac{\partial \mathbf{s_i}}{\partial \mathbf{W}}\]
\[\frac{\partial \mathcal{L}}{\partial \mathbf{b}} = \frac{1}{N}\sum_{i=1}^{N} \frac{\partial \mathcal{L}}{\partial \mathbf{\hat{y_i}}} \frac{\partial \mathbf{\hat{y_i}}}{\partial \mathbf{s_i}} \frac{\partial \mathbf{s_i}}{\partial \mathbf{b}}\]

Montrer que :

\[\frac{\partial \mathcal{L}}{\partial \mathbf{s_i}} = \mathbf{\delta^y_i} = \frac{\partial \mathcal{L}}{\partial \mathbf{\hat{y_i}}} \frac{\partial \mathbf{\hat{y_i}}}{\partial \mathbf{s_i}} = \mathbf{\hat{y_i}} - \mathbf{y_i^*}\]

En déduire que les gradients de \(\mathcal{L}\) par rapport aux paramètres du modèle s’écrivent :

(4)\[\frac{\partial \mathcal{L}}{\partial \mathbf{W}} = \frac{1}{N} \mathbf{X}^T (\mathbf{\hat{Y}} - \mathbf{Y^*}) = \frac{1}{N} \mathbf{X}^T \mathbf{\Delta^y}\]
(5)\[\frac{\partial \mathcal{L}}{\partial \mathbf{b}} = \frac{1}{N}\sum_{i=1}^{N}(\mathbf{\hat{y_i}} - \mathbf{y_i^*})\]

\(\mathbf{X}\) est la matrice des données (taille \(60000\times 784\)), \(\mathbf{\hat{Y}}\) est la matrice des labels prédits sur l’ensemble de la base d’apprentissage (taille \(60000\times 10\)) et \(\mathbf{Y^*}\) est la matrice des labels issue de la supervision (ground truth, taille \(60000\times 10\)), et \(\mathbf{\Delta^y}=\mathbf{\hat{Y}}-\mathbf{Y^*}\).

Implémentation de l’apprentissage

Les gradients aux équations (8) et (9) s’écrivent sous forme « vectorielle », ce qui rend les calculs efficaces avec des librairies de calculs scientifique comme numpy. Après calcul du gradient, les paramètres seront mis à jour de la manière suivante :

(6)\[\mathbf{W}^{(t+1)} = \mathbf{W}^{(t)} - \eta \frac{\partial \mathcal{L}}{\partial \mathbf{W}}\]
(7)\[\mathbf{b}^{(t+1)} = \mathbf{b}^{(t)} - \eta \frac{\partial \mathcal{L}}{\partial \mathbf{b}}\]

\(\eta\) est le pas de gradient (learning rate).

Pour implémenter l’algorithme d’apprentissage, on utilisera une descente de gradient stochastique, c’est à dire que les gradients aux équations (8) et (9) ne seront pas calculés sur l’ensemble des \(N=60000\) images d’apprentissage, mais sur un sous-ensemble appelé batch. Cette technique permet une mise à jour des paramètres plus fréquente qu’avec une descente de gradient classique, et une convergence plus rapide (au détriment d’un calcul de gradient approximé).

On demande de mettre en place un script exo1.py qui implémente l’algorithme de régression logistique sur la base MNIST.

Après avoir chargé les données (exercice 0), on utilisera le code suivant pour générer des étiquettes (labels) au format 0-1 encoding - (2).

from keras.utils import np_utils
K=10
# convert class vectors to binary class matrices
Y_train = np_utils.to_categorical(y_train, K)
Y_test = np_utils.to_categorical(y_test, K)

On mettra alors en place un code dont le squellette est donné ci-dessous :

import numpy as np
N = X_train.shape[0]
d = X_train.shape[1]
W = np.zeros((d,K))
b = np.zeros((1,K))
numEp = 20 # Number of epochs for gradient descent
eta = 1e-1 # Learning rate
batch_size = 100
nb_batches = int(float(N) / batch_size)
gradW = np.zeros((d,K))
gradb = np.zeros((1,K))

for epoch in range(numEp):
  for ex in range(nb_batches):
     # FORWARD PASS : compute prediction with current params for examples in batch
     # BACKWARD PASS :
     # 1) compute gradients for W and b
     # 2) update W and b parameters with gradient descent

Pour compléter ce code, vous devez :

  • Mettre en place une fonction forward(batch, W, b) qui va calculer la prédiction pour un batch de données. La fonction forward sera appelée pour chaque itération de la double boucle précédente.

  • Si on considère un batch des données de taille \(tb\times 784\), les paramètres \(\mathbf{W}\) (taille \(784\times 10\)) et \(\mathbf{b}\) (taille \(1\times 10\)), la fonction forward renvoie la prédiction \(\mathbf{\hat{Y}}\) sur le batch (taille \(tb\times 10\)).

  • On pourra utiliser la fonction suivante pour calculer la fonction softmax sur chaque élément de de la matrice de la projection linéraire (taille \(tb\times 10\)) :

def softmax(X):
 # Input matrix X of size Nbxd - Output matrix of same size
 E = np.exp(X)
 return (E.T / np.sum(E,axis=1)).T
  • Compléter le code pour la passe backward, consistant à :

  • Calculer les gradients comme indiqué dans les équations (8) et (9).

  • Mettre à jour les paramètres par descente de gradient comme indiqué dans les équations (6) et (7).

Enfin, vous pouvez utiliser la fonction accuracy(W, b, images, labels) fournie pour calculer le taux de bon classement (bonne reconnaissance) du modèle. Ceci permet de mesurer l’évolution des performances au cours des époques de l’algorithme d’apprentissage, et sur la base de test une fois le modèle appris.

Vous devez obtenir un score de l’ordre de 92% sur la base de test pour ce modèle de régression logistique.

def accuracy(W, b, images, labels):
  pred = forward(images, W,b )
  return np.where( pred.argmax(axis=1) != labels.argmax(axis=1) , 0.,1.).mean()*100.0