求Backpropagation简化解释:JS感知器扩展神经网络遇瓶颈
嘿,我完全懂反向传播一开始那种“每个字都认识但放一起就看不懂”的崩溃感——我刚入门的时候对着满屏的偏导数公式愣了快一周!其实你已经在写的感知器训练逻辑,就是单神经元版本的反向传播,咱们从你现有的代码出发,一步步把它扩展到多层网络的反向传播,保证讲得明明白白。
先看你写的train方法:
train(input, expected) { var result = this.evaluate(input); var error = expected - result; this.weights[0] += this.learningRate*(error)*input[0]; this.weights[1] += this.learningRate*(error)*input[1]; this.bias += this.learningRate*error; }
这其实就是反向传播最核心的思想:用误差来调整权重——你算出输出和预期的差距,然后让权重往“减少误差”的方向挪一点。只不过因为你的感知器只有一层(输入→输出),所以误差可以直接用,不用“传递”。
当你把多个感知器堆成多层网络(比如输入层→隐藏层→输出层)的时候,问题就来了:隐藏层的神经元没有“预期输出”,你没法直接用expected - result算它的误差。那怎么知道隐藏层的权重该怎么调?
反向传播的核心就是:从输出层的误差倒着算,找出每个隐藏层神经元对最终误差的“贡献值”,然后用这个贡献值来更新隐藏层的权重。就像你考试考砸了,倒着找原因:是最后一道大题错了→那是因为知识点B没掌握→那是因为之前的知识点A没学好,一步步倒推责任。
咱们用“多层感知器(输入→隐藏→输出)”来举例:
前向传播:先跑一遍网络
和你现在的evaluate方法一样,把输入喂进去,算出每个隐藏层神经元的输出,再算出输出层的最终结果。这一步你已经完全会了。倒着算误差:从输出层往回找“锅”
- 输出层的误差:和你现在一样,
预期输出 - 实际输出 - 隐藏层的误差:不能直接算,得看这个隐藏层神经元连接到输出层的哪些神经元,用输出层的误差乘以对应的权重,再结合激活函数的“灵敏度”(简单说就是激活函数在当前输出值上的导数,用来衡量这个神经元的输出变化会多大程度影响下游)。
举个例子:如果隐藏层神经元H1连接到输出层神经元O1的权重是0.5,O1的误差是-2,那H1对这个误差的贡献就是0.5 * (-2) = -1,再乘以H1激活函数的导数,就是H1的误差值。
- 输出层的误差:和你现在一样,
更新权重:用误差调整每个连接
这一步和你感知器的逻辑几乎一样:- 对于输出层的权重:
权重 += 学习率 * 输出层误差 * 隐藏层神经元的输出 - 对于隐藏层的权重:
权重 += 学习率 * 隐藏层误差 * 输入层的输入值
本质还是“让权重往减少误差的方向挪”,只是误差的来源从直接的预期差,变成了倒推出来的贡献值。
- 对于输出层的权重:
咱们把你的感知器扩展成一个两层网络(输入→1个隐藏层→输出),核心看反向传播的部分:
class SimpleMLP { constructor(inputCount, hiddenCount, outputCount) { // 初始化权重:输入→隐藏,隐藏→输出,还有偏置 this.inputToHiddenWeights = Array.from({length: inputCount}, () => Array.from({length: hiddenCount}, () => Math.random()) ); this.hiddenToOutputWeights = Array.from({length: hiddenCount}, () => Array.from({length: outputCount}, () => Math.random()) ); this.hiddenBias = Array.from({length: hiddenCount}, () => Math.random()); this.outputBias = Array.from({length: outputCount}, () => Math.random()); this.learningRate = 0.1; this.activation = x => 1/(1+Math.exp(-x)); // Sigmoid激活函数 this.activationDerivative = x => x*(1-x); // Sigmoid的导数,用来算误差 } // 前向传播,保存每层的输出(方便反向传播用) forward(input) { // 计算隐藏层输出 const hiddenOutput = this.inputToHiddenWeights.map((weights, i) => this.activation(input[i] * weights + this.hiddenBias[i]) ); // 计算输出层输出 const output = this.hiddenToOutputWeights.map((weights, i) => this.activation(hiddenOutput[i] * weights + this.outputBias[i]) ); return { hiddenOutput, output }; } // 反向传播核心 backpropagate(input, expected) { const { hiddenOutput, output } = this.forward(input); // 1. 算输出层误差 const outputError = output.map((out, i) => expected[i] - out); // 结合激活函数导数,得到输出层的误差梯度 const outputGradient = outputError.map((err, i) => err * this.activationDerivative(output[i])); // 2. 算隐藏层误差(倒推) const hiddenError = this.hiddenToOutputWeights.map((weights, i) => outputGradient.reduce((sum, grad, j) => sum + grad * weights[j], 0) ); // 隐藏层的误差梯度 const hiddenGradient = hiddenError.map((err, i) => err * this.activationDerivative(hiddenOutput[i])); // 3. 更新权重和偏置 // 更新隐藏→输出的权重 this.hiddenToOutputWeights = this.hiddenToOutputWeights.map((weights, i) => weights.map((w, j) => w + this.learningRate * outputGradient[j] * hiddenOutput[i]) ); // 更新输入→隐藏的权重 this.inputToHiddenWeights = this.inputToHiddenWeights.map((weights, i) => weights.map((w, j) => w + this.learningRate * hiddenGradient[j] * input[i]) ); // 更新偏置 this.outputBias = this.outputBias.map((b, i) => b + this.learningRate * outputGradient[i]); this.hiddenBias = this.hiddenBias.map((b, i) => b + this.learningRate * hiddenGradient[i]); } }
你可以对比这个代码和你原来的感知器:
- 前向传播就是把
evaluate扩展到了多层,还保存了中间输出 - 反向传播的权重更新逻辑和你感知器的
train方法本质一样,只是误差变成了从下游倒推出来的梯度
不用一开始就啃复杂的公式,先从“两层网络”开始实现,把反向传播的每一步都打印出来(比如输出层误差、隐藏层误差、权重变化),看着数值变化慢慢理解——你已经掌握了最基础的权重更新逻辑,只要把“误差倒推”这一步搞懂,多层网络的反向传播就水到渠成了!
内容的提问来源于stack exchange,提问作者G4mm4R4y

