Introduction à l’intelligence artificielle

Intelligence artificielle

L’intelligence artificielle (IA) est définie par l’Encyclopédie Larousse comme « l’ensemble des théories et des techniques mises en œuvre en vue de réaliser des machines capables de simuler l’intelligence ».

L’objectif principal de l’IA est donc de mettre en œuvre des techniques mobilisant des théories afin de « remplacer » les capacités d’analyse et de décision de l’humain, bien que ces processus soient encore mal connus, en tentant de se comporter comme le cerveau humain.

L’Intelligence Artificielle mobilise une grande quantité d’informations, issues généralement d’une base de données, classifiées. Par exemple, dans le cas de l’usage de l’intelligence artificielle pour reconnaître l’identité d’une personne, des milliers d’images de chaque individu sont nécessaires, en vue de prédire l’identité d’une personne à partir d’une nouvelle photographie.

Une intelligence artificielle essaye donc de prédire un résultat (reconnaissance faciale, de caractères, de risques financiers etc.). Après une phase d’apprentissage, l’IA pourra être utilisée :

  • Phase d’apprentissage à partir d’une base de données pré-classifiées, basée sur l’extraction de caractéristiques et d’obtention des règles de classifications ;
  • Phase d’utilisation de l’IA à partir des règles de classification pour prédire un résultat depuis des données en entrée.

Il existe principalement deux types d’IA, le Machine Learning, où les règles de classification sont obtenues par une analyse statistique des caractéristiques, et le Deep Learning où les règles de classification des caractéristiques sont obtenues par un réseau de neurones.

Les réseaux de neurones artificiels

Les réseaux de neurones artificiels sont simplement des systèmes inspirés du fonctionnement des neurones biologiques.

Le réseau neuronal multicouche (appelé Perceptron multicouche ou MLP : Multi Layer Perceptron ou encore réseau de propagation avant) est un système capable d’apprendre par l’expérience.

Il s’agit un ensemble de neurones organisés en couche. Le signal d’entrée se propage d’une couche à l’autre jusqu’à la sortie, en activant ou pas les neurones du réseau.

Les neurones sont connectés aux neurones de la couche précédente.  Le neurone traite les données reçues et si la somme pondérée dépasse un seuil un signal est envoyé à la couche suivante.

Le principe d’apprentissage est de regarder la sortie par rapport à ce qui était attendu et de mettre à jour les liaisons entre les neurones (les renforcer ou les inhiber) pour améliorer le résultat final. La sortie est donc une prédiction de la valeur attendue.

Exemples :

  1. En entrée, on donne les pixels d’une image. En sortie, un neurone donne 1 si l’image était un visage et 0 sinon.
  2. On entraîne un réseau à estimer la valeur d’un ou exclusif sur 3 entrées (e1⊕e2⊕e3). La sortie indiquera 0 ou 1, prédiction du résultat de l’opération logique.

Ce type de réseau neuronal est organisé en couches :

  1. Couche d’entrée, composée de neurone d’entrée
  2. Couches cachées (plusieurs couches en général)
  3. Couche de sortie, composée de neurone de sortie, présentant la prédiction du résultat.

Tous les neurones d’une couche sont connectés aux neurones de la couche suivante.

Pour chaque neurone on fait la somme pondérée par le poids des signaux arrivant. Pour les neurones cachés on ajoute le biais et la sortie est activée si le résultat dépasse une certain seuil (activation du neurone ou non).

Par exemple, ci-dessus, en sortie du 1er neurone d’entrée on a : e1

On a donc

\(x_1=f_{seuil}(biais1+(u_0\cdot e_{1}+v_0\cdot e_{2}+w_0\cdot e_{3})) \)

et

\(x_2=f_{seuil}(biais2+(u_1\cdot e_{1}+v_1\cdot e_{2}+w_1\cdot e_{3})) \) \(u\), \(v\) et \(w\) sont les poids de la liaison. C’est ce poids qui va être adapté tout au long de l’apprentissage pour permettre au réseau de prédire efficacement (en général il reste entre 0 et 1 ou -1 et 1).

Le biais permet de décaler la fonction d’activation, ainsi le réseau aura de plus grandes opportunités d’apprentissage.

La fonction d’activation \(f_{seuil}\) représente un seuil à partir duquel le neurone va émettre un signal.

Processus d’apprentissage

La phase d’apprentissage repose sur une méthode mathématique qui permet, à partir du résultat souhaité et du résultat obtenu, de revenir à l’entrée en modifiant les poids et les biais. Lorsque l’on regarde l’erreur commise par le réseau sur une propagation (un essai avec poids et biais fixés), on peut évaluer l’impact qu’a eu un poids en particulier sur cette erreur. Grâce à cette information, on peut également évaluer, si augmenter ou diminuer ce poids va améliorer ou empirer notre erreur. On met alors les poids à jour un par un à partir de cet écart, en partant de la fin et en remontant. C’est ce qu’on appelle la rétro-propagation.

Il faut donc faire de nombreux apprentissage pour obtenir une bonne prédiction.

Exemple du Perceptron simple

Un neurone possède des entrées, chaque entrée possède un poids et la sortie est une fonction du poids et des entrées :


\(S = f(u_1 \cdot e_1 + u_2 \cdot e_2 + u_3 \cdot e_3 )\)

f est appelée fonction d’activation. Elle peut être une fonction  :

  • telle que \(f(x)=0\) si \(x < seuil\) et \(f(x)=1\) sinon (fonction de Heaviside),
  • telle que \(f(x)=0\) si \(x < seuil1\), \(f(x)=1\) si \(x > seuil2\) et \(f(x)=x\) si \(seuil1 \leq x \leq seuil2\) (fonction linéaire),
  • etc. (fonction sigmoïde, Tangente hyperbolique …)

Algorithme d’apprentissage :

  1. Initialiser aléatoirement les poids
  2. Calculer la sortie pour un couple d’entrée donné
  3. Calculer la prédiction, si la différence entre la valeur attendue et la prédiction est positive alors \(erreur_{prédiction}\) = 1, sinon erreur =0
  4. Mettre à jour les poids : \(\Delta u_{i} = \eta (S_{attendue} – erreur) \times e_{i}\) avec \(\eta\) le taux d’apprentissage (\(0<\eta<1\))
  5. Recommencer jusqu’à que l’erreur de prédiction soit acceptable

Remarque : \(\eta\) représente le taux d’apprentissage. S’il est mal choisit il peut y avoir oscillation ou le calcul très long !

Exemple AND

On cherche à ce que le Perceptron simple ci-dessous reproduise la fonction logique AND :

Cela revient à déterminer \(u_1, u_2 et u_3\) pour obtenir la table de vérité suivante :

\(e_2\) \(e_3\) \(S_{attendue}\)
0 0 0
0 1 0
1 0 0
1 1 1

En prenant comme fonction d’activation une fonction de Heaviside telle que \(h(x)=0\) si \(x < 0\) et \(h(x)=1\) si \(x \geq 0\), on peut déterminer les poids \(u_1\) et \(u_2\) en utilisant l’algorithme précédent d’apprentissage :

  1. Initialiser aléatoirement les poids :
    • \(u_1=0,1 ; u_2=0,2 ; u_3=0,05\)
  2. Calculer la sortie pour un couple d’entrée donné :
    • \(e_2=0 ; e_3=0\) alors \(S_{prediction_1}=0,1 \times 1 + 0,2 \times 0 + 0,05 \times 0 = 0,1 > 0 \) soit \(S_{prediction_1} = h(0,1) = 1\)
  3. Calculer l’erreur de prédiction : ici
    • \(erreur_{prediction_1}=S_{attendue} – S_{prediction_1} = 0 – 1 = -1\)
  4. Mettre à jour les poids :
    • \(\Delta u_{1} = \eta \times (-1) \times e_1 = 0,1 \times (-1) \times 1 = -0,1\) car \(e_1=1\) pour cet apprentissage.
    • On adapte \(u_1=u_1+ \Delta u_1 = 0,1 + (-0,1) = 0\)
    • \(\Delta u_{2} = \eta \times (-1) \times e_2 = 0,1 \times (-1) \times 0 = 0\) car \(e_2=0\) pour cet apprentissage.
    • On adapte \(u_2=u_2+ \Delta u_2 = 0,2+0 = 0,2\)
    • \(\Delta u_{3} = \eta \times (-1) \times e_3 = 0,1 \times (-1) \times 0 = 0\) car \(e_3=0\) pour cet apprentissage.
    • On adapte \(u_3=u_3+ \Delta u_3 = 0,05+0 = 0,05\)
  5. Recommencer jusqu’à que l’erreur de prédiction soit acceptable

 

Code Python

import random

#Poids du perceptron
u1=random.uniform(0,1)
u2=random.uniform(0,1)
u3=random.uniform(0,1)
#Entrées
e1=[1,1,1,1]
e2=[0,0,1,1]
e3=[0,1,0,1]
#Sorties de la fonction AND
s=[0,0,0,1]
#Sorties calculées et erreurs de prédiction
sp=[0,0,0,0]
err=[1,1,1,1]
#Taux d'apprentissage
n=0.1

def h(x):
#fonction de Heaviside
    if x >= 0:
        return 1
    else:
        return 0

for i in range(4):

    while abs(err[i])>0.01:
        #Prédiction de la sortie
        sp[i]=h(u1*e1[i]+u2*e2[i]+u3*e3[i])
        #Calcul de l'erreur
        err[i]=s[i]-sp[i]
        #Mise à jour des poids
        u1 += n*err[i]*e1[i]
        u2 += n*err[i]*e2[i]
        u3 += n*err[i]*e3[i]

print("Poids (u1,u2 et u3) : ",u1,u2,u3)
print("Sorties : ")
for i in range(4):
    print(e2[i]," AND ",e3[i]," = ",sp[i]) 

Exemple du Perceptron multicouche

Exemple XOR

On peut réaliser le OU exclusif avec un réseau de neurones. voici la table de vérité :

A B Q
0 0 0
0 1 1
1 0 1
1 1 0

On voit que le réseau de neurones proposé ci-dessous, avec ces poids et biais correspond bien à la table de vérité ci-dessus.

Code Arduino – XOR

La bibliothèque NeuralNetwork pour carte Arduino permet d’effectuer les calculs de propagation mais surtout de rétro-propagation, c’est à dire de faire l’apprentissage d’un réseau de neurones de type MLP.

La fonction FeedForward alimente le réseau de neurones avec les entrées et retourne les sorties du réseau.

La fonction BackProp effectue l’apprentissage du réseau de neurones (ajuste les poids et les biais) par comparaison des valeurs souhaitées en sortie et celles calculées.

On va implémenter, sur la carte Arduino, un réseau de neurones à 3 couches, avec 2 neurones d’entrée, 1 de sortie, et 4 neurones sur la couche cachée.

#define NumberOf(arg) ((unsigned int) (sizeof (arg) / sizeof (arg [0]))) //Calcule le nombre de couche (ici 3)
#include <NeuralNetwork.h>

/*
3 couches : 
1ere couche avec 2 neurones d'entrée
2e couche avec 4 neurones cachées
3e couche avec 1 neurone de sortie
On peut mettre plusieurs couches cachées et augmenter le nombre de neurones, mais attention à la place en mémoire !
*/
const unsigned int layers[] = {2,4,1}; 
float *outputs; // sorties de la couche de sortie (ici une seule sortie)

const float inputs[4][2] = { //Entrées, ici les valeurs de A et B
  {0, 0}, //0 xor 0 = 0
  {0, 1}, //0 xor 1 = 1
  {1, 0}, //1 xor 0 = 1
  {1, 1}  //1 xor 1 = 0
};
// valeurs souhaitées en couche de sortie ici Q (résultats du xor sur les entrées) :
const float expectedOutput[4][1] = {{0},{1},{1},{0}}; 

NeuralNetwork NN(layers,NumberOf(layers)); // Creation du réseau de neurones

void setup() {
  Serial.begin(115200);
  
  //Apprentissage du réseau de neurones avec 3000 retropropagations 
  for(int i=0; i < 3000; i++) { // Apprentissage

    for (int j = 0; j < NumberOf(inputs); j++) {
    NN.FeedForward(inputs[j]); // Propagations des entrées dans le réseau et calcul des sorties
    NN.BackProp(expectedOutput[j]); // Rétropropagation et ajustement des poids et biais
  }
  }
  
  //Affichage des résultats par propagation des entrées après apprentissage
  for (int i=0; i < NumberOf(inputs); i++) {
    outputs = NN.FeedForward(inputs[i]);  //Propagation de la données n°i depuis la couche d'entrée et calcul de la sortie
    Serial.println(outputs[0], 7); // Affichage de la sortie avec 7 chiffres significatifs
  }
  NN.print(); // Affichage des poids et biais des couches cachées
}

void loop() {
    outputs = NN.FeedForward(inputs[0]);//Entrées : 0, 0 et 0 souhaité en sortie
    Serial.print("0 xor 0 = ");
    Serial.println(outputs[0], 7);
    
    outputs = NN.FeedForward(inputs[1]);//Entrées : 0, 1 et 1 souhaité en sortie
    Serial.print("0 xor 1 = ");
    Serial.println(outputs[0], 7);

    outputs = NN.FeedForward(inputs[2]);//Entrées : 1, 0 et 1 souhaité en sortie
    Serial.print("1 xor 0 = ");
    Serial.println(outputs[0], 7);

    outputs = NN.FeedForward(inputs[3]);//Entrées : 1, 1 et 0 souhaité en sortie
    Serial.print("1 xor 1 = ");
    Serial.println(outputs[0], 7);
    
    delay(1000);
}

TP reconnaissance du nombre affiché par un afficheur 7 segments

Proposez un programme Arduino utilisant la bibliothèque NeuralNetwork permettant l’apprentissage puis le test d’un réseau de neurones qui traduira la table de vérité suivante :

a b c d e f g N (décimal)
1 1 1 1 1 1 0 0
0 1 1 0 0 0 0 1
1 1 0 1 1 0 1 2
1 1 1 1 0 0 1 3
0 1 1 0 0 1 1 4
1 0 1 1 0 1 1 5
0 0 1 1 1 1 1 6
1 1 1 0 0 0 0 7
1 1 1 1 1 1 1 8
1 1 1 0 0 1 1 9

La bibliothèque NeuralNetwork donne des valeurs de sortie de type float < 1. Il faudra donc ajuster les sorties souhaitées par des valeurs < 1, soient 0.1 pour 1 0.2 pour 2 etc.

Correction

Attention, l’apprentissage dure environ 7.5 minutes sur Arduino Uno !

#define NumberOf(arg) ((unsigned int) (sizeof (arg) / sizeof (arg [0]))) //calculates the amount of layers (in this case 4)

#include <NeuralNetwork.h>

unsigned int layers[] = {7, 9, 9, 1}; // 4 couches neurales. La 1ere couche avec 7 neurones d'entrée. Les 2e et 3e couches contenant chacune 9 neurones cachées et la 4e couche avec 1 neurone de sortie
float *outputs; // Sorties pour la 4e couche (ici 1 sortie)

//Valeurs d'entrée
const float inputs[10][7] = {
  { 1, 1, 1, 1, 1, 1, 0 },  // 0
  { 0, 1, 1, 0, 0, 0, 0 },  // 1
  { 1, 1, 0, 1, 1, 0, 1 },  // 2
  { 1, 1, 1, 1, 0, 0, 1 },  // 3
  { 0, 1, 1, 0, 0, 1, 1 },  // 4
  { 1, 0, 1, 1, 0, 1, 1 },  // 5
  { 0, 0, 1, 1, 1, 1, 1 },  // 6
  { 1, 1, 1, 0, 0, 0, 0 },  // 7 
  { 1, 1, 1, 1, 1, 1, 1 },  // 8
  { 1, 1, 1, 0, 0, 1, 1 }   // 9
};

// Valeurs de sorties (/10 pour être < 1) souhaitées au niveau de la 4e couche du réseau neuronal. 
// Va servir pour l'apprentissage du réseau neuronal
const float expectedOutput[10][1] = {{0.0}, {0.1}, {0.2}, {0.3}, {0.4}, {0.5}, {0.6}, {0.7}, {0.8}, {0.9}};


NeuralNetwork NN(layers, NumberOf(layers)); // Creation du reseau de neurones

void setup()
{

  Serial.begin(115200);
  Serial.println("Apprentissage...");
  //Entrainement du réseau de neurones avec 8000 boucle d'apprentissages
  for (int i = 0; i < 8000; i++)
  {
    for (int j = 0; j < NumberOf(inputs); j++)
    {
      NN.FeedForward(inputs[j]); // Propagation avant (feedforward) des données depuis les neurones d'entrées (1ere couche) vers la couche de sortie (ici 1 donnée sur la 4e couche)
      NN.BackProp(expectedOutput[j]); // Rétropropagation pour ajuster l'apprentissage.
    }
  }
  Serial.println("Apprentissage terminé");

  //Après apprentissage on vérifie. 
  for (int i = 0; i < NumberOf(inputs); i++) //On parcoure les entrées
  {
    outputs = NN.FeedForward(inputs[i]); // Propagation des données dans le réseau et calcul de la sortie
    Serial.println(10*outputs[0]); //Afficher les sorties (comme on a fait sortie_souhaitée/10 on multiplie par 10)
  }

  NN.print(); // Afficher les poids et les biais pour chaque couche

}

void loop() {
  for (int i = 0; i < NumberOf(inputs); i++){

    outputs = NN.FeedForward(inputs[i]);
    for ( int j = 0; j < 7; j++ ) {
      Serial.print(inputs[i][j],0);
    }
    Serial.print(" = ");
    Serial.println(10*outputs[0], 0);
  }
  delay(2000);
}

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *