Notes
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de vous connecter ou de modifier des répertoires.
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de modifier des répertoires.
Plongez dans les réseaux neuronaux
Télécharger l'exemple de code
Un réseau neuronal artificiel (généralement appelé simplement « réseau neuronal ») est une abstraction plus ou moins calquée sur les neurones et les synapses biologiques. Bien que les réseaux neuronaux aient été étudiés depuis des décennies, de nombreuses implémentations de code de réseaux neuronaux sur Internet ne sont pas, à mon avis, très bien expliquées. Dans l'article de ce mois, je vais vous expliquer ce que sont les réseaux neuronaux artificiels et présenter le code C# qui met en œuvre un réseau neuronal.
Pour voir où je vous emmène, jetez un coup d'œil aux illustration 1 et illustration 2. Une façon de penser aux réseaux neuronaux est de les considérer comme des mécanismes d'entrée et de sortie numériques. Le réseau neuronal de l'illustration 1 a trois entrées marquées x0, x1 et x2, avec les valeurs 1,0, 2,0 et 3,0, respectivement. Le réseau neuronal a deux sorties marquées y0 et y1, avec les valeurs 0,72 et -0,88, respectivement. Le réseau neuronal de l'illustration 1 a une couche de neurones dits cachés et peut être décrit comme un réseau d'action directe, totalement connecté, à trois couches, avec trois entrées, deux sorties et quatre neurones cachés. Malheureusement, la terminologie du réseau neuronal varie un peu. Dans cet article, je vais utiliser généralement, mais pas systématiquement, la terminologie décrite dans l'excellente FAQ des réseaux neuronaux sur bit.ly/wfikTI.
Illustration 1 Structure d'un réseau neuronal
Illustration 2 Programme de démonstration d'un réseau neuronal
L'illustration 2 illustre la sortie produite par le programme de démonstration présenté dans cet article. Le réseau neuronal utilise à la fois une fonction d'activation sigmoïde et une fonction d'activation tanh. Ces fonctions sont proposées par les deux équations avec les lettres grecques phi dans l'illustration 1. Les sorties produites par un réseau neuronal sont fonction des valeurs d'un ensemble de poids et d'écarts numériques. Dans cet exemple, il existe au total 26 poids et écarts dont les valeurs sont 0,10, 0,20, etc. -5.00. Une fois que les valeurs des poids et des écarts sont chargées dans le réseau neuronal, le programme de démonstration charge les trois valeurs d'entrée (1,0, 2,0 et 3,0), puis effectue une série de calculs tels que suggérés par les messages sur les sommes entrée-à-caché et les sommes caché-à-sortie. Le programme de démonstration se termine par l'affichage des deux valeurs de sortie (0,72 et -0,88).
Je vais vous guider dans le programme qui a produit le résultat illustré à l'illustration 2. Cet article suppose que vous avez des compétences de niveau moyen en programmation, mais ne suppose pas que vous avez des connaissances sur les réseaux neuronaux. Le programme de démonstration est codé à l'aide du langage C#, mais vous ne devriez avoir aucun mal à refactoriser le code de la démonstration dans un autre langage tel que Visual Basic, .NET ou Python. Le programme présenté dans cet article est essentiellement un didacticiel et une plate-forme d'expérimentation ; il ne résout directement aucun problème d'ordre pratique ; je vais donc vous expliquer comment développer le code pour résoudre des problèmes significatifs. Je pense que vous trouverez ces informations très intéressantes, et quelques-unes des techniques de programmation peuvent être un complément utile à l'éventail de vos compétences en codage.
Modélisation d'un réseau neuronal
Sur le plan conceptuel, les réseaux neuronaux artificiels sont calqués sur le comportement de véritables réseaux neuronaux biologiques. Dans l'illustration 1, les cercles représentent les neurones où le traitement a lieu et les flèches représentent à la fois les flux d'informations et les valeurs numériques appelées poids. Dans de nombreuses situations, les valeurs d'entrée sont copiées directement dans les neurones d'entrée sans aucune pondération et émises directement sans traitement, de sorte que la première action réelle a lieu dans les neurones de la couche cachée. Supposons que les valeurs d'entrée 1,0, 2,0 et 3,0 sont émises par les neurones d'entrée. Si vous examinez l'illustration 1, vous pouvez voir une flèche représentant une valeur de poids entre chacun des trois neurones d'entrée et chacun des quatre neurones cachés. Supposons que les trois flèches de poids indiquées pointant vers le neurone caché supérieur sont nommées w00, w10 et w20. Dans cette notation, le premier index représente l'index du neurone d'entrée de source et le second index représente l'index du neurone caché de destination. Le traitement des neurones se déroule en trois étapes. Dans la première étape, une somme pondérée est calculée. Supposons que w00 = 0,1, w10 = 0,5 et w20 = 0,9. La somme pondérée pour le neurone caché supérieur est (1,0)(0,1) + (2,0)(0,5) + (3,0)(0,9) = 3,8. La deuxième étape de traitement consiste à ajouter une valeur d'écart. Supposons que la valeur d'écart est de -2,0 ; la somme pondérée ajustée devient alors 3,8 + (-2,0) = 1,8. La troisième étape consiste à appliquer une fonction d'activation à la somme pondérée ajustée. Suppose que la fonction d'activation est la fonction sigmoïde définie par 1,0 / (1,0 * Exp(-x)), où Exp représente la fonction exponentielle. La sortie du neurone caché devient 1,0 / (1,0 * Exp(-1,8)) = 0,86. Cette sortie devient alors partie de la somme pondérée entrée dans chacun des neurones de la couche de sortie. Dans l'illustration 1, ce processus en trois étapes est proposé par l'équation avec la lettre grecque phi : les sommes pondérées (xw) sont calculées, un écart (b) est ajouté et une fonction d'activation (phi) est appliquée.
Après que toutes les valeurs des neurones cachés ont été calculées, les valeurs des neurones de la couche de sortie sont calculées de la même manière. La fonction d'activation utilisée pour calculer les valeurs des neurones de sortie peut être la même fonction utilisée pour le calcul des valeurs des neurones cachés ou une fonction d'activation différente. Le programme de démonstration dont l'exécution est représentée à l'illustration 2 utilise la fonction tangente hyperbolique comme fonction d'activation caché-en-sortie. Après que toutes les valeurs des neurones de la couche de sortie ont été calculées, ces valeurs ne sont pas, dans la plupart des cas, pondérées ou traitées, mais simplement émises comme valeurs finales de sortie du réseau neuronal.
Structure interne
La clé pour comprendre la mise en œuvre du réseau neuronal présenté ici est d'examiner de près l'illustration 3, qui pourrait, à première vue, sembler extrêmement compliquée. Mais patientez un moment : l'illustration n'est pas aussi complexe qu'elle n'y paraît à première vue. L'illustration 3 représente un total de huit tableaux et deux matrices. Le premier tableau est étiqueté « this.inputs ». Ce tableau contient les valeurs d'entrée du réseau neuronal, qui sont 1,0, 2,0 et 3,0 dans cet exemple. Vient ensuite l'ensemble des valeurs des poids qui sont utilisées pour calculer les valeurs dans la couche dite cachée. Ces poids sont stockés dans une matrice 3 x 4, étiquetée poids i-h, où i-h signifie « entrée-à-caché ». Remarquez que dans l'illustration 1, le réseau neuronal de démonstration comporte quatre neurones cachés. La matrice des poids i-h a un nombre de lignes égal au nombre d'entrées et un nombre de colonnes égal au nombre de neurones cachés.
Illustration 3 Structure interne d'un réseau neuronal
Le tableau étiqueté sommes i-h est un tableau vide utilisé pour le calcul. Remarquez que la longueur de la matrice sommes i-h sera toujours égale au nombre de neurones cachés (quatre, dans cet exemple). Vient ensuite un tableau étiqueté écarts i-h. Les écarts du réseau neuronal sont des poids supplémentaires utilisés pour calculer les neurones cachés et de la couche de sortie. La longueur du tableau des écarts i-h sera la même que la longueur du tableau sommes i-h, qui à son tour est égale au nombre de neurones cachés.
Le tableau étiqueté sorties i-h est un résultat intermédiaire et les valeurs de ce tableau sont utilisées comme entrées de la couche suivante. Le tableau sommes i-h a une longueur égale au nombre de neurones cachés.
Vient ensuite une matrice étiquetée poids h-o où h-o désigne caché-à-sortie. Ici, la matrice poids h-o est de dimensions 4 x 2 car il y a quatre neurones cachés et deux sorties. Le tableau sommes h-o, le tableau des écarts h-o et le tableau this.outputs ont tous des longueurs égales au nombre de sorties (deux, dans cet exemple).
Le tableau étiqueté poids, situé en bas de l'illustration 3, comprend tous les poids et les écarts entrée-à-caché et caché-à-sortie. Dans cet exemple, la longueur du tableau poids est (3 * 4) + 4 + (4 * 2) + 2 = 26. En général, si Ni est le nombre des valeurs d'entrée, Nh le nombre des neurones cachés et No le nombre de sorties, alors la longueur du tableau poids sera Nw = (Ni * Nh) + Nh + (Nh * No) + No.
Calcul des sorties
Après création des huit tableaux et des deux matrices décrits dans la section précédente, un réseau neuronal peut calculer sa sortie en fonction de ses entrées, de ses poids et de ses écarts. La première étape consiste à copier les valeurs d'entrée dans le tableau this.inputs. L'étape suivante consiste à attribuer des valeurs au tableau poids. Pour les besoins de démonstration, vous pouvez utiliser toutes les valeurs de poids que vous préférez. Ensuite, les valeurs du tableau poids sont copiées dans la matrice poids i-h, le tableau écarts i-h, la matrice poids h-o et le tableau écarts h-o. L'illustration 3 permet d'éclaircir cette relation.
Les valeurs indiquées au tableau sommes i-h sont calculées en deux étapes. La première étape consiste à calculer les sommes pondérées en multipliant les valeurs du tableau des entrées par les valeurs de la colonne appropriée de la matrice poids i-h. Par exemple, la somme pondérée du neurone caché [3] (où j'utilise une indexation basée sur un zéro) utilise chaque valeur d'entrée et les valeurs de la colonne [3] de la matrice poids i-h : (1.0)(0.4) + (2.0)(0.8) + (3.0)(1.2) = 5.6. La seconde étape du calcul de valeurs de la somme i-h consiste à ajouter chaque valeur d'écart à la valeur de la somme actuelle i-h. Par exemple, puisque l'écart i-h [3] a une valeur de -7,0, la valeur des sommes i-h [3] devient alors 5,6 + (-7,0) = -1,4.
Après le calcul de toutes les valeurs du tableau sommes i-h, la fonction d'activation entrée-à-caché est appliquée à ces sommes afin de produire les valeurs de sortie entrée-à-caché. Il y a beaucoup de fonctions d'activation possibles. La fonction d'activation la plus simple est appelée fonction étape et elle retourne simplement 1,0 pour toute valeur d'entrée supérieure à zéro et 0,0 pour toute valeur d'entrée inférieure ou égale à zéro. Une autre fonction d'activation usuelle, utilisée dans cet article, est la fonction sigmoïde, qui est définie par f(x) = 1,0 / (1,0 * Exp(-x)). Le graphe de la fonction sigmoïde est illustré à l'illustration 4.
Illustration 4 La fonction sigmoïde
Notez que la fonction sigmoïde retourne une valeur dans la plage strictement supérieure à zéro et strictement inférieure à un. Dans cet exemple, si la valeur de sommes i-h [3], après l'ajout de la valeur d'écart, est -1,4, alors la valeur des sorties i-h [3] devient 1,0 / (1,0 * Exp(-(-1,4))) = 0,20.
Après le calcul de toutes les valeurs de neurones de sortie entrée-à-caché, ces valeurs servent d'entrées pour les calculs de neurones de la couche caché-à-sortie. Ces calculs fonctionnent de la même manière que les calculs entrée-à-caché : les sommes pondérées préliminaires sont calculées, les écarts sont ajoutés et ensuite une fonction d'activation est appliquée. Dans cet exemple j'utilise la fonction tangente hyperbolique, abrégée en tanh, pour la fonction d'activation caché-à-sortie. La fonction tanh est étroitement liée à la fonction sigmoïde. Le graphe de la fonction tanh a une courbe en forme de S similaire à la fonction sigmoïde, mais tanh retourne une valeur dans la plage (-1,1) au lieu de (0,1).
Combinaison des poids et des écarts
Toutes les implémentations de réseaux neuronaux que j'ai vues sur Internet ne maintiennent pas des tableaux distincts de poids et d'écart, mais combinent plutôt les poids et les écarts dans la matrice des poids. Comment est-ce possible ? N'oubliez pas que le calcul de la valeur du neurone entrée-à-caché [3] est (i0 * w03) + (i1 * w13) + (i2 * w23) + b3, où i0 est une valeur d'entrée [0], w03 est le poids pour l'entrée [0] et le neurone [3], et b3 est la valeur de l'écart pour le neurone caché [3]. Si vous créez une entrée fausse supplémentaire [4] qui a une valeur factice de 1,0 et une ligne supplémentaire de poids qui contiennent les valeurs de l'écart, alors le calcul décrit précédemment devient : (i0 * w03) + (i1 * w13) + (i2 * w23) + (i3 * w33), où i3 est la valeur d'entrée factice 1,0 et w33 est l'écart. L'argument est que cette approche simplifie le modèle de réseau neuronal. Je ne suis pas d'accord. À mon avis, la combinaison des poids et des écarts rend un modèle de réseau neuronal plus difficile à comprendre et plus sujet aux erreurs de mise en œuvre. Cependant, je suis apparemment le seul auteur qui semble avoir cette opinion ; vous devez donc prendre votre propre décision de conception.
Implémentation
J'ai mis en œuvre le réseau neuronal indiqué dans les illustrations 1, 2 et 3 à l'aide de Visual Studio 2010. J'ai créé une application de console en C# nommée NeuralNetworks. Dans la fenêtre Explorateur de solutions, je clique avec le bouton droit sur le fichier Program.cs et le renomme en NeuralNetworksProgram.cs, qui a également changé le nom de classe généré par modèle à NeuralNetworksProgram. La structure générale du programme, avec la plupart des déclarations WriteLine supprimées, est illustrée dans l'illustration 5.
Illustration 5 Structure du programme d'un réseau neuronal
using System;
namespace NeuralNetworks
{
class NeuralNetworksProgram
{
static void Main(string[] args)
{
try
{
Console.WriteLine("\nBegin Neural Network demo\n");
NeuralNetwork nn = new NeuralNetwork(3, 4, 2);
double[] weights = new double[] {
0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.1, 1.2,
-2.0, -6.0, -1.0, -7.0,
1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9, 2.0,
-2.5, -5.0 };
nn.SetWeights(weights);
double[] xValues = new double[] { 1.0, 2.0, 3.0 };
double[] yValues = nn.ComputeOutputs(xValues);
Helpers.ShowVector(yValues);
Console.WriteLine("End Neural Network demo\n");
}
catch (Exception ex)
{
Console.WriteLine("Fatal: " + ex.Message);
}
}
}
class NeuralNetwork
{
// Class members here
public NeuralNetwork(int numInput, int numHidden, int numOutput) { ... }
public void SetWeights(double[] weights) { ... }
public double[] ComputeOutputs(double[] xValues) { ... }
private static double SigmoidFunction(double x) { ... }
private static double HyperTanFunction(double x) { ... }
}
public class Helpers
{
public static double[][] MakeMatrix(int rows, int cols) { ... }
public static void ShowVector(double[] vector) { ... }
public static void ShowMatrix(double[][] matrix, int numRows) { ... }
}
} // ns
J'ai supprimé tous les modèles générés à l'aide des déclarations à l'exception de celui du référencement de l'espace des noms System. Dans la fonction Main, après avoir affiché un message de démarrage, j'instancie un objet NeuralNetwork nommé nn avec trois entrées, quatre neurones cachés et deux sorties. Ensuite, j'attribue 26 poids et écarts arbitraires à un tableau nommé poids. Je charge les poids dans l'objet réseau neuronal en utilisant une méthode nommée SetWeights. J'attribue les valeurs 1,0, 2,0 et 3,0 à un tableau nommé xValues. J'utilise la méthode ComputeOutputs pour charger les valeurs d'entrée dans le réseau neuronal et déterminer les sorties qui en découlent, que je vais extraire dans un tableau nommé yValues. La démonstration se termine par l'affichage des valeurs de sortie (0,72 et -0,88).
La classe NeuralNetwork
La définition de la classe NeuralNetwork commence :
class NeuralNetwork
{
private int numInput;
private int numHidden;
private int numOutput;
...
Comme l'expliquent les sections précédentes, la structure d'un réseau neuronal est déterminée par le nombre de valeurs d'entrée, le nombre de neurones de la couche cachée et le nombre de valeurs de la sortie. La définition de la classe continue ainsi :
private double[] inputs;
private double[][] ihWeights; // input-to-hidden
private double[] ihSums;
private double[] ihBiases;
private double[] ihOutputs;
private double[][] hoWeights; // hidden-to-output
private double[] hoSums;
private double[] hoBiases;
private double[] outputs;
...
Ces sept tableaux et deux matrices correspondent à celles indiquées à l'illustration 3. J'utilise un préfixe ih pour les données entrée-à-caché et un préfixe ho pour les données sortie-à-caché. Rappelez-vous que les valeurs dans le tableau ihOutputs servent d'entrées pour les calculs de la couche de sortie ; nommer ce tableau est donc précisément un peu gênant.
L'illustration 6 montre comment le constructeur de la classe NeuralNetwork est défini.
Illustration 6 Le constructeur de la classe NeuralNetwork
public NeuralNetwork(int numInput, int numHidden, int numOutput)
{
this.numInput = numInput;
this.numHidden = numHidden;
this.numOutput = numOutput;
inputs = new double[numInput];
ihWeights = Helpers.MakeMatrix(numInput, numHidden);
ihSums = new double[numHidden];
ihBiases = new double[numHidden];
ihOutputs = new double[numHidden];
hoWeights = Helpers.MakeMatrix(numHidden, numOutput);
hoSums = new double[numOutput];
hoBiases = new double[numOutput];
outputs = new double[numOutput];
}
Après avoir copié les valeurs du paramètre d'entrée numInput, numHidden et numOutput dans leurs champs de classe respectifs, chacun des neuf tableaux et matrices membres se voit alloué les dimensions que j'ai expliquées plus tôt. J'implémente les matrices comme des tableaux de tableaux plutôt que d'utiliser le type de tableau multidimensionnel de C# afin que vous puissiez refactoriser plus facilement mon code dans un langage qui ne prend pas en charge les types de tableaux multidimensionnels. Puisque chaque ligne de mes matrices doit être attribuée, il est commode d'utiliser une méthode d'assistance telle que MakeMatrix.
La méthode SetWeights accepte un tableau de valeurs de poids et d'écart et remplit ihWeights, ihBiases, hoWeights et hoBiases. La méthode commence comme suit :
public void SetWeights(double[] weights)
{
int numWeights = (numInput * numHidden) +
(numHidden * numOutput) + numHidden + numOutput;
if (weights.Length != numWeights)
throw new Exception("xxxxxx");
int k = 0;
...
Comme expliqué précédemment, le nombre total de poids et d'écarts, Nw, dans un réseau neuronal d'action directe, totalement connecté, est Ni * Nh) + (Nh * No) + Nh + No. J'effectue un simple contrôle pour voir si le paramètre du tableau poids a la bonne longueur. Où « xxxxxx » désigne un message d'erreur descriptif. Ensuite, j'initialise une variable d'index k au début du paramètre du tableau poids. La méthode SetWeights se termine ainsi :
for (int i = 0; i < numInput; ++i)
for (int j = 0; j < numHidden; ++j)
ihWeights[i][j] = weights[k++];
for (int i = 0; i < numHidden; ++i)
ihBiases[i] = weights[k++];
for (int i = 0; i < numHidden; ++i)
for (int j = 0; j < numOutput; ++j)
hoWeights[i][j] = weights[k++];
for (int i = 0; i < numOutput; ++i)
hoBiases[i] = weights[k++]
}
Chaque valeur dans le paramètre du tableau poids est copiée de manière séquentielle dans ihWeights, ihBiases, hoWeights et hoBiases. Notez qu'aucune valeur n'est copiée dans ihSums ou hoSums parce que ces deux tableaux vides sont utilisés pour le calcul.
Calcul des sorties
Le cœur de la classe NeuralNetwork est la méthode ComputeOutputs. La méthode est étonnamment courte et simple et commence ainsi :
public double[] ComputeOutputs(double[] xValues)
{
if (xValues.Length != numInput)
throw new Exception("xxxxxx");
for (int i = 0; i < numHidden; ++i)
ihSums[i] = 0.0;
for (int i = 0; i < numOutput; ++i)
hoSums[i] = 0.0;
...
D'abord, je vérifie si la longueur du tableau x-values d'entrée est la bonne dimension de l'objet NeuralNetwork. Puis, je réinitialise les tableaux ihSums et hoSums. Si la méthode ComputeOutputs est appelée une seule fois, alors cette initialisation explicite n'est pas nécessaire, mais si ComputeOutputs est appelée plus d'une fois, parce que ihSums et hoSums sont des valeurs accumulées, l'initialisation explicite est alors absolument nécessaire. Une approche alternative de conception est de ne pas déclarer, ni allouer ihSums et hoSums comme membres de la classe, mais de les rendre plutôt locales avec la méthode ComputeOutputs. La méthode ComputeOutputs continue ainsi :
for (int i = 0; i < xValues.Length; ++i)
this.inputs[i] = xValues[i];
for (int j = 0; j < numHidden; ++j)
for (int i = 0; i < numInput; ++i)
ihSums[j] += this.inputs[i] * ihWeights[i][j];
...
Les valeurs du paramètre du tableau xValues sont copiées sur le membre du tableau des entrées de classe. Dans certains scénarios de réseau neuronal, les valeurs du paramètre d'entrée sont normalisées, en effectuant par exemple une transformation linéaire de telle sorte que toutes les entrées soient mises à l'échelle entre -1,0 et +1,0, mais aucune normalisation n'est effectuée ici. Ensuite, une boucle imbriquée calcule les sommes pondérées comme indiqué dans les illustrations 1 et 3. Notez que pour indexer ihWeights sous une forme standard, où l'index i est l'index de ligne et l'index j est l'index de la colonne, il est nécessaire d'avoir j dans la boucle externe. La méthode ComputeOutputs continue ainsi :
for (int i = 0; i < numHidden; ++i)
ihSums[i] += ihBiases[i];
for (int i = 0; i < numHidden; ++i)
ihOutputs[i] = SigmoidFunction(ihSums[i]);
...
Chaque somme pondérée est modifiée par l'ajout de la valeur d'écart appropriée. À ce stade, pour produire le résultat illustré à l'illustration 2, j'ai utilisé la méthode Helpers.ShowVector pour afficher les valeurs actuelles dans le tableau ihSums. Ensuite, j'applique la fonction sigmoïde à chacune des valeurs dans ihSums et j'assigne les résultats au tableau ihOutputs. Je vais vous présenter le code de la méthode SigmoidFunction d'ici peu... La méthode ComputeOutputs continue ainsi :
for (int j = 0; j < numOutput; ++j)
for (int i = 0; i < numHidden; ++i)
hoSums[j] += ihOutputs[i] * hoWeights[i][j];
for (int i = 0; i < numOutput; ++i)
hoSums[i] += hoBiases[i];
...
J'utilise les valeurs calculées dans ihOutputs et les poids dans hoWeights pour calculer les valeurs dans hoSums, puis j'ajoute les valeurs d'écart caché-à-sortie appropriées. Encore une fois, pour produire le résultat illustré à l'illustration 2, j'ai appelé Helpers.ShowVector. La méthode ComputeOutputs se termine comme suit :
for (int i = 0; i < numOutput; ++i)
this.outputs[i] = HyperTanFunction(hoSums[i]);
double[] result = new double[numOutput];
this.outputs.CopyTo(result, 0);
return result;
}
J'applique la méthode HyperTanFunction pour hoSums afin de générer les sorties finales en sorties de membre privé de tableau de classe. Je copie ces sorties dans un tableau de résultat local et j'utilise ce tableau comme une valeur de retour. Un autre choix de conception serait d'implémenter ComputeOutputs sans valeur de retour, mais d'implémenter une méthode publique GetOutputs de sorte que les sorties de l'objet réseau neuronal puissent être récupérées.
Les fonctions d'activation et les méthodes d'assistance
Voici le code de la fonction sigmoïde utilisé pour calculer les sorties entrée-à-caché :
private static double SigmoidFunction(double x)
{
if (x < -45.0) return 0.0;
else if (x > 45.0) return 1.0;
else return 1.0 / (1.0 + Math.Exp(-x));
}
Puisque certaines implémentations de la fonction Math.Exp peuvent produire un débordement arithmétique, la vérification de la valeur du paramètre d'entrée est réalisée habituellement. Le code de la fonction tanh utilisé pour calculer les résultats caché-à-sortie est :
private static double HyperTanFunction(double x)
{
if (x < -10.0) return -1.0;
else if (x > 10.0) return 1.0;
else return Math.Tanh(x);
}
La fonction tangente hyperbolique renvoie des valeurs entre -1 et +1, de sorte que le débordement arithmétique ne constitue pas un problème. Ici, la valeur d'entrée est vérifiée seulement pour améliorer les performances.
Les méthodes d'utilitaires statiques dans la classe Helpers sont juste des commodités de codage. La méthode MakeMatrix utilisée pour allouer les matrices dans le constructeur NeuralNetwork attribue chaque ligne d'une matrice implémentée comme un tableau de tableaux :
public static double[][] MakeMatrix(int rows, int cols)
{
double[][] result = new double[rows][];
for (int i = 0; i < rows; ++i)
result[i] = new double[cols];
return result;
}
Les méthodes ShowVector et ShowMatrix affichent les valeurs dans un tableau ou une matrice pour la console. Vous pouvez voir le code de ces deux méthodes dans le téléchargement de code qui accompagne cet article (disponible à l'adresse archive.msdn.microsoft.com/mag201205TestRun).
Étapes suivantes
Le code présenté ici devrait vous donner une base solide pour la compréhension et l'expérimentation des réseaux neuronaux. Vous voudriez peut-être examiner les effets de l'utilisation de différentes fonctions d'activation et de la variation du nombre des entrées, des sorties et des neurones de la couche cachée. Vous pouvez modifier le réseau neuronal en le rendant partiellement connecté, où certains neurones ne sont pas logiquement connectés aux neurones de la couche suivante. Le réseau neuronal présenté dans cet article a une seule couche cachée. Il est possible de créer des réseaux neuronaux plus complexes, ayant deux ou même plusieurs couches cachées, et vous voudriez peut-être étendre le code présenté ici pour implémenter de tels réseaux neuronaux.
Les réseaux neuronaux peuvent être utilisés pour résoudre de nombreux problèmes pratiques, y compris les problèmes de classification. Afin de résoudre ces problèmes, il faut relever plusieurs défis. Par exemple, vous devez connaître la façon de coder des données non numériques et l'apprentissage du réseau neuronal pour trouver le meilleur ensemble de poids et d'écarts. Je présenterai un exemple d'utilisation des réseaux neuronaux pour la classification dans un prochain article.
Le Dr. James McCaffrey travaille pour Volt Information Sciences Inc., où il gère la formation technique d'ingénieurs logiciels travaillant sur le Campus Microsoft à Redmond (État de Washington). Il a collaboré à plusieurs produits Microsoft, comme Internet Explorer et MSN Search. Il est l'auteur de « NET Test Automation Recipes » (Apress, 2006). Vous pouvez le contacter à l'adresse jammc@microsoft.com.
Merci aux experts techniques Microsoft suivants d'avoir relu cet article : Dan Liebling et Anne Loomis Thompson