Réseau de neurones : perceptron monocouche

mercredi 4 décembre 2019
par  Emmanuel Adam
popularité : 6%

Un très simple code montrant l’adaptation des poids pour un réseau de neurone.

Pour aller plus loin, voir https://github.com/EmmanuelADAM/IntelligenceArtificiellePython

perceptron monocouche

Un perceptron monocouche est un réseau de neurones contenant n neurones en entrée et m neurones en sortie.

Ici pour simplifier l’écriture du code, nous prendront un réseau n-1 (n neurones en entrée et 1 en sortie)

Ce type de réseau est capable d’apprendre à classer des éléments en 2 catégories.
par exemple, il est capable d’apprendre à détecter à partir des mots trouvés dans le sujet d’un mail s’il s’agit d’un spam ou non.

apprendre sur base de 1 exemple

Tout d’abord, voyons comment modifier les coefficients des synapses reliant le neurone de sortie aux neurones d’entrée.

Le schéma est le suivant :

I1 -->  w1  \
I2 -->  w2  --> calcul s = w1.I1 + w2.I2 + w3.I3 --> sortie f(s)
I3 -->  w3  /
  • où I1, I2, I3 sont des entrées dans le domaine [0,1]
  • où w1, w2, w3 sont des coefficients réels, appelés poids (weights)
  • et où f est une fonction dite d’activation dont la sortie est dans le domaine [0,1]
    selon la valeur de s, f(s) détermine la valeur de sortie du neurone

Nous avons donc besoin d’un vecteur colonne d’entrées, et d’un vecteur ligne de poids.
Les entrées sont fournies, les poids sont ceux du perceptron, on peut donc commencer à définir la classe suivante :

  1. /**perceptron with the input layer of n neurons, and only 1 neuron in output
  2.  * 1 = true, 0 = false*/
  3. public class PerceptronN1 {
  4.   /**in the output neuron : weights on the synapses connected to the input neurons*/
  5.   double[] weights;
  6.  
  7.   /**perceptron with the input layer of n neurons, and only 1 neuron in output
  8.    * initialize the vector of weights with n values between 0 and 1
  9.    * @param n nb of neurons in the input layer*/
  10.   PerceptronN1(int n) {
  11.     weights = new double[n];
  12.     Arrays.setAll(weights, i->Math.random());
  13.   }

Intéressons-nous la fonction d’activation $f(x) : \Re \rightarrow [0,1]$, celle-ci peut avoir plusieurs définitions, ce qui joue sur la vitesse d’apprentissage :

  • à seuil, la plus brutale et moins performante, passe directement de 0 à 1 dès que x dépasse 0.5
  • linéaire : =0 si x<0, =1 si x>1, et suivre la droite f(x) = x entre 0 et 1
  • sigmoïde : $f(x) = \dfrac{1}{1+e^{-a.(2x -1)}}$
    • f(x) représente une courbe en S (d’où son nom), où ’a’ influence la pente du s ; f(x) tend vers 0 pour x < 0 et tend vers 1 pour x > 1
  • etc...

Appliquer le réseau

A partir d’une série de données en entrée, on calcule la somme pondérée (I x W), puis on applique f sur cette somme :

  1.   /**Compute the weighted sum and apply the activation function on it
  2.    * @param inputs the n values that come from the input layer
  3.    * @return the result of the activation function applied to the weighted sum of the weights by the inputs*/
  4.   double feedForward(double[] inputs) {
  5.     double sum = 0;
  6.     for (int i = 0; i < weights.length; i++) sum += inputs[i]*weights[i];
  7.     return activation(sum);
  8.   }
  9.  
  10.   /**activation function (sigmoid) : f(x) = 1/(1 + e^(-a * (2.x- 1)))
  11.    * @param x the sum
  12.    * @return the activation value (between 0 and 1)*/
  13.   double activation(double x) {
  14.     double a = 10;
  15.     return (1/(1 + Math.pow(Math.E, -a*(2*x-1))));
  16.   }

S’entraîner : Appliquer le réseau et se corriger

Bien sûr appliquer le réseau avec des coefs qui ont été tirés aléatoirement risque peu de donner résultat attendu.

Il faut donc calculer l’erreur pour chaque poids, c’est à dire la différence entre la sortie calculée et la sortie attendue.

  • Plus l’erreur est grande, plus la correction doit être forte,
  • ce coeff d’apprentissage peut être calculée en utilisant la fonction f
    • La formule est $w_i \gets w_i + erreur \times f(|erreur|) \times I_i $

La fonction suivante réalise cela ; à partir de données d’entrée et du résultat attendu, elle calcule l’erreur et modifie chaque poids en fonction.
Puis la fonction retourne l’erreur émise à partir des nouveaux poids.

  1.    /**train once to learn how to reach the desired output from the given inputs
  2.    * @param inputs the n inputs from the input layer
  3.    * @param theoricalOutput the desired output from the given inputs
  4.    * @return the error (theoretical output - computed output)
  5.    * */
  6.   double train(double[] inputs, double theoreticalOutput) {
  7.     double computedOutput = feedForward(inputs);
  8.     double error = theoreticalOutput - computedOutput;
  9.     double coefLearning = activation(Math.abs(error)) ;
  10.     Arrays.setAll(weights,i-> weights[i] +  error  * coefLearning * inputs[i]);
  11.     computedOutput = feedForward(inputs);
  12.     return (theoreticalOutput - computedOutput);
  13.   }

Apprendre ...

Le principe de l’apprentissage consiste à répéter plusieurs fois l’entraînement jusqu’à obtenir un taux d’erreur satisfaisant, ou jusqu’à ce qu’il n’y a plus d’évolution (l’erreur est stabilisée).
La fonction suivante définit :

  • les entrées et la sortie attendue (obtenir 1 à partir des entrées (1,0) )
  • la marge d’erreur acceptable (0.5 %)
  1. /**test the perceptron with only 1 sample*/
  2. static void testPerceptronOneSample()
  3. {
  4.   double []inputs = new double[]{1,0};
  5.   double desiredOutput = 1;
  6.   double acceptation = 0.005;
  7.   PerceptronN1 p = new PerceptronN1(2);
  8.   double error = desiredOutput - p.feedForward(inputs);
  9.   double previousError = 2;
  10.   while (error>acceptation && !moreOrLessEquals(error, previousError, 10)) {
  11.     previousError = error;
  12.     error = Math.abs(p.train(inputs, desiredOutput));
  13.   }
  14.   System.out.printf(", outputs = %.4f , error = %.4f\n" , p.feedForward(inputs), error);
  15. }

La fonction ’moreOrLessEqual’ teste si deux réels a et b sont à peu près égaux, selon une précision donnée.

  1. /**test of equality between two double
  2.  * @param a a double
  3.  * @param b another double to be compared to a
  4.  * @param precision the number of decimals that have to be shared between a and b
  5.  * @return true if b - 10^precision < a < b + 10^precision*/
  6. static boolean moreOrLessEquals(double a, double b, int precision)
  7. {
  8.   return (Math.pow(10, precision) * Math.abs(a-b))<1;
  9. }

Utiliser

Il ne reste plus qu’à tester...

  1. /**test the small perceptron*/
  2. public static void main(String[] args)
  3. {
  4.   testPerceptronOneSample();
  5. }

Le code suivant est la même fonction d’apprentissage, en ajoutant des affichages pour suivre l’évolution.

  1. /**test the perceptron with only 1 sample*/
  2.   static void testPerceptronOneSample()
  3.   {
  4.     double []inputs = new double[]{1,0};
  5.     double desiredOutput = 1;
  6.     double acceptation = 0.005;
  7.     int iterations = 0;
  8.     PerceptronN1 p = new PerceptronN1(2);
  9.     double error = desiredOutput - p.feedForward(inputs);
  10.     double previousError = 2;
  11.     while (error>acceptation && !moreOrLessEquals(error, previousError, 10))
  12.     {
  13.       iterations++;
  14.       previousError = error;
  15.       System.out.printf(", outputs = %.4f , error = %.4f\n" , p.feedForward(inputs), error);
  16.       error = Math.abs(p.train(inputs, desiredOutput));
  17.       System.out.print("weights = ");
  18.       for(double i:p.weights) System.out.printf("%.4f ", i);    
  19.       System.out.println();
  20.     }
  21.     System.out.print("stop in " + iterations + " iterations, when inputs = ");
  22.     for(double i:inputs) System.out.print(i + " ");    
  23.     System.out.printf(", outputs = %.4f , error = %.4f\n" , p.feedForward(inputs), error);
  24.     System.out.print("weights = ");
  25.     for(double i:p.weights) System.out.printf("%.4f ", i);    
  26.     System.out.println();
  27.   }