Perceptron multi-couches

Dans ce TP, on manipule des perceptrons multi-couches (PMC).
Remarque : la manipulation de perceptrons multi-couches est loin d'être aussi simple que la manipulation d'un perceptron. Ce qui est présenté dans ce TP consiste juste en quelques notions simples. Pour une utilisation sérieuse de perceptrons multi-couches, ce qui est expliqué ici ne suffit pas, c'est juste une introduction.

Définition et utilisation d'un perceptron multi-couches en python

Pour créer un perceptron multi-couches, on doit au minimum spécifier son architecture (nombre de couches cachées et taille de chaque couche cachée) et la fonction d'activation des perceptrons. Comme on l'a vu dans le TP précédent, il faut aussi spécifier le générateur de nombres pseudo-aléatoires.
Si on veut créer un PMC composé de deux couches cachées, la première composée de 5 perceptrons, la seconde de 2 perceptrons, ces perceptrons ayant une fonction d'activation tangente hyperbolique, on pourra écrire :

architecture_pmc = (5, 2)
from sklearn.neural_network import MLPClassifier
pmc = MLPClassifier (hidden_layer_sizes = architecture_pmc,
                     activation = "tanh",
                     solver = "lbfgs",
                     max_iter = 500,
                     random_state = gnpa)

Explication des paramètres :

Pour calculer les poids, on utilise ensuite la méthode fit() :

pmc.fit (X_train, Y_train)

Dans cet exemple, on calcule les poids en utilisant les exemples d'entraînement que l'on a stockés dans la matrice X_train et dont les étiquettes sont dans le vecteur Y_train. On reprend les mêmes notations que dans le TP précédent.
Comme dans le TP sur la DGS, les itérations de calcul des poids s'arrêtent soit au bout d'un nombre fixé d'itérations (paramètre max_iter de MLPClassifier()), soit lorsque la performance du PMC ne s'améliore plus significativement (paramètre tol de MLPClassifier()) pendant un certain nombre d'itérations (paramètre n_iter_no_change de MLPClassifier()).

On peut ensuite prédire l'étiquette de données quelconques. En supposant qu'elles sont stockées dans la matrice X_test, on écrira :

Y_pred = pmc.predict (X_test)

et Y_pred sera un vecteur contenant les prédictions pour chacune des données contenues dans X_test.
On peut ensuite comparer ces étiquettes prédites aux étiquettes correctes Y_test. Par exemple, np.sum (Y_pred != Y_test) / len (Y_test) calcule la proportion d'exemples de test pour lesquels la prédiction est incorrecte.

La grande question

Quand on utilise un perceptron multi-couches, la grande question concerne l'architecture à utiliser : combien de couches cachées, combien de perceptrons par couche ?
Il n'y a pas de réponse toute faite à cette question. Avec l'expérience, on apprend petit à petit que pour tel jeu d'exemples, telle architecture devrait convenir. Néanmoins, en général, on teste différentes architectures et on conserve celle qui donne les résultats les plus satisfaisants. C'est ce que l'on va faire.
Il faut bien avoir en tête que la performance d'un perceptron multi-couches peut varier à chaque calcul des poids et varie en fonction du partionnement du jeu d'exemples. Aussi, il faut construire plusieurs PMC avec une architecture donnée et prendre en compte la performance moyenne.

Un PMC pour les iris

On considère le jeu de données des iris et la détermination des Virginica. On aura donc ce bout de code :

from sklearn import datasets
iris = datasets.load_iris()
entrées_iris = iris.data

# Je calcule la sortie attendue pour chaque donnée.
sorties_iris = []
for i in range (len (iris.target)):
    if iris.target [i] == 2:
        sorties_iris.append (1)
    else:
        sorties_iris.append (0)
sorties = sorties_iris

On considère les 4 attributs des iris (longueur et largeur des pétales et des sépales). entrées_iris contient la matrice des 150 iris décrit chacun par ses 4 attributs et sorties qui contient l'étiquette à prédire pour chacun.

On partitionne ce jeu d'exemples comme dans le TP précédent. On centre et on réduit les attributs comme on l'a vu dans les TP précédents.
Ensuite, comme indiqué ci-dessus, vous créez un PMC, vous calculez les poids avec X_train et Y_train et ensuite, vous mesurez l'erreur de prédiction sur X_test.

Test de différentes architectures de PMC

Comparez l'erreur de prédiction obtenue ci-dessus avec celle obtenue en utilisant d'autres architectures, par exemple (6, 3), (2, 2), (1, 1), (3), (1). Laquelle donne les meilleurs résultats ?

Poids calculés

Une fois les poids calculés, on peut les obtenir avec pmc.coefs_ et pmc.intercepts_. Le premier contient les poids sauf les biais, le second contient les biais.

Par exemple, pour un PMC avec deux couches cachées contenant respectivement 5 et 2 perceptrons, on obtient quelque chose comme cela :

>>> pmc.coefs_
[array([[ 2.24382386,  1.69776656, -2.24867825,  4.34250259,  0.08151146],
       [ 2.63399065, -0.41853075, -1.01340237, -7.0183241 ,  0.9974338 ],
       [ 1.99398809, -5.64443327, -3.77687842,  3.12228963,  0.81729629],
       [ 1.06561158, -1.97843109, -2.73752938,  0.85438636,  0.91595649]]),
 array([[-2.17720553,  2.22410213],
        [-1.64392124,  1.8328595 ],
        [-2.4161274 ,  1.96437139],
        [ 3.68371142, -3.34386375],
        [0.04460816, -1.65029824]]),
 array([[ 9.64872995], [-9.44429162]])]

On y voit trois matrices (array). La première matrice correspond aux 4 premières lignes, chaque ligne contient 5 nombres : ce sont les poids entre chacune des 4 entrées (4 entrées car 4 attributs) et chacun des 5 perceptrons de la première couche cachée.
Ensuite, on voit une deuxième matrice de 5 lignes et 2 colonnes : ce sont les poids entre chacun des 5 perceptons de la première couche cachée et chacun des 2 perceptrons de la seconde couche cachée.
Enfin, on voit une troisième matrice qui est composée de seulement 2 lignes et 1 colonne : ce sont les poids entre les sorties des perceptrons de la seconde couche cachée avec le perceptron de sortie.

Pour les biais, on obtient quelque chose comme :

>>> pmc.intercepts_
  [array([ 1.8724528 ,  2.86780535,  3.57288241,  0.53149196, -1.37838133]),
   array([-2.61940886,  1.27119812]),
   array([-3.22011893])]

On y voit à nouveau 3 matrices d'une seule colonne chacune. La première contient 5 lignes, la deuxième 2 et la troisième 1 seule. Ce sont les biais des 2 couches cachées et de la couche de sortie composées respectivement de 5, 2 et 1 perceptrons.
En connaissant les poids, vous pouvez calculer vous-même la sortie du PMC pour une donnée.

Confiance dans la prédiction

En utilisant la méthode predict_proba() à la place de predict(), on obtient non pas la classe prédite mais la probabilité de prédire chacune des deux classes. Cela permet de détecter les données pour lesquelles la prédiction est peu sûre : dans ce cas, la probabilité de chacune des classes est aux alentours de 0,5.
Calculez predict_proba(X_test) et inspectez le résultat. Si pour la majorité des données la probabilité de l'une des deux classes est très faible, il y a probablement une ou quelques données pour lesquelles les deux probabilités sont à peu près égales. Par exemple, dans mon cas, les probabilités de chacune des classes pour l'une des données sont [6.66756803e-01, 3.33243197e-01] et pour une autre [6.90766191e-01, 3.09233809e-01]. Vous pouvez ensuite vérifier la position de ces données dans le plan longueur x largeur des pétales.

Plus de 2 classes

On peut traiter des problèmes dans lesquels il y a plus de 2 classes. Jusqu'à maintenant, on a toujours transformé les iris en un problème à 2 classes. On peut remplacer :

# Je calcule la sortie attendue pour chaque donnée.
sorties_iris = []
for i in range (len (iris.target)):
    if iris.target [i] == 2:
        sorties_iris.append (1)
    else:
        sorties_iris.append (0)
sorties = sorties_iris

par simplement :

sorties_iris = iris.target

et le perceptron multi-couches aura 3 sorties puisqu'il y a 3 classes.
Pour le reste, rien ne change pour utiliser le perceptron multi-couches.

À faire : appliquez cette démarche aux iris :


À faire : sur les iris, comparez les performances de ces deux approches :

Comme on l'a vu juste au dessus, il faudra réaliser plusieurs exécutions pour obtenir une valeur de l'erreur moyenne et de son écart-type.
Laquelle des deux approches donne les meilleurs résultats ? D'après vous, pourquoi ?

Activité en autonomie

Nous pouvons maintenant appliquer tout ce que l'on vient de voir à d'autres jeux de données. Par exemple, pour changer des iris, on peut s'intéresser au jeu de données wine qui ne comporte pas de difficulté. Il possède également 3 classes. Chaque donnée correspond à la description chimique d'un échantillon de vin. La classe correspond à l'origine géographique de l'échantillon.
Pour utiliser ce jeu d'exemples, il suffit de remplacer iris = datasets.load_iris() par vin = datasets.load_wine(). Et ensuite, vous remplacez iris par wine dans les commandes python que je vous ai indiqué.
Appliquez ce que l'on a vu. Cette tâche ne comporte aucune difficulté. La seule différence avec les iris est que vous ne pouvez pas faire de représentation graphique.

Pour finir

Le source de votre programme doit respecter les points suivants :

Pour finir, vous m'envoyez votre/vos script(s) par email, en mettant votre binôme en cc.