从零实现的单隐藏层MLP无法学习,损失持续上升求排查
嘿,我太懂这种从零撸MLP结果模型完全不干活还反向搞事情的挫败感了!下面我帮你梳理几个无库实现时最容易踩的坑,这些大概率就是你模型学不起来、损失还飙升的罪魁祸首:
排查核心问题
1. 权重初始化踩了大坑
如果你的__init__里是直接用random.random()生成0-1的权重,那基本直接宣判“模型死刑”——过大的初始值会让sigmoid激活函数直接进入饱和区(输出趋近于0或1),此时梯度几乎为0,反向传播根本更新不了权重;要是你初始值全设成0,还会导致神经元对称,永远学不出差异化特征。
- 正确姿势:用小范围随机值初始化,比如从
-0.1到0.1之间取随机数,或者用1/√输入维度来缩放权重,避免激活饱和。
2. 激活函数的梯度算错了
你用到了sigmoid(从math.exp能看出来),那反向传播时的梯度必须严格对应:sigmoid的导数是σ(x)*(1-σ(x))。如果这里算错了,梯度方向直接反转,权重越更新越离谱,损失自然一路飙升。
- 额外提醒:XOR是二分类任务,输出层用sigmoid没问题,但要确保损失函数和输出匹配(比如用均方误差或交叉熵)。
3. 反向传播的链式法则搞反了
这是无库实现最容易翻车的环节,尤其是隐藏层到输出层、输入层到隐藏层的梯度传递:
- 输出层的误差项应该是
(预测值-真实值)*输出层激活导数; - 隐藏层的误差项是
输出层误差 × 输出层权重矩阵转置 × 隐藏层激活导数;
要是你把矩阵乘法的顺序搞反了,或者漏乘了激活导数,权重更新逻辑完全错误,模型根本不可能学习。
4. 学习率设置不合理
学习率太大,权重更新会“冲过头”,直接跳过最优解导致损失震荡飙升;学习率太小,模型龟速学习,看起来像完全没动静。
- 建议先从
0.01或0.1开始试,慢慢调整到合适的值。
5. 损失函数计算逻辑错误
比如你用均方误差的话,标准写法是0.5*(预测值-真实值)**2(加0.5是为了求导后简化计算),如果搞反了预测和真实值的顺序,或者漏了平方,哪怕不影响梯度方向,也会拖累学习效率,甚至让你看到的损失上升是“假信号”。
实用调试小技巧
- 打印中间值:正向传播时打印隐藏层的激活值,要是大部分都接近0或1,说明权重初始化太大导致激活饱和;反向传播时打印梯度值,看看是不是全为0或者异常大。
- 先测简单任务:别直接上XOR,先让模型学个简单的线性任务(比如
y=x),如果线性任务都学不会,那肯定是正向/反向传播的基础逻辑错了。 - 逐行核对公式:把你的反向传播代码和标准单隐藏层MLP的公式逐行对比,尤其是权重更新部分:
- 输出层权重更新:
weights_out -= 学习率 × 输出层误差 × 隐藏层激活值 - 隐藏层权重更新:
weights_hidden -= 学习率 × 隐藏层误差 × 输入值
- 输出层权重更新:
参考权重初始化示例
给你贴个符合要求的__init__片段,对应你的代码结构:
import random from math import exp class MLP: def __init__(self, num_inputs, num_hidden, num_outputs): self.num_inputs = num_inputs self.num_hidden = num_hidden self.num_outputs = num_outputs # 小范围随机初始化权重,避免激活饱和 self.weights_hidden = [[random.uniform(-0.1, 0.1) for _ in range(num_inputs)] for _ in range(num_hidden)] self.weights_output = [[random.uniform(-0.1, 0.1) for _ in range(num_hidden)] for _ in range(num_outputs)] # 偏置也要同步小范围初始化 self.bias_hidden = [random.uniform(-0.1, 0.1) for _ in range(num_hidden)] self.bias_output = [random.uniform(-0.1, 0.1) for _ in range(num_outputs)]
内容的提问来源于stack exchange,提问作者KOB
相关产品推荐
相关产品推荐

