从0开始学习 Deep Learning - 02手写纯JS的深度学习玩耍 Chrome 恐龙游戏(三)

746 阅读2分钟

前两篇文章,已经自上而下的去理解如何使用神经网络,以及他的一些核心的结构和算法。本篇文章,主要围绕在如何去实现一个低配版的神经网络,并且从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);
}

image.png

通过确定输出层的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);
}

image.png

其中每个 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));
  }

image.png

推理 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了

image.png

  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… 去自行体验。