一层网络只能画一条直线。可现实里要找的边界常常是弯的——一圈环、一条螺旋。 解决办法只有一个:再叠一层。 叠上去之后有两笔学费要交。第一笔是怎么把误差传回去(反向传播); 第二笔是传得回去多远(梯度消失)。这一节把两笔都量一遍。
这一节是第 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 段那个环的内半径从 0.30 改成 0.05(变成实心圆),重跑。 两层网络会更快学好,单层还是不行——实心圆也是弯的边界。
- 把第 2 段的
EPS从 1e-6 改成 1e-2,重跑梯度校验。 相对误差会跳到 1e-4 以上——差分步长太大,截断误差就盖过真值了。 - 把第 4 段的
lr从 0.4 改成 0.05,重跑三种激活。 ReLU 的名次会往上走——这就是「学习率没调好,不能怪激活函数」。
常见误区
- 以为「多一层总比少一层强」。Demo 第 5 段:8 个隐层的测试集比 4 个隐层低。 层数是超参,不是越多越好。
- 以为反向传播是「另一种学习算法」。它只是求梯度的方法, 跟梯度下降是两件事——前者算方向,后者迈步子。
- 以为手推的梯度「看着对就行」。一个转置错了照样能跑,只是训得慢。 必须拿数值差分对一遍(1e-08 量级才算对)。
- 以为 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 普及之前,人们解决「训不动」的办法很奇怪: 先用一堆不看标签的小模型把每一层单独训好,再拼起来微调。 那个小模型,就是受限玻尔兹曼机。
- ☐ (本节完,下一节 9.2 受限玻尔兹曼机)