前两篇文章,已经自上而下的去理解如何使用神经网络,以及他的一些核心的结构和算法。本篇文章,主要围绕在如何去实现一个低配版的神经网络,并且从0到1的实现核心的算法,例如神经网络训练初始化、梯度下降算法、正向传播和反向传播。
初始化
基于神经网络的逻辑结构,通过初始化参数来确定神经网络的输入层个数,隐藏层个数,每个 neuron 和上一层 neuro 的 weigth 和 bias。
private initHiddenLayer() {
const hiddenLayer: TLayer = [];
const { hiddenLayerNeuronCount, inputCount } = this.options;
for (let index = 0; index < hiddenLayerNeuronCount; index++) {
const weights = [];
for (let j = 0; j < inputCount; j++) {
weights.push(Math.random());
}
const bias = Math.random();
hiddenLayer.push({ weights, bias } as INeuron);
}
this.network.push(hiddenLayer);
}
通过确定输出层的neuron个数来确定 output layer 结构
private initOutputLayer() {
const outputLayer: TLayer = [];
const { hiddenLayerNeuronCount, outputCount } = this.options;
for (let index = 0; index < outputCount; index++) {
const weights = [];
for (let j = 0; j < hiddenLayerNeuronCount; j++) {
weights.push(Math.random());
}
const bias = Math.random();
outputLayer.push({ weights, bias } as INeuron);
}
this.network.push(outputLayer);
}
其中每个 neuron 和前一层的 neuron 的 weight 和 bias 就是基于线性回归模型的变量,在反向传播和梯度下降过程中需要对其不停的优化。也就是所谓的训练过程。
正向传播 forwardPropagate
在正向传播过程中,每个 neuron 传递给下一层的 neuron 是信号,而不是值,所以每个 neuron 在处理完自己的值后,需要通过激活函数对值进行信号处理。
private activate(neuron: INeuron, inputs: Array<number>) {
const { weights, bias } = neuron;
const ret = weights.reduce(
(prev, weight, index) => prev + weight * inputs[index],
bias
);
return ret;
}
// 激活函数
private sigmoid(activation: number) {
return 1 / (1 + Math.exp(-activation));
}
推理 predict
推理的本质是正向传播
public predict(dataset: Array<number>) {
const output = this.forwardPropagate(dataset);
return output;
}
训练 fit & 逆向传播 backwardPropagateError
直接根据线性回归模型随机初始化的 weight 和 bias 来进行推理得到的结果很难匹配到期望的结果,例如恐龙是否起跳。所以需要根据监督学习的有效label进行逆向传播,修改每一层layer的每一个 neuron 的 weight 和 bias。
// training data
nn.fit(trainingData.input, trainingData.output, {
// after trainning
});
根据设置的训练迭代次数进行训练。先通过样本数据(trainData)进行向前传播(forwardPropagate)得到输出值,再和准确的值进行误差计算,拿到误差值传进行反向传播
public async fit(trainData: Array<Array<number>>, expectedOutput: Array<Array<number>>, options: IFitOptions) {
for (let i = 0; i < this.options.epoch; i++) {
// 误差
let sumError = 0;
for (let j = 0; j < trainData.length; j++) {
const row = trainData[j];
const output = this.forwardPropagate(trainData[j]);
const expected = expectedOutput[j];
sumError += expected.reduce((prev, curr, index) => {
return prev + (expected[index] - output[index]) ** 2;
}, sumError);
this.backwardPropagateError(expected);
this.updateWeights(row);
await options.onEpochFinish.apply(this, [row]);
}
}
}
将误差值依次传递给每个 neuron,通过反向传播求导获取误差值
/**
* 误差进行反向传播
* @param expected 期望值
*/
private backwardPropagateError(expected: Array<number>) {
for (
let layerIndex = this.network.length - 1;
layerIndex >= 0;
layerIndex--
) {
const layer = this.network[layerIndex];
const errors = [];
// 计算每一个神经元的误差
if (layerIndex === this.network.length - 1) {
layer.forEach((neuron, neuronIndex) => {
errors.push(expected[neuronIndex] - neuron.output);
});
} else {
layer.forEach((neuron, neuronIndex) => {
let error = 0;
// 获取下一层神经元
let nextLayer = this.network[layerIndex + 1];
// 当前 neuron 在下一层每个 neuron 对应的 weights
nextLayer.forEach((nextNeuron) => {
const nextNeuronWeight = nextNeuron.weights[neuronIndex];
const nextNeuronDelta = nextNeuron.delta;
error += nextNeuronWeight * nextNeuronDelta;
});
errors.push(error);
});
}
layer.forEach((neuron, index) => {
neuron.delta = errors[index] * this.transferDerivative(neuron.output);
});
}
}
计算好每个 neuron 的 delta 值后,就可以更新每个 neuron 的 weight 和 bias了
private updateWeights(inputs: Array<number>) {
for (let layerIndex = 0; layerIndex < this.network.length; layerIndex++) {
const layer = this.network[layerIndex];
let _inputs = inputs;
// 第一层取 input,后续各层取上一层 output
if (layerIndex !== 0) {
const prevLayer = this.network[layerIndex - 1];
_inputs = prevLayer.map((neuron) => neuron.output);
}
// 更新当前层的 weight & bias
for (let neuronIndex = 0; neuronIndex < layer.length; neuronIndex++) {
const neuron = layer[neuronIndex];
for (let w = 0; w < _inputs.length; w++) {
neuron.weights[w] +=
this.options.learnReate * neuron.delta * _inputs[w];
}
neuron.bias += this.options.learnReate * neuron.delta;
}
}
}
自此一个完整的线性回归深度学习模型就完成了。最终效果可以通过 github.com/Cowboy-Jr/a… 去自行体验。