1. 梯度爆炸:为什么大梯度可能把训练带偏?
先看不带动量、也不带 weight decay 的普通 SGD:
是参数, 是学习率, 是梯度。假设 ,:
| 梯度 | 更新后的参数 | 更新幅度 |
|---|---|---|
我当时算出两次更新幅度相差 ,也意识到第二种情况一步跨得太远。
梯度描述当前位置附近的变化。沿负梯度方向走足够小的一步,通常能降低可微 loss;走很远却没有这个保证。因此,大梯度配上固定学习率,可能让参数跨过合适的位置,反而增大 loss。
RNN 为什么容易出现这种情况?用一个去掉输入和激活函数的标量例子:
若末端梯度为 ,,反传三步得到:
经过十步,同样的连乘因子是 。这里算的是对 hidden state 的梯度;计算参数梯度时,也会用到这些反传信号,所以参数梯度可能随之变大。
“时间步增加”指反向路径跨过更多序列位置,不是说训练轮数增加就必然爆炸。实际向量 RNN 涉及 Jacobian 连乘,也不是所有路径都会持续放大。这个标量例子展示的是一种机制。
2. Gradient clipping:限制长度,保留方向
我一开始把裁剪理解成“过滤异常值”。更准确的说法是:限制过大梯度带来的影响,而不是丢掉梯度。 大梯度本身也不一定说明数据有问题。
把待裁剪的梯度整理成向量 ,设最大长度为 ,L2 norm clipping 为:
例如 ,长度为 。阈值为 时:
所有分量乘以同一个正数,所以方向保持不变,长度缩成 。如果直接改成 ,比例从 变成 ,方向就不同了;那不是这里的 norm clipping。
课程 Lecture 5 第 44 页介绍了这个方法,所引用的 Pascanu 等人的论文提出用 gradient norm clipping 应对梯度爆炸。
为什么不直接调低学习率?
我最初的想法是:“调低学习率只能压住前面,后面还是会爆炸。”这里需要修正:小学习率会缩小每次更新,不只是前几步。真正的区别在于,它连正常大小的梯度更新也一起缩小。
| 做法 | 梯度为 时的更新幅度 | 梯度为 时的更新幅度 |
|---|---|---|
| 学习率 | ||
| 学习率 ,裁剪阈值 |
以上仍限定普通 SGD。裁剪只在超过阈值时介入,小学习率则统一缩小更新,两者也可以配合使用。
裁剪发生在梯度算完之后,不会取消反传中的连乘。它也解决不了梯度消失: 小于阈值 ,裁剪后仍是 。
3. LSTM:保留旧记忆,也能写入新内容
梯度消失不只是“步子太小”。后面的 loss 传到早期位置时信号太弱,会让模型难以学会利用很久以前的信息。
LSTM 维护 cell state ,并通过门控制记忆更新:
| 符号 | 含义 |
|---|---|
| 旧记忆 | |
| forget gate,旧记忆保留多少 | |
| 候选记忆,准备写入的内容 | |
| input gate,候选内容写入多少 |
是逐元素相乘。实际这些量通常都是向量,每个分量有自己的门值;以下先用一个分量手算。
旧记忆为 ,遗忘门为 ,先保留 。候选记忆为 ,输入门为 :
这里 可以超过 ;经过 tanh 的候选记忆分量则在 内。这个 的例子假设记忆已经在此前积累形成。
门值是人为固定的吗?
我接着问:“输入门的数值怎么定义?遗忘门是固定值吗?”
它们由输入、上下文和可训练参数共同算出:
是 sigmoid,把有限输入映射到 ,作为通过比例。比如 sigmoid 的输入分别为 时,输出约为 。
这里采用列向量约定:若输入维度为 、隐藏维度为 ,则 ,,,,;其他门同理。
各时间步共享同一套参数,但输入与上下文不同,所以门值可以不同。 训练更新的是这些参数;门值是每次前向计算的结果。
4. 输出门:记住了,不等于现在就呈现出来
LSTM 还有 output gate:
我能算出 ,却又问了一次:“这里的 到底代表什么?”
这次区分清楚了: 是内部记忆, 是当前 hidden state。 可以送到输出层做预测,也参与下一步的门值与候选内容计算; 则沿记忆路径继续传递。标准 LSTM 的这些状态关系可对照 PyTorch LSTM 公式。
例如语言模型可以计算 ,再经过 Softmax 预测下一个词。 本身不是词,也不是概率。
输出门接近 时,对应的 分量接近零,但 不会因此直接被清空。想保留某条记忆、暂时不呈现、也不写入新内容,可以让对应的:
我还纠正了一个说法:“输出门打开,输出就按照旧记忆来。”实际上,输出门作用于更新后的 。刚才记忆已经变成 ,所以输出门为 的理想情况对应 ,不是 。
如果希望记忆基本原样保留,除了 ,还需要新增项接近零,例如 。只让遗忘门接近 ,仍可能继续写入内容。
5. 为什么 LSTM 能缓解梯度消失?
沿着旧记忆直接传到新记忆的路径,每个分量的直接局部导数是对应的 分量。因此当连续五步都为 时,这条路径的梯度因子是:
相比 ,信号保留得多得多。我的理解是:“ 接近 更能保留旧信息,每步乘 就会逐渐遗忘。”还要补上反向传播:梯度也更容易沿这条直接路径传回早期位置。
这里的“直接”很重要。完整计算图还包含门值和候选内容经 产生的依赖,不能把完整导数一概写成遗忘门连乘。LSTM 提供了更有利的传播路径,但不保证梯度永远不消失或不爆炸。
6. GRU:用更新门混合旧状态与候选状态
GRU 不单独维护 ,而是更新 。本文采用 PyTorch GRU 的约定:
是 update gate, 是候选状态。这里 越接近 ,越保留旧状态;越接近 ,越采用候选状态。
有些资料让 乘候选状态,记号含义正好相反。阅读时要看公式,不能只记门的名字。
例如旧状态为 ,候选状态为 ,:
我当时总结为“要么保留更多旧状态,要么保留更多新输入”。这里需要把“新输入”改成“候选状态”:它不是原始 ,而是经过计算的内容,本身也可能包含历史信息。
7. Reset gate 与 forget gate 的区别在哪里?
GRU 的 reset gate 出现在候选状态的生成过程。继续采用 PyTorch 写法:
它乘在旧状态经过变换后的贡献上。 时,候选内容少参考历史; 时,允许这部分历史贡献通过。PyTorch 的 reset 放置位置与部分原始实现不同,本文始终使用上面这一版。
“重置”这个名字让我最初猜反了,以为想重新开始就该接近 。看位置才知道,这里数值表示通过比例。
我随后问:“它和 LSTM 的遗忘门是不是作用相似?”二者都控制历史信息,但位置不同:
| 门 | 控制的位置 |
|---|---|
| LSTM | 旧记忆直接保留到新记忆的路径 |
| GRU | 生成候选状态时,旧状态的贡献 |
| GRU | 最终混合时,旧状态的保留比例 |
所以在“直接保留旧状态”这一点上,本文约定里的 更像 。
两个组合帮助我真正分清了它们: 时,候选内容少参考历史,但最终仍主要保留旧状态; 时,最终主要采用候选状态,但候选状态仍可能包含加工后的历史信息。允许参考历史,不等于保证原样保留历史。
8. BPTT:什么时候相乘,什么时候相加?
门控参数由任务 loss 学习,不需要给每个门额外标注正确数值。但 BPTT 是不是每反传一步,就更新一次那一步的参数?
通常不是。各时间步共享参数,BPTT 收集并累加梯度贡献,之后再由 optimizer 更新。
问答中给出两个时间步对同一参数的贡献 和 ,我误算成了乘积 。其实应该是:
规则仍然是:沿同一条路径,局部导数相乘;多条路径汇到同一参数,贡献相加。
即使只有第二步的 loss,共享参数 也有不同路径:
一次使用通过 影响 loss,另一次通过 影响它。分别沿路径求导,再相加。
“不同时间步是不同路径”是一个入门理解。更准确地说,不同时间步的参数使用形成不同路径,而每次使用还可能影响多个后续 loss。所谓某一步的梯度贡献,必须已经包含相关后续传播,不能漏算或重复计算。
9. Optimizer:拿到梯度之后,真正修改参数
BPTT 负责算梯度,Optimizer 负责利用梯度更新参数。最简单的例子仍是普通 SGD:
三个时间步的贡献为 ,总梯度是 。若参数为 、学习率为 :
下一轮若梯度为 :
负梯度让参数增加。更新后的同一套参数,会用于下一次前向计算的各时间步。这里的计算对应 SGD 不启用动量、weight decay 等额外项的情况。
Momentum 和 Adam 会进一步利用历史信息决定更新,这次只建立了 Optimizer 的职责与基本 SGD 计算,还没有展开这些算法。
10. 这次串起来的训练流程
一次常见的训练迭代可以顺着读下来:清理上一轮梯度 → 前向计算状态、门值和预测 → 计算任务 loss → BPTT 计算并累加共享参数的梯度 → 必要时做 clipping → Optimizer 更新参数。
实际也有跨 batch 累积梯度、截断 BPTT 等安排,因此不必把“一整条序列结束才更新”当作唯一做法。核心分工不变:求导和更新参数是不同步骤。
这轮我最想记住的三点是:裁剪限制过大的梯度,不能恢复消失的信号;门控让模型学习怎样保留和使用历史,但门值不等于参数;反传时沿路径相乘,汇总共享参数的贡献时相加。
下次再看到一条复杂公式,我会先问:这个量控制的是哪一条路径?它是当前算出来的状态,还是训练要更新的参数?这比只记住门的名字更可靠。