首页 / 监督学习 / 神经网络

神经网络:一层不够,是因为世界不是直的

逻辑回归只能切一条直线;神经网络的每一层都在把坐标掰弯一次, 弯到一定程度,直线就够用了。整件事只有两步: 前向传播把输入逐层拧过去, 反向传播回头问每个参数"这次错你占几分责任",然后照责任大小修一小步。 反向传播不是新数学,它就是链式法则 + 一个聪明的计算顺序。 这一页可以亲手加层、换激活函数、调学习率,看决策边界怎么变。

决策边界:网络学会了怎样划分这块平面
训练曲线:损失怎么降下来的
当前网络的状况

                
              
这一页的 MLP 是自己实现的——前向传播、反向传播、 mini-batch SGD、Xavier 初始化全是手写,没有用任何深度学习框架。 两组判据同时把关:把权重塞进 sklearn 比前向概率(差 ±2.2e-16)、 解析梯度对数值梯度(相对误差 1e-8 量级)。

① 前向传播:每层就是"加权求和 + 掰弯"

第 层做的事就两行:

第一行是线性变换:把上一层的输出重新加权组合。 第二行是非线性激活 :把结果掰弯。

关键:非线性的来源是激活函数,不是层数。 如果 是恒等映射,那 ——两层复合起来仍是一个线性变换,等于白加。 把页面的激活函数切到「线性(无激活)」,数据集切到「异或」试试: 不管加多深多宽,准确率都上不去, 损失恒等于 (这正是"完全无信息"的值)。

② 反向传播:链式法则 + 聪明的顺序

要给每个参数算出"该往哪修",本质就是对损失求偏导。笨办法是一个参数一个参数地 求,参数量一大就彻底算不动(复杂度是参数量的平方)。

反向传播的关键观察是:第 层的梯度 可以由第 层的梯度推出来。 所以从输出层往回走一遍就够了,每层的梯度都复用上一层的结果:

拿到每层的 后,权重梯度就是 , 偏置梯度是 。

注意 这个结果干净得可疑, 但它不是巧合: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.0112 / 1297.1%
0.0512 / 1296.0%
0.1(本页默认)10 / 1293.1%
0.20 / 1252.8%
0.3 及以上0 / 1233.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 的 和 , 两边跑前向、比输出概率——这样随机性就被完全绕开了。

这是对拍随机性算法的一个通用套路: 把随机产物喂给两边,只比确定性的那一段。