① 前向传播:每层就是"加权求和 + 掰弯"
第 层做的事就两行:
第一行是线性变换:把上一层的输出重新加权组合。
第二行是非线性激活
:把结果掰弯。
关键:非线性的来源是激活函数,不是层数。
如果 是恒等映射,那
——两层复合起来仍是一个线性变换,等于白加。
把页面的激活函数切到「线性(无激活)」,数据集切到「异或」试试:
不管加多深多宽,准确率都上不去,
损失恒等于
(这正是"完全无信息"的值)。
② 反向传播:链式法则 + 聪明的顺序
要给每个参数算出"该往哪修",本质就是对损失求偏导。笨办法是一个参数一个参数地
求,参数量一大就彻底算不动(复杂度是参数量的平方)。
反向传播的关键观察是:第 层的梯度
可以由第 层的梯度推出来。
所以从输出层往回走一遍就够了,每层的梯度都复用上一层的结果:
拿到每层的 后,权重梯度就是
,
偏置梯度是 。
注意 这个结果干净得可疑,
但它不是巧合:sigmoid 的导数里那个
项,正好和交叉熵求导产生的
项互相抵消了。
这正是分类任务几乎总用交叉熵而不是 MSE 的原因——
用交叉熵时输出层误差就是简单的"预测减真值",梯度不会在饱和区被压成 0。
③ 激活函数:为什么现在几乎都用 ReLU
三种激活函数在这一页都能选。它们在深层网络里的差别非常悬殊,
我们用 8 层网络量了一下第一层拿到的梯度相对最后一层小多少:
- 线性(无激活):比值 ≈ 0.52,但整个网络退化成线性模型,没有表达力。
- tanh:比值 ≈ 0.58,衰减不明显——比传说中好得多。
- ReLU:比值 ≈ 0.90,几乎不衰减,这就是它成为默认选择的理由。
- logistic (sigmoid):比值 ≈ 5.9 × 10⁻⁷——
第一层的梯度比最后一层小了六个数量级,等于没在学。
这就是梯度消失的真身。但它只发生在饱和型激活上:
sigmoid 在两端导数趋近 0,连乘几十次就没了。
所以「层数多就梯度消失」这个说法并不准确——ReLU 就不怎么受影响。
把页面隐层堆到 4 层、激活选 logistic,再对比 ReLU,能直接看出学习速度的差距。
④ 二分类的输出层:1 个 logit + sigmoid
一个容易搞错的细节:二分类时输出层只有 1 个神经元,不是 2 个。
它输出一个 logit ,再用 sigmoid 压成
,
而 。
两类概率天生互补,没必要用两个自由的数去描述。
多分类才用 softmax 输出 K 个 logit。这两者形式不同,
但对拍时必须分清楚——本页脚本专门验证了这一点:
我的 sigmoid 结果和 sklearn 差 ,
而强行 softmax 的结果和 sklearn 差了 0.467,差得非常远。
⑤ 学习率和数据尺度是绑在一起的
把数据集切到「鸢尾花」,然后把学习率往上拉,你会看到准确率突然垮掉。
我们在这份数据上跑了一组对照(结构 ,
每个学习率跑 12 个随机种子,看有几个能训到 90% 以上):
| 学习率 |
成功种子 |
平均准确率 |
| 0.01 | 12 / 12 | 97.1% |
| 0.05 | 12 / 12 | 96.0% |
| 0.1(本页默认) | 10 / 12 | 93.1% |
| 0.2 | 0 / 12 | 52.8% |
| 0.3 及以上 | 0 / 12 | 33.3% |
注意 那里是一道悬崖,
不是缓坡: 还能到 93%,
翻到 就 12 个种子全军覆没。
失败时的准确率
正是"三类各占 1/3",损失精确等于
—— 网络完全没在学。
原因不是实现错了,是数据尺度:
二维玩具数据都落在 附近,
而鸢尾花是原始量纲(萼片长 4.3~7.9、花瓣宽 0.1~2.5,
差了近两个数量级),梯度大得多,同样的步长就迈过头了。
这就是实践中"先标准化,再调学习率"的原因。
真实项目里更省事的做法是标准化 + 自适应优化器(Adam),
它会给每个参数单独缩放步长,对量纲没那么敏感。
⑥ 别忘了:损失在降,不代表模型是对的
这一页开发时踩过一个值得记下的坑:反向传播里我把 one-hot 的第 0 列
当成了 sigmoid 的目标值,于是真实标签为 1 时目标变成了 0
——网络被训成了"每次都预测反的"。损失照样从 0.69 一路降到 0.002,
而准确率是 0%。
梯度检验抓不到这类错误(数学是自洽的,只是瞄错了目标),
必须靠"训练完准确率是多少"这种端到端判据兜底。
所以本页对拍脚本里专门有一项:异或训练完准确率必须 ≥ 95%。
凡是用梯度做优化,都要有一个独立的、能反映最终目标的验收指标。
1. 我给网络加到了 4 层,为什么异或还是学不会?
先看激活函数是不是「线性(无激活)」。如果是,那这几层在数学上
等价于一个线性变换:
,
再深的复合也只是把若干个矩阵乘在一起,还是一个矩阵。
而异或线性不可分——任何一条直线都分不开它。
换个非线性的激活函数(哪怕只有 2 个隐层神经元),异或立刻就能解:
实测 ReLU 只要 4~6 轮就到 100% 准确率。
深度不是表达力的来源,非线性才是。
2. 页面把激活切到"线性"时,准确率有时显示 75%,不是才 50% 吗?
这个 75% 是假象,千万别信。当时我差点把它写上页面,
一查才发现:模型的损失恒为
(这正好是"完全无信息"的值),
概率跨度只有
——基本上是个常数 0.5。
那 75% 怎么来的?因为网络碰巧把多数点分给了同一个类
(预测分布 40 : 120),而这些点的真实标签恰好多数是那一类。
这是统计学上的巧合,不是学习。
教训很实用:当类别不平衡、或者模型退化时,准确率会骗人。
判断"有没有学到东西"要看损失和概率分布,别只看准确率。
3. 隐层神经元越多越好吗?从「螺旋」和「同心圆」上看。
不是"越多越好",是
"够用才行",而且有实际代价。实测:
- 同心圆:4 个神经元 → 71.3%(学不会闭合边界);
8 个 → 100%;双层 8+8 → 100% 但收敛快一倍。
- 螺旋:2 个 → 92.8%;8 个 → 97.8%;双层 16+16 → 97.8%
但达标用的轮数从 28 降到 8。
上面的共同规律:
宽度/深度不够是"学不动",够了之后继续加不涨准确率,
只加快收敛。而参数越多越容易过拟合、越慢——
所以一般是"先加够,再加正则"。
4. 学习率调大到 3.0 也没崩,那"学习率太大会发散"是骗人的?
不是骗人,是取决于数据尺度和损失形状。
这一页的数据已经归一化到 [-1, 1] 附近,梯度本身很小,
所以 lr 大一点也没事(实测 lr=3.0 依然收敛到 99.2%)。
但 lr=0.01 时,300 轮才到 87.1%,损失几乎卡在 0.295 不动
——太小的问题是"走得慢,看着像不收敛"。
真实数据如果不做缩放,特征量级差得远,那 lr 稍大就真的会炸。
所以学习率没有"通用好值",它和数据尺度、批量大小、优化器
都耦合。这也是为什么实践中要配合学习率调度或自适应优化器。
5. 反向传播和"对每个参数单独求导",结果不一样吗?
结果完全一样,区别只在算得多快。
单独求导(数值梯度)是每个参数扰动两次、重算一遍损失,
复杂度随参数量线性增长;有 个参数
就要前向 次。
反向传播复用中间结果,一次前向 + 一次反向
就能拿到全部参数的梯度。参数量上百万时,这是"能算"和"算不动"的区别。
也正因为它俩结果一样,我们才能用数值梯度去检验反向传播写对没有
——这一页的对拍脚本就是这么做的:解析梯度和数值梯度的相对误差在
量级(最差一个有
,
那是 sigmoid 饱和区把有效精度吃掉了)。
6. 为什么对拍时"两边各训一个网络然后比结果"是错的?
因为权重是随机的,我的随机数发生器和 numpy 的不可能同步。
两个网络从不同的起点出发,走同一条路也会停在不同的地方,
比出来的差异说明不了实现对不对。
正确的对拍方式是"把同一份权重喂给两边,只比前向":
我用 初始化一组权重,
直接写进 sklearn 的
和 ,
两边跑前向、比输出概率——这样随机性就被完全绕开了。
这是对拍随机性算法的一个通用套路:
把随机产物喂给两边,只比确定性的那一段。