Le surentraînement illustré dans un problème de classification binaire en 2D.
Le sur-entraînement (overfitting) est l'un des problèmes majeurs en machine learning, si ce n'est le plus important.
Dans ce post, on va illustrer le sur-entraînement dans le contexte d'un petit problème de classification à 2D.
Mais ce que je vais expliquer ici est valable en général, et devrait toujours être gardé à l'esprit, même lorsque l'on travaille sur des problèmes plus complexes.
Nous allons apprendre:
Pré-requis :
Pour faire tourner ce code, vous pouvez simplement l'ouvrir dans Google Colab
Une autre possibilité, si vous avez déjà installé Anaconda (2.X ou 3.X) est de:
Downloads/maldives-master
Downloads/maldives-master/overfitting
overfitting.ipynb
Tout d'abord, initialisons nos outils:
import matplotlib.pyplot as plt
import numpy as np
%matplotlib inline
np.random.seed(0xdeadbeef)
# blah
Nous allons créer un échantillon d'exemples avec deux valeurs x1 et x2, appartenant à deux catégories. Pour la catégorie 0, la densité de probabilité sous-jacente est une Gaussienne centrée sur (0,0), de largeur 1 dans les deux direction. Pour la catégorie 1, la Gaussienne est centrée sur (1,1). On assigne l'étiquette 0 à la catégorie 0, et l'étiquette 1 à la catégorie 1.
def make_sample(nexamples, means=([0.,0.],[1.,1.]), sigma=1.):
normal = np.random.multivariate_normal
# largeur au carré:
s2 = sigma**2.
# ci-dessous on donne les coordonnées de la moyenne
# de la Gaussienne en premier argument, et ensuite
# la matrice de covariance qui décrit sa largeur suivant
# les deux directions.
# on engendre nexamples examples pour chaque catégorie.
sgx0 = normal(means[0], [[s2, 0.], [0.,s2]], nexamples)
sgx1 = normal(means[1], [[s2, 0.], [0.,s2]], nexamples)
# étiquettes pour chaque catégorie
sgy0 = np.zeros((nexamples,))
sgy1 = np.ones((nexamples,))
sgx = np.concatenate([sgx0,sgx1])
sgy = np.concatenate([sgy0,sgy1])
return sgx, sgy
Ici, nous créons un tout petit échantillon d'entraînement avec seulement 30 exemples dans chaque catégorie, et un échantillon de test avec 200 exemples par catégorie. Nous prenons un petit échantillon de test car, comme nous allons le montrer, le sur-entraînement se produit lorsque la taille de l'échantillon est trop faible.
sgx, sgy = make_sample(30)
tgx, tgy = make_sample(200)
# Notez comme les deux catégories sont tracées
# d'un seul coup en donnant le tableau d'étiquettes
# comme argument de couleur (c=sgy)
plt.scatter(sgx[:,0], sgx[:,1], alpha=0.5, c=sgy)
plt.xlabel('x1')
plt.ylabel('x2')
Avec si peu d'exemples, il n'est pas évident à l'oeil que les échantillons suivent une densité de probabilité Gaussienne. De plus, comme les Gaussiennes sont proches, les deux catégories se mélangent et il va être difficile de les séparer.
Essayons cependant d'effectuer la classification avec un réseau de neurones de scikit-learn. Voici une explication des paramètres utilisés ci-dessous:
from sklearn.neural_network import MLPClassifier
mlp = MLPClassifier(hidden_layer_sizes=(50,50,50), activation='relu', max_iter=10000, random_state=1)
mlp.fit(sgx,sgy)
Maintenant, on va définir une petite fonction pour afficher nos résultats. Cette fonction tracera les exemples des deux catégories, ainsi que la probabilité qu'un point (x1,x2) appartienne à la catégorie 1 (le noir veut dire que cette probabilité est proche de 1, et le blanc de 0).
def plot_result(sample, targets, linrange=(-5,5,101)):
xmin, xmax, npoints = linrange
gridx1, gridx2 = np.meshgrid(np.linspace(xmin,xmax,npoints), np.linspace(xmin,xmax,npoints))
grid = np.c_[gridx1.flatten(), gridx2.flatten()]
probs = mlp.predict_proba(grid)
plt.pcolor(gridx1, gridx2, probs[:,1].reshape(npoints,npoints), cmap='binary')
plt.colorbar()
plt.scatter(sample[:,0], sample[:,1], c=targets, cmap='plasma', alpha=0.5, marker='.')
plt.xlabel('x1')
plt.ylabel('x2')
plt.show()
plot_result(sgx,sgy)
La distribution de probabilité est très loin d'être optimale. À la frontière, le réseau de neurones fait tout ce qu'il peut pour suivre les fluctuations de l'échantillon d'entraînement. Il est capable de le faire car son grand nombre de paramètres le rend très flexible.
Mais voyons ce qui se passe si on trace la distribution de probabilité avec l'échantillon de test, qui est plus grand:
plot_result(tgx,tgy)
De nombreux exemples de test sont classés dans la mauvaise catégorie. Ce réseau de neurones est très bon avec l'échantillon d'entraînement, mais a perdu sa généralité, et il est donc inutile en pratique.
C'est le sur-entraînement.
Maintenant, essayons à nouveau, mais avec un réseau de neurones bien moins complexe : une seule couche cachée avec seulement 5 neurones. Le réseau est entraîné avec le petit échantillon d'entraînement, et les résultats sont affichés avec l'échantillon de test, de taille plus importante.
mlp = MLPClassifier(hidden_layer_sizes=(5,), activation='relu', max_iter=10000, random_state=1)
mlp.fit(sgx,sgy)
plot_result(tgx,tgy)
Cette fois-ci, le sur-entraînement ne pose pas de problème. Le réseau n'a pas assez de paramètres pour pouvoir suivre les fluctuations de l'échantillon d'entraînement. Par conséquent, il se comporte bien avec l'échantillon de test.
Essayons encore autre chose : un réseau complexe, mais avec beaucoup plus de données d'entrainement, 10 000 exemples par catégorie au lieu de 30.
sgx, sgy = make_sample(10000)
mlp = MLPClassifier(hidden_layer_sizes=(50,50,50), activation='relu', max_iter=10000, random_state=1)
mlp.fit(sgx,sgy)
plot_result(tgx, tgy)
L'entraînement du réseau a pris un peu plus de temps, mais il y a suffisamment de données d'entraînement pour contraindre l'ensemble des paramètres du réseau. Les performances de classification resteront bonnes en général.
Eh bien, pour pouvoir décrire des données complexes! Et pour que ces réseaux puissent être efficaces, nous devons les entraîner avec beaucoup de données.
Dans cette section, nous allons construire un échantillon complexe avec beaucoup de données, et voir si nous parvenons à le classer.
Pour construire l'échantillon, nous réutilisons juste notre fonction make_sample plusieurs fois, avant de concaténer les échantillons résultants.
sgxa, sgya = make_sample(1000, ([0.,0],[3.,3.]), 0.3)
sgxb, sgyb = make_sample(1000, ([1.,1],[4.,4.]), 0.3)
sgxc, sgyc = make_sample(1000, ([5.,5.],[-2.,-2.]), 0.6)
sgxd, sgyd = make_sample(1000, ([-1,3.],[3.,-1.]), 0.3)
sgx = np.concatenate([sgxa,sgxb,sgxc,sgxd])
sgy = np.concatenate([sgya,sgyb,sgyc,sgyd])
plt.scatter(sgx[:,0], sgx[:,1], alpha=0.5, c=sgy)
plt.xlabel('x1')
plt.ylabel('x2')
Maintenant, essayons de classer tout ça avec un petit réseau:
mlp = MLPClassifier(hidden_layer_sizes=(3,), activation='relu', max_iter=10000, random_state=1)
mlp.fit(sgx,sgy)
plot_result(sgx,sgy,linrange=(-4,7,201))
Le réseau n'a pas assez de paramètres pour s'adapter aux données d'entraînement.
C'est le sous-entraînement.
Cependant, le réseau fait quand même du bon boulot avec ses trois neurones.
Augmentons un peu le nombre de neurones sur la couche cachée:
mlp = MLPClassifier(hidden_layer_sizes=(5,), activation='relu', max_iter=10000, random_state=1)
mlp.fit(sgx,sgy)
plot_result(sgx,sgy,linrange=(-4,7,201))
Pas mal! 5 neurones sont déjà suffisants pour s'adapter aux données, mais nous avons eu de la chance. Avec une topologie différente, nous aurions pu rater un paquet Gaussien.
Augmentons encore la complexité du modèle:
mlp = MLPClassifier(hidden_layer_sizes=(50,50,50), activation='relu', max_iter=10000, random_state=1)
mlp.fit(sgx,sgy)
plot_result(sgx,sgy,linrange=(-4,7,201))
Toujours pas de surentraînement. Le réseau produit maintenant une frontière plus régulière, et je pense qu'il n'aurait aucun mal à s'adapter à des topologies différentes en cas de besoin.
Maintenant, revenons sur le site pour conclure!
Aujourd'hui, nous avons appris ce que sont le sur-entraînement et le sous-entraînement, et comment les éviter:
Nous nous sommes mis dans le cadre d'un problème de classification en 2D pour pouvoir tracer la distribution de probabilité estimée par le modèle. Mais les concepts expliqués ici doivent être gardés à l'esprit même lorsque l'on travaille sur des problèmes de machine learning plus complexes.
N'hésitez pas à me donner votre avis dans les commentaires ! Je répondrai à toutes les questions.
Et si vous avez aimé cet article, vous pouvez souscrire à ma newsletter pour être prévenu lorsque j'en sortirai un nouveau. Pas plus d'un mail par semaine, promis!
Rejoignez ma mailing list pour plus de posts et du contenu exclusif: