Do-it-yourself-neuronales Netzwerk von Grund auf neu. Teil 2. Implementierung

Wie ich in der Einleitung zum ersten Teil sagte , bin ich ein Frontend-Entwickler und meine Muttersprache ist JavaScript. Wir werden unser neuronales Netzwerk in diesem Artikel damit implementieren. Zunächst ein paar Worte zur Struktur. Mit Ausnahme verschiedener berechneter Eigenschaften und Methoden enthält das neuronale Netzwerkobjekt eine Reihe von Schichten, jede Schicht eine Reihe von Neuronen und jedes Neuron wiederum eine Reihe von „Eingaben“ - Verbindungen mit den Neuronen der vorherige Eingabeebene. Da wir für das gesamte Netzwerk Gemeinsamkeiten haben, wie z. B. die Aktivierungsfunktion, ihre Ableitung und die Lernrate, und wir von jedem Neuron aus darauf zugreifen müssen, stimmen wir zu, dass das Neuron eine _layer-Verknüpfung zu der Ebene hat, zu der es gehört es gehört, und die Schicht wird _network haben - eine Verbindung zum Netzwerk selbst.





Gehen wir von privat zu allgemein und beschreiben zunächst die Eingabeklasse für das Neuron.





class Input {
 constructor(neuron, weight) {
   this.neuron = neuron;
   this.weight = weight;
 }
}
      
      



Hier ist alles ziemlich einfach. Jede Eingabe hat ein numerisches Gewicht und einen Verweis auf das Neuron der vorherigen Schicht. Gehen wir weiter. Beschreiben wir die Klasse des Neurons selbst.





class Neuron {
 constructor(layer, previousLayer) {
   this._layer = layer;
   this.inputs = previousLayer
     ? previousLayer.neurons.map((neuron) => new Input(neuron, Math.random() - 0.5))
     : [0];
 }

 get $isFirstLayerNeuron() {
   return !(this.inputs[0] instanceof Input)
 }

 get inputSum() {
   return this.inputs.reduce((sum, input) => {
     return sum + input.neuron.value * input.weight;
   }, 0);
 }

 get value() {
   return this.$isFirstLayerNeuron
     ? this.inputs[0]
     : this._layer._network.activationFunction(this.inputSum);
 }

 set input(val) {
   if (!this.$isFirstLayerNeuron) {
     return;
   }

   this.inputs[0] = val;
 }

 set error(error) {
   if (this.$isFirstLayerNeuron) {
     return;
   }

   const wDelta = error * this._layer._network.derivativeFunction(this.inputSum);

   this.inputs.forEach((input) => {
     input.weight -= input.neuron.value * wDelta * this._layer._network.learningRate;
     input.neuron.error = input.weight * wDelta;
   });
 }
}
      
      



Mal sehen, was hier los ist. Wir können zwei Parameter an den Konstruktor des Neurons übergeben: die Schicht, auf der sich dieses Neuron befindet, und, wenn dies nicht die Eingangsschicht des neuronalen Netzwerks ist, eine Verbindung zur vorherigen Schicht.





Im Konstruktor erstellen wir für jedes Neuron der vorherigen Schicht eine Eingabe, die die Neuronen verbindet und ein zufälliges Gewicht hat, und schreiben alle Eingaben in das Eingabearray. Wenn dies die Eingabeschicht des Netzwerks ist, besteht das Eingabearray aus einem einzelnen numerischen Wert, den wir an die Eingabe übergeben.





$isFirstLayerNeuron - , , . , , .





inputSum - readonly , (, ) .





value - . , , inputSum.





:





input - , .





- error. , , error . , , .





. .





class Layer {
 constructor(neuronsCount, previousLayer, network) {
   this._network = network;
   this.neurons = [];
   for (let i = 0; i < neuronsCount; i++) {
     this.neurons.push(new Neuron(this, previousLayer));
   }
 }

 get $isFirstLayer() {
   return this.neurons[0].$isFirstLayerNeuron;
 }

 set input(val) {
   if (!this.$isFirstLayer) {
     return;
   }

   if (!Array.isArray(val)) {
     return;
   }

   if (val.length !== this.neurons.length) {
     return;
   }

   val.forEach((v, i) => this.neurons[i].input = v);
 }
}
      
      



- , neurons , , , .





$isFirstLayer - , , , input, , , . , .





, ,





class Network {
 static  sigmoid(x) {
   return 1 / (1 + Math.exp(-x));
 }

 static sigmoidDerivative(x) {
   return Network.sigmoid(x) * (1 - Network.sigmoid(x));
 }

 constructor(inputSize, outputSize, hiddenLayersCount = 1, learningRate = 0.5) {
   this.activationFunction = Network.sigmoid;
   this.derivativeFunction = Network.sigmoidDerivative;
   this.learningRate = learningRate;

   this.layers = [new Layer(inputSize, null, this)];

   for (let i = 0; i < hiddenLayersCount; i++) {
     const layerSize = Math.min(inputSize * 2 - 1, Math.ceil((inputSize * 2 / 3) + outputSize));
     this.layers.push(new Layer(layerSize, this.layers[this.layers.length - 1], this));
   }

   this.layers.push(new Layer(outputSize, this.layers[this.layers.length - 1], this));
 }

 set input(val) {
   this.layers[0].input = val;
 }

 get prediction() {
   return this.layers[this.layers.length - 1].neurons.map((neuron) => neuron.value);
 }

 trainOnce(dataSet) {
   if (!Array.isArray(dataSet)) {
     return;
   }

   dataSet.forEach((dataCase) => {
     const [input, expected] = dataCase;

     this.input = input;
     this.prediction.forEach((r, i) => {
       this.layers[this.layers.length - 1].neurons[i].error = r - expected[i];
     });
   });
 }

 train(dataSet, epochs = 100000) {
   return new Promise(resolve => {
     for (let i = 0; i < epochs; i++) {
       this.trainOnce(dataSet);
     }
     resolve();
   });
 }
}
      
      



, learning rate.





input - , .





prediction - , . .





trainOnce dataset - , , , - . , , . , , , .





train - , . . , .then, main thread.





, . - XOR.





.





const network = new Network(2, 1);
      
      



:





const data = [
 [[0, 0], [0]],
 [[0, 1], [1]],
 [[1, 0], [1]],
 [[1, 1], [0]],
];

      
      



, .





network.train(data).then(() => {
 const testData = [
   [0, 0],
   [0, 1],
   [1, 0],
   [1, 1],
 ];

 testData.forEach((input, index) => {
   network.input = input;
   console.log(`${input[0]} XOR ${input[1]} = ${network.prediction}`)
 });
});
      
      



, . .








All Articles