📘 第9章 深度学习
📄 一句话概括

一层网络只能画一条直线。可现实里要找的边界常常是弯的——一圈环、一条螺旋。 解决办法只有一个:再叠一层。 叠上去之后有两笔学费要交。第一笔是怎么把误差传回去(反向传播); 第二笔是传得回去多远(梯度消失)。这一节把两笔都量一遍。

📝 本节和前面几节的关系

这一节是第 7 章的收口,也是第 9 章的开头。7.2 里那些模型——kNN、决策树、 逻辑回归——全都是「一层」的结构:输入进去,判断直接出来。 它跟 7.2 的过拟合是一条线: 单层太简单(欠拟合),多层又容易学太满(过拟合)。这中间那段「刚好」, 就是整个深度学习的调参空间。

一、为什么非要叠一层

先看一个单层无论如何都做不对的问题。 平面上撒 600 个点,半径在 0.30 到 0.80 之间的算正例(占 43.3%),其余算负例。 正例围成一个环,负例在环里和环外。

结构 参数量 训练集 测试集 最后损失
单层(只有输出层) 3 0.5667 0.5450 0.6836
两层(16 个隐单元) 65 0.9983 0.9725 0.0187
单层的测试集停在 0.5450,比瞎猜好一点点。两层到了 0.9725。
这不是训练不够,是表达上限。一个单层网络的决策边界是一条直线——
你把这条线怎么摆,都围不住一圈环。
多一层,网络就多了一次「先把平面切成几块、再把块拼起来」的机会。
所有关于深度学习的故事,都是从这一步开始的。

二、反向传播:就是链式法则,但必须验

叠了层就得知道每一层的权重该怎么改。 这件事的形式化名字叫反向传播,内容其实只有一句话:链式法则。 前向一次把每层的输出存下来; 反向一次,从输出层往回,每过一层就乘上「这一层的权重转置」和「这一层激活的导数」。 一趟就把所有参数的梯度都算出来了。

def fwd(Ws, bs, X, kind):            # 一层一层往前推
    As, a = [X], X
    for i in range(len(Ws)):
        a = a.dot(Ws[i]) + bs[i]
        a = af(a, kind) if i < len(Ws) - 1 else 1.0 / (1.0 + np.exp(-a))
        As.append(a)
    return As

def bwd(Ws, As, Y, kind):            # 一层一层往回传
    d = (As[-1] - Y) / Y.shape[0]    # BCE 配 sigmoid,这一项正好化简成这样
    for i in range(len(Ws) - 1, -1, -1):
        gW[i] = As[i].T.dot(d)       # 这一层的权重梯度
        gb[i] = d.sum(0, keepdims=True)
        if i > 0:
            d = d.dot(Ws[i].T) * ad(As[i], kind)   # 往上传:乘权重、乘导数
    return gW, gb

# 一票否决式自查:手推的梯度 vs 数值差分
num = (loss(W + EPS) - loss(W - EPS)) / (2 * EPS)
ana = GW[li][i, j]
print(abs(num - ana) / (abs(num) + abs(ana)))

# 把隐单元的激活画在平面网格上:一个单元就是一条直线的一侧
HA = fwd(Ws2, bs2, GX, u"tanh")[1]
for k in range(8):
    print(panel(HA[:, k]))

它省的是计算量,不是数学。要是用数值差分,每个参数都得跑两遍前向。 Demo 那个小网络有 31 个参数, 还跑得动;真实的模型有几十亿个参数,那就变成「算一遍」和「算几十亿遍」的区别。 手推的公式却极容易写错一个转置,而且错了不报错,只是训不好。所以有一条必备的自查: 拿数值差分对一遍。

项 数值 怎么读
结构 2-4-3-1,31 个参数 小到能手算
抽检 18 个权重逐个比 两边都算一遍
最大相对误差 1.142e-08 对了
1e-08 量级就是浮点数的舍入误差。要是有个地方漏了转置,
这个数会直接跳到 1 附近。它是个非常好的「一票否决」检查——
成本只有几十次前向,却能省掉一整天的瞎调。

三、隐单元到底在干什么

把平面铺成 19 乘 19 的网格,看每个隐单元的激活值长什么样。Demo 里印了前 8 个,结论非常干净: 每一个隐单元就是一条直线切出来的半边。没有一个是弯的——它的激活值只跟「在直线的哪一侧」有关。 可 16 个这样的半边叠起来,边界就能围成一个圈。 这就是「多层」的全部魔法:单个还是直的,组合起来就弯了。 还有个顺手能数出来的事:16 个隐单元里有 5 个的激活几乎不随位置变化(全黑或全白)—— 它们等于白占位置。 这解释了一件常被问的事:隐单元给多了会不会坏事? 不太会,多余的会自己「躺平」;给少了才是真的不够用。 判断标准不是「用了几个」,是「测试集还降不降」。

四、激活函数:信号能传多远

反向传播那个「乘上激活的导数」,是这一节所有麻烦的源头。三种常见激活的导数上限差得很远: sigmoid 最大 0.25,tanh 最大 1.00,ReLU 是 0 或 1、不衰减。

激活函数 训练集 测试集 最后损失 导数最大
sigmoid 0.9850 0.9600 0.0815 0.25
tanh 0.9983 0.9725 0.0187 1.00
relu 0.9333 0.9125 0.2058 不封顶
这张表要老实读:这次 ReLU 反而最差(0.9125)。
不是 ReLU 不行,是这张表只调了一个学习率、给了同样的步数。ReLU 对学习率敏感得多——
0.4 对 sigmoid 和 tanh 刚好,对它偏大。
这就是「读实验表」的第一条规矩:只换一个变量的表,只能说明「在这个设定下谁更好」,
不能说明「谁更好」。
不过 sigmoid 那个 0.25 的上限是真的会咬人的。一层的时候它只让你慢一点;
层数一多,这个 0.25 会被连乘好几次——下一段就见分晓。

五、深了就难训:一次算清楚

固定隐单元宽度 8,只改层数。注意看训练集:全都做到了 1.0000。

结构 参数量 训练集 测试集
1 个隐层乘 8 33 0.9800 0.9600
2 个隐层乘 8 105 1.0000 0.9600
4 个隐层乘 8 249 1.0000 0.9850
8 个隐层乘 8 537 1.0000 0.9675
训练集全是 1.0000,所以 8 层掉分不是「学不动」,是「学太满了」。参数量从 33 涨到 537,
训练样本只有 600 条——这是过拟合,不是欠拟合。
那「深了难训」这句话是假的吗?不假,得单独量。建一个 10 个隐层的网络,
看反向传回来的梯度还剩多少:
激活函数 输出层梯度 第一层梯度 第一层除输出层
sigmoid 8.742e-04 9.861e-11 1.13e-07
tanh 8.739e-04 1.292e-04 1.48e-01
relu 1.011e-03 1.328e-03 1.31e+00
sigmoid 那一行最能说明问题:梯度传回第一层只剩输出层的 1.1e-07,缩了 887 万倍。
前面几层几乎收不到信号,等于没在学。
tanh 好一些,ReLU 最好——因为它的导数要么是 0、要么是 1,不会越乘越小。
这就是「深度网络难训」的第一层含义:不是它表达不了,是它学不动。
下一节先讲当年怎么绕过去(预训练),再下一节讲后来怎么正面解决(换激活)。

六、动手 Demo:一层不够,就再叠一层

五段:单层学不会的环、梯度校验、隐单元的真面目、三种激活、深了的代价。 第 1 段和第 5 段是一对:一个说「必须叠」,一个说「叠了要付账」。 这段代码没有用任何框架,全部是 numpy 手写的。 目的不是造轮子,是让「反向传播」这四个字变成几行看得懂的乘法。

def fwd(Ws, bs, X, kind):            # 一层一层往前推
    As, a = [X], X
    for i in range(len(Ws)):
        a = a.dot(Ws[i]) + bs[i]
        a = af(a, kind) if i < len(Ws) - 1 else 1.0 / (1.0 + np.exp(-a))
        As.append(a)
    return As

def bwd(Ws, As, Y, kind):            # 一层一层往回传
    d = (As[-1] - Y) / Y.shape[0]    # BCE 配 sigmoid,这一项正好化简成这样
    for i in range(len(Ws) - 1, -1, -1):
        gW[i] = As[i].T.dot(d)       # 这一层的权重梯度
        gb[i] = d.sum(0, keepdims=True)
        if i > 0:
            d = d.dot(Ws[i].T) * ad(As[i], kind)   # 往上传:乘权重、乘导数
    return gW, gb

# 一票否决式自查:手推的梯度 vs 数值差分
num = (loss(W + EPS) - loss(W - EPS)) / (2 * EPS)
ana = GW[li][i, j]
print(abs(num - ana) / (abs(num) + abs(ana)))

# 把隐单元的激活画在平面网格上:一个单元就是一条直线的一侧
HA = fwd(Ws2, bs2, GX, u"tanh")[1]
for k in range(8):
    print(panel(HA[:, k]))

实跑输出:

--------------------------------------------------------------------
一层不够,就再叠一层
--------------------------------------------------------------------
==============================================================

[1] 一个单层网络学不会的问题

  数据是一圈环:半径在 0.30 到 0.80 之间算正例,占 43.3%。
  **单层网络只能画一条直线**,你怎么摆这条线,都圈不住一圈环。

    结构                  参数量      训练集      测试集      最后损失
    ----------------------------------------------------------------------
    单层(只有输出层)    3           0.5667      0.5450      0.6836
    两层(16 个隐单元)   65          0.9983      0.9725      0.0187

  **单层的测试集停在 0.5450 附近,两层到了 0.9725。**
  **差的那 40 多个点不是训练不够,是那一条直线的表达上限。**
  多一层,网络就多了一次「先切成几块、再拼起来」的机会。

[2] 反向传播:手推的梯度对不对

  反向传播不是新数学,**就是链式法则**。但手推的公式容易写错一个转置。
  **最靠谱的自查办法:拿数值差分对一遍。**

    结构 2-4-3-1,抽 18 个权重逐个比对:
    解析梯度与数值梯度的最大相对误差 1.142e-08

  **1e-08 量级,说明手推的公式没错。**要是有个地方漏了转置,
  **这个数会直接跳到 1 附近——它是个非常好的「一票否决」检查。**

  **反向传播真正省的是什么?**前向一次、反向一次,就把所有参数的梯度都算出来了。
  **要是用数值差分,每个参数要跑两遍前向。**Demo 这个小网络有 31 个参数,
  **规模一大就是「算一遍」和「算几百万遍」的差别。**

[3] 隐单元到底在干什么

  把平面铺成 19 乘 19 的网格,看前 8 个隐单元各自被什么点亮:

    隐单元 1             隐单元 2             隐单元 3             隐单元 4           
    +##################  .                    ###################               .+####
    +##################  ...                  ###################               .+####
    ++#################  ....                 ###################               :#####
    +++################  ......               ###################               :#####
    ++++###############  ........             ###################              .+#####
    +++++##############  .........            ###################              .+#####
    ++++++#############  ...........          ###################              :######
    :++++++############  ::...........        ###################             .:######
    ::++++++###########  :::...........       :++################             .+######
    :::++++++##########  :::::...........     ..::++#############             .+######
    ::::++++++#########  :::::::...........      ..::+###########             :#######
    :::::++++++########  ::::::::...........        ..:++########            .+#######
    .::::++++++########  ++::::::::.........          ..::++#####            .+#######
    ..::::++++++#######  ++++::::::::.......             ..::++##            :+#######
    ...::::++++++######  +++++::::::::......                ..:++            :########
     ...::::++++++#####  #++++++::::::::....                  ..:           .+########
      ...::::++++++####  ##+++++++:::::::...                                .+########
       ...::::++++++###  ####++++++::::::::.                                :#########
       ....::::++++++##  ######++++++:::::::                               .:#########

    隐单元 5             隐单元 6             隐单元 7             隐单元 8           
                         +##################  ###################  ###################
                         +##################  ###################  ###################
                         :##################  ###################  ###################
    .                    .##################  ###################  ###################
    ++:.                  +#################  ###################  ###################
    ###+::.               :#################  ###################  ###################
    ######+:..            .#################  ###################  ###################
    #########+:..         .+################  ###################  ###################
    ###########++:.        :################  ###################  +##################
    ##############++:.     :################  ###################  .+#################
    #################+:    .+###############  ###################   :+################
    ###################     +###############  #################+:   .:+###############
    ###################     :###############  ################:.     .:###############
    ###################     .###############  ##############+:        .+##############
    ###################      +##############  #############:.          .+#############
    ###################      :##############  ###########+.             .+############
    ###################      .##############  #########+:.               :+###########
    ###################      .+#############  ########+.                 .:+##########
    ###################       :#############  ######+:.                   .:##########

  **每一个隐单元就是一条(或两条)直线切出来的半边。**没有一个是弯的——
  它们的激活值只跟「在直线的哪一侧」有关。
  **可 16 个这样的半边叠起来,边界就能围成一个圈。**
  **这就是「多层」的全部魔法:单个还是直的,组合起来就弯了。**

  顺手数一下:这 16 个隐单元里,有 5 个的激活几乎不随位置变化(全黑或全白),
  **它们等于白占位置。**隐单元给多了不怕,给少了就真的不够用。

[4] 激活函数:sigmoid 的天花板

  同一个结构 2-16-1、同样的学习率、同样 4000 步,只换激活函数:

    激活函数        训练集      测试集      最后损失      梯度最大能有多大
    ------------------------------------------------------------------------
    sigmoid         0.9850      0.9600      0.0815        0.25
    tanh            0.9983      0.9725      0.0187        1.00
    relu            0.9333      0.9125      0.2058        不封顶

  **sigmoid 的导数最大只有 0.25(在输入为 0 时),而 tanh 最大到 1.00。**
  **这一条差距,会在层数一多的时候被连乘放大成灾难——第 5 段直接量它。**
  **ReLU 的导数是 0 或 1,没有上限也没有衰减,所以深网络几乎都用它。**
  代价是「一半的单元可能永远不激活」,这个问题叫神经元死亡。

  **这一段要老实说一句:这次 ReLU 反而最差(0.9125)。**
  **不是 ReLU 不行,是这张表只调了一个学习率、给了同样的步数。**
  ReLU 对学习率敏感得多:0.4 对 sigmoid 和 tanh 刚好,对它偏大。
  **这就是「读实验表」的规矩:只换一个变量的表,只能说明在这个设定下谁更好。**

[5] 深与宽:多叠几层到底买到了什么

  固定隐单元宽度 8,只改层数。同样的数据、同样的步数:

    结构                      参数量      训练集      测试集      最后损失
    ------------------------------------------------------------------------
    1 个隐层 × 8              33          0.9800      0.9600      0.0689
    2 个隐层 × 8              105         1.0000      0.9600      0.0039
    4 个隐层 × 8              249         1.0000      0.9850      0.0001
    8 个隐层 × 8              537         1.0000      0.9675      0.0000

  **两个隐层、四个隐层、八个隐层的训练集全是 1.0000。**
  **所以这次 8 层掉分不是「学不动」,是「学太满了」。**
  参数量从 33 涨到 537,而训练样本只有 600 条——**这是典型的过拟合。**

  **但「深了就难训」这件事是真的,得单独量。**
  建一个 10 个隐层的网络,看反向传回来的梯度有多强:

    激活函数        输出层梯度        第一层梯度        第一层 ÷ 输出层
    ----------------------------------------------------------------------
    sigmoid         8.742e-04         9.861e-11         1.13e-07
    tanh            8.739e-04         1.292e-04         1.48e-01
    relu            1.011e-03         1.328e-03         1.31e+00

  **sigmoid 那一行最能说明问题:第一层梯度只有输出层的 1.1e-07,缩了 887 万倍。**
  **前面几层几乎收不到信号,等于没在学。**
  **tanh 好一些,ReLU 最好——因为它的导数要么是 0、要么是 1,不会越乘越小。**
  **这就是「深度网络难训」的第一层含义:不是它表达不了,是它学不动。**
  **9.3 会给出三条真正的解法:换激活、逐层预训练、以及残差连接。**

  五段连起来:**一层画不出圈**,所以要叠层;**叠了就得把梯度算对**,
  所以要反向传播;**隐单元各自切一刀**,组合才弯得起来;
  **激活函数决定信号能传多远**,于是深了难训。
  **深度学习的全部麻烦,都从「叠起来」这一步开始。**

第 1 段那张表的对比最直白:3 个参数对 65 个参数,0.5450 对 0.9725。 别把这 40 多个点当成「训练得更久就好了」——单层再练一万步也到不了。 第 2 段那个 1.142e-08 是这一节最该抄下来的一个数。 以后你自己写反传,第一件事就是做这个检查。它比任何调试手段都快。 第 3 段那几张图值得盯着看半分钟。每个隐单元都是一条直线的两侧—— 没有一个例外。而 16 个这样的东西加起来,边界就是弯的。 第 5 段的两张表要分开读。前一张说「深了测试集掉下来」,原因是过拟合(训练集 1.0000); 后一张说「深了梯度传不回去」,原因是连乘。两个毛病不一样,解法也不一样。

30 秒实验

  1. 把第 1 段那个环的内半径从 0.30 改成 0.05(变成实心圆),重跑。 两层网络会更快学好,单层还是不行——实心圆也是弯的边界。
  2. 把第 2 段的 EPS 从 1e-6 改成 1e-2,重跑梯度校验。 相对误差会跳到 1e-4 以上——差分步长太大,截断误差就盖过真值了。
  3. 把第 4 段的 lr 从 0.4 改成 0.05,重跑三种激活。 ReLU 的名次会往上走——这就是「学习率没调好,不能怪激活函数」。

常见误区

  1. 以为「多一层总比少一层强」。Demo 第 5 段:8 个隐层的测试集比 4 个隐层低。 层数是超参,不是越多越好。
  2. 以为反向传播是「另一种学习算法」。它只是求梯度的方法, 跟梯度下降是两件事——前者算方向,后者迈步子。
  3. 以为手推的梯度「看着对就行」。一个转置错了照样能跑,只是训得慢。 必须拿数值差分对一遍(1e-08 量级才算对)。
  4. 以为 sigmoid 和 tanh 差不多。导数上限 0.25 对 1.00, 在 10 层网络上会把第一层的梯度差出六个数量级。
自测三问(点开对答案) 1. 为什么单层网络连「一圈环」都学不会? —— 因为**单层网络的决策边界是一条直线**(高维时是一个超平面)。 **一圈环的正例在中间、负例在里外两侧,任何一条直线都切不出这种结构。** Demo 里单层停在 0.5450,**这不是训练不够,是表达上限——再练一万步也一样。** 2. 反向传播为什么比数值差分快那么多? —— 因为**数值差分每个参数都要跑两遍前向**,而反向传播**一趟就把所有参数的梯度都算出来了**。 Demo 那个小网络只有 31 个参数,差别还看不出来; **真实的模型有几十亿个参数,那就是「算一遍」和「算几十亿遍」的区别。** 3. 「深了掉分」和「深了训不动」是同一件事吗? —— **不是。**Demo 第 5 段里 8 个隐层的**训练集是 1.0000**,说明它学得动、而且学太满了, **掉分是过拟合**; 而 10 层 sigmoid 的那个实验里,**第一层梯度只剩输出层的 1.13e-07,那是训不动(梯度消失)**。 **两个毛病,一个靠正则化治,一个靠换激活治。**

下一节讲一段很少被提起的历史。在 ReLU 普及之前,人们解决「训不动」的办法很奇怪: 先用一堆不看标签的小模型把每一层单独训好,再拼起来微调。 那个小模型,就是受限玻尔兹曼机。

相关笔记