TP 4 : Réseaux de Neurones

Introduction aux concepts de l’Intelligence Artificielle

14 Mars 2024

  1. Découvrir les réseaux de neurones :
    • Connaître la définition d’un neurone formel.
    • Connaître la définition d’un réseau de neurones.
    • Savoir évaluer un réseau de neurones sur une entrée donnée.
  2. Comprendre les grands principes permettant d’apprendre un réseau de neurones à partir de données :
    • Comprendre le fonctionnement général de la descente de gradients.
    • Comprendre comment elle est utilisée dans les réseaux de neurones.

Introduction

Dans sa forme la plus pure, un réseau de neurones (neural networks en anglais) est simplement une structure de données permettant de représenter une fonction f: ℝn → ℝm comme une composition complexe de “petites” fonctions . Cette structure de données existe depuis les débuts de l’intelligence artificielle en tant que discipline de recherche : elle était vue à l’origine comme une modélisation informatique du cerveau et de son fonctionnement et une croyance répandue dans certaines communautés de recherche dans les années 50 était qu’on pourrait créer un “cerveau électronique” imitant le cerveau humain. On s’est rapidement rendu compte que cette vision était utopique et que les ordres de grandeur qu’on savait traiter à l’époque (et qu’on sait encore traiter aujourd’hui) sont loin des ordres de grandeur du cerveau humain. L’analogie entre les réseaux de neurones et le cerveau humain a laissé sa marque dans le vocabulaire utilisé quand on parle de réseau de neurones. Cependant, elle atteint rapidement ses limites en pratique et nous essaierons ici de ne pas trop utiliser l’intuition des réseaux de neurones comme un cerveau électronique mais de voir cela comme des objets mathématiques et informatique permettant de calculer des choses.

Si on a rapidement abandonné l’espoir d’imiter le cerveau humain, les réseaux de neurones restent des structures de données très intéressantes en pratique pour l’intelligence artificielle pour les raisons suivantes :

Aujourd’hui ils sont utilisés pour des tâches d’apprentissage très variées : reconnaissance d’images, génération de texte, jeux, traduction automatique, robotique etc.

Les réseaux de neurones représentent une famille de structure très riches, puissantes et très utilisées en pratique. Cependant, nous sommes encore loin d’avoir une méthode complètement générale permettant d’apprendre n’importe quoi à partir d’exemples. L’utilisation des réseaux de neurones demande une bonne compréhension du problème d’apprentissage sous-jacent pour assurer qu’on choisit une structure adaptée au problème qu’on veut résoudre, qu’on normalise correctement les données pour que le réseau généralise suffisamment bien. On se repose en général pour cela sur des observations empiriques passées sans parfois exactement comprendre pourquoi cela marche. Les réseaux de neurones souffrent aussi d’un désavantage majeur par rapport aux arbres de décision vus au TP précédent : il est difficile de comprendre ce que fait le réseau une fois qu’on l’a appris depuis les données. On peut vérifier empiriquement qu’il donne de bons résultats sur les données que l’on veut traiter, mais on peut difficilement acquérir une intuition de pourquoi cela marche ni de garanties formelles.

Dans ce TP, nous n’arborderons pas encore ces aspects qui seront approfondis l’an prochain dans le cours Exploiter les ensembles de données. Le but de ce TP est de se familiariser avec le concept de réseau de neurones et d’essayer de comprendre à quel point leur fonctionnement et leur précision dépendent de choix structurels en amont.

Le neurone formel

Un neurone formel est une fonction N: ℝn → ℝm de la forme :

N(x1,…,xn) = F(∑i = 1..nwixi+b).

C’est donc la composition d’une fonction affine iwixi + b et d’une fonction F appelée la fonction d’activation. Les coefficients wi sont appelés les poids et b est appelé le biais du neurone.

On représente généralement un neurone formel graphiquement comme suit :

g F F w1F w₁ w1F->F wpF ... wpF->F wnF wₙ wnF->F b b b->F x₁ x₁ x₁->w1F p ... p->wpF xₙ xₙ xₙ->wnF

Plusieurs fonctions d’activation ont été considérées dans la litérature. On donne ici une liste non-exhaustive :

2024-02-20T15:11:53.237187 image/svg+xml Matplotlib v3.6.3, https://matplotlib.org/
2024-02-20T15:10:41.668475 image/svg+xml Matplotlib v3.6.3, https://matplotlib.org/
2024-03-10T13:48:52.459539 image/svg+xml Matplotlib v3.6.3, https://matplotlib.org/
2024-03-10T13:49:43.308589 image/svg+xml Matplotlib v3.6.3, https://matplotlib.org/

Le problème des fonctions signe et H est qu’elles ne sont pas continues, ce qui empêche d’utiliser de nombreux outils mathématiques provenant de la théorie de l’optimisation. On peut voir la fonction sigmoïde comme une “approximation” continue de la fonction signe et la fonction ReLU comme une “approximation” de la fonction Heaviside.

On considère le neurone formel N(x1,x2,x3) suivant :

g F σ w1F 2.5 w1F->F w2F 1 w2F->F w3F 0.2 w3F->F 1 1 1->F x₁ x₁ x₁->w1F x₂ x₂ x₂->w2F x₃ x₃ x₃->w3F

Calculez :

  1. N(0,0,0).
  2. N(1,1,1).
  3. N(0,1,0).

Même question pour le neurone

g F ReLU w1F 2.5 w1F->F w2F -1 w2F->F w3F 0.2 w3F->F b -3 b->F x₁ x₁ x₁->w1F x₂ x₂ x₂->w2F x₃ x₃ x₃->w3F

Réseaux de neurones

Un réseau de neurones est une composition successive de neurones formels. Par exemple (les biais ont été enlevés pour alléger le dessin) :

g F1 ReLU w1F3 2 F1->w1F3 w1F4 -1 F1->w1F4 F2 ReLU w2F3 1 F2->w2F3 w2F4 -1 F2->w2F4 F3 σ y₁ y₁ F3->y₁ F4 ReLU y₂ y₂ F4->y₂ w1F1 2.5 w1F1->F1 w2F1 -1 w2F1->F1 w1F2 -1 w1F2->F2 w2F2 1 w2F2->F2 w1F3->F3 w2F3->F3 w1F4->F4 w2F4->F4 x₁ x₁ x₁->w1F1 x₁->w1F2 x₂ x₂ x₂->w2F1 x₂->w2F2

Le réseau précédent prend deux valeurs en entrée, x1 et x2 et produit deux valeurs y1 et y2. Calculez les valeurs y1 et y2 pour le réseau de neurones ci-dessus lorsqu’on a les valeurs d’entrée suivantes :

  1. x1 = 0, x2 = 1
  2. x1 = 1, x2 = 1
  3. x1 = 1, x2 = 0

Pour des raisons pratiques, tous les réseaux de neurones que l’on considèrera sont organisés par couches (layers en anglais). La première couche, appelée couche d’entrée (input layer), est la couche qui contient les paramètres d’entrée que l’utilisateur peut choisir. La dernière couche, appelée couche de sortie (output layer), sont les valeurs que le réseau calcule en fonction des valeurs données en entrée. Les couches intermédiaires sont appelées couches cachées (hidden layers) puisque leurs valeurs durant le calcul ne sont pas explicitement données à l’utilisateur.

Même si en théorie on pourrait faire autrement, en pratique, on ne considère que des réseaux où tous les neurones ont la même fonction d’activation. De plus, chaque neurone de la couche i est supposé être connecté à tous les neurones de la couche i + 1. Cela veut dire que chaque neurone de la couche i + 1 reçoit si valeurs en entrée, où si est le nombre de neurone dans la couche i, une entrée par neurone de la couche précédente. Un neurone v de la couche i + 1 calcule donc une fonction de la forme F(∑j ≤ siwv, jaj)aj est la valeur de sortie du neurone j de la couche i et wv, j est un poids d’entrée spécifique au neurone v.

g cluster_c1 cluster_input F1 ReLU F2 ReLU w1F1 w₁₁ w1F1->F1 w2F1 w₂₁ w2F1->F1 w3F1 w₃₁ w3F1->F1 w1F2 w₁₂ w1F2->F2 w2F2 w₂₂ w2F2->F2 w3F2 w₂₃ w3F2->F2 none11 ... v₁ v₁ none11->v₁ none12 ... none12->v₁ none13 ... none13->v₁ none21 ... v₂ v₂ none21->v₂ none22 ... none22->v₂ none23 ... none23->v₂ none31 ... v₃ v₃ none31->v₃ none32 ... none32->v₃ none33 ... none33->v₃ v₁->w1F1 v₁->w1F2 v₂->w2F1 v₂->w2F2 v₃->w3F1 v₃->w3F2
Une couche contenant 3 neurones connectée à une couche contenant 2 neurones.

On dira que le nombre de neurones d’une couche est la taille d’une couche.

Combien y’a-t-il de poids différents entre une couche de taille 3 (contenant 3 neurones) et une autre couche de taille 2 (contenant 2 neurones) ? On peut s’aider de la figure ci-dessus.

Même question : entre une couche de taille 3 et une couche de taille 10 ? Entre une couche de taille s et une couche de taille s ?

g cluster_input cluster_c1 cluster_output C2 Couche 3 Cp ... C2->Cp y₁ y₁ Cp->y₁ y₂ y₂ Cp->y₂ yp ... Cp->yp yₘ yₘ Cp->yₘ x₁ x₁ w1F1 w₁₁ x₁->w1F1 w1F2 w₁₂ x₁->w1F2 w1Fp ... x₁->w1Fp w1Fm w₁ₘ x₁->w1Fm x₂ x₂ w2F1 w₂₁ x₂->w2F1 w2F2 w₂₂ x₂->w2F2 w2Fp ... x₂->w2Fp w2Fm w₂ₘ x₂->w2Fm p ... wpF1 ... p->wpF1 wpF2 ... p->wpF2 wpFp ... p->wpFp wpFm ... p->wpFm xₙ xₙ wnF1 wₙ₁ xₙ->wnF1 wnF2 wₙ₂ xₙ->wnF2 wnFp ... xₙ->wnFp wnFm wₙₘ xₙ->wnFm n1 ReLU n1->C2 n2 ReLU n2->C2 np ... np->C2 nm ReLU nm->C2 w1F1->n1 w2F1->n1 wpF1->n1 wnF1->n1 w1F2->n2 w2F2->n2 wpF2->n2 wnF2->n2 w1Fp->np w2Fp->np wpFp->np wnFp->np w1Fm->nm w2Fm->nm wpFm->nm wnFm->nm
Un réseau de neurones où tous les neurones d’une couche sont connectées à tous les neurones de la couche suivante.

Dans ce cas, un réseau de neurones est entièrement caractérisé par :

  1. La fonction d’activation des neurones,
  2. Le nombre de couches c (donc avec c − 2 couches cachées)
  3. Les tailles (s1,…,sc) de chacune des couches (s1 est le nombre d’entrées, sc le nombre de sorties)
  4. L’ensemble W des poids qui sont entre chaque neurone.

Un réseau de neurones de ce type calcule donc une fonction N: ℝs1 → ℝsc, de la forme N1 ∘ N2 ∘ … ∘ Nc − 1Ni est la fonction Ni: ℝsi → ℝsi + 1 calculée par la couche i. On peut d’ailleurs voir que chaque Ni est une fonction affine composée avec la fonction d’application F. Cela explique en partie pourquoi les GPU forment une architecture très adaptée aux réseaux de neurones puisqu’ils sont construits pour faire rapidement des calculs matriciels, ce qui est exactement ce dont on a besoin pour calculer des fonctions affines.

Empiriquement, on a pu vérifier que les performances des réseaux de neurones s’améliorent avec la profondeur du réseau, c’est-à-dire le nombre de couches du réseau. Les puissances de calcul actuel combiné avec les architectures de type GPU ont permis d’entraîner des réseaux de neurones assez profond sur des grosse quantité de données pour résoudre des tâches par apprentissage qu’on pensait impossible jusqu’ici. Cette approche porte le nom de deep learning.

Apprentissage supervisé

Dans cette partie, on va rapidement expliquer comment on peut utiliser les réseaux de neurones pour réaliser une tâche d’apprentissage supervisé pour de la classification. Le but est de vous donner le vocabulaire nécessaire pour comprendre l’outil présenté à la partie suivante.

Pour un problème où on cherche à classifier des vecteurs de dimension n dans m classes, on va choisir un réseau de neurones avec une couche d’entrée de taille n et une couche de sortie de taille m. On a donc un réseau qui calcule une fonction de N: ℝn → ℝm. Implicitement, on voit N(x) comme une fonction associant un score à chaque classe. On veut que N(x)c soit grand lorsque x est de classe c et petit sinon. On voit donc N(x) comme une fonction calculant (de façon non normalisée) la probabilité que x soit de classe c.

Lorsqu’on veut apprendre avec un réseau de neurones, on commence par fixer sa structure (on parle parfois d’hyperparamètres) : on choisit les fonctions d’activations, le nombre et la taille de chaque couche cachée. On choisit aussi de façon aléatoire les poids W0 du réseau. On démarre donc avec une fonction N(x;W0). On mesure à quel point cette fonction est bonne sur les données D qu’on a dans notre jeu de données avec une fonction L(W0) qui mesure l’erreur (loss en anglais) de N(⋅;W0) sur les données. Le but est de trouver W* tel que L(W*) est minimale.

Quelque soit la fonction d’erreur L choisie, la phase d’apprentissage est toujours la même : on cherche à trouver W* qui minimise L(W). On utilise pour cela la descente de gradient. Pour cela, on utilise le fait que L(W) est (en général) une fonction dérivable selon chaque coordonnée de W. La descente de gradient est la méthode consistant à mettre à jour W incrémentalement de la façon suivante converge en général vers un minimum (local) :

Wi + 1 = Wi − αL(Wi).

Dans cet algorithme, α est appelé le pas (learning rate en anglais) et indique à quelle vitesse on suit le gradient. Un α trop grand risque de donner de mauvais résultats car on risque d’osciller autour d’une valeur optimale, un α trop petit convergera trop doucement. On choisit en général α de façon adaptative (intuitivement, grand au début puis de plus en plus petit).

Aller plus loin

Fonction d’erreur

Une fonction d’erreur souvent considérée pour la classification est la fonction softmax définie par :

L(W) = (1/|D|)∑(x,c) ∈ D  − log ℓ(x,c)

ℓ(x,c) = eN(x;W)c/∑zeN(x;W)z. Implicitement, on suppose que le vecteur de sortie N(x;W) contient, pour chaque classe c, le logarithme de la probabilité que x soit dans la classe c. On souhaite donc que ℓ(x,c) soit grand lorsque N “classe bien” x, c’est-à-dire que N(x,c) est grand.

Calcul efficace du gradient

Nous ne rentrons pas trop dans les détails ici mais un aspect permettant aux réseaux de neurones de passer à des grandes échelles est la façon dont on peut faire une descente de gradients sur L(W). En effet, on peut montrer que calculer L(W) peut être fait très rapidement via un algorithme connu sous le nom de backpropagation. L’idée est qu’on peut calculer le gradient n pour chaque neurone n du réseau dans une couche i à partir des gradients n des neurones dans la couche i + 1. On propage donc cette information depuis la dernière couche vers la première pour obtenir L(W) rapidement puis on met à jour W. Ces deux opérations peuvent être traduites en des opérations matricielles, accentuant encore l’avantage matériel d’utiliser des GPU.

Gradient stochastique

Dans quasiment tous les cas, l’erreur L(W) s’exprime sous la forme (1/n)∑iℓ(xi,ci,W) pour un jeu de données D = (x1,c1), …, (xn,cn), c’est-à-dire que L(W) est une moyenne des erreurs commises sur chaque élément du jeu de données. Lorsque n est grand, calculer L(W) explicitement est trop coûteux. Plutôt que de calculer L(W), on préfère parfois mettre à jour W en lui ajoutant  − α∇ℓ(xi,ci,W) plutôt que  − αL(W) qui est plus long à calculer. L’algorithme du gradient stochastique consiste à mettre à jour W ainsi : on commence par mélanger le jeu de données aléatoirement et on définit Wi + 1 = Wi − α∇ℓ(xi,ci,W). Une fois qu’on a atteint le dernier élément du jeu de données, on le mélange à nouveau et on recommence.

Le processus de mettre les poids à jour en traversant tout le jeu de données s’appelle une epoch. En général, on entraîne un réseau de neurones epoch par epoch, jusqu’à avoir une erreur stable.

Expériences

On va faire quelques expériences sur le site http://playground.tensorflow.org/ qui permet de joueur avec des petits réseaux de neurones et des petits jeux de données. Dans les questions suivantes, on gardera les options par défaut du logiciel sauf lorsqu’on vous demande explicitement de les modifier.

Entraîner un réseau sans couche cachée pour le jeu de données .

Essayer d’entraîner un réseau avec une couche cachée de taille 3 avec fonction d’activation ReLU et un pas (learning rate) de 1 pour le jeu de donnée Exclusive Or . Même question mais avec un pas de 0.1 (attention à bien réinitialiser votre réseau avant de le réentraîner).

Qu’observez-vous ? Comment peut-on l’expliquer ?

Essayer d’entraîner un réseau avec deux couches cachées de taille 2 avec fonction d’activation linéaire (la fonction d’activation est l’identité) et un pas (learning rate) de 0.1 pour le jeu de donnée Exclusive Or . Même question avec un pas 3.

Quel est l’ordre de grandeur de l’erreur que vous obtenez à la fin pour chaque pas ? Comment peut-on l’expliquer ?

Pour chaque jeu de données proposés dans le playground, quels sont ceux qui semblent rapidement converger vers un classifieur qui fait une petite erreur pour le cas d’un réseau avec une couche cachée de contenant 1 neurones avec une fonction d’activation de type ReLU et un learning rate de 0.03 ? Même question avec une couche cachée contenant 2, puis 3 neurones puis 8 neurones.

Essayez de trouver une structure de réseau de neurones permettant de classifier la spiral avec une petite erreur. Quelle est la meilleure erreur que vous arrivez à obtenir ?