Le perceptron à une couche cachée et les réseaux de neurones qui en découlent sont l’étape intermédiaire entre le perceptron et les réseaux de neurones profonds à convolutions qui font aujourd’hui toutes ces choses merveilleuses qui nous font penser qu’ils sont finalement peut-être « réellement intelligents ».
Même s’ils sont maintenant un peu obsolètes, ces réseaux élémentaires restent très utiles dans le cadre pédagogique car leur relative simplicité permet d’expliquer des concepts complexes toujours d’actualité et également de mettre en pratique sur des exemples réels.
Quelques concepts essentiels
Les fonctions d’activation et d’erreur
Ces deux fonctions jouent un rôle essentiel dans la convergence des réseaux de neurones.
En ce qui concerne la fonction d’activation, le choix est loin d’être simple et il faut parfois en tester plusieurs avant de trouver celle qui s’adapte le mieux au sujet traité.
En effet, contrairement à la limite posée par le théorème de Cybenko, la fonction sigmoïde n’est pas la seule fonction d’activation qui permette la rétro-propagation du gradient. De nombreuses autres font tout aussi bien l’affaire, comme l’ont démontré d’autres théorèmes ultérieurs.
La fonction sigmoïde a été très utilisée dans les réseaux peu profonds, mais aujourd’hui on lui préfère des fonctions comme le « ReLU » qui donne Y=0 pour une entrée X négative et Y=X pour une entrée X positive ou le « SoftMax »(*), qui transforme les sorties des réseaux de séparation de classes en taux de probabilités dont la somme est égale à 1.
Dans le cas de la fonction d’erreur le choix est plus limité et donc plus simple. Pour les problèmes de régression, on utilise la très classique « distance quadratique ». Pour les problèmes de classification, on a le choix entre la « Cross Entropy Loss »(*) généralement limitée à la séparation de deux classes et la « Negative Log Likelihood Loss »(*) pour la séparation de classes multiples.
(*) L’explication mathématique de ces trois fonctions dépassent mon propos. L’exercice proposé en seconde partie de ce billet permet de mettre en application la fonction d’activation « SoftMax » et la fonction d’erreur « Negative Log Likelihood Loss ». C’est une façon pratique de bien les appréhender.
Les différents types de descente de gradient
La descente de gradient classique
Cette méthode utilisée dès le début de l’algorithmique consiste, pour chaque jeu de paramètres du réseau de neurone, à propager tous les exemples du jeu de test à travers le réseau, calculer l’erreur pour chacun d’entre eux et en faire la moyenne. C’est avec cette moyenne qu’on modifie les paramètres des neurones en utilisant l’algorithme de descente de gradient. C’est ce modèle que j’utilise dans ma vidéo explicative de la descente de gradient.
Cette méthode converge rapidement (puisqu’on utilise le gradient complet à chaque étape) mais elle est très gourmande en calculs, de plus en plus gourmande au fur et à mesure que la taille de la base de données de test augmente.
La descente de gradient stochastique
Du temps de l’algorithmique déjà, des mathématiciens avaient proposé, pour chaque jeu de paramètres, de ne propager qu’un seul exemple pris au hasard dans le jeu de test et de modifier les paramètres de l’algorithme en utilisant cette valeur particulière de l’erreur. C’est d’ailleurs ce que fait Rosenblatt dans son perceptron en 1957.
Mais cette méthode, très économe en calcul, converge de façon un peu erratique (le gradient d’un seul exemple aléatoire ayant peu de chance d’être aligné avec le gradient complet) de sorte que sa vitesse de convergence se révèle beaucoup plus lente, tellement lente que cette méthode restera inutilisée pendant de nombreuses années. œ
Et finalement le vainqueur est….
Les nombreux travaux effectués sur ce sujet ont démontré que le ratio entre vitesse de convergence de la descente de gradient et lourdeur des calculs étaient lié à la taille de la base de données de test, ce qui n’est pas pour nous surprendre.
Longtemps, la dimension limitée des bases de données et l’augmentation rapide de la puissance de calcul des ordinateurs ont fait pencher la balance en faveur de la méthode traditionnelle. La méthode dite « par batches », qui travaille avec des portions de la base de données plutôt que son ensemble, a progressivement pris la relève en optimisant convergence rapide et quantité de calculs.
Mais, avec l’émergence des réseaux de neurones profonds travaillant avec des très grandes bases de données, comme « ImageNet » par exemple, la balance a finalement pencher en faveur de la solution stochastique, majoritairement utilisée aujourd’hui.
Ceux d’entre vous qui se lanceront dans la mise en pratique proposée en seconde partie de ce billet de blog, mettrons en application la méthode « par batches ».
La rétro-propagation du gradient
Sur le plan mathématique, un neurone n’est finalement rien de plus qu’une fonction composée de trois fonctions : un produit scalaire, une fonction d’activation et une fonction d’erreur.
Sur la base du théorème selon lequel la dérivée d’une fonction composée est le produit des dérivées de chacune des fonctions, il suffit, pour faire remonter le gradient d’erreur du réseau de neurone, de multiplier entre eux les gradients des fonctions intermédiaires.
Un exemple valant mieux qu’un long discours, je vous propose d’illustrer mon propos en l’appliquant à un neurone à deux entrées doté d’une fonction d’activation sigmoïde et d’une fonction d’erreur quadratique :
1 les trois fonctions mathématiques impliquées sont donc :
La fonction d’erreur quadratique : (x-y)2/2 et sa dérivée : (x-y)
La fonction sigmoïde : y = 1/(1+e-x) et sa dérivée : y*(1-y)
Et le produit scalaire y=ax+b et sa dérivée : a (par rapport à x), x (par rapport à a) et 1 (par rapport à b)
2 Appliquons ces trois fonctions à notre neurone :
– dErreur/dYcalc = (Ycalc-Yréel)
– dErreur/dYinter = Ycalc*(1-Ycalc)*(Ycalc-Yréel)
et enfin :
– dErreur/dW1 = X1*Ycalc*(1-Ycalc)*(Ycalc-Yréel)
– dErreur/dB = 1*Ycalc*(1-Ycalc)*(Ycalc-Yréel)
– dErreur/dX1 = W1*Ycalc*(1-Ycalc)*(Ycalc-Yréel)
Pour modifier les paramètres de notre neurone, il suffit d’appliquer la méthode de la descente de gradient de la façon suivante (où P est un pas de variation fixe pré-défini) :
W1update = W1 – P*dErreur/dW1
B1update = B1 – P*dErreur/dB
Enfin, si notre neurone est le neurone de sortie d’un réseau, dErreur/dX1 devient le gradient d’erreur du neurone dont la sortie est connectée à l’entrée X1 de notre neurone. Il suffit donc de reproduire les mêmes calculs sur ce neurone pour en mettre à jour les paramètres, et ainsi de suite !
Comme vous pouvez le voir sur cet exemple, ce n’est pas si compliqué en théorie… mais, en pratique, cela demande une extrême précision dans la mise en place de l’architecture du réseau. C’est là que les outils de programmation spécialisés comme PyTorch ou TensorFlow se révèlent indispensables.
MNIST, le « Hello World » des Réseaux de Neurones
Remarque préliminaire : même s’il n’est pas nécessaire d’être un expert en programmation pour faire cet exercice, il convient cependant d’avoir un minimum de connaissances de la programmation en général et du langage Python en particulier.
Afin de mieux appréhender tous ces concepts qui sont, encore aujourd’hui, au cœur des réseaux de neurones les plus complexes, je vous encourage à vous lancer dans la création de votre premier réseau de neurones. Comme pour tous les langages de programmation, il existe un réseau particulier recommandé pour ce premier exercice pratique : le réseau de connaissance de chiffres écrits à la main appelé « MNIST » du nom de la Base de Données OpenSource qu’il utilise.
Vous trouverez de nombreux sites et vidéos sur Internet pour vous guider dans cette tâche car c’est la référence pour les débutants. Toutes ne se valent pas et vous constaterez que l’exercice est nettement plus compliqué qu’un simple « Hello World ! ». Mais si vous avez la ténacité et la patience de suivre ce chemin jusqu’au bout, vous allez vous approprier de façon intime des notions aussi fondamentales que la descente de gradient, la rétro-propagation du gradient, la fonction d’activation et la fonction d’erreur (« SoftMax » et « negative log likelihood loss » en l’occurrence).