Como eu disse na introdução da primeira parte , sou um desenvolvedor front-end e minha linguagem nativa é JavaScript, vamos implementar nossa rede neural neste artigo usando-a. Primeiro, algumas palavras sobre estrutura. Exceto por várias propriedades e métodos computados, o objeto de rede neural conterá uma matriz de camadas, cada camada conterá uma matriz de neurônios e cada neurônio, por sua vez, conterá uma matriz de "entradas" - conexões com os neurônios do camada de entradas anterior. Além disso, uma vez que temos coisas comuns para toda a rede, como a função de ativação, sua derivada e a taxa de aprendizagem, e precisamos acessá-los de cada neurônio, vamos concordar que o neurônio terá um link _layer para a camada para a qual ele pertence, e a camada terá _network - um link para a própria rede.
Vamos passar de privado a geral e primeiro descrever a classe de entrada para o neurônio.
class Input {
constructor(neuron, weight) {
this.neuron = neuron;
this.weight = weight;
}
}
Tudo é muito simples aqui. Cada entrada possui um peso numérico e uma referência ao neurônio da camada anterior. Vamos mais longe. Vamos descrever a classe do próprio neurônio.
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;
});
}
}
Vamos ver o que está acontecendo aqui. Podemos passar dois parâmetros para o construtor do neurônio: a camada em que esse neurônio está localizado e, se esta não for a camada de entrada da rede neural, um link para a camada anterior.
No construtor, para cada neurônio da camada anterior, criaremos uma entrada que conectará os neurônios e terá um peso aleatório, e escreveremos todas as entradas no array de entradas. Se esta for a camada de entrada da rede, a matriz de entradas consistirá em um único valor numérico, aquele que passamos para a 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}`)
});
});
, . .