Hágalo usted mismo desde cero. Parte 2. Implementación

Como dije en la introducción de la primera parte , soy un desarrollador de frontend y mi idioma nativo es JavaScript, implementaremos nuestra red neuronal en este artículo usándola. Primero, algunas palabras sobre la estructura. Excepto por varias propiedades y métodos calculados, el objeto de red neuronal contendrá una matriz de capas, cada capa contendrá una matriz de neuronas y cada neurona, a su vez, contendrá una matriz de "entradas": conexiones con las neuronas del capa de entradas anterior. Además, dado que tenemos cosas en común para toda la red, como la función de activación, su derivada y la tasa de aprendizaje, y necesitamos acceder a ellas desde cada neurona, acordamos que la neurona tendrá un enlace _layer a la capa a la que pertenece, y la capa tendrá _network - un enlace a la propia red.





Pasemos de privado a general y describamos primero la clase de entrada para la neurona.





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



Aquí todo es bastante simple. Cada entrada tiene un peso numérico y una referencia a la neurona de la capa anterior. Vayamos más lejos. Describamos la clase de neurona en sí.





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;
   });
 }
}
      
      



Veamos qué está pasando aquí. Podemos pasar dos parámetros al constructor de la neurona: la capa en la que se encuentra esta neurona y, si esta no es la capa de entrada de la red neuronal, un enlace a la capa anterior.





En el constructor, para cada neurona de la capa anterior, crearemos una entrada que conectará las neuronas y tendrá un peso aleatorio, y escribiremos todas las entradas en la matriz de entradas. Si esta es la capa de entrada de la red, entonces la matriz de entradas consistirá en un solo valor numérico, el que pasamos a la entrada.





$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