web-dev-qa-db-fra.com

Faire fonctionner un réseau neuronal simple à partir de zéro en C ++

J'ai essayé d'obtenir un simple réseau de neurones double XOR) et j'ai du mal à obtenir une rétropropagation pour former un réseau de neurones à action directe très simple.
J'ai surtout essayé de suivre ce guide pour obtenir un réseau de neurones, mais au mieux j'ai créé des programmes qui apprennent à un rythme extrêmement lent.

Si je comprends bien les réseaux de neurones:

  1. Les valeurs sont calculées en prenant le résultat d'une fonction sigmoïde de la somme de toutes les entrées de ce neurone. Ceci est ensuite alimenté à la couche suivante en utilisant le poids de chaque neurone
  2. À la fin de l'exécution, l'erreur est calculée pour les neurones de sortie, puis en utilisant les poids, l'erreur est propagée en arrière simplement en multipliant les valeurs, puis en sommant à chaque neurone
  3. Lorsque toutes les erreurs sont calculées, les poids sont ajustés par le delta = poids de la connexion * dérivée du sigmoïde (la valeur du poids du neurone va) * la valeur du neurone que la connexion est à * l'erreur du neurone * la quantité d'erreur de sortie de neurone allant à * beta (une constante pour le taux d'apprentissage)

This est ma boue de code actuelle que j'essaie de faire fonctionner. J'ai beaucoup d'autres tentatives quelque peu mélangées, mais la principale fonction de rétropropagation que j'essaie de faire fonctionner est sur la ligne 293 dans Net.cpp

31
Matthew FL

Jetez un oeil à 15 étapes pour mettre en œuvre un réseau neuronal , il devrait vous aider à démarrer.

21
Gregory Pakosz

J'ai écrit un simple "Tutoriel" que vous pouvez consulter ci-dessous.

Il s'agit d'une implémentation simple du modèle perceptron. Vous pouvez imaginer un perceptron comme un réseau neuronal avec un seul neurone. Il y a du code maudit que vous pouvez tester que j'ai écrit en C++. Je passe par le code étape par étape afin que vous ne rencontriez aucun problème.

Bien que le perceptron ne soit pas vraiment un "réseau neuronal", il est vraiment utile si vous voulez commencer et pourrait vous aider à mieux comprendre comment fonctionne un réseau neuronal complet.

J'espère que cela pourra aider! À votre santé! ^ _ ^



Dans cet exemple, je vais passer par l'implémentation du modèle perceptron en C++ afin que vous puissiez avoir une meilleure idée de son fonctionnement.

Tout d'abord, c'est une bonne pratique d'écrire un algorithme simple de ce que nous voulons faire.

Algorithme:

  1. Faites un vecteur pour les poids et initialisez-le à 0 (N'oubliez pas d'ajouter le terme de biais)
  2. Continuez à ajuster les poids jusqu'à ce que nous obtenions 0 erreur ou un faible nombre d'erreurs.
  3. Faites des prédictions sur des données invisibles.

Après avoir écrit un algorithme super simple, écrivons maintenant certaines des fonctions dont nous aurons besoin.

  • Nous aurons besoin d'une fonction pour calculer l'entrée du filet (e.i * x * wT * multipliant les entrées fois les poids)
  • Une fonction de pas pour obtenir une prédiction de 1 ou -1
  • Et une fonction qui trouve les valeurs idéales pour les poids.

Alors sans plus tarder, allons droit au but.

Commençons simple en créant une classe perceptron:

class perceptron
{
public:

private:

};

Ajoutons maintenant les fonctions dont nous aurons besoin.

class perceptron
{
public:
    perceptron(float eta,int epochs);
    float netInput(vector<float> X);
    int predict(vector<float> X);
    void fit(vector< vector<float> > X, vector<float> y);
private:

};

Remarquez comment la fonction fit prend comme argument un vecteur du vecteur <float>. En effet, notre ensemble de données d'apprentissage est une matrice d'entrées. Essentiellement, nous pouvons imaginer cette matrice comme un couple de vecteurs x empilés l'un sur l'autre et chaque colonne de cette matrice étant une fonctionnalité.

Ajoutons enfin les valeurs que notre classe doit avoir. Tels que le vecteur w pour contenir les poids, le nombre de = epochs qui indique le nombre de passes que nous effectuerons sur l'ensemble de données d'entraînement. Et la constante eta qui est le rythme d'apprentissage dont on va multiplier chaque mise à jour de poids afin de faire l'entraînement procédure plus rapide en composant cette valeur ou si eta est trop élevé, nous pouvons le composer pour obtenir le résultat idéal (pour la plupart des applications du perceptron, je suggère une valeur eta de 0,1).

class perceptron
{
public:
    perceptron(float eta,int epochs);
    float netInput(vector<float> X);
    int predict(vector<float> X);
    void fit(vector< vector<float> > X, vector<float> y);
private:
    float m_eta;
    int m_epochs;
    vector < float > m_w;
};

Maintenant avec notre ensemble de classe. Il est temps d'écrire chacune des fonctions.

Nous partirons du constructeur ( perceptron (float eta, int epochs); )

perceptron::perceptron(float eta, int epochs)
{
    m_epochs = epochs; // We set the private variable m_epochs to the user selected value
    m_eta = eta; // We do the same thing for eta
}

Comme vous pouvez le voir, ce que nous allons faire est très simple. Passons donc à une autre fonction simple. La fonction de prédiction ( int predict (vecteur X);). N'oubliez pas que ce que fait la fonction all prédire prend l'entrée nette et renvoie une valeur de 1 si le netInput est plus grand que 0 et -1 ailleurs.

int perceptron::predict(vector<float> X)
{
    return netInput(X) > 0 ? 1 : -1; //Step Function
}

Notez que nous avons utilisé une instruction if en ligne pour nous faciliter la vie. Voici comment fonctionne l'instruction inline if:

condition? if_true: else

Jusqu'ici tout va bien. Passons à l'implémentation de la fonction netInput ( float netInput (vecteur X);)

Le netInput effectue les opérations suivantes; multiplie le vecteur d'entrée par la transposition du vecteur de poids

* x * wT *

En d'autres termes, il multiplie chaque élément du vecteur d'entrée x par l'élément correspondant du vecteur de poids w puis prend leur somme et ajoute le biais.

* (x1 * w1 + x2 * w2 + ... + xn * wn) + biais *

* biais = 1 * w0 *

float perceptron::netInput(vector<float> X)
{
    // Sum(Vector of weights * Input vector) + bias
    float probabilities = m_w[0]; // In this example I am adding the perceptron first
    for (int i = 0; i < X.size(); i++)
    {
        probabilities += X[i] * m_w[i + 1]; // Notice that for the weights I am counting
        // from the 2nd element since w0 is the bias and I already added it first.
    }
    return probabilities;
}

Très bien, nous avons maintenant à peu près terminé la dernière chose que nous devons faire est d'écrire la fonction fit qui modifie la poids.

void perceptron::fit(vector< vector<float> > X, vector<float> y)
{
    for (int i = 0; i < X[0].size() + 1; i++) // X[0].size() + 1 -> I am using +1 to add the bias term
    {
        m_w.Push_back(0); // Setting each weight to 0 and making the size of the vector
        // The same as the number of features (X[0].size()) + 1 for the bias term
    }
    for (int i = 0; i < m_epochs; i++) // Iterating through each Epoch
    {
        for (int j = 0; j < X.size(); j++) // Iterating though each vector in our training Matrix
        {
            float update = m_eta * (y[j] - predict(X[j])); //we calculate the change for the weights
            for (int w = 1; w < m_w.size(); w++){ m_w[w] += update * X[j][w - 1]; } // we update each weight by the update * the training sample
            m_w[0] = update; // We update the Bias term and setting it equal to the update
        }
    }
}

C'était donc essentiellement ça. Avec seulement 3 fonctions, nous avons maintenant une classe de perceptron fonctionnelle que nous pouvons utiliser pour faire des prédictions!

Dans le cas où vous souhaitez copier-coller le code et l'essayer. Voici la classe entière (j'ai ajouté des fonctionnalités supplémentaires telles que l'impression du vecteur de poids et des erreurs dans chaque époque ainsi que la possibilité d'importer/exporter des poids.)

Voici le code:

L'en-tête de classe:

class perceptron
{
public:
    perceptron(float eta,int epochs);
    float netInput(vector<float> X);
    int predict(vector<float> X);
    void fit(vector< vector<float> > X, vector<float> y);
    void printErrors();
    void exportWeights(string filename);
    void importWeights(string filename);
    void printWeights();
private:
    float m_eta;
    int m_epochs;
    vector < float > m_w;
    vector < float > m_errors;
};

Le fichier de classe .cpp avec les fonctions:

perceptron::perceptron(float eta, int epochs)
{
    m_epochs = epochs;
    m_eta = eta;
}

void perceptron::fit(vector< vector<float> > X, vector<float> y)
{
    for (int i = 0; i < X[0].size() + 1; i++) // X[0].size() + 1 -> I am using +1 to add the bias term
    {
        m_w.Push_back(0);
    }
    for (int i = 0; i < m_epochs; i++)
    {
        int errors = 0;
        for (int j = 0; j < X.size(); j++)
        {
            float update = m_eta * (y[j] - predict(X[j]));
            for (int w = 1; w < m_w.size(); w++){ m_w[w] += update * X[j][w - 1]; }
            m_w[0] = update;
            errors += update != 0 ? 1 : 0;
        }
        m_errors.Push_back(errors);
    }
}

float perceptron::netInput(vector<float> X)
{
    // Sum(Vector of weights * Input vector) + bias
    float probabilities = m_w[0];
    for (int i = 0; i < X.size(); i++)
    {
        probabilities += X[i] * m_w[i + 1];
    }
    return probabilities;
}

int perceptron::predict(vector<float> X)
{
    return netInput(X) > 0 ? 1 : -1; //Step Function
}

void perceptron::printErrors()
{
    printVector(m_errors);
}

void perceptron::exportWeights(string filename)
{
    ofstream outFile;
    outFile.open(filename);

    for (int i = 0; i < m_w.size(); i++)
    {
        outFile << m_w[i] << endl;
    }

    outFile.close();
}

void perceptron::importWeights(string filename)
{
    ifstream inFile;
    inFile.open(filename);

    for (int i = 0; i < m_w.size(); i++)
    {
        inFile >> m_w[i];
    }
}

void perceptron::printWeights()
{
    cout << "weights: ";
    for (int i = 0; i < m_w.size(); i++)
    {
        cout << m_w[i] << " ";
    }
    cout << endl;
}

Aussi, si vous voulez essayer un exemple, voici un exemple que j'ai fait:

main.cpp:

#include <iostream>
#include <vector>
#include <algorithm>
#include <fstream>
#include <string>
#include <math.h> 

#include "MachineLearning.h"

using namespace std;
using namespace MachineLearning;

vector< vector<float> > getIrisX();
vector<float> getIrisy();

int main()
{
    vector< vector<float> > X = getIrisX();
    vector<float> y = getIrisy();
    vector<float> test1;
    test1.Push_back(5.0);
    test1.Push_back(3.3);
    test1.Push_back(1.4);
    test1.Push_back(0.2);

    vector<float> test2;
    test2.Push_back(6.0);
    test2.Push_back(2.2);
    test2.Push_back(5.0);
    test2.Push_back(1.5);
    //printVector(X);
    //for (int i = 0; i < y.size(); i++){ cout << y[i] << " "; }cout << endl;

    perceptron clf(0.1, 14);
    clf.fit(X, y);
    clf.printErrors();
    cout << "Now Predicting: 5.0,3.3,1.4,0.2(CorrectClass=-1,Iris-setosa) -> " << clf.predict(test1) << endl;
    cout << "Now Predicting: 6.0,2.2,5.0,1.5(CorrectClass=1,Iris-virginica) -> " << clf.predict(test2) << endl;

    system("PAUSE");
    return 0;
}

vector<float> getIrisy()
{
    vector<float> y;

    ifstream inFile;
    inFile.open("y.data");
    string sampleClass;
    for (int i = 0; i < 100; i++)
    {
        inFile >> sampleClass;
        if (sampleClass == "Iris-setosa")
        {
            y.Push_back(-1);
        }
        else
        {
            y.Push_back(1);
        }
    }

    return y;
}

vector< vector<float> > getIrisX()
{
    ifstream af;
    ifstream bf;
    ifstream cf;
    ifstream df;
    af.open("a.data");
    bf.open("b.data");
    cf.open("c.data");
    df.open("d.data");

    vector< vector<float> > X;

    for (int i = 0; i < 100; i++)
    {
        char scrap;
        int scrapN;
        af >> scrapN;
        bf >> scrapN;
        cf >> scrapN;
        df >> scrapN;

        af >> scrap;
        bf >> scrap;
        cf >> scrap;
        df >> scrap;
        float a, b, c, d;
        af >> a;
        bf >> b;
        cf >> c;
        df >> d;
        X.Push_back(vector < float > {a, b, c, d});
    }

    af.close();
    bf.close();
    cf.close();
    df.close();

    return X;
}

La façon dont j'ai importé le jeu de données iris n'est pas vraiment idéale, mais je voulais juste quelque chose qui fonctionnait.

Les fichiers de données peuvent être trouvés ici.

J'espère que vous avez trouvé cela utile!

Remarque: Le code ci-dessus n'est là qu'à titre d'exemple. Comme indiqué par juzzlin, il est important que vous utilisiez const vector<float> &X et en général passez le vector/vector<vector> objets par référence car les données peuvent être très volumineuses et les transmettre par valeur en fera une copie (ce qui est inefficace).

13
Panos

Il me semble que vous avez du mal avec backprop et ce que vous décrivez ci-dessus ne correspond pas tout à fait à ce que je comprends, et votre description est un peu ambiguë.

Vous calculez le terme d'erreur de sortie à rétropropager comme la différence entre la prédiction et la valeur réelle multipliée par la dérivée de la fonction de transfert. C'est cette valeur d'erreur que vous propagez ensuite à l'envers. La dérivée d'un sigmoïde est calculée tout simplement comme y(1-y) où y est votre valeur de sortie. Il existe de nombreuses preuves de cela disponibles sur le Web.

Pour un nœud sur la couche intérieure, vous multipliez cette erreur de sortie par le poids entre les deux nœuds et additionnez tous ces produits en tant qu'erreur totale de la couche extérieure propagée au nœud de la couche intérieure. L'erreur associée au nœud interne est ensuite multipliée par la dérivée de la fonction de transfert appliquée à la valeur de sortie d'origine. Voici un pseudocode:

total_error = sum(output_errors * weights)
node_error = sigmoid_derivative(node_output) * total_error

Cette erreur est ensuite propagée vers l'arrière de la même manière tout au long des pondérations de la couche d'entrée.

Les poids sont ajustés à l'aide de ces termes d'erreur et des valeurs de sortie des nœuds

weight_change = outer_error * inner_output_value

le taux d'apprentissage est important car le changement de poids est calculé pour chaque modèle/ligne/observation dans les données d'entrée. Vous voulez modérer le changement de poids pour chaque ligne afin que les poids ne soient pas indûment modifiés par une seule ligne et que toutes les lignes aient un effet sur les poids. Le taux d'apprentissage vous donne cela et vous ajustez le changement de poids en le multipliant

weight_change = outer_error * inner_output_value * learning_rate

Il est également normal de se souvenir de ces changements entre les époques (itérations) et d'en ajouter une fraction au changement. La fraction ajoutée est appelée momentum et est censée vous accélérer à travers les régions de la surface d'erreur où il n'y a pas beaucoup de changement et vous ralentir là où il y a des détails.

weight_change = (outer_error*inner_output_value*learning_rate) + (last_change*momentum)

Il existe des algorithmes pour ajuster le rythme d'apprentissage et l'élan au fur et à mesure de la formation.

Le poids est ensuite mis à jour en ajoutant la modification

new_weight = old_weight + weight_change

J'ai jeté un coup d'œil à votre code, mais plutôt que de le corriger et de poster que j'ai pensé qu'il était préférable de décrire le prop back pour vous afin que vous puissiez le coder vous-même. Si vous le comprenez, vous pourrez également l'adapter à votre situation.

HTH et bonne chance.

6
Simon

Que diriez-vous de ce code open-source. Il définit un simple réseau de 1 couche cachée (2 entrées, 2 cachées, 1 sortie) et résout le problème XOR:

http://www.sylbarth.com/mlp.php

4
Oleg Shirokikh