Avec tout ce qui a été présenté dans la partie 1, j’espère que vous aurez compris comment fonctionne de manière générale un réseau de neurones, avec les deux étapes principales que sont la forward propagation et la back propagation. Et c’est déjà pas mal, ça fait beaucoup de concepts à murir ! Dans cette partie, sans trop rentrer dans les détails non plus, je vais vous présenter quelques notions pour aller encore un peu plus loin sur l’utilisation de réseaux de neurone.
Initialisation des poids et des biais
On n’en a quasiment pas parlé jusqu’ici mais pour commencer l’étape de forward propagation, il faut bien que tous les poids et les biais du modèle aient une valeur au départ (avant d’être modifiés par la descente de gradient), sinon il y a peu de chances de produire une prédiction… Alors comment est ce qu’on initialise notre modèle ? On met tous les poids et les biais à 0 ? Ce n’est pas une très bonne idée… Si tous les poids sont initialisés à 0, le résultat de la dérivée de la fonction coût par rapport à chaque poids du modèle sera le même pour tous les poids. Ca reviendra donc à avoir un seul poids dans tout le réseau de neurones, et donc on ne fait pas beaucoup mieux qu’un modèle linéaire. C’est ce qu’on appelle le problème de symétrie parce que toutes les couches cachées seront identiques. On préférera en général initialiser les poids et les biais avec une distribution gaussienne centrée sur 0 et avec un écart type de 1 par exemple. Certains auteurs préconisent également une distribution gaussienne centrée sur 0 avec un écart type de \frac{1}{\sqrt(n_{in})} avec n_{in} le nombre de poids associés à chaque neurone, de manière à éviter une saturation des neurones (et donc qu’ils apprennent moins vite).
La fonction de cross-entropie
Comme on l’a vu, pour évaluer la qualité d’un modèle de réseau de neurones et pour réaliser l’étape de backpropagation, on a besoin d’une fonction de coût, tout simplement parce qu’il faut faire comprendre au modèle quand il prédit bien et quand il prédit mal. La fonction de coût qui a été présentée en début d’article est la fonction quadratique, qui est une fonction assez classique pour pénaliser de plus en plus fortement les prédictions estimées qui s’éloignent de plus en plus des prédictions réelles. Néanmoins, cette fonction peut être sujette au problème de l’apprentissage lent des neurones (le « learning slowdown », que je détaille juste après) et une autre fonction lui souvent préférée, celle de la cross-entropie :
- \frac{1}{n} \sum_{x} [y \times log(a) + (1-y) \times log(1-a)]
Comme on l’a répété plusieurs fois, l’objectif de l’étape de back propagation, c’est de calculer la dérivée de la fonction coût par rapport aux poids du modèle (toujours le fameux \frac{\partial C_{x}}{\partial w_{1}}). Avec la fonction quadratique, nous avions calculé plus haut (je vous laisse vous reporter aux équations) :
\frac {\partial C_{x}}{\partial w_{1}} = \frac {\partial z(x)}{\partial w_{1}} \times \frac {\partial C_{x}}{\partial z(x)} = a_{1}(x) \times 2 \times (\phi(z(x))-y(x)) \times \phi'(z(x))
Le problème du learning slowdown vient du dernier terme de l’équation : \phi'(z(x)). Dans le cas de la fonction d’activation sigmoïde, le souci, c’est qu’on a vu quelle était plate aux extrémités de son intervalle de définition, ce qui signifie que sa dérivée peut être très faible si la valeur d’activation d’un neurone est proche de 1 ou de 0 (là où la courbe sigmoïde est plate). Si la dérivée est très faible, le gradient le sera aussi et donc le modèle apprendra très lentement (et on a déjà parlé des risques d’un apprentissage très lent et se retrouver bloqué dans un minimum local de la fonction de coût). Mais avec la fonction de cross-entropie, il n’y a pas ce problème, parce que croyez moi ou non, lorsque l’on calcule la dérivée de la fonction de coût par rapport aux poids du modèle, il n’y a pas le terme \phi'(z(x)), on est donc sauvé ! Ce n’est pas non plus complètement au hasard puisque la fonction de cross entropie a été mise en place pour ça… Je vous laisse aller voir les dérivées si cela vous intéresse.
Note : Il y a bien un signe négatif devant la fonction de cross entropie, ce n’est pas une erreur ! Le coût sera toujours positif, pas d’inquiétude !
D’une manière générale, on aura plutôt tendance à utiliser la fonction de cross entropie pour les problèmes de classification et la fonction quadratique pour les problèmes de régression. Dans notre exemple très simple de début d’article, j’ai préféré utiliser la fonction de coût quadratique pour l’étape de back propagation soit plus facilement appréhendable (la fonction quadratique fait quand même un peu moins peur que la fonction de cross entropie).
Régularisation et Drop Out
Un des problèmes classiques auquel on peut faire face quand on essayer de modéliser un phénomène est le sur-apprentissage ou sur-ajustement (overfitting en anglais). Comme son nom l’indique, il y a sur-apprentissage quand le modèle apprend trop sur les données qui lui ont été fournies et n’est pas capable de généraliser ses prédictions à des données d’entrée nouvelles. C’est d’ailleurs pour ça qu’il faut prendre l’habitude de séparer son jeu de données en un jeu d’apprentissage sur lequel le modèle apprend et un jeu de validation sur lequel le modèle est validé pour justement voir si le modèle est capable de généraliser à un jeu de données différent du jeu d’apprentissage. Pour être totalement transparent, il faudrait même avoir un troisième jeu de données complètement différent de son jeu de données initial (acquis dans d’autres conditions par exemple) pour être sûr de la capacité de généralisation du modèle initial. Par contre, valider son modèle sur le jeu de données qui a aussi servi au modèle à apprendre n’a pas de sens… A proscrire !! Comment faire dans le cas de réseau de neurones pour limiter cette problématique de sur-apprentissage ? Plusieurs solutions s’offrent à nous ! Je n’en présenterai que trois ici :
Première solution : Rajouter des éléments à son jeu de données initial. Bon, plus facile à dire qu’à faire… Si c’est pour repartir pour une campagne d’échantillonnage, on n’est pas sortis de l’auberge. Ca reste quand même une bonne option ! Lorsque l’on n’est pas capable de rajouter des échantillons réels à son jeu de données, il reste la possibilité de rajouter des échantillons artificiels. Par exemple, dans le cas d’images d’adventices, on peut imaginer plusieurs pré-traitements pour accroitre artificiellement son jeu de données : rotation, distortion, rajout de bruit dans l’image…
Deuxième solution : Mettre à jour les poids et les biais du réseau en n’utilisant pas l’ensemble des neurones du modèle, c’est la méthode « Dropout ». A chaque échantillon que l’on va faire passer dans le modèle pour estimer les variations de poids et de biais nécessaires pour améliorer le modèle, on va supprimer aléatoirement et temporairement des neurones du réseau (on ne les supprime pas complètement, vous pouvez les considérer comme des neurones fantômes). Une fois l’échantillon passé, les neurones effacés réapparaissent et un nouveau tirage aléatoire de neurones est réalisé pour choisir lesquels seront effacés pour l’échantillon suivant. Grâce à cette approche Dropout, le modèle est plus flexible dans le sens où il n’apprend pas tout le temps avec les mêmes neurones, ce qui permet de généraliser les prédictions plus facilement à de nouvelles données.
Troisième solution : Régulariser la fonction de coût utilisée pour ne pas que les poids utilisés dans le modèle ne soient trop importants. Plusieurs méthodes de régularisation existent mais nous allons nous concentrer sur celle correspondant à la régularisation dite L2 ou « weight decay ». La fonction de coût régularisée est la suivante :
C = \frac {1}{n} \sum C_{x} + \frac {\lambda}{2n} \sum w^2
Vous retrouvez ici la formule de coût que l’on a utilisé auparavant C = \frac {1}{n} \sum C_{x} (peut importe laquelle d’ailleurs, que ce soit la fonction quadratique, de cross entropie ou une autre) auquel on a rajouté la somme des carrés des poids du réseau pondérés par le rapport \frac {\lambda}{2n}. Le facteur \lambda est appelé le paramètre de régularisation (\lambda est positif). Le terme n est toujours le nombre d’échantillons d’apprentissage de notre modèle.
Si \lambda est fixé à une valeur très petite, la fonction de coût régularisée redevient tout bonnement la fonction de coût initiale puisque le terme de régularisation n’a plus aucune importance. Par contre, si \lambda est fixé à une valeur forte, alors le terme de régularisation prend de l’importance ! Je rappelle que notre objectif principal est de produire les meilleures estimations possibles via notre modèle donc ce qui nous intéresse, c’est d’avoir une valeur de coût la plus faible possible. Si \lambda est grand, il va donc falloir que la somme des poids du modèle soit faible, sinon peu de chance d’avoir une valeur de coût faible. La régularisation favorise donc des poids faibles dans les modèles de réseaux de neurones. Lorsque le modèle est composé de poids faibles, ça veut dire que le modèle ne va pas être trop fortement impacté par des changement brusques dans les données d’entrée et donc qu’il ne va pas être trop spécifique à ces données d’entrée, ni au bruit qu’elles peuvent contenir. C’est donc une manière d’être relativement flexible et de pouvoir généraliser des prédictions à des données nouvelles. Si les poids du modèle sont trop importants, on imagine qu’une donnée nouvelle un peu différente des autres peut complètement bousculer le modèle, ce qui n’est pas forcément le plus opportun.
Compromis biais-variance
Dans la section précédente, nous nous sommes intéressés au problème du sur-apprentissage, c’est-à-dire le fait de ne pas être capable de généraliser nos prédictions à de nouvelles données d’entrée. On peut également parler du problème inverse, c’est-à-dire le sous-apprentissage, où, au contraire, le modèle n’a pas assez appris sur les données et n’est donc pas capable de réaliser de bonnes prédictions. Si nos prédictions sont mauvaises, comment alors savoir si l’on fait face à un problème de sous apprentissage ou de sur-apprentissage ? On en discute ici avec le dilemme ou le compromis biais/variance. Le biais d’un modèle peut être vu comme l’incapacité d’un modèle à apprendre les bonnes règles de décision (c’est donc plutôt lié au sous-apprentissage). La variance d’un modèle, elle, peut être vue comme la sensibilité du modèle aux petites fluctuations de l’échantillon d’apprentissage (c’est donc plutôt lié au sur-apprentissage). Et on parle de dilemme biais/variance parce que, manque de bol, on ne peut pas diminuer ces deux composantes en même temps… Si on diminue le biais du modèle, on sur-apprend sur les données d’apprentissage et donc on se retrouve avec une variance forte et inversement. Tout le jeu est donc d’arriver à un compromis… Dans le tableau suivant, je vous donne quelques exemples pour juger, en fonction de l’erreur de prédiction que l’on obtient avec son jeu d’apprentissage ou son jeu de validation, l’état du biais et de la variance de votre modèle (le compromis biais/variance n’est bien évidemment pas spécifiques aux réseaux de neurones)
Erreur de prédiction (%) | ||||
Jeu d’apprentissage | 2 | 12 | 12 | 1 |
Jeu de validation | 15 | 13 | 25 | 2 |
Interprétation | Variance forte | Biais fort | Variance et biais forts | Variance et biais faibles |
Un petit retour sur les hyper-paramètres, avec quelques petits nouveaux dans la liste
Vous aurez peut-être commencé à le sentir mais il y a pas mal de réglages d’hyperparamètres dans les réseaux de neurones : on a par exemple déjà parlé du learning rate (\eta ou \alpha selon les notations), de dropout (avec le pourcentage de suppression de neurones), et de régularisation avec \lambda. On distingue ces réglages généraux (les hyperparamètres donc) des paramètres du modèle qui seront calculés en interne par le modèle (les poids, biais…). Voyons les quelques autres hyperparamètres avec lesquels on peut être amenés à interférer :
- Batch size: C’est le nombre d’échantillons qui va être propagé à travers le réseau de neurones. Au lieu d’utiliser par exemple en une seule fois ses 1000 échantillons et de ne mettre à jour qu’une seule fois les paramètres du modèle (biais, poids..), on peut propager ses échantillons en 10 groupes de 100 échantillons et mettre à jour plus régulièrement les paramètres du modèle. L’intérêt majeur étant que cette procédure permet d’utiliser moins de mémoire et permet au modèle de neurones d’apprendre également plus vite. Attention néanmoins à ne pas utiliser une taille de groupe trop petite ce qui nuirait fortement à la descente de gradient (puisque les paramètres seraient constamment ré-évalués sur un tout petit nombre d’échantillons donc pas vraiment représentatifs…)
- Epoch: C’est le nombre de fois où le jeu de données complet va être propagé dans le réseau de neurones. Un epoch correspond au passage du jeu de données complet une fois dans le réseau. En faisant passer plusieurs fois le jeu de données complet avec des batchs différents, on s’approche mieux de l’optimum de notre modèle
- Itérations : C’est le nombre de Batchs qui sont nécessaires pour arriver à un epoch. Pour reprendre l’exemple précédent, si je divise mon jeu de données de 1000 échantillons en batchs de 100 échantillons, il faudra 10 itérations pour arriver à un epoch.
- Momentum: Cet hyperparamètre, que l’on note généralement \mu (ou \beta selon les notations), permet de rajouter une notion de vitesse à la descente de gradient que l’on a vu précédemment. Le momentum varie entre 0 et 1. Jusqu’ici, lors de la descente de gradient, on a cherché la direction la plus intéressante pour aller vers l’optimal de la fonction de coût. Mais on a essentiellement parlé de direction (c’est-à-dire est ce qu’on va dans le bon sens ou pas) mais on ne s’est pas demandé à quelle vitesse on allait dans cette direction. Dans la version initiale de la descente de gradient, je vous ai expliqué comment mettre à jour les poids du réseau de neurones :
w^{l} = w^{l} - \eta \times \frac {\partial C}{\partial w^{l}}
Avec la notion de momentum, on rajoute une mémoire court-terme à la descente de gradient (\v est ici un vecteur de vélocités)
v^{l} = \mu \times v^{l} - \eta \times \frac {\partial C}{\partial w^{l}}
Et on met ensuite à jour les poids du modèle
w^{l} = w^{l} + v^{l}
En choisissant un momentum de 0, on retrouve la descente de gradient classique que l’on a utilisé. En choisissant un momentum de 1, on voit que l’on ajoute un terme de gradients à chaque itération ce qui va accélérer de plus en plus la descente de gradient. C’est intéressant au début mais le problème, c’est qu’on a en quelque sorte besoin que cette descente ralentisse au fur et à mesure que le modèle se rapproche de son optimum. C’est pour cela que des valeurs entre 0 et 1 sont plus généralement choisies parce ça permet de rajouter une notion de friction à la descente ou une décélération si c’est plus clair comme ça.
Apprentissages supervisés, non supervisés et par renforcement
Dans cet article, on a, à plusieurs reprises, parlé d’apprentissage ! Notre objectif était effectivement que notre modèle de réseau de neurones apprenne. Et dans les exemples que l’on a mis en place, les modèles devaient apprendre à prédire un rendement de blé sur une parcelle à partir d’un certain nombre de paramètres d’entrée. Nous avons jusqu’ici exclusivement travaillé sur de l’apprentissage dit supervisé. Nous avions un set de variables d’entrée (pluviométrie, température, type de sol) et nous savions à quel résultat le modèle devait aboutir : un rendement faible ou un rendement fort. Il restait donc au modèle à paramétrer l’ensemble de ces poids et de ces biais pour arriver au meilleur taux de prédiction. L’apprentissage est supervisé parce que l’utilisateur sait exactement à quoi il veut arriver et il supervise donc l’algorithme pour arriver à ses fins. L’apprentissage supervisé reste de loin la forme d’apprentissage la plus utilisée avec les réseaux de neurones, notamment du fait qu’elle permet d’atteindre des résultats assez impressionnants en termes de qualité de prédiction.
A l’inverse de l’apprentissage supervisé, on trouve l’apprentissage dit « non supervisé ». Dans ce cas-là, on part de la même base que dans le cadre de l’apprentissage supervisé, c’est-à-dire que l’on a un jeu de données avec un certain nombre de paramètres d’entrée, mais par contre, la sortie du modèle est totalement inconnue. Il faut comprendre que l’utilisateur n’a pas (ou peu) d’idées préconçues sur ce que le modèle doit retourner. L’utilisateur demande en fait au modèle d’extraire des grandes tendances, des patrons, des groupes, des motifs, ou des arrangements de données particuliers dans le jeu de données fourni en entrée du modèle. Si on reprend l’exemple de la prédiction de rendement de blé, au lieu de dire au modèle : « ces paramètres d’entrée ont conduit à un rendement faible et celles-là ont conduit à un rendement fort, trouve-moi comment pour que je puisse prédire le rendement de nouvelles données d’entrée » (apprentissage supervisé), on lui dirait plutôt « j’ai toutes ces données d’entrée, trouve-moi une façon de les regrouper en différent sous-groupes » (apprentissage non supervisé). Et à l’issue de ce travail non supervisé, on peut imaginer espérer obtenir deux sous-groupes qui seront caractérisés par un rendement faible et un rendement fort. L’apprentissage non supervisé reste la forme d’apprentissage la plus complexe (le modèle se débrouille tout seul et n’est pas guidé) mais vous imaginez bien que c’est la forme d’apprentissage que tout le monde voudrait être capable de réaliser.
Le troisième cas d’apprentissage que je présente succinctement ici est l’apprentissage par renforcement. C’est la forme d’apprentissage qui a été remise en lumière assez récemment avec le succès du programme informatique AlphaGo de l’entreprise DeepMind qui a réussi à battre le champion mondial du jeu de Go, un jeu de stratégie de plateau particulièrement complexe. Dans cette forme d’apprentissage, l’utilisateur apprend au modèle à apprendre. L’algorithme évolue dans un environnement où toutes les actions que l’algorithme peut réaliser sont connues, chacune étant associée à une récompense ou à une pénalité. L’objectif est de réussir à minimiser une fonction de coût (par exemple, dans le cas d’AlphaGo, on pourrait imaginer que c’est de perdre le moins de parties possibles). L’algorithme enchaine donc un ensemble d’actions connues (poser un pion ici, poser un pion là, etc) et chacune des actions est récompensée ou non en fonction du résultat de la partie. Il faut bien comprendre que l’algorithme ne choisit pas nécessairement à enchainer les récompenses directes puisque qu’une récompense peut cacher derrière une grosse pénalité (dans l’exemple des échecs, on peut se dire que c’est bien d’arriver manger un pion mais si c’est pour se faire manger sa reine juste derrière, ce n’est quand même pas terrible…). C’est un peu la façon d’apprendre d’un humain. Par exemple, lorsque l’on apprend à faire du vélo, on peut considérer que notre fonction de coût, c’est de ne pas tomber et d’être capable d’aller assez vite. On connait aussi toutes les actions que l’on peu réaliser (pédaler, se pencher, accélérer, ralentir…). Au fur et à mesure de nos tentatives, on comprend de mieux en mieux comment faire du vélo et on sait ce qu’il ne faut pas faire (par exemple, si on se penche trop, on tombe). Grâce à cette forme d’apprentissage, le programme AlphaGo a été capable de jouer des milliers de parties contre lui-même et de trouver par lui-même comment minimiser sa fonction de coût. Certains experts ont d’ailleurs été surpris de coups joués par le programme informatique, qui se sont révélés particulièrement efficaces par la suite (parce que l’algorithme savait que ces coups conduiraient plus loin dans la partie à une récompense positive). J’insiste sur le fait que, dans cette forme d’apprentissage, l’algorithme doit connaitre toutes les actions qu’il est susceptible de pouvoir réaliser (les règles du jeu en quelque sorte). Si vous lui demandez de jouer aux échecs et que vous ne lui apprenez pas que la reine peut manger en diagonale, il ne pourra pas l’apprendre tout seul… Mais par contre, s’il connait bien toutes les règles du jeu, il pourra tout à fait être capable de réagir à de nouvelles situations (peut être de façon très surprenante d’ailleurs). Ce type d’apprentissage commence aussi à être utilisé dans des jeux vidéos de stratégie en temps réel (par exemple StarCraft) pour tenter de battre des joueurs du monde entier, là où la complexité est absolument ahurissante (chaque joueur ayant sa propre stratégie et pouvant réaliser une quantité d’actions différentes impressionnantes).
Soutenez les articles de blog d’Aspexit sur TIPEEE
Un p’tit don pour continuer à proposer du contenu de qualité et à toujours partager et vulgariser les connaissances =) ?