从零实现的JS神经网络解决XOR问题时输出收敛至0.5的问题排查
看起来你碰到了从零搭建神经网络时的经典“卡壳”问题——训练XOR后所有输出都收敛到0.5左右。这大概率是反向传播逻辑错误+初始化的低级bug共同导致的,咱们一步步拆解问题,逐个修复:
1. 致命bug:所有节点共享同一组权重和偏置
先看你的构造函数里这段代码:
var weightsAndBiases = [Array(layerSizes[i-1]).fill(random(0, 1))] weightsAndBiases.push(0) nodes.fill(weightsAndBiases) this.layers.push(nodes)
这里的问题是:nodes.fill(weightsAndBiases)会把同一个数组引用填充到所有节点里!也就是说,同一层的所有节点的权重和偏置完全共享,根本没法独立学习。相当于你的每个隐藏层/输出层只有一个“有效节点”,这怎么可能学出XOR的非线性边界?
修复方案:给每个节点单独创建权重和偏置数组,不要用fill填充引用:
// 替换构造函数里的循环部分 for (let i=0; i<this.numLayers; i++){ const nodes = []; if (i === 0) { // 输入层不需要权重偏置,初始化空结构即可 for (let j=0; j<layerSizes[i]; j++) { nodes.push(null); } } else { for (let j=0; j<layerSizes[i]; j++) { // 每个节点单独生成权重和偏置 const weights = Array(layerSizes[i-1]).fill().map(() => random(-1, 1)); // 改成对称初始化 const bias = random(-1, 1); // 偏置也随机初始化 nodes.push([weights, bias]); } } this.layers.push(nodes); }
另外,权重初始化全用random(0,1)的正数也有问题——会导致激活函数输入全正(比如用sigmoid的话都在右半段,导数接近0,梯度消失),改成对称的random(-1,1)会好很多。
2. 反向传播核心错误:隐藏层Delta计算逻辑完全错了
看你计算隐藏层delta的代码:
deltas[i][j] += this.state[i+1][k] * this.layers[i+1][k][0][j]
正确的反向传播中,隐藏层节点的delta应该是下一层每个节点的delta乘以对应权重的总和,再乘以当前节点激活函数的导数。你这里用了this.state[i+1][k](下一层的输出),而不是deltas[i+1][k](下一层的误差项),这直接导致梯度完全传不回隐藏层,网络根本学不到有用的参数!
修复方案:把this.state[i+1][k]换成deltas[i+1][k]:
for (let k=0; k<this.layerSizes[i+1]; k++){ deltas[i][j] += deltas[i+1][k] * this.layers[i+1][k][0][j]; } deltas[i][j] *= this.activationPrime(this.inverseActivation(this.state[i][j]));
3. 权重梯度计算错误
看你计算权重更新量的代码:
let dCostdWeight = this.layers[this.numLayers-1][i][0][j] * this.state[this.numLayers-2][j] * deltas[this.numLayers-1][i]
正确的权重梯度应该是上一层节点的输出乘以当前节点的delta,你这里多乘了一个当前权重this.layers[...],这完全扭曲了梯度的方向和大小,导致参数更新完全错误。
修复方案:去掉多余的权重相乘:
// 输出层权重梯度 let dCostdWeight = this.state[this.numLayers-2][j] * deltas[this.numLayers-1][i]; // 隐藏层权重梯度同理 let dCostdWeight = this.state[i-1][k] * deltas[i][j];
4. 其他小问题(非致命但影响训练效率)
- 你用
JSON.parse(JSON.stringify(this.layers))来拷贝参数的方式虽然能工作,但效率很低,建议直接遍历层和节点,逐个拷贝权重和偏置。 - 训练时
setInterval(() => {render(); }, 1)的频率太高,一个epoch还没跑完就开始下一个,其实可以改成每次run执行多个epoch,或者控制训练节奏。
修复后验证
把上面几个核心bug修复后,再调整学习率(比如0.1-0.5之间),训练几十到几百个epoch,你应该就能看到网络输出逐渐趋近XOR的正确结果:[0,1,1,0],而不是全0.5了。
备注:内容来源于stack exchange,提问作者R. Reed

