Travaux pratiques - Perceptron multi-couche

L’objectif de cette seconde séance de travaux pratiques est d’étendre le modèle de régression logistique de la semaine précédente afin de mettre en place des modèles de prédictions plus riches. En particulier, on va s’intéresser aux Perceptron multi-couches (« Multi-Layer Perceptron », MLP). Contrairement à la régression logistique qui se limite à des séparateurs linéaires, le Perceptron permet l’apprentissage de frontières de décisions non linéaires, et constituent des approximateurs universels de fonctions.

L’objectif de la séance de travaux pratiques est de mettre en place le code pour effectuer des prédictions et entraîner un Perceptron à une couche cachée.

Commençons par recharger notre jeu de 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 1 : Prédiction avec un Perceptron (Forward)

L’architecture du perpcetron à une couche cachée est montrée dans la figure ci-dessous.

Si on considère les données de la base MNIST, chaque image est représentée par un vecteur de taille \(28^2=784\). Le perpcetron va effectuer les différentes étapes de transformation pour produire la prédiction finale, i.e. la catégorie sémantique de l’image :

  • Une étape de projection linéaire, qui va projeter chaque image sur un vecteur de taille \((1,L)\), e.g. \(L=100\). En considérant que chaque exemple \(\mathbf{x_i}\)est un vecteur ligne - taille \((1,784)\)- la projection linéaire peut être représentée par la matrice \(\mathbf{W^h}\) (taille \((784, L)\)), et le vecteur de biais \(\mathbf{b^h}\) (taille \((1, L)\)) : \(\mathbf{\hat{u_i}} =\mathbf{x_i} \mathbf{W^h} + \mathbf{b^h}\).
  • Une étape d’application de non linéarité, e.g. de type sigmoïde : \(\forall j \in \left\lbrace 1; L \right\rbrace ~ h_{i,j} = \frac{1}{1+exp(-u_{i,j})}\).
  • Une seconde étape de projection linéaire, qui va projeter le vecteur latent de taille \((1,L)\)sur un vecteur de taille \((1,K)=10\) (nombre de classes). Cette opération de projection linéaire sera représentée par la matrice \(\mathbf{W^y}\) (taille \((L, K)\)), et le vecteur de biais \(\mathbf{b^y}\) (taille \((1, K)\)) : \(\mathbf{\hat{v_i}} =\mathbf{h_i} \mathbf{W^y} + \mathbf{b^y}\).

  • Une étape d’application de non linéarité de type soft-max vue la semaine précédente pour la régression logistique : \(\forall j \in \left\lbrace 1; K \right\rbrace ~ y_{i,j} = \frac{exp(v_{i,j})}{\sum\limits_{i=1}^K exp(v_{i,k})}\)

On demande de mettre en place un l’algorithme du Percpetron sur la base MNIST.

On commence par transformer les labels en vecteur encodé au format “one-hot”:

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)

L’algorithme reprendra le squelette du code de la régression logistique. Pour adapter ce code pour le Perceptron pour la partie prédiction, vous devez :

  • Modifier la fonction de prédiction, afin disposer la fonction forward(batch, Wh, bh, Wy, by) qui reverra à la fois la prédiction pour le batch ainsi que la matrice des couches latentes.

  • Si on considère un batch des données de taille \(t_b\times 784\), les paramètres \(\mathbf{W^h}\) (taille \(784\times L\)), \(\mathbf{b^h}\) (taille \(1\times L\)), \(\mathbf{W^y}\)(taille \(L\times K\)) et \(\mathbf{b^y}\) (taille \(1\times K\)), la fonction forward renvoie la prédiction \(\mathbf{\hat{Y}}\) sur le batch (taille \(t_b\times K\)) et la matrice de variables latentes (taille \(t_b\times L\)).

def forward(batch,Wh,bh,Wy,by):
    #À compléter

    return Ypred, H

Exercice 2 : Apprentissage du Perceptron (Backward)

Afin d’entraîner le Perceptron, on va utiliser l’algorithme de rétro-propagation de l’erreur. On rappelle que pour chaque batch d’exemples, l’algorithme va effectuer une passe forward (Exercice 1), permettant de calculer la prédiction du réseau.

Une fonction de coût (ici l’entropie croisée) entre la sortie prédite et la la sortie donnée par la supervision va permettre de calculer le gradient de l’erreur par rapport à tous les paramètres du modèle, i.e. \(\mathbf{W^y}\) (taille \((L, K)\)), \(\mathbf{b^y}\) (taille \((1, K)\)), \(\mathbf{W^h}\) (taille \((784, L)\)), et \(\mathbf{b^h}\) (taille \((1, L)\)).

On rappelle les équations des gradients, effectuées depuis la sortie vers l’entrée du réseau :

  1. Mise à jour de \(\mathbf{W^y}\) et \(\mathbf{b^y}\):

\[\frac{\partial \mathcal{L}}{\partial \mathbf{v_i}} = \mathbf{\delta^y_i} = \mathbf{\hat{y_i}} - \mathbf{y_i^*}\]
\[\frac{\partial \mathcal{L}}{\partial \mathbf{W^y}} = \frac{1}{t_b} \mathbf{H}^T (\mathbf{\hat{Y}} - \mathbf{Y^*}) = \frac{1}{t_b} \mathbf{H}^T \mathbf{\Delta^y} \tag{1}\]
\[\frac{\partial \mathcal{L}}{\partial \mathbf{b^y}} = \frac{1}{t_b}\sum_{i=1}^{t_b}(\mathbf{\hat{y_i}} - \mathbf{y_i^*}) \tag{2}\]

\(\mathbf{H}\)est la matrice des couches cachées sur le batch (taille \(t_b\times L\)), \(\mathbf{\hat{Y}}\)est la matrice des labels prédits sur l’ensemble de la base d’apprentissage (taille \(t_b\times K\)) et \(\mathbf{Y^*}\)est la matrice des labels donnée issue de la supervision (« ground truth », taille \(t_b\times K\)), et \(\mathbf{\Delta^y}=\mathbf{\hat{Y}}-\mathbf{Y^*}\).

  1. Mise à jour de \(\mathbf{W^h}\)et \(\mathbf{b^h}\):

Montrer que :

\[\frac{\partial \mathcal{L}}{\partial \mathbf{u_i}} = \mathbf{\delta^h_i} = \mathbf{\delta^y_i} \mathbf{W^{y}}^T \odot \sigma^{'}(\mathbf{u_i}) = \mathbf{\delta^y_i} \mathbf{W^{y}}^T \odot (\mathbf{h_i} \odot (1-\mathbf{h_i}))\]

En déduire que les gradients de :math:`mathcal{L}`par rapport à :math:`mathbf{W^h}`et :math:`mathbf{b^h}`s’écrivent matriciellement :

\[\frac{\partial \mathcal{L}}{\partial \mathbf{W^h}} = \frac{1}{t_b} \mathbf{X}^T \mathbf{\Delta^h}\]
\[\frac{\partial \mathcal{L}}{\partial \mathbf{b^h}} = \frac{1}{t_b}\sum_{i=1}^{t_b}(\delta^h_i)\]

\(\mathbf{X}\) est la matrice des données sur le batch (taille \(t_b\times 784\)), et \(\mathbf{\Delta^h}\) est la matrice des \(\delta^h_i\) sur le batch (taille \(t_b\times L\)).

Adapter la partie backward de l’algorithme pour entraîner le Perceptron

def backward(Ypred,Y,X,H):
    # À compléter
    return gradWy, gradby, gradWh, gradbh

Questions

  • la fonction de coût de l’Eq. (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 ?

  • Tester en initialisant les matrices à 0 comme pour la régression logistique. Quelles performances obtenez-vous ? Que peut-on en conclure ?

  • Vous testerez deux autres initialisations : une qui initialise les poids avec une loi normale de moyenne nulle et d’écart type à fixer, e.g. \(10^-1\) , et une autre connue sous le nom de Xavier [GB10], qui divise la valeur de la gaussienne par \(\sqrt{ni}\), où \(ni\) est le nombre de neurone dans la couche d’entrée.

  • Évaluer les performances du modèle. Vous devez obtenir un score de l’ordre de 98% sur la base de test pour ce réseau de neurones à une couche cachée.

[GB10] Xavier Glorot and Yoshua Bengio. Understanding the difficulty of training deep feedforward neural networks. In In Proceedings of the International Conference on Artificial Intelligence and Statistics (AISTATS’10). Society for Artificial Intelligence and Statistics. 2010.

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