le
• Horace Guy• durée de lecture estimée :
45 minutes
Introduction
Dans un article précédent, nous avons vu les principes de la différenciation automatique et l’algorithme du gradient.
Nous allons voir ici des exemples d’applications.
Dérivée automatique
Code
Avec un moteur de différenciation automatique comme JAX, on peut calculer les dérivées d’une fonction arbitraire.
Prenons le cas de la fonction qui donne le carré de la norme d’un vecteur en deux dimensions. On peut visualiser les valeurs prises par cette fonction avec une surface. La norme du gradient est proportionnelle à l’inclinaison de la surface dans la direction de plus grande pente ; c’est donc la généralisation de la pente en une dimension.
Code
Algorithme de descente
Pour minimiser une fonction, c’est-à-dire trouver un minimum, en sachant uniquement évaluer cette fonction en un point donné, on peut appliquer l’algorithme de la descente de gradient. On part d’un point aléatoire, et on se dirige dans la direction inverse au gradient, proportionnellement à sa norme.
Si la fonction a de bonnes propriétés, on va trouver un minimum.
Code
Régression
Nous allons résoudre des tâches de régression avec l’algorithme de descente du gradient. JAX calcule automatiquement le gradient pour nous, il ne nous reste qu’à implémenter l’algorithme de descente.
Régression linéaire
On s’intéresse à un problème en basse dimension: une feature, une cible et 100 observations.
Code
On va entraîner un modèle linéaire pour apprendre à prédire la cible en fonction de l’unique feature.
Pour chaque point de notre jeu d’entraînement, le modèle fait une prédiction. En comparant cette prédiction à la cible (la “vraie” valeur), on obtient un écart.
Nous allons prendre une métrique classique pour les problèmes de régression, c’est-à-dire la mse, soit la moyenne des carrés des écarts.
Notre modèle linéaire possède deux paramètres: le coefficient directeur de la droite, le poids, et l’ordonnée à l’origine, le biais. Il s’agit de trouver automatiquement les paramètres qui vont minimiser notre métrique.
Pour ce faire, on initialise les paramètres au hasard, puis on applique la descente du gradient en minimisant une fonction de coût (ou erreur, ou encore risque). Ici, on va minimiser directement notre métrique ; la fonction de coût et la métrique sont donc identiques.
Cette technique a l’air de bien fonctionner: en effet, on retrouve des paramètres très proches de ceux qui nous ont servi à créer ce problème synthétitque.
On peut se demander à quoi ressemble la fonction de coût par rapport à nos deux paramètres. Cela se visualise bien car il y a deux paramètre scalaires, donc nous sommes en dimension 2 (x et y). La troisième dimension z est la valeur de la fonction de coût en chaque point (x, y).
Code
On peut voir que la tâche est simple pour notre algorithme : il suffit en effet de suivre la direction de plus grande pente pour tomber rapidement sur un minimiseur de la fonction de coût.
Descente et inertie
La descente ci-dessus a le mérite de fonctionner ici, mais ce n’est pas tout le temps le cas. En effet, dans le cas où il existe des minima locaux, l’algorithme peut rester bloqué dans une configuration sous-optimale.
Pour améliorer l’algorithme, on peut s’inspirer des lois de la physiques : une bille qui tombe dans une cuvette possède une inertie (momentum), qui va la faire remonter un peu moins haut de l’autre côté, puis redescendre, etc.
Cette variante de l’algorithme initial est bien connue des chercheurs, et fonctionne mieux que l’original pour les problème plus complexes 1.
Code
Cet exemple de régression linéaire est utile pour comprendre, mais peu utilisé en pratique. En effet, pour un problème linéaire on peut simplement calculer le gradient une fois pour toutes, à l’aide d’une formule mathématique, et résoudre le problème. Cela est permis par le fait que la fonction de coût a des bonnes propriétés (convexité), ce qui fait qu’il y a une unique solution optimale à notre problème, soit un unique minimum global.
Néanmoins, la descente de gradient est bien plus générale, en ce qu’elle peut s’appliquer à des modèles bien plus complexes pour lesquels calculer le gradient n’est pas simple, ou bien la fonction de coût n’est pas totalement convexe.
Un problème non-linéaire
Code
Régression linéaire
Code
Coût initial: 56.88
Coût final: 0.36
Comme on peut le voir, ce n’est pas une bonne prédiction. C’est expliqué par la nature de ce problème: il n’est pas linéaire.
Étrangement, la fonction de coût décroît très rapidement et on peut avoir l’impressions que les paramètres finaux permettent au modèle d’être performant. Pourtant, lorsque l’on affiche les prédictions, on voit bien que ce n’est pas satisfaisant.
Qu’est-ce que le modèle voit dans ces conditions?
Code
L’algorithme de descente a bien fonctionné, car il a bien trouvé un minimiseur de la fonction de coût.
Cela n’est pas satisfaisant car le processus de génération de données n’est pas linéaire, ce qui veut dire qu’on ne peut pas approximer la solution en traçant une droite.
Régression sinusoïdale
Une astuce consiste à appliquer une fonction sinus après la sortie de la fonction linéaire. Comme la donnée a été générée avec une fonction similaire, on sait que le modèle est capable d’une bonne approximation.
Code
Coût initial: 1.25
Coût final: 1.04
La fonction approximée ne fonctionne pas.
À quoi ressemble l’espace de la fonction de coût ?
Code
En examinant la descente, on voit que l’algorithme est resté bloqué dans un minimum local.
Pour résoudre ceci, on peut utiliser de l’inertie:
Code
Coût initial: 1.25
Coût final: 0.03
L’espace est plus complexe, mais la fonction a quand même réussi à trouver un bon minimum. Les conditions initiales ont sûrement joué.
Comme on peut le voir, la nouvelle fonction colle presque parfaitement aux données.
Un problème linéaire large
On peut se demander ce qu’il se passe lorsqu’on rajoute des dimensions.
Nous allons ici prendre un problème de type large: nous avons 40 dimensions (features), pour 150 observations et une cible à une dimension et une relation linéaire entre les features et la cible.
Pour rajouter de la difficulté, on va utiliser des features qui sont corrélées entre elles. Seulent 10 features apportent l’information initiale, et les 30 restantes ne sont qu’une répétition d’une autre feature mutipliée par un coefficient au hasard.
Reprenons notre modèle linéaire, car ici il n’y a pas de raison que la donnée oscille à la manière du sinus.
Code
Final loss: 0.75
On a l’impression que le processus fonctionne bien : la fonction de coût décroit pour atteindre une valeur proche de zéro.
Néanmoins, nous allons voir que le modèle (linéaire) n’est pas capable de généraliser sur de nouvelles données.
Pour bien comprendre le phénomène, nous allons utiliser une technique de machine learning qui consiste à séparer notre jeu de données en deux :
Un jeu de donnnées pour l’entraînement x_tr, y_tr
Un jeu de données pour la validation x_te, y_te
On effectue une descente de gradient sur l’ensemble d’entraînement, puis on évalue la performance en calculant la fonction de coût sur l’ensemble de test :
Code
Train loss: 0.7077
Test loss: 12.389101
Le coût est presque 20 fois plus élevé sur le jeu de données de test !
Cela indique un phénomène bien identifié dans le machine learning: le sur-apprentissage (overfitting).
Pour réduire ce phénomène, on peut par exemple modifier la fonction de coût utilisée pour entraîner notre modèle en pénalisant la complexité du modèle : c’est ce qu’on appelle la régularisation.
Régularisation Ridge
Cela peut se faire en ajoutant la norme (au carré) des paramètres à la fonction de coût, multipliée par un coefficient qui mesure l’intensité de la pénalisation : c’est un hyperparamètre, qui n’est pas appris par le modèle mais sélectionné manuellement par la personne qui expérimente.
Ensuite, il ne faut pas oublier de mesurer la performance avec la fonction de coût originale, sur l’ensemble de test. Ce processus nous sert de métrique.
Code
Train loss: 1.2346672
Test loss: 8.390494
On peut voir que l’erreur a diminué sur l’ensemble de test, ce qui signifie que la technique de régularisation a bien fonctionné : on a un modèle plus performant.
Néanmoins, on a dû sélectionner à la main deux hyper-paramètres : celui contrôlant la régularisation d’une part, et le taux d’apprentissage d’autre part.
Comment peut-on sélectionner les meilleurs hyperparamètres ?
Meta-apprentissage
A rebours des techniques classiques gradient-free comme la grid search, nous allons ici employer une fois encore le gradient. Cette fois-ci, ce n’est pas le gradient de la fonction de coût par rapport aux paramètres que nous souhaitons apprendre que nous allons évaluer, mais plutôt le gradient de notre métrique de performance sur l’ensemble de test, par rapport à nos hyperparamètres :
Le taux d’apprentissage
Le coefficient de régularisation
Nous avons donc affaire à des gradients imbriqués.
Code
Loss finale: 3.2621257
Meilleur coefficient de régularisation: 0.76270264
Meilleur taux d'apprentissage: 10.747642
Code
En prenant les meilleurs paramètres, et en appliquant la descente du gradient originale, on retrouve sans surprise la même valeur de coût :
Code
Train loss: 0.65201813
Test loss: 3.2627606
Réseau de neurones
Pour finir, nous allons essayer avec un petit réseau de neurones à une couche (non-profond donc).
Nous allons prendre 10 neurones, et une activation [RELU](https://fr.wikipedia.org/wiki/Redresseur_(r%C3%A9seaux_neuronaux).
Code
Train loss 0.00030381413
Test loss 32.594643
Comme on peut le voir, la performance est terrible. Ce n’est pas étonnant: un réseau de neurones n’est pas adapté à ce problème tabulaire, et overfit beaucoup.
Dropout
Ici on régularise les réseaux de neurons avec la technique du dropout.
Code
Train loss: 36.658325
Test loss: 46.8016
On voit que dans ce cas, le réseau n’overfit plus car il a quasiment le même score sur le jeu de données d’entraînement et le jeu de validation. Néanmoins, la performance est terrible sur les deux.
On peu donc conclure que les réseaux de neurones à architecture simple comme ceux-cis ne sont pas adaptés à ce genre de problème.
Conclusion
On a vu la puissance de la descente de gradient dans plusieurs cas que l’on pourrait qualifier de simple en terme de temps de calcul. En effet, on n’a eu ici juste eu besoin d’un processeur (CPU) et non de carte graphique (GPU) pour entraîner tous les modèles.
Pour résoudre des problèmes plus complexes et moins pédagogiques, on a besoin d’architecture beaucoup plus profondes et d’une grosse puissance de calcul.
Postez votre commentaire :
Votre commentaire a bien été envoyé ! Il sera affiché une fois que nous l'aurons validé.
Vous devez activer le javascript ou avoir un navigateur récent pour pouvoir commenter sur ce site.