卷积网络假设输入是固定大小的图。序列不一样:长度可变,而且顺序有意义。 RNN 的全部机关就一个式子:这一步的隐状态等于 tanh(输入乘 Wx 加上一步隐状态乘 Wh 加 b)。 Wh 就是记忆。 实测 8 维隐状态的网络只有 98 个参数,但梯度从第 12 步往回传,退到第 1 步剩 0.000000。 延迟复述任务上 800 轮训练损失一动没动(0.6909),测试准确率 0.5000——等于瞎猜。
第 4 章的反向传播在这里变成「沿时间反向传播」,名字叫 BPTT,本质还是链锁律。 只是乘的项从一层变成了一步。 第 5 章的卷积靠「局部连接加权重共享」省参数, RNN 靠「所有时间步共享同一组权重」省参数——出发点一样,省的方向不同。 6.2 和 6.3 的两个门控结构,都是来修这一节末尾那个梯度消失的。
一、序列不是图
一张猫的图片左右翻转,还是只猫。一句话里的 20 个词随便换个顺序,意思立刻变了。 这就是序列和图的根本差别:位置本身携带信息。 要把顺序吃进来,模型必须在第 t 步还能看见第 t 减 1 步处理过的东西。 那个「处理过的东西」就是隐状态。 换成大白话说:它每读一个输入,就把「到目前为止读到什么」更新一遍,存成一个小向量。 读完整个序列,这个小向量就是整句话的摘要。
二、一个式子
| 参数 | 形状 | 个数 | 作用 |
|---|---|---|---|
| Wx | 1×8 | 8 | 输入到隐状态 |
| Wh | 8×8 | 64 | 上一步隐状态到这一步 |
| b | 8 | 8 | 隐状态偏置 |
| Wo | 8×2 | 16 | 隐状态到输出 |
| bo | 2 | 2 | 输出偏置 |
合计 98 个。前四行是隐状态那一层,后两行是输出层——输出层和普通全连接一模一样。 唯一值得注意的是 Wh: 它的左边和右边都是 8 维隐状态,乘的是「上一步的自己」。这一步就是「记忆」的全部实现, 没有别的机关。 换个角度看,把隐状态当成一本 8 维的笔记本, Wx 决定「这一步看到的怎么写进去」,Wh 决定「旧内容怎么和新内容混」。
三、展开之后就是很深的前馈网络
把时间步一层层摊开看:它长得和一个 8 层的全连接网络一模一样,只不过每一层共用同一组权重。 参数只有 98 个,而这个摊开的网络有 8 层。 这就是权重共享的威力,也是它的代价——同一组权重必须在所有位置都合适,位置之间不能有各自的偏好。 反向传播在这张摊开的图上跑,就叫 BPTT。第 4 章那四步一个字都没变,只是层数变成了时间步数。
四、梯度消失:实测
从第 12 步退到第 1 步,梯度范数从 1.000000 掉到 0.000000。只用了 11 步。 每退一步要乘两样东西: 一个是 Wh,一个是 tanh 的导数(1 减 h 的平方)。tanh 的输出在 −1 到 1 之间,导数最大是 1, 绝大多数时候只有 0.1 到 0.5。 连乘 11 次,0.3 的 11 次方是 0.0000018。 和实测的 0.000013 同一个量级。这不是 bug,是连乘的必然结果。 直接后果:第一位的输入对最终损失几乎没有影响。网络学不会「记住开头」。
五、延迟复述任务:800 轮不动
任务设计得很干净:第 1 位是 0 或 1,中间七位全是 0,第 8 步要求复述第 1 位。要答对, 网络必须把第 1 位的信息带过 7 个空步。 损失从头到尾停在 0.6909。 这个数不是随便来的:二分类完全瞎猜的交叉熵是 ln2,约 0.6931, 0.6909 说明输出基本锁在 0.5 对 0.5。 测试准确率 0.5000。它不是学得慢,是压根没开始学。 原因还是那个: 梯度传不回第 1 步,第 1 位那个输入对应的权重就永远收不到「你猜错了」这个信号。 而它恰恰是唯一有用的信息。
六、RNN 还能干什么
6.4 的字符级语言模型:一次猜一个字符。那是它最直接的应用。 6.5 的 Seq2Seq:读一整句、写一整句。编码器和解码器都是 RNN。 此外还有时间序列预测、语音识别、手写识别。凡是前后有顺序的东西,都是它的地盘。 它的地位后来被注意力取代了(6.6),但「用隐状态携带历史」这个思想留了下来。
七、动手 Demo:8 维隐状态与一个延迟复述任务
先建一个输入 1 维、隐状态 8 维、输出 2 类的 RNN,数清参数;喂一个 5 步的 0/1 序列看隐状态怎么变; 再把梯度从第 12 步往回退,看它剩多少。最后跑那个延迟复述任务。
import numpy as np
class RNN:
def __init__(self, d, h, k, rg, alpha=0.3):
self.d, self.h, self.k = d, h, k
s = alpha / np.sqrt(h)
self.Wx = rg.normal(0, s, (d, h)) # 输入 -> 隐状态
self.Wh = rg.normal(0, s, (h, h)) # 上一步隐状态 -> 这一步
self.b = np.zeros(h)
self.Wo = rg.normal(0, s, (h, k)) # 隐状态 -> 输出
self.bo = np.zeros(k)
def forward(self, X):
B, T, _ = X.shape
hs = [np.zeros((B, self.h))] # 初始隐状态是 0
for t in range(T):
hs.append(np.tanh(X[:, t, :] @ self.Wx
+ hs[-1] @ self.Wh + self.b))
return hs
rg = np.random.default_rng(1)
m = RNN(1, 8, 2, rg) # 98 个参数
# 梯度从第 12 步往回传:每退一步乘一次 Wh 和 tanh 的导数
hl = m.forward((rg.random((1, 12, 1)) > 0.5).astype(float))
v = np.ones((1, 8)) / np.sqrt(8.0)
for t in range(12, 0, -1):
print(t, "%.6f" % float(np.linalg.norm(v)))
v = (v * (1.0 - hl[t] * hl[t])) @ m.Wh.T
# 延迟复述任务:第 1 位是信号,中间七位全是 0
X = np.zeros((300, 8, 1))
bit = (rg.random((300, 1)) > 0.5).astype(float)
X[:, 0, 0] = bit[:, 0]
实跑输出:
--------------------------------------------------------------------
递回神经网络:把上一步的记忆带进来
--------------------------------------------------------------------
一个 8 维隐状态、输入输出各 1 维的循环网络:
参数 形状 个数
--------------------------
Wx 1×8 8
Wh 8×8 64
b 8 8
Wo 8×2 16
bo 2 2
合计 98 个。
**注意 Wh 是「这一步的隐状态乘上一步的隐状态」,这一步就是「记忆」的全部实现——没有别的机关。**
喂一个 5 步的 0/1 序列,看隐状态怎么一步步变(打印范数):
第 1 步 输入 0 隐状态范数 0.0000
第 2 步 输入 1 隐状态范数 0.2178
第 3 步 输入 0 隐状态范数 0.0644
第 4 步 输入 0 隐状态范数 0.0173
第 5 步 输入 0 隐状态范数 0.0029
再看一件更要紧的事:梯度从第 12 步传回第 1 步,还剩多少。
从第 12 步退到第 1 步 梯度范数 0.000000
从第 12 步退到第 2 步 梯度范数 0.000001
从第 12 步退到第 3 步 梯度范数 0.000003
从第 12 步退到第 4 步 梯度范数 0.000012
从第 12 步退到第 5 步 梯度范数 0.000050
从第 12 步退到第 6 步 梯度范数 0.000149
从第 12 步退到第 7 步 梯度范数 0.000731
从第 12 步退到第 8 步 梯度范数 0.002939
从第 12 步退到第 9 步 梯度范数 0.009932
从第 12 步退到第 10 步 梯度范数 0.054830
从第 12 步退到第 11 步 梯度范数 0.220670
从第 12 步退到第 12 步 梯度范数 1.000000
**从 1 掉到 0.0000,只用了 11 步。**
这就是梯度消失:每退一步都乘一次 Wh 和一个小于 1 的导数因子,连乘下来指数衰减。
它的直接后果是:**第 1 步的输入对最终损失几乎没有影响,网络学不会「记住开头」。**
下面这个任务就是专门考这件事的。
任务:序列第 1 位是信号,中间七位全是 0,要求最后输出第 1 位是什么。
第 100 轮 损失 0.690925
第 300 轮 损失 0.690924
第 500 轮 损失 0.690924
第 700 轮 损失 0.690924
**800 轮下来损失一动没动,就在 0.6909 附近。**测试准确率 0.5000——等于瞎猜。
它不是「训得不够」,是**梯度根本传不回第 1 步**。这一步一卡住,后面的门控结构才有意义。
参数表最后一行合计 98。其中 64 个都在 Wh 上——这是唯一一处「自己乘自己」。 隐状态范数那五行:0.0000、0.2178、0.0644、0.0173、0.0029。 输入是 0 的步,隐状态也在衰减,因为它只乘 Wh 再过 tanh。 这说明一件要紧的事:这个网络的「记忆」是会自己衰减的,不是一块永久存储。能记多久, 取决于 Wh 把范数放大还是缩小。 梯度那一列最能说明问题:1.000000 一路掉到 0.000000,中间没有任何一处停顿。它是平滑的指数衰减, 所以调学习率、加训练轮数都没用。 延迟复述任务的四行损失 0.690925、0.690924、0.690924、0.690924。小数点后四位一个没变。 这不是收敛,这是脊柱被掐断。损失不动、准确率 0.5000,两者是同一件事的两个侧面。 要修它,只能改结构——让梯度有一条不必连乘的路。那就是下一节 LSTM 的全部动机。
30 秒实验
- 把隐层从 8 加到 32,梯度退 8 步能剩多少?先猜,再改 RNN(1, 32, 2, rg) 跑一遍。
- 把任务改成「第 1 位是信号,但要求在第 3 步就复述」,普通 RNN 能学会吗?为什么?
- 如果把初始化尺度 alpha 从 0.3 改成 1.0,梯度消失会变慢还是更快?
常见误区
- 以为 RNN 的「记忆」是一份额外的存储。没有。 记忆就是那个 8 维向量本身,它每一步都被整个覆写。
- 以为训练久一点就能学会长距离依赖。梯度消失是结构性的,800 轮和 8000 轮的结果一样。
- 以为隐状态越大就越能记住。加宽确实能装更多, 但能不能学到,取决于梯度传不传得回去,光加宽不解决问题。
自测三问(点开对答案)
1. RNN 里哪一组参数承担「记忆」? —— Wh,也就是「上一步隐状态到这一步隐状态」那一块。 它每个时间步都被重复使用,所以模型在第 t 步还能看到前面处理过的信息。 2. 梯度消失的直接后果是什么? —— 早期时间步的输入对损失几乎没有影响,网络学不会「记住开头」。 实测从第 12 步退到第 1 步,梯度范数只剩 0.000000。 3. 为什么说 RNN 摊开就是一个很深的前馈网络? —— 每个时间步相当于一层,层与层之间靠隐状态连接。 区别只在于所有层共用同一组权重,所以参数不随序列变长而增加。普通 RNN 卡在「记不住开头」。 6.2 的 LSTM 用一条只有乘加的传送带绕开了这个连乘,同一个任务上它能到 1.0000。
- ☐ (本节完,下一节 6.2 LSTM 类神经网络)