📘 第5章 权重和优化程序
📄 一句话概括

神经网络不是「会思考的网」,是一堆权重加一个误差。它的全部本事来自一件事: 中间那层能把直线掰弯。 Demo 里同样 33 个参数,中间层用 tanh 就是 98% 的准确率, 把 tanh 换成「什么都不做」就掉回 52%——参数一个没变,答案全变了。

📝 本节和前面几节的关系

这一节是第 5 章的收尾,也是全书的分水岭。前面四节的方法都有一个共同点: 你能指着某个系数说「因为这一项大」。从这一节开始,这个能力没有了。 它跟 5.2 也有直接的亲戚关系:一个神经网络画出来就是一张图,神经元是点、权重是边, 前向传播就是沿着边走一遍,反向传播是沿着边倒着走一遍。上一节的图和这一节的图,是同一张图。

一、一条直线分不开的时候

先造一批必须用弯线才分得开的数据: 内圈 160 个点(半径 0.5)、外圈 160 个点(半径 1.0),都加了抖动。任务是给一个坐标, 判断它在内圈还是外圈。 这个任务最气人的地方在于:数据一点毛病都没有。两个特征(横坐标、纵坐标)都在,噪声也不大, 可任何一条直线切下去,都会把某一侧的圈内点和圈外点混在一起。不是数据不够,是模型的形状不对。 先试最朴素的:两个输入直接接一个输出,输出大于 0 判外圈。这等于在平面上画一条直线。结果:

模型 参数个数 训练集 测试集
直接线性(一条直线) 3 55.91% 53.00%
训练集和测试集都在一半上下,这个组合很说明问题:
它连训练集都记不住。所以这不是过拟合(过拟合是训练集很好、测试集很差),
是「模型根本没有这个能力」。直线能做的事,到这儿就到头了。
这种情况在真实工作里极常见,而且最容易被误判成「数据不行」。
判据很简单:先看训练集准确率。训练集都上不去,换数据没用,得换模型。

二、中间插一层,让它自己拧出一条弯线

结构改成:两个输入 → 8 个中间神经元(每个都过一遍 tanh)→ 一个输出。参数从 3 个变成 33 个。

模型 参数个数 训练集 测试集
直接线性 3 55.91% 53.00%
8 个隐藏神经元 33 99.09% 98.00%
多 30 个参数,测试集从 53% 跳到 98%。它学到的界线长这样(# 是判成外圈的地方,
空白是判成内圈的地方,o 和 + 是样本点):
|############################################|
|######################+#####################|
|#################+#+###+###+################|
|#########################+###+##############|
|############### +o  oo  ####################|
|#############    oo     o         ##########|
|###########      o           o      ########|
|########### o              o        ##+#####|
|########+##                         ########|
|############  oo            o     ##########|
|#######+#####         o o o    ####++#######|
|###############     oo     #################|
|######+#####+#+#############################|
|#################+#######+##################|
|######################+#####################|
|############################################|

界线围成了一个圈,正好把内圈那团 o 兜住。这条线不是谁画上去的——是 33 个权重一起挪出来的。 它凭什么能围出一个圈?因为八个隐藏神经元各自画一条直线, 输出层再把八条线加起来。八条直线加权求和,边角一凑,就成了多边形——多边形足够细, 看起来就是个圈。这就是「一层线性不行、两层能行」的直觉解释。

三、学是怎么发生的

权重是随机起的步,所以第一轮的答案基本是瞎猜。接下来每轮做四件事, 这一节的所有魔法都在这四行里:

步骤 做什么 代码里的样子
前向 把输入一路乘权重加偏置传下去 A1 = tanh(X @ W1 + b1)
算误差 预测和答案差多少 d = (p - y) / n
反向 把误差按权重比例分回每一层 dZ1 = (d @ W2.T) * (1 - A1²)
下山 每个权重朝「让误差变小」的方向挪一步 W1 -= lr * (X.T @ dZ1)
第三步(反向传播)是全部难点所在。误差是从输出那头算出来的,可中间那八个神经元谁该负责多少?
办法是把误差按「连到它的权重有多大」分下去——权重大的那条线背的锅多,权重小的背得少。
这就是链式法则在做的事,没有任何玄机。
第四步那个 lr(学习率)是步长。太大就一步跨过谷底来回荡,太小就半天走不到。
Demo 里用的是 0.5,挪 2000 轮。
这里必须点明一件事:神经网络里没有「逻辑」,也没有「规则」。
整个过程只是「算一个数(误差)→ 求它的偏导 → 按偏导反着挪一点」。
所谓「学习」,就是这两万多个数字各自挪了几千次之后停在了某个位置。

四、真正管用的是非线性,不是层数

现在做一个对照实验: 结构一模一样(33 个参数)、初始化一模一样,只把中间层的 tanh 换成「什么都不做」。

中间层的激活 参数个数 训练集 测试集
tanh 33 99.09% 98.00%
什么都不做 33 52.73% 52.00%
参数一个没变,准确率掉了一半。原因不复杂:
如果中间层只做加权求和,那么两层的复合仍然是加权求和——数学上,
两个线性函数叠起来还是一个线性函数。所以这一层等于没加。
这就是非线性激活函数一个都不能省的原因。tanh、ReLU、sigmoid,选哪个可以讨论,
但「必须有一个」没有讨论余地——没有它,一百层深度网络和一个线性回归完全等价。
顺便说清「深度」这个词的意思。深度本身不是目的,目的是「用很少的参数表达很复杂的弯」:
一层宽的也能拟合,但参数要多得多。
深度带来的是一种「复用」——浅层的简单弯,被深层反复组合成复杂的弯。
这也是为什么这一章标题叫「权重和优化程序」。

五、中间放几个、初始化怎么起

宽度扫一遍,能看见一个明显的台阶:

中间神经元 参数个数 训练集 测试集
0 个 3 55.91% 53.00%
1 个 5 63.91% 59.80%
2 个 9 80.09% 77.60%
4 个 17 98.09% 96.00%
8 个 33 99.09% 97.60%
16 个 65 99.00% 97.40%
32 个 129 99.09% 97.80%
0 到 1 个基本没救,2 个到七成多,4 个跳到 96%,再往上几乎不动。这不是巧合:
要围住一个圈,至少得有四段弧。四个神经元刚好够拼出一个封闭的多边形,
再多就是把这个多边形修得更圆一点。
再往上加,涨的是参数量,不是准确率(16 个和 32 个甚至比 8 个略低,那是随机初始化的波动,
不是变差了)。这是「够用就行」最朴素的一个例子。
还有一个坑必须踩一次才知道:权重全设成 0。
初始权重 训练集 测试集
随机 99.09% 98.00%
全 0 50.00% 50.00%
全 0 那一行死死停在一半。原因不神秘:
八个神经元起手一模一样,收到的梯度也一模一样,于是永远一模一样——等于只有一个神经元在干活。
这叫对称性没被打破。
所以初始化必须带随机,而且种子不同结果也不同(上面每一行都是五次平均)。
这件事还能推出一个更一般的教训:凡是「对称的起点 + 对称的更新」,结果一定还是对称的。
想让它学出不一样的东西,起点就得先不一样。

六、它能给你什么,拿不走什么

它给的是准确率,而且给得很干脆。33 个参数、2000 轮, 在一个「一条直线绝对分不开」的问题上做到 98%。 这个能力是前面几节的方法都没有的——回归只能给一条线,图的搜索只能在一个已经画好的图上走。 它拿不走的是解释。那条圈住内圈的界线, 没人能写成一句话说清它凭什么。你最多能说「某些隐藏神经元对着某个方向亮起来」, 但那不是原因,是现象。 这个交换值不值,取决于你要拿结果干什么。判一张图是不是猫,98% 就够; 决定给谁放贷、给谁看病,就得先能解释。这也是为什么「可解释性」成了这一行的一个独立方向。 最后把这一章串一下。5.1 把关系存成点和边,5.2 在图上找一条最优的路, 5.3 在没有路的地方靠繁殖找,5.4 顺着坡度找。四种找法,对象都是同一个东西:一堆可以调的数。 「权重和优化程序」这个章名,说的就是这件事。

七、动手 Demo:两个同心环,直线分不开

fit 就是全部:前向算一遍、算误差、误差反向传一次、权重挪一步,循环 2000 次。 没有框架、没有自动求导,链式法则自己写。 注意 dZ1 = (d @ W2.T) * (1.0 - A1 * A1) 这一行:前半截是把误差按权重分下来, 后半截是 tanh 的导数。 第二半截要是写错(比如漏了),程序照样跑、照样不报错,只是学得慢或者学不动。

def fit(X, y, h=8, steps=2000, lr=0.5, seed=0):
    r2 = np.random.default_rng(seed)
    W1 = r2.normal(0, 1.0, (2, h)); b1 = np.zeros(h)   # 输入 -> h 个隐藏
    W2 = r2.normal(0, 1.0, (h, 1)); b2 = np.zeros(1)   # 隐藏 -> 1 个输出
    for _ in range(steps):
        A1 = np.tanh(X @ W1 + b1)          # 前向:过一遍 tanh
        z = A1 @ W2 + b2
        p = 1.0 / (1.0 + np.exp(-np.clip(z, -30, 30)))
        d = (p - y[:, None]) / len(X)      # 输出层的误差
        dZ1 = (d @ W2.T) * (1.0 - A1 * A1) # 反向:误差穿过 tanh
        W2 -= lr * (A1.T @ d)              # 梯度下降:往误差变小处挪
        W1 -= lr * (X.T @ dZ1)
        b1 -= lr * dZ1.sum(0)
        b2 -= lr * d.sum(0)
    return W1, b1, W2, b2

实跑输出:

--------------------------------------------------------------------
两个同心环,直线分不开
--------------------------------------------------------------------
==============================================================

[1] 造一批「一条直线分不开」的数据

  内圈 160 个点(半径 0.5),外圈 160 个点(半径 1.0),都加了抖动。
  任务是:给它一个点的坐标,判断它在内圈还是外圈。

    |                                            |
    |                 + +  + +                   |
    |               + + ++ ++ + +                |
    |             + ++     + ++  ++++ + +        |
    |       ++  +++ ++o  oo       +++  + ++      |
    |      ++  ++  o oooo ooooooooo    o+        |
    |     +++   ooooooo o  o  oo oo    o +       |
    |    +++ o   o o o       o  oooooo   ++++    |
    |     + ++  oooooo         oooo ooo   +++    |
    |    + + +    ooo   o    ooooooo o  ++       |
    |     ++++++ + o oo o oo oooooo   ++++       |
    |        + + ++   oooooooo  +  ++ + +        |
    |      +    +++++   + + +   +   ++ +         |
    |            + +  ++  ++  +++                |
    |               +   + +++                    |
    |                        +                   |

  o 是内圈,+ 是外圈。**任何一条直线切下去,都会把某一侧的 o 和 + 混在一起。**
  这就是这一节要处理的问题:**特征没变,模型得换。**

[2] 先试最朴素的:一个线性模型

  输入两个数 (x, y),直接接一个输出,输出大于 0 判外圈。
  这等于在平面上画一条直线,把两边分开。

    参数 3 个,训练集准确率 55.91%,测试集 53.00%

  训练集和测试集都在一半上下,**说明它连训练集都记不住,不是过拟合。**
  直线能做的事到头了。

[3] 插一层进去,让它自己拧出一条弯的界线

  结构改成:两个输入 → 8 个中间神经元(每个都过一遍 tanh)→ 一个输出。
  8 个中间神经元各自画一条直线,输出层再把 8 条线拼起来,
  **拼出来的就不一定是直线了。**

    参数 33 个,训练集准确率 99.09%,测试集 98.00%

  它学到的界线长这样(# 是判成外圈的地方,空白是判成内圈的地方):

    |############################################|
    |######################+#####################|
    |#################+#+###+###+################|
    |#########################+###+##############|
    |############### +o  oo  ####################|
    |#############    oo     o         ##########|
    |###########      o           o      ########|
    |########### o              o        ##+#####|
    |########+##                         ########|
    |############  oo            o     ##########|
    |#######+#####         o o o    ####++#######|
    |###############     oo     #################|
    |######+#####+#+#############################|
    |#################+#######+##################|
    |######################+#####################|
    |############################################|

  **界线围成了一个圈,正好把内圈那团 o 兜住。**
  这条线不是谁画上去的——是 33 个权重一起挪出来的。

  挪的办法叫梯度下降:每轮算一次「预测离答案差多远」,
  再让每个权重朝着「能让差变小」的方向挪一小步。这里挪了 2000 轮。

[4] 把 tanh 换成「什么都不做」会怎样

  中间那层如果把 tanh 拿掉,神经元就只做加权求和,输出还是输入的线性组合。
  **两层线性叠在一起,数学上等于一层线性**,界线圈不起来。

    中间层的激活    参数个数    训练准确率    测试准确率
    --------------------------------------------------------
    tanh            33          99.09%        98.00%
    什么都不做      33          52.73%        52.00%

  两行的参数个数一模一样(都是 33 个),准确率却差了将近一倍。
  **所以真正管用的不是「层数」,是那一层里的非线性。**
  这也是深度网络里非线性激活函数一个都不能省的原因。

[5] 中间放几个神经元合适

  多了浪费参数,少了表达不够。扫一遍看:

    中间神经元    参数个数    训练准确率    测试准确率
    ------------------------------------------------------
    0 个          3           55.91%        53.00%
    1 个          5           63.91%        59.80%
    2 个          9           80.09%        77.60%
    4 个          17          98.09%        96.00%
    8 个          33          99.09%        97.60%
    16 个         65          99.00%        97.40%
    32 个         129         99.09%        97.80%

  0 到 1 个基本没救,2 个能到七成多,4 个就跳到九成六,再往上收效很小。
  这不是巧合:**要围住一个圈,至少得有四段弧。**
  再往上加神经元,涨的是参数量,不是准确率。

[6] 一个容易踩的坑:权重全设成 0

  有人觉得「从零开始学」听起来最公平,就把所有初始权重设成 0。

    初始权重随机:训练集 99.09%,测试集 98.00%
    初始权重全 0:训练集 50.00%,测试集 50.00%

  **全 0 的那一行死死停在一半。**原因不神秘:八个神经元起手一模一样,
  它们收到的梯度也一模一样,**于是永远一模一样,等于只有一个人。**
  这种毛病叫对称性没打破。所以初始化必须带随机,
  而且随机数种子不同,训练出来的结果也会不同——上面每一行都是五次平均。

[7] 这一节要知道的三件事

  一、**神经网络不是「会思考的网」,是一堆权重加一个误差。**
  上面那个 33 个参数的小东西,做的事就是「把输入乘一乘加起来,再看符号」。
  学习 = 按误差把权重挪一挪。这一节从头到尾没别的花样。

  二、**它的本事全在「中间那层怎么弯」。**
  一层线性不行,加一层非线性就行;参数个数一样,差别全在非线性的位置和形状。
  所以调网络,调的其实是「弯成什么样」,不是「层数越多越好」。

  三、**它给不出解释。**
  那条圈住内圈的界线,没人能写成一句话说清它凭什么。
  这也是它跟前面几节(回归、加权、图搜索)最大的不同:
  **前面那些方法能指着系数说「因为这一项大」,这里只能指着准确率。**

第一段那张散点图不是装饰。它让你亲眼看见: 两个圈套在一起,没有任何一条直线能切开。 这类数据在机器学习里有个专门名字叫「异或问题」的连续版,它是「单层网络不够用」最经典的反例。 第三段那张决策边界图是这一节最该盯着看的东西。注意边界的形状不是圆—— 它是八段直线拼出来的多边形,只是段数够多,看起来像圆。 这就是「八个神经元各画一条线,输出层加起来」的字面意思。 第四段那个对照实验(tanh 对「什么都不做」)值得自己重跑一遍。 参数个数、初始化、步数全都一样,只有激活函数不同,准确率差了一倍。 这个实验能一次性打消「深度学习的本事来自参数多」这个误解—— 参数多只是必要条件,非线性才是充分的那一半。 第五段那张宽度表里,最值得记的是「四个神经元就够」。 从 4 个到 32 个,参数涨了 7 倍,准确率涨了不到 2 个百分点。真实工程里这条曲线更极端: 大模型有几亿参数,可你要解决的小问题往往几千个参数就够了。 第六段那个全 0 初始化,跑出来正好是 50.00%。这个数太整齐了,反而说明原因简单: 模型从头到尾没学过任何东西,一直在猜。 遇到「训练完全不动」的情况,先查初始化,再查学习率。

30 秒实验

  1. 把 steps 从 2000 改成 100,重跑第三段。 准确率会掉到七八成,决策边界还是个歪歪扭扭的形状——这说明「学」是需要轮数的, 不是一步到位。再改成 20000,准确率几乎不变——2000 轮已经收敛了。
  2. 把 h 改成 2 重跑,看那张边界图。 边界会变成两段直线拼成的一条折线,围不住那个圈,准确率大约七成多。 这一步能看见「宽度不够」到底长什么样——不是慢慢变差,是「形状根本不对」。
  3. 把数据的两个环改成「上下两个半月」(经典的 two moons 形状),重跑。 八个神经元仍然够用,但边界形状会完全不同——它不再是封闭的圈,而是一条穿过去的弧。 同一个结构、同一段代码,换个数据就学出另一个样子。

常见误区

  1. 以为「层数越多越强」。Demo 里同样的两层结构, 去掉非线性就掉回线性水平——一百层线性叠起来仍然是一个线性函数。 层数只是「能弯几次」的上限,能不能弯,取决于非线性。
  2. 以为训练集准确率低就是「数据不够」。55.91% 那一行,加十倍数据也没用——模型没有这个能力。 判据永远是先看训练集:训练集上不去是能力问题,训练集高测试集低才是过拟合问题。
  3. 以为权重可以从 0 开始学。全 0 初始化会让八个神经元永远一模一样, 准确率死死停在 50%。这件事的普适版本是:对称的起点加上对称的更新,结果一定对称。
  4. 以为神经网络「自己发现了规律」。它发现的是「一组让误差变小的数」, 这组数有没有物理意义,它不管也不知道。 所以拿它做决策之前,先想清楚「我能不能接受一个说不出理由的答案」。
自测三问(点开对答案) 1. 为什么两个线性层叠起来,效果等于一个线性层? —— 因为**线性函数的复合还是线性函数**。 写成矩阵就是 `W2(W1x + b1) + b2`, 展开成 `(W2W1)x + (W2b1 + b2)`—— **两个矩阵乘起来还是一个矩阵,两段偏置加起来还是一段偏置。**所以中间那层等于白加。 **这也是「必须有一个非线性」的全部理由。** 2. 为什么权重全设成 0 就学不动? —— 因为**八个神经元的起点完全相同,它们收到的梯度也完全相同**, 于是更新之后仍然完全相同——**八个神经元退化成一个。**更糟的是,输出层的权重也是 0, 误差传不回中间层,**中间层永远收不到任何信号。**所以必须用随机初始化打破对称。 3. 四个隐藏神经元就够围住一个圈,为什么? —— 因为**每个神经元贡献一条直线,四条直线能围出一个封闭的四边形**, 四边形已经能把内圈和外圈分开了。**再多的神经元只是把这个多边形修得更圆**,准确率涨得很少。 **所以「需要多少个神经元」这个问题,答案往往是「够拼出目标形状的最少段数」,** **不是「越多越好」。**

第 5 章到这儿结束。 下一章转向「不确定」:现实里的数据很少是确定的,更多是「大概是这样」。贝叶斯、MCMC、 HMM 这三样东西,就是专门用来把「大概」写成公式的。

相关笔记