Neurales Nummernetzwerk trainieren

  • C#
  • .NET (FX) 4.0

Es gibt 6 Antworten in diesem Thema. Der letzte Beitrag () ist von faxe1008.

    Neurales Nummernetzwerk trainieren

    Hi,

    Ich bin neulich mal auf jenes Video gestoßen:


    Dachte mir, ok scheint jetzt nicht so abartig kompliziert zu werden - kann man ja mal versuchen.
    Das ist mein Ansatz:

    NeuralHelper

    C#-Quellcode

    1. using System;
    2. using System.Collections.Generic;
    3. using System.Linq;
    4. using System.Text;
    5. using System.Threading.Tasks;
    6. namespace NeuralNumberNetwork {
    7. class NeuralHelper {
    8. public static double SigmoidFunction(double x) {
    9. double eX = Math.Pow(Math.E, x);
    10. return eX / (1 + eX);
    11. }
    12. public static String getRandomName(int length) {
    13. StringBuilder sb = new StringBuilder();
    14. Random rand = new Random();
    15. for (int i = 0; i < length; i++) {
    16. var ch = Convert.ToChar(49+rand.Next(20));
    17. sb.Append(ch);
    18. }
    19. return sb.ToString();
    20. }
    21. }
    22. }


    NerualNetwork

    C#-Quellcode

    1. using System;
    2. using System.Collections.Generic;
    3. using System.Linq;
    4. using System.Text;
    5. using System.Threading.Tasks;
    6. namespace NeuralNumberNetwork {
    7. abstract class NeuralNetwork {
    8. private List<Neuron> _InputNeurons = new List<Neuron>();
    9. public List<Neuron> InputNeurons { get { return _InputNeurons; } }
    10. private List<Neuron> _ResultNeurons = new List<Neuron>();
    11. public List<Neuron> ResultNeurons { get { return _ResultNeurons; } }
    12. public abstract void InitializeInputNeurons(Object inputData);
    13. public abstract void IntializeHiddenLayers();
    14. public abstract void IntializeResultNeurons();
    15. public Neuron runNetwork() {
    16. Neuron result = new Neuron() {Activation = 0 };
    17. for (int i = 0; i < ResultNeurons.Count; i++) {
    18. double neuronResult = ResultNeurons[i].EvaluateActivation();
    19. if (neuronResult > result.Activation) {
    20. result = ResultNeurons[i];
    21. }
    22. }
    23. return result;
    24. }
    25. public double evaluateFailure(int indexExpectedResult) {
    26. double sqaureSum = 0;
    27. for (int i = 0; i < ResultNeurons.Count; i++) {
    28. if (i == indexExpectedResult) {
    29. sqaureSum += Math.Pow(1 - ResultNeurons[i].Activation, 2);
    30. } else {
    31. sqaureSum += Math.Pow(0 - ResultNeurons[i].Activation, 2);
    32. }
    33. }
    34. return sqaureSum;
    35. }
    36. }
    37. }


    Neuron

    C#-Quellcode

    1. using System;
    2. using System.Collections.Generic;
    3. using System.Linq;
    4. using System.Text;
    5. using System.Threading.Tasks;
    6. namespace NeuralNumberNetwork {
    7. class Neuron {
    8. public Neuron(String Name) {
    9. this.Name = Name;
    10. }
    11. public Neuron() {
    12. this.Name = NeuralHelper.getRandomName(20);
    13. }
    14. public Neuron(double Activation, bool isBase) {
    15. this.Activation = Activation;
    16. this.Name = NeuralHelper.getRandomName(20);
    17. this._IsBaseNeuron = isBase;
    18. }
    19. /// <summary>
    20. /// Name of the Neuron
    21. /// </summary>
    22. public String Name { get; }
    23. /// <summary>
    24. /// Is the degree of Activation of the Neuron
    25. /// </summary>
    26. private double _Activation = 0 ;
    27. public double Activation {
    28. get {
    29. return _Activation;
    30. }
    31. set {
    32. if (value > 1 || value < 0) {
    33. throw new ArgumentException("Neuron Activation has to be between 0 and 1.");
    34. }
    35. _Activation = value;
    36. }
    37. }
    38. /// <summary>
    39. /// The Bias of the Neuron
    40. /// </summary>
    41. public double Bias { get; set; }
    42. /// <summary>
    43. /// Determines if Neuron is in the starter layer of the network
    44. /// </summary>
    45. private readonly bool _IsBaseNeuron = false;
    46. public bool IsBaseNeuron { get { return _IsBaseNeuron; } }
    47. /// <summary>
    48. /// List of all the Predeccessor Neurons connected to the Neuron
    49. /// </summary>
    50. private List<Neuron> _PredeccessorNeurons = new List<Neuron>();
    51. public List<Neuron> PredeccessorNeurons { get { return _PredeccessorNeurons; } }
    52. /// <summary>
    53. /// List of all the Weights of the Predeccessor Neurons connected to this Neuron
    54. /// </summary>
    55. private List<double> _PredeccessorWeights = new List<double>();
    56. public List<double> PredeccessorWeights { get { return _PredeccessorWeights; } }
    57. /// <summary>
    58. /// Calculation of the Activation using recursion
    59. /// </summary>
    60. /// <returns></returns>
    61. public double EvaluateActivation() {
    62. //Evalutation only on inner Neurons need when they have not been evaluated before
    63. if (!this.IsBaseNeuron) {
    64. double weightedSum = 0;
    65. for (int i = 0; i < this.PredeccessorNeurons.Count; i++) {
    66. weightedSum += this.PredeccessorNeurons[i].EvaluateActivation() * this.PredeccessorWeights[i];
    67. }
    68. //Subtract the Bias
    69. weightedSum = weightedSum - Bias;
    70. //Calculate Sigmoid function (e^x)/(1+e^x), optimization possible
    71. this.Activation = NeuralHelper.SigmoidFunction(weightedSum);
    72. }
    73. return this.Activation;
    74. }
    75. public void randomizeWeights() {
    76. Random rand = new Random();
    77. //Order of weight to Neuron is mixed but does not matter since it is random anyways
    78. for (int i = 0; i < this.PredeccessorNeurons.Count; i++) {
    79. this.PredeccessorWeights.Add(rand.NextDouble());
    80. }
    81. }
    82. public override string ToString() {
    83. return this.Name + " :" + this.Activation;
    84. }
    85. }
    86. }


    Crappy-Implementierung:
    Spoiler anzeigen

    C#-Quellcode

    1. using System;
    2. using System.Collections.Generic;
    3. using System.Drawing;
    4. using System.Linq;
    5. using System.Text;
    6. using System.Threading.Tasks;
    7. namespace NeuralNumberNetwork.NumberNeuralNetwork {
    8. class NumberNeuralNetwork : NeuralNetwork {
    9. /// <summary>
    10. /// inputData is an Image
    11. /// </summary>
    12. /// <param name="inputData"></param>
    13. public override void InitializeInputNeurons(object inputData) {
    14. Bitmap bmp = (Bitmap)inputData;
    15. for (int x = 0; x < bmp.Size.Width; x++) {
    16. for (int y = 0; y < bmp.Size.Height; y++) {
    17. var activation = getActivation(bmp.GetPixel(x,y));
    18. this.InputNeurons.Add(new Neuron(activation,true));
    19. }
    20. }
    21. }
    22. public double getActivation(Color c) {
    23. return (c.R + c.B + c.G) / (3 * 255);
    24. }
    25. public override void IntializeResultNeurons() {
    26. for (int i = 0; i < 10; i++) {
    27. ResultNeurons.Add(new Neuron("Number_"+i+"_Neuron"));
    28. }
    29. }
    30. public override void IntializeHiddenLayers() {
    31. List<Neuron> Shapes = new List<Neuron>();
    32. Shapes.Add(new Neuron("TOPLOOP") );
    33. Shapes.Add(new Neuron("BOTTOMLOOP"));
    34. Shapes.Add(new Neuron("TOPLEFTEDGE"));
    35. Shapes.Add(new Neuron("TOPRIGHTEDGE"));
    36. Shapes.Add(new Neuron("BOTTOMLEFTEDGE"));
    37. Shapes.Add(new Neuron("BOTTOMTIGHTEDGE"));
    38. Shapes.Add(new Neuron("MIDDLELINE"));
    39. foreach (var item in ResultNeurons) {
    40. foreach (var middle in Shapes) {
    41. item.PredeccessorNeurons.Add(middle);
    42. }
    43. item.randomizeWeights();
    44. }
    45. foreach (var item in Shapes) {
    46. foreach (var input in InputNeurons) {
    47. item.PredeccessorNeurons.Add(input);
    48. }
    49. item.randomizeWeights();
    50. }
    51. }
    52. }
    53. }


    Idee ist dass ich quasi bei einem ResultNeuron EvaluateActivation aufrufe und dann rekursiv durchgerechnet wird. Das funktioniert alles auch soweit wie es soll.
    Jetzt habe ich allerdings Verständnis Fragen um weiter zu kommen:
    1. Habe mal nur eine Layer eingefügt, die gleiche zusammengesetzte Formen erkennen soll z.B. TOPLOOP als oberer Kringel von einer 8.
    Da es allerdings nahezu unzumutbar ist da von Hand Gewichte ranzubauen - (28*28)<Bildgröße>*(6)<Anzahl der Subformen>*(10)<Anzahl möglicher Ziffern> habe ich eine Methode geschrieben die die Dinger zufällig wählt. Was die Frage bei mir aufwirft, ob man sich über solche Unterteilungsgeschichten überhaupt Gedanken machen muss, oder ob man einfach beliebig viele Neuronen in die Zwischenlayers haut und hofft, dass sie sich halt nach langem Training anpassen?


    2. Wie ziehe ich aufgrund der Fehlerrate (Quadratsumme der Abweichung) Rückschlüsse darauf wie ich die Gewichte nachjustieren muss?

    3. Ist die Vorgehensweise für ein allgemeines Konzept eines Neuralen Netzwerkes annehmbar?

    8-) faxe1008 8-)
    Ich glaube, du hast ein paar grundlegende Sachen noch nicht ganz verstanden:

    faxe1008 schrieb:



    Idee ist dass ich quasi bei einem ResultNeuron EvaluateActivation aufrufe und dann rekursiv durchgerechnet wird


    Man fängt aber beim ersten Layer (Eingabelayer) an und rechnet dann weiter bis zum Ausgabelayer... Für die Ausgabe eines Neurons
    $y_k$
    eines Layers gilt:

    $y_k = o\left( \sum_{i=0}^{N} w_ix_i\right)$


    $o$
    - Aktivierungsfunktion
    $N$
    - Anzahl Gewichte (Eingänge) des Neurons
    $w_i$
    -
    $i$
    tes Gewicht des Neurons
    $x_i$
    -
    $i$
    te Eingabe des Neurons

    Du bestimmst also
    $y_k$
    für jedes Neuron
    $k$
    des Eingabelayers und fährst dann mit dem nächsten Layer fort. Die
    $y_k$
    werden dabei zu den
    $x_i$
    des nachfolgenden Layers.

    Ein separater Bias ist nicht zwingend notwending, wenn man die erste Eingabe
    $x_0 = 1$
    setzt. Dies macht auch die Implementierung der Backpropagation einfacher (siehe unten)

    faxe1008 schrieb:


    2. Wie ziehe ich aufgrund der Fehlerrate (Quadratsumme der Abweichung) Rückschlüsse darauf wie ich die Gewichte nachjustieren muss?


    Mittels Backpropagation des Fehlergradienten (partielle Ableitungen der Fehlerfunktion bezüglich der Gewichte).

    Dieser Beitrag wurde bereits 11 mal editiert, zuletzt von „Quadsoft“ ()

    Quadsoft schrieb:

    Man fängt aber beim ersten Layer (Eingabelayer) an und rechnet dann weiter bis zum Ausgabelayer


    Ist das nicht genau das was die Rekursion beginnend bei den Ergebnis Neuronen tut?


    Wenn ich das ganze so berechne wie in meinem Modell dann würde das ja so aussehen:

    Quellcode

    1. L3_1 = w(L2_1) * L2_1 + w(L2_2) * L2_2
    2. L2_1 = w(L1_1)*L1_1+w(L1_2)*L1_2+w(L1_3)*L1_3
    3. L2_2 = w(L1_1)*L1_1+w(L1_2)*L1_2+w(L1_3)*L1_3


    Wobei w() das Gewicht der Neuroneverbindung ist. Ich hab die Sigmoid Function nicht mit rein genommen, da das ganze sonst nur unübersichtlich wird. Ich bekomme allerdings für jedes Resulat Neuron dieselbe Aktivierung O.o ?

    Quadsoft schrieb:

    Mittels Backpropagation des Fehlergradienten (partielle Ableitungen der Fehlerfunktion bezüglich der Gewichte).


    Danke werde ich mir ansehen sobald das eigentliche Berechnen funktioniert :)

    EDIT:// Habe mal die Projektmappe ohne Executable angehängt, damit man sich es einfacher ansehen kann.
    Dateien

    8-) faxe1008 8-)

    Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von „faxe1008“ ()

    @faxe1008

    Dein Code aus dem Anhang stürzt ab, da die PredecessorWeights nicht gesetzt sind. Der Ansatz, das rekursiv zu berechnen ist zwar denkbar, aber umständlich und bei großen Netzen nicht performant.

    Ich habe das mal geschrieben, wie ich mir das vorstelle:

    Layer.cs

    C#-Quellcode

    1. using System;
    2. namespace NeuralNumberNetwork
    3. {
    4. class Layer
    5. {
    6. int neuronCount;
    7. int inputsPerNeuron;
    8. double[,] weights;
    9. double[] activations;
    10. Activation activation;
    11. public Layer(int neuronCount, int inputsPerNeuron, Activation activation)
    12. {
    13. this.neuronCount = neuronCount;
    14. this.inputsPerNeuron = inputsPerNeuron;
    15. this.activation = activation;
    16. weights = new double[neuronCount, inputsPerNeuron + 1]; // extra Input für Bias
    17. activations = new double[neuronCount];
    18. }
    19. public void ComputeActivations(double[] inputs)
    20. {
    21. if (inputs.Length != inputsPerNeuron)
    22. throw new Exception("Input length does not match this layer");
    23. for (int i = 0; i < neuronCount; i++)
    24. {
    25. double sum = weights[i, inputs.Length]; // Bias
    26. for (int j = 0; j < inputs.Length; j++)
    27. {
    28. sum += inputs[j] * weights[i, j]; // Inputs
    29. }
    30. activations[i] = activation(sum);
    31. }
    32. }
    33. public void InitializeWeights() // Mit Zufallszahlen befüllen
    34. {
    35. Random rand = new Random();
    36. for (int i = 0; i < neuronCount; i++)
    37. {
    38. for (int j = 0; j < inputsPerNeuron + 1; j++)
    39. {
    40. weights[i, j] = rand.NextDouble() * (rand.Next(2) > 1 ? 1.0 : -1.0);
    41. }
    42. }
    43. }
    44. public double[] Activations
    45. {
    46. get { return this.activations; }
    47. }
    48. }
    49. }



    Activation.cs

    C#-Quellcode

    1. using System;
    2. namespace NeuralNumberNetwork
    3. {
    4. delegate double Activation(double x);
    5. static class Activations
    6. {
    7. public static double Sigmoid(double x)
    8. {
    9. return 1.0 / (1 + Math.Exp(-x));
    10. }
    11. }
    12. }



    Program.cs

    C#-Quellcode

    1. using System;
    2. namespace NeuralNumberNetwork {
    3. class Program {
    4. static void Main(string[] args) {
    5. double[] inputs = { 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 };
    6. Layer hiddenLayer = new Layer(30, 10, Activations.Sigmoid);
    7. Layer outputLayer = new Layer(10, 30, Activations.Sigmoid);
    8. hiddenLayer.InitializeWeights();
    9. outputLayer.InitializeWeights();
    10. hiddenLayer.ComputeActivations(inputs);
    11. outputLayer.ComputeActivations(hiddenLayer.Activations);
    12. foreach (double d in outputLayer.Activations)
    13. Console.Write("{0} ", d);
    14. Console.ReadLine();
    15. }
    16. }
    17. }


    Der Input in Program.cs ist nur ein Beispiel, natürlich kannst du da auch die Pixelwerte aus deinem Bild nehmen.

    Der nächste Schritt ist jetzt das Hinzufügen der Fehlerfunktion und die Durchführung der Backpropagation.

    Dieser Beitrag wurde bereits 3 mal editiert, zuletzt von „Quadsoft“ ()

    @Quadsoft
    Habe mir deine Implementierung mal angesehen, manchmal kann man echt unnötig irgendwo Rekursion reinfummeln wollen ^^ . *Hust*

    Rückwärtspropagation habe ich diese Seite hier gefunden: codingvision.net/miscellaneous…kpropagation-tutorial-xor
    Habe Theorie verstanden nur bleibt die Frage stehen ob die Formel für die hidden layers die er da verwendet, dann auch für ein Netz mit 2 Hiddenlayers gilt:

    InputH1H2Output
    i1H11H21O1
    i2H12H22O2
    Sprich der Artikel erklärt wie von O1 auf H21 und H22 gerechnet wird. Funktioniert das Rückwärtspropagieren genauso von H21 zu H11 bzw. H12, respektive dann im nächsten Schritt die Input Layer?

    BTW: Habe deine Idee mal in eine abstrakte Klasse überführt:

    C#-Quellcode

    1. ​public abstract class NeuralNetwork {
    2. private List<Layer> _Layers = new List<Layer>();
    3. public List<Layer> layers { get { return _Layers; } }
    4. public NeuralNetwork(int inputLayerCount, int[] hiddenLayerNeurons, int outputLayer, Activation a) {
    5. layers.Add(new Layer(inputLayerCount, 0, a));
    6. layers.Add(new Layer(hiddenLayerNeurons[0], inputLayerCount, a));
    7. for (int i = 1; i < hiddenLayerNeurons.Length; i++) {
    8. layers.Add(new Layer(hiddenLayerNeurons[i], hiddenLayerNeurons[i-1], a));
    9. }
    10. layers.Add(new Layer(outputLayer, hiddenLayerNeurons[hiddenLayerNeurons.Length-1], a));
    11. foreach (var item in layers) {
    12. item.InitializeWeights();
    13. }
    14. }
    15. public void ComputeActivations() {
    16. for (int i = 1; i < layers.Count; i++) {
    17. layers[i].ComputeActivations(layers[i - 1].Activations);
    18. }
    19. }
    20. public abstract void populateInputLayer(Object obj);
    21. }

    8-) faxe1008 8-)

    Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von „faxe1008“ ()

    faxe1008 schrieb:

    Funktioniert das Rückwärtspropagieren genauso von H21 zu H11 bzw. H12, respektive dann im nächsten Schritt die Input Layer?


    Prinzipiell ja, nur ist das Beispiel von der verlinkten Website recht einfach, denn es gibt nur ein einziges Ausgabe-Neuron. Wenn es mehrere Nachfolger-Neuronen gibt, musst du die Summe der gewichteten Fehlerterme betrachten:

    $\text{error}_{i,l} = \begin{cases} (\text{target}_i - \text{output}_{i,l}) f'(\text{output}_{i,l}) & \text {für } l = N \text{ (Ausgabelayer)} \\ \left( \sum_{k} \text{error}_{k, l+1}w_{i,k,l} \right) f'(\text{output}_{i,j}) & \text {sonst} \end{cases} $


    $\text{error}_{i,l}$ - Fehlerterm für das $i$te Neuron im $l$ten Layer

    $\text{output}_{i,l}$ - Ausgabe des $i$ten Neurons im $l$ten Layer

    $\text{target}_{i}$ - Zielausgabe des $i$ten Neurons im Ausgabelayer (das was gelernt werden soll)

    $\text{w}_{i,k,l}$ - Gewicht zwischen dem $i$ten Neuron im Layer $l$ und dem $k$ten Neuron im nachfolgenden Layer


    Das Updaten der Gewichte folgt der Formel aus dem Tutorial. In der Regel macht man aber nicht lediglich

    $w = w + \text{error}\cdot\text{output}$


    sondern fügt eine Lernrate
    $\varepsilon$
    ein, die steuert, wie stark die Anpassung der Gewichte stattfindet:

    $w = w + \text{error}\cdot\text{output}\cdot \varepsilon$


    In der Regel ist
    $\varepsilon$
    eine Zahl betragsmäßig kleiner 1, z.B. 0.05. Dies verbessert die Konvergenz des Gradientenabstiegs.
    @Quadsoft

    Habe die Fehlerfunktion mal implementiert, bin mir allerdings nicht sicher ob ich bei den ganzen Indicies durcheinander gekommen bin:

    C#-Quellcode

    1. private double errorNeuronInLayer(int layerIndex, int NeuronIndex, int expectedNeuronResultIndex) {
    2. //check if its the output layer
    3. if (layerIndex == layers.Count - 1) {
    4. var output_i_l = this.layers[layerIndex].Activations[NeuronIndex];
    5. return ((NeuronIndex == expectedNeuronResultIndex ? 1 : 0) - output_i_l) * this.ActivationFunc.ActivationFunctionDerivative(output_i_l);
    6. } else {
    7. double sum = 0;
    8. for (int k = 0; k < this.layers[layerIndex-1].Activations.Length; k++) {
    9. sum += errorNeuronInLayer(layerIndex - 1, k, 0) * this.layers[layerIndex].Weights[NeuronIndex, k];
    10. }
    11. sum *= this.ActivationFunc.ActivationFunctionDerivative(this.layers[layerIndex].Activations[NeuronIndex]);
    12. return sum;
    13. }
    14. }


    Habe die Aktivierungsfunktion in ein eigenes Interface gepackt, da man ja immer paarweise Archivierungsfunktion und deren Ableitung braucht.

    C#-Quellcode

    1. ​public interface IActivationFunction {
    2. double ActivationFunction(double x);
    3. double ActivationFunctionDerivative(double x);
    4. }



    Layers werden ja nach diesem Schema in die LayerListe gegeben:
    IndexLayer
    0Eingabelayer
    1Hiddenlayer 1
    2Hiddenlayer 2
    3Ausgabelayer

    Ich war verwirrt welcher Richtung mit "nächsten Layer" jetzt gemeint war ^^ , denn eigentlich geht man vom index her zurück (daher ja auch backpropagation)

    8-) faxe1008 8-)