Nano-neurone - 7 fonctions JavaScript simples montrant comment la machine peut "apprendre"

Un nano-neurone est une version simplifiée d'un neurone du concept d'un réseau neuronal. Le nano-neurone effectue la tâche la plus simple et est formé pour convertir la température de degrés Celsius en degrés Fahrenheit.


Le code NanoNeuron.js se compose de 7 fonctions JavaScript simples impliquant l'apprentissage, la formation, la prévision et la propagation directe et en arrière du signal du modèle. Le but de l'écriture de ces fonctions était de donner au lecteur une explication minimale (intuition) de la façon dont, après tout, une machine peut «apprendre». Le code n'utilise pas de bibliothèques tierces. Comme le dit le proverbe, seules les fonctions JavaScript "vanille" simples.


Ces fonctions ne sont en aucun cas un guide exhaustif de l'apprentissage automatique. De nombreux concepts d'apprentissage automatique sont manquants ou simplifiés! Cette simplification est autorisée dans le seul but - donner au lecteur la compréhension et l'intuition les plus élémentaires de la façon dont une machine peut, en principe, "apprendre", de sorte que, par conséquent, "MAGIE de l'apprentissage automatique" sonne de plus en plus pour le lecteur comme "MATHÉMATIQUES de l'apprentissage automatique".


Nanoneuron


Ce que notre nano-neurone «apprendra»


Vous avez peut-être entendu parler des neurones dans le contexte des réseaux de neurones . Un nano-neurone est une version simplifiée de ce même neurone. Dans cet exemple, nous allons écrire son implémentation à partir de zéro. Par souci de simplicité, nous ne construirons pas de réseau de nano-neurones. Nous allons nous concentrer sur la création d'un seul nano-neurone et essayer de lui apprendre à convertir la température de degrés Celsius en degrés Fahrenheit. En d'autres termes, nous lui apprendrons à prédire la température en degrés Fahrenheit en fonction de la température en degrés Celsius.


À propos, la formule de conversion des degrés Celsius en degrés Fahrenheit est la suivante:


Celsius à fahrenheit


Mais pour le moment, notre nano-neurone ne sait rien de cette formule ...


Modèle de nano-neurone


Commençons par créer une fonction qui décrit le modèle de notre nano-neurone. Ce modèle est une simple relation linéaire entre x et y , qui ressemble à ceci: y = w * x + b . Autrement dit, notre nano-neurone est un enfant qui peut tracer une ligne droite dans le système de coordonnées XY .


Les variables w et b sont des paramètres de modèle. Un nano-neurone ne connaît que ces deux paramètres d'une fonction linéaire. Ces paramètres sont précisément ce que notre nano-neurone apprendra au cours du processus d'entraînement.


La seule chose qu'un nano-neurone peut faire à ce stade est de simuler des relations linéaires. Il le fait dans la méthode predict() , qui prend une variable x à l'entrée et prédit la variable y à la sortie. Pas de magie.


 function NanoNeuron(w, b) { this.w = w; this.b = b; this.predict = (x) => { return x * this.w + this.b; } } 

_ (... attendez ... la régression linéaire c'est vous, ou quoi?) _


Conversion degrés Celsius en degrés Fahrenheit


La température en degrés Celsius peut être convertie en degrés Fahrenheit selon la formule: f = 1.8 * c + 32 , où c est la température en degrés Celsius et f est la température en degrés Fahrenheit.


 function celsiusToFahrenheit(c) { const w = 1.8; const b = 32; const f = c * w + b; return f; }; 

En conséquence, nous voulons que notre nano-neurone puisse simuler cette fonction particulière. Il devra deviner (apprendre) que le paramètre w = 1.8 et b = 32 sans le savoir à l'avance.


Voici à quoi ressemble la fonction de conversion sur le graphique. C'est ce que notre «bébé» nano-neuronal doit apprendre à «dessiner»:


Conversion de Celsius en Fahrenheit


Génération de données


En programmation classique, nous connaissons les données d'entrée ( x ) et l'algorithme de conversion de ces données (paramètres w et b ), mais les données de sortie ( y ) sont inconnues. La sortie est calculée sur la base de l'entrée en utilisant un algorithme connu. En apprentissage automatique, au contraire, seules les données d'entrée et de sortie ( x et y ) sont connues, mais l'algorithme de passage de x à y inconnu (paramètres w et b ).


C'est la génération d'entrées et de sorties que nous allons maintenant faire. Nous devons générer des données pour la formation de notre modèle et des données pour tester le modèle. La fonction d'assistance celsiusToFahrenheit() nous y aidera. Chacun des ensembles de données d'apprentissage et de test est un ensemble de paires x et y . Par exemple, si x = 2 , alors y = 35,6 et ainsi de suite.


Dans le monde réel, la plupart des données sont susceptibles d'être collectées et non générées . Par exemple, ces données collectées peuvent être un ensemble de paires de «photos de visage» -> «nom de la personne».

Nous utiliserons l'ensemble de données TRAINING pour entraîner notre nano-neurone. Avant qu'il ne grandisse et soit capable de prendre des décisions par lui-même, nous devons lui apprendre ce qui est «vrai» et ce qui est «faux» en utilisant des données «correctes» d'un ensemble d'entraînement.


Soit dit en passant, le principe de vie «ordures à l'entrée - ordures à la sortie» est clairement tracé. Si un nano-neurone jette un "mensonge" dans le kit de formation que 5 ° C est converti en 1000 ° F, puis après de nombreuses itérations de formation, il le croira et convertira correctement toutes les valeurs de température sauf 5 ° C. Nous devons être très prudents avec les données d'entraînement que nous chargeons quotidiennement dans notre réseau neuronal cérébral.

Distrait. Continuons.


Nous utiliserons l'ensemble de données TEST pour évaluer dans quelle mesure notre nano-neurone s'est entraîné et peut faire des prédictions correctes sur de nouvelles données qu'il n'a pas vues pendant sa formation.


 function generateDataSets() { // xTrain -> [0, 1, 2, ...], // yTrain -> [32, 33.8, 35.6, ...] const xTrain = []; const yTrain = []; for (let x = 0; x < 100; x += 1) { const y = celsiusToFahrenheit(x); xTrain.push(x); yTrain.push(y); } // xTest -> [0.5, 1.5, 2.5, ...] // yTest -> [32.9, 34.7, 36.5, ...] const xTest = []; const yTest = []; //   0.5    1,       //   ,       . for (let x = 0.5; x < 100; x += 1) { const y = celsiusToFahrenheit(x); xTest.push(x); yTest.push(y); } return [xTrain, yTrain, xTest, yTest]; } 

Estimation d'erreur de prédiction


Nous avons besoin d'une certaine métrique (mesure, nombre, évaluation) qui montrera à quel point la prédiction d'un nano-neurone est vraie. En d'autres termes, ce nombre / métrique / fonction devrait montrer à quel point le nano neurone est correct ou non. C'est comme à l'école, un élève peut obtenir une note de 5 ou 2 pour son contrôle.


Dans le cas d'un nano-neurone, son erreur (erreur) entre la vraie valeur de y et la valeur prédite de prediction sera produite par la formule:


Coût de prédiction


Comme le montre la formule, nous considérerons l'erreur comme une simple différence entre les deux valeurs. Plus les valeurs sont proches les unes des autres, plus la différence est faible. Nous utilisons ici la quadrature pour se débarrasser du signe, de sorte qu'à la fin (1 - 2) ^ 2 équivalent à (2 - 1) ^ 2 . La division par 2 se produit uniquement dans le but de simplifier la signification de la dérivée de cette fonction dans la formule de rétro-propagation d'un signal (voir ci-dessous).


La fonction d'erreur dans ce cas ressemblera à ceci:


 function predictionCost(y, prediction) { return (y - prediction) ** 2 / 2; // ie -> 235.6 } 

Propagation directe du signal


La propagation directe du signal à travers notre modèle signifie faire des prédictions pour toutes les paires à partir du jeu de données d'apprentissage xTrain et yTrain et calculer l'erreur moyenne (erreur) de ces prédictions.


Nous laissons simplement notre nano-neurone «s'exprimer», lui permettant de faire des prédictions (convertir la température). En même temps, un nano-neurone à ce stade peut être très mauvais. La valeur moyenne de l'erreur de prédiction nous montrera à quel point notre modèle est / est proche de la vérité en ce moment. La valeur d'erreur est ici très importante, car en modifiant à nouveau les paramètres w et b et en propageant à nouveau la propagation directe du signal, nous pouvons évaluer si notre nano-neurone est devenu «plus intelligent» avec de nouveaux paramètres ou non.


L'erreur de prédiction moyenne d'un nano-neurone sera effectuée en utilisant la formule suivante:


Coût moyen


m est le nombre de copies d'apprentissage (dans notre cas, nous avons 100 paires de données).


Voici comment nous pouvons implémenter cela dans le code:


 function forwardPropagation(model, xTrain, yTrain) { const m = xTrain.length; const predictions = []; let cost = 0; for (let i = 0; i < m; i += 1) { const prediction = nanoNeuron.predict(xTrain[i]); cost += predictionCost(yTrain[i], prediction); predictions.push(prediction); } //     . cost /= m; return [predictions, cost]; } 

Propagation inverse du signal


Maintenant que nous savons comment notre nano-neurone a raison ou tort dans ses prédictions (basées sur la valeur moyenne de l'erreur), comment pouvons-nous rendre les prédictions plus précises?


La propagation inverse du signal nous y aidera. La propagation du signal de retour est le processus d'évaluation de l'erreur d'un nano-neurone, puis d'ajustement de ses paramètres w et b afin que les prochaines prédictions du nano-neurone pour l'ensemble des données d'apprentissage deviennent un peu plus précises.


C'est là que l'apprentissage automatique devient comme de la magie. Le concept clé ici est un dérivé de la fonction , qui montre quel pas de taille et quelle voie nous devons prendre afin d'approcher le minimum de la fonction (dans notre cas, le minimum de la fonction d'erreur).


Le but ultime de la formation d'un nano-neurone est de trouver le minimum de la fonction d'erreur (voir fonction ci-dessus). Si nous pouvons trouver de telles valeurs de w et b pour lesquelles la valeur moyenne de la fonction d'erreur est petite, cela signifiera que notre nano-neurone s'adapte bien aux prévisions de température en degrés Fahrenheit.


Les dérivés sont un sujet important et distinct que nous ne traiterons pas dans cet article. MathIsFun est une excellente ressource qui peut fournir une compréhension de base des dérivés.


Une chose que nous devons apprendre de l'essence de la dérivée et qui nous aidera à comprendre comment fonctionne la rétropropagation du signal est que la dérivée d'une fonction à un point spécifique x et y , par définition, est une tangente à la courbe de cette fonction à x et y et nous indique la direction au minimum de la fonction .


Pente dérivée


Image prise à partir de MathIsFun


Par exemple, dans le graphique ci-dessus, vous voyez qu'au point (x=2, y=4) pente de la tangente nous montre que nous devons nous déplacer vers la et pour atteindre le minimum de la fonction. Notez également que plus la pente de la tangente est grande, plus nous devons nous déplacer rapidement vers le point minimum.


Les dérivées de notre fonction d'erreur moyenne averageCost par averageCost aux paramètres w et b ressembleront à ceci:


dW


dB


m est le nombre de copies d'apprentissage (dans notre cas, nous avons 100 paires de données).


Vous pouvez lire plus en détail sur la façon de prendre la dérivée de fonctions complexes ici .


 function backwardPropagation(predictions, xTrain, yTrain) { const m = xTrain.length; //           'w'  'b'. //      0. let dW = 0; let dB = 0; for (let i = 0; i < m; i += 1) { dW += (yTrain[i] - predictions[i]) * xTrain[i]; dB += yTrain[i] - predictions[i]; } //    . dW /= m; dB /= m; return [dW, dB]; } 

Formation modèle


Nous savons maintenant comment estimer l'erreur / erreur des prédictions de notre modèle de nano-neurone pour toutes les données d'entraînement (propagation directe du signal). Nous savons également ajuster les paramètres w et b modèle des nano-neurones (rétropropagation du signal) afin d'améliorer la précision des prédictions. Le problème est que si nous effectuons une propagation avant et arrière du signal une seule fois, cela ne suffira pas pour que notre modèle identifie et apprenne les dépendances et les lois dans les données d'apprentissage. Vous pouvez comparer cela à la visite d'une journée d'un élève à l'école. Il / elle doit aller à l'école régulièrement, jour après jour, année après année, afin d'apprendre tout le matériel.


Il faut donc répéter plusieurs fois la propagation avant et arrière du signal. C'est trainModel() . Elle est comme une «enseignante» pour le modèle de notre nano-neurone:


  • elle passera un certain temps ( epochs ) avec notre nano-neurone encore idiot, essayant de le former,
  • elle utilisera des livres spéciaux (jeux de données xTrain et yTrain ) pour la formation,
  • il encourage notre «étudiant» à étudier plus diligemment (plus rapidement) en utilisant le paramètre alpha , qui contrôle essentiellement la vitesse d'apprentissage.

Quelques mots sur le paramètre alpha . Il s'agit simplement d'un coefficient (multiplicateur) pour les valeurs des variables dW et dB , que nous calculons lors de la dW du signal. Ainsi, la dérivée nous a montré la direction vers le minimum de la fonction d'erreur (les signes des valeurs de dW et dB nous le disent). La dérivée nous a également montré à quelle vitesse nous devons nous déplacer vers le minimum de la fonction (les valeurs absolues de dW et dB nous le disent). Maintenant, nous devons multiplier la taille du pas par alpha afin d'ajuster la vitesse de notre approche au minimum (la taille totale du pas). Parfois, si nous utilisons de grandes valeurs pour alpha , nous pouvons aller dans des étapes si grandes que nous pouvons simplement dépasser le minimum de la fonction, la sautant ainsi.


Par analogie avec le «professeur», plus elle forcerait notre «nano-étudiant» à apprendre, plus vite il apprendrait, MAIS, si vous le forcez et exercez une pression très forte sur lui, alors notre «nano-étudiant» peut subir une dépression nerveuse et apathie complète et il n’apprendra rien du tout.


Nous mettrons à jour les paramètres de notre modèle w et b comme suit:


w


b


Et voici à quoi ressemble la formation elle-même:


 function trainModel({model, epochs, alpha, xTrain, yTrain}) { //     -.  . const costHistory = []; //    ()  for (let epoch = 0; epoch < epochs; epoch += 1) { //   . const [predictions, cost] = forwardPropagation(model, xTrain, yTrain); costHistory.push(cost); //   . const [dW, dB] = backwardPropagation(predictions, xTrain, yTrain); //    -,    . nanoNeuron.w += alpha * dW; nanoNeuron.b += alpha * dB; } return costHistory; } 

Assembler toutes les fonctionnalités


Il est temps d'utiliser ensemble toutes les fonctions précédemment créées.


Créez une instance du modèle de nano-neurone. Pour le moment, le nano-neurone ne sait rien des paramètres w et b . Définissons donc w et b au hasard.


 const w = Math.random(); // ie -> 0.9492 const b = Math.random(); // ie -> 0.4570 const nanoNeuron = new NanoNeuron(w, b); 

Nous générons des ensembles de données de formation et de test.


 const [xTrain, yTrain, xTest, yTest] = generateDataSets(); 

Essayons maintenant de former notre modèle en utilisant de petites étapes ( 0.0005 ) pour 70000 époques. Vous pouvez expérimenter ces paramètres, ils sont déterminés empiriquement.


 const epochs = 70000; const alpha = 0.0005; const trainingCostHistory = trainModel({model: nanoNeuron, epochs, alpha, xTrain, yTrain}); 

Vérifions comment la valeur d'erreur de notre modèle a changé pendant la formation. Nous nous attendons à ce que la valeur d'erreur après la formation soit nettement inférieure à celle avant la formation. Cela signifierait que notre nano-neurone serait plus sage. L'option inverse est également possible, lorsque après l'entraînement, l'erreur des prédictions n'a fait qu'augmenter (par exemple, les grandes valeurs de l'étape d'apprentissage alpha ).


 console.log('  :', trainingCostHistory[0]); // ie -> 4694.3335043 console.log('  :', trainingCostHistory[epochs - 1]); // ie -> 0.0000024 

Et voici comment la valeur de l'erreur de modèle a changé pendant la formation. Sur l'axe des x trouvent les époques (en milliers). Nous prévoyons que le graphique diminuera.


Processus de formation


Voyons quels paramètres notre nano-neurone a «appris». Nous nous attendons à ce que les paramètres w et b soient similaires aux paramètres du même nom de la fonction celsiusToFahrenheit() ( w = 1.8 et b = 32 ), car c'est son nano-neurone que j'ai essayé de simuler.


 console.log(' -:', {w: nanoNeuron.w, b: nanoNeuron.b}); // ie -> {w: 1.8, b: 31.99} 

Comme vous pouvez le voir, le nano-neurone est très proche de la fonction celsiusToFahrenheit() .


Voyons maintenant à quel point les prédictions de notre nano-neurone sont précises pour les données de test qu'il n'a pas vues pendant l'entraînement. L'erreur de prédiction pour les données de test doit être proche de l'erreur de prédiction pour les données d'apprentissage. Cela signifiera que le nano-neurone a appris les dépendances correctes et peut correctement extraire son expérience de données inconnues auparavant (c'est toute la valeur du modèle).


 [testPredictions, testCost] = forwardPropagation(nanoNeuron, xTest, yTest); console.log('   :', testCost); // ie -> 0.0000023 

Maintenant, puisque notre «nano-bébé» était bien formé à «l'école» et sait maintenant convertir avec précision des degrés Celsius en degrés Fahrenheit même pour des données qu'il n'a pas vues, nous pouvons l'appeler assez «intelligent». Maintenant, nous pouvons même lui demander des conseils sur la conversion de température, et c'était le but de toute la formation.


 const tempInCelsius = 70; const customPrediction = nanoNeuron.predict(tempInCelsius); console.log(`- "",  ${tempInCelsius}°C   :`, customPrediction); // -> 158.0002 console.log('  :', celsiusToFahrenheit(tempInCelsius)); // -> 158 

Très proche! Comme les gens, notre nano-neurone est bon, mais pas parfait :)


Codage réussi!


Comment exécuter et tester un nano-neurone


Vous pouvez cloner le référentiel et exécuter le nano neurone localement:


 git clone https://github.com/trekhleb/nano-neuron.git cd nano-neuron 

 node ./NanoNeuron.js 

Concepts manqués


Les concepts d'apprentissage automatique suivants ont été omis ou simplifiés pour faciliter l'explication.


Séparation des ensembles de données de formation et de test


Habituellement, vous avez un ensemble de données volumineuses. Selon le nombre d'exemplaires de cet ensemble, sa division en ensembles d'apprentissage et de test peut être effectuée dans la proportion de 70/30. Les données de l'ensemble doivent être mélangées au hasard avant d'être divisées. Si la quantité de données est importante (par exemple, des millions), la division en ensembles de test et de formation peut être effectuée dans des proportions proches de 90/10 ou 95/5.


Puissance en ligne


Habituellement, vous ne trouverez pas de cas où un seul neurone est utilisé. La force réside dans le réseau de ces neurones. Un réseau de neurones peut apprendre des dépendances beaucoup plus complexes.


Toujours dans l'exemple ci-dessus, notre nano-neurone peut ressembler davantage à une simple régression linéaire qu'à un réseau neuronal.


Normalisation d'entrée


Avant l'entraînement, il est d'usage de normaliser les données d'entrée .


Implémentation vectorielle


Pour les réseaux de neurones, les calculs vectoriels (matriciels) sont beaucoup plus rapides que les calculs for boucles. Habituellement, la propagation directe et inverse du signal est effectuée à l'aide d'opérations matricielles utilisant, par exemple, la bibliothèque Python Numpy .


Fonction d'erreur minimale


La fonction d'erreur que nous avons utilisée pour le nano neurone est très simplifiée. Il doit contenir des composants logarithmiques . Un changement dans la formule de la fonction d'erreur entraînera également un changement dans les formules de propagation avant et arrière du signal.


Fonction d'activation


Habituellement, la valeur de sortie du neurone passe par la fonction d'activation. Pour l'activation, des fonctions telles que Sigmoid , ReLU et autres peuvent être utilisées.

Source: https://habr.com/ru/post/fr479220/


All Articles