想象你是一名剪辑师,面前摆着一卷长镜头胶片。观众说最后一幕有问题,你不能只盯着最后一帧改——因为那一帧的表情,是由几百帧之前的灯光、走位和摄影手一起决定的。要找出责任,你必须把胶片按时间摊开,然后从最后一帧开始倒放,逐格往回看:这一帧的变化,到底传给了下一帧多少?这一步传一点,下一步传一点,最后才能算清每个老镜头该背多少锅。这就是 BPTT 的生活版本:把循环神经网络按时间步展开,再反向传播梯度。本文会带着这卷胶片,走过展开、链式法则、梯度累积、梯度消失、梯度爆炸,以及梯度裁剪和门控机制这两种救援方案,把这些概念变成你能随手使用的工程直觉。

岗位关联

掌握 BPTT 核心思想,你就能理解 RNN/LSTM 训练时为什么慢、为什么炸、为什么记不住远方的信息,从而在序列建模的工程调参中找到正确的发力点。

目录
  1. 可视化演示
  2. 1. 序列展开:把胶片摊在灯箱上
  3. 2. 链式法则:逐格传导的“手感”
  4. 3. 梯度累积:每一帧都来投一票
  5. 4. 梯度消失:旧镜头的指纹越来越淡
  6. 5. 梯度爆炸:倒放倒出山体滑坡
  7. 6. 梯度裁剪:给回放器装刹车
  8. 7. 门控机制:给记忆装上红绿灯
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

展示系统处理流水线、实时性能指标曲线及多方案对比分析,包含数据流转、吞吐延迟监控与方案评估。

本节配有交互动效(进入视口后自动加载,约 3KB)。

系统可视化 处理流程:输入→处理→模型→输出 性能监控:实时指标 P=L×T

1. 序列展开:把胶片摊在灯箱上

小陈在剪辑台上把一卷胶片展开,在灯箱上一格一格排开。他发现每个画面不是孤立的:上一帧的曝光、移动,会直接影响下一帧。于是他在相邻帧之间画上箭头,得到一张时间因果图。RNN 处理序列时做的是同样的事:一个长度为 T 的输入序列,就像 T 个连续镜头。

在 RNN 里,每个时间步 t 接收当前镜头 x_t,也接收上一格留下的工作记忆 h_{t-1}。二者经过同一个“剪辑参数”加工后,生成新的记忆 h_t。这里的 W、U、b 在每个时间步都一样,这就是参数共享:同一个摄影师在整卷胶片里使用同一套手感。

把所有时间步的公式串起来,就是一张把循环展开成深层的计算图。BPTT 的第一步,就是把这卷胶片完整铺开;铺得越整齐,后面反向追责的路线就越清晰。

ht=tanh⁡(Wht−1+Uxt+b)\displaystyle h_t = \tanh(W h_{t-1} + U x_t + b)
这个公式说的是:新记忆 = 旧记忆按权重 W 保留一部分 + 当前画面按权重 U 进入一部分,最后用 tanh 压到 -1 到 1 之间。
  1. 旧记忆 h_{t-1}上一帧结束时留下的剪辑台状态,是一个向量。
    ht−1h_{t-1}
  2. 记忆权重 W旧记忆对当前决策的影响,相当于“上一帧影响下一帧”的传导系数。
    Wht−1W h_{t-1}
  3. 输入权重 U当前镜头 x_t 的“新鲜画面”被放大或缩小后进入工作记忆。
    UxtU x_t
  4. 激活函数tanh 把汇总结果压到 -1 到 1 之间,保证记忆不发散。
    ht=tanh⁡(⋅)h_t = \tanh(\cdot)

2. 链式法则:逐格传导的“手感”

观众喊停,小陈开始倒放。他先问最后一帧:这里的亮度偏差是多少?然后向前一格问:这格亮度的改变,传给了最后一帧多少?每一格只回答一个局部问题,但把局部答案乘起来,就得到从远处传到终点的手感。

损失 L 对隐藏状态 h_{t-1} 的偏导数,等于 L 对 h_t 的偏导数,乘上 h_t 对 h_{t-1} 的偏导数。这就是复合函数的链式法则在时间轴上的应用。BPTT 之所以叫 through time,是因为这串乘法在展开图上沿着时间反向一路做下去。

每倒放一格,就完成一次链式乘法。这个乘法本身很温和,但连续做很多次后,温和的系数也可能变成温柔的陷阱——这就是接下来要展开的故事。

∂L∂ht−1=∂L∂ht⋅∂ht∂ht−1\displaystyle \frac{\partial L}{\partial h_{t-1}} = \frac{\partial L}{\partial h_t} \cdot \frac{\partial h_t}{\partial h_{t-1}}
一句话理解:要想知道“上一格该背多少锅”,先看“这一格对最终结果有多敏感”,再看“上一格对这一刻有多敏感”,两者相乘就是答案。
  1. 当前损失导数成片偏差对第 t 帧工作记忆的敏感度。
    ∂L∂ht\displaystyle \frac{\partial L}{\partial h_t}
  2. 局部传导系数第 t 帧记忆对第 t-1 帧记忆的敏感度,也就是前一帧动作传到当前帧的比例。
    ∂ht∂ht−1\displaystyle \frac{\partial h_t}{\partial h_{t-1}}
  3. 链式相乘两个敏感度相乘,把“责任”从第 t 帧继续送回第 t-1 帧。
    ∂L∂ht⋅∂ht∂ht−1\displaystyle \frac{\partial L}{\partial h_t} \cdot \frac{\partial h_t}{\partial h_{t-1}}

3. 梯度累积:每一帧都来投一票

小陈发现,责任不能只算最后一帧。如果某个运镜在整卷胶片里反复出现,那么每一帧的偏差都可能与它有关。于是他把每个时间步算出的“对同一参数的怨念”放到同一个记账本里,逐项累加,得出一笔总账。

训练时总损失通常是各时间步损失之和,L = Σ L_t。导数是线性运算,所以总损失对 W 的梯度,等于每个时间步损失对 W 的梯度相加。换句话说,每一个时间步都在给同一组参数投一票。

这份总账就是 BPTT 区别于普通反向传播的地方:普通网络每个模块往往只被一条路径使用,而 RNN 的同一个 W 被 T 个时间步共用,所以必须把 T 份梯度攒在一起。账算得越全,参数更新越不偏心。

∂L∂W=∑t=1T∂Lt∂W\displaystyle \frac{\partial L}{\partial W} = \sum_{t=1}^{T} \frac{\partial L_t}{\partial W}
每个时间步的损失都会产生一份“该怎样调整 W”的建议,最终更新方向是所有建议的总和。
  1. 单步损失 L_t第 t 帧成片与剧本的偏差。
    LtL_t
  2. 单步梯度该帧偏差对共享参数 W 的敏感度。
    ∂Lt∂W\displaystyle \frac{\partial L_t}{\partial W}
  3. 求和累加把所有时间步的敏感度相加,得到最终更新方向。
    ∑t=1T∂Lt∂W\displaystyle \sum_{t=1}^{T} \frac{\partial L_t}{\partial W}

4. 梯度消失:旧镜头的指纹越来越淡

小陈倒得越远,发现问题越来越模糊。第一版剪辑时的某个微小抖动,本来在最后有一丁点影响,可中间隔着几十格画面,每格都把它打了个折:0.9 × 0.9 × …… 乘到最后,信号几乎没了。这就是“旧镜头指纹淡出”。

数学上,h_T 对 h_k 的敏感度是沿途每一步 ∂h_j / ∂h_{j-1} 的连乘。如果每一步的范数都小于 1,尤其是在 tanh 或 sigmoid 的饱和区,连乘项会随距离指数级衰减。这个连乘项正好出现在 BPTT 的梯度公式中,于是遥远时间步拿不到有效更新。

梯度消失不等于梯度变成零,而是早期时间步的梯度在总账里无足轻重。小陈的应对方式有两种:从回放端踩刹车,或在拍摄端打开一条近恒等的高速路。

∂hT∂hk=∏j=k+1T∂hj∂hj−1\displaystyle \frac{\partial h_T}{\partial h_k} = \prod_{j=k+1}^{T} \frac{\partial h_j}{\partial h_{j-1}}
这个连乘式的本质是“因果链上每一环都打折”。只要每一环都略小于 1,跨越的时间步一多,远端梯度就会被稀释到几乎消失。
  1. 远端相关性最后帧对第 k 帧的敏感度,是判断 RNN 能否学到长程依赖的钥匙。
    ∂hT∂hk\displaystyle \frac{\partial h_T}{\partial h_k}
  2. 连乘路径沿途每一步的局部导数相乘,而不是相加。乘积比加和更容易暴涨或暴缩。
    ∏j=k+1T∂hj∂hj−1\displaystyle \prod_{j=k+1}^{T} \frac{\partial h_j}{\partial h_{j-1}}
  3. 衰减条件当每一步的某种长度度量都小于 1 时,乘积随 T-k 增大趋向 0。
    lim⁡T−k→∞∂hT∂hk=0\displaystyle \lim_{T-k \to \infty} \frac{\partial h_T}{\partial h_k} = 0

5. 梯度爆炸:倒放倒出山体滑坡

如果打折反过来成了放水,每倒放一格都把信号放大 1.1 倍,几十格之后,小陈手里的回放器就像被踩死油门,越倒越快,最后直接把剪辑台撞翻。训练中的表现是参数猛地跳一大步,损失飞向 NaN。

当权重矩阵的谱半径,也就是连接强度放大倍数的上限,大于 1 时,连乘项会指数级放大。用范数描述,就是梯度的范数可以被一个大于 1 的 γ 的 (T-k) 次方框住。展开的深度越长,爆炸越猛烈。

梯度爆炸常与参数初始化、学习率过大一起出现。它不是梯度消失了,而是梯度太“兴奋”。小陈的第一反应是给回放器装刹车——这就是梯度裁剪。

∥∂hT∂hk∥≤γ T−k,γ>1\displaystyle \left\lVert \frac{\partial h_T}{\partial h_k} \right\rVert \le \gamma^{\,T-k}, \quad \gamma > 1
梯度爆炸就是“每一帧都有一点点放大,放得多了就变成巨浪”。巨浪拍向参数,损失曲线会剧烈震荡甚至直接变成 NaN。
  1. 梯度范数衡量回传信号“有多猛”。
    ∥∂hT∂hk∥\displaystyle \left\lVert \frac{\partial h_T}{\partial h_k} \right\rVert
  2. 放大因子每一步的局部灵敏度都可能大于 1。
    γ>1\gamma > 1
  3. 指数次数距离越远,指数越大,梯度可能迅速撑爆数值范围。
    γT−k\gamma^{T-k}

6. 梯度裁剪:给回放器装刹车

小陈在回放器上加了一个限速器:如果倒放速度超过某个安全值,就按下“等比降速”,路线和方向都不变,只是速度被压回安全区。这样既不会扯断胶片,也保留了回放的方向感。

梯度裁剪的常见做法是看整个梯度向量的范数 ∥g∥。当范数超过阈值 θ 时,把 g 乘以 θ / ∥g∥。由于缩放因子是正数,梯度方向不变,只是步长变小;若范数没超阈值,则原样保留。

梯度裁剪是工程上最便宜的救援:一个判断,一个乘法。但它是治标不治本——如果老镜头本身太难记,我们还要从源头修一条能长期保存记忆的路,也就是门控。

g^={g,∥g∥≤θg⋅θ∥g∥,∥g∥>θ\displaystyle \hat{g} = \begin{cases} g, & \|g\| \le \theta \\ g \cdot \frac{\theta}{\|g\|}, & \|g\| > \theta \end{cases}
裁剪只改变梯度的“长度”,不改变梯度的“朝向”。就像限速器只降车速,不改变行车路线。
  1. 原始梯度 g回放器当前的速度矢量。
    gg
  2. 范数判断只有当速度矢量的长度超过阈值 θ 时才启动刹车。
    θ/∥g∥\theta / \|g\|
  3. 等比缩放把整条速度矢量的长度压到阈值,方向保持不变。
    g^=g⋅θ∥g∥\hat{g} = g \cdot \frac{\theta}{\|g\|}

7. 门控机制:给记忆装上红绿灯

小陈另一种思路是在胶片档案室里装红绿灯:每一帧结束前,让一个“遗忘灯”决定上一段记忆是保留、部分放行还是清空。绿灯放行、红灯阻断、黄灯留一半,于是记忆可以在时间长河里走一条快速通道,而不是每格都被压缩一次。

LSTM 等门控 RNN 引入细胞状态 c_t。遗忘门 f_t 控制旧细胞 c_{t-1} 保留多少,输入门 i_t 控制新候选 c̃_t 写入多少。关键递推公式是细胞状态更新式。当遗忘门接近 1 时,∂c_t / ∂c_{t-1} 也接近 1,BPTT 的连乘路径被“短路”成一条近恒等的高速路,梯度不再快速消失。

门控没有取消 BPTT,反而让 BPTT 的连乘链路更友善。输出门再决定当前要吐露多少记忆,最终仍要用链式法则把所有门的梯度算清。所以剪辑师小陈的倒放工作没丢,只是路上多了红绿灯,不再动不动就迷路。

ct=ft⊙ct−1+it⊙c~t\displaystyle c_t = f_t \odot c_{t-1} + i_t \odot \tilde{c}_t
门控的本质是“让旧记忆可以选择原封不动地通过”,而不是每一帧都必须被非线性激活函数重新压一遍。
  1. 遗忘门 f_t0 到 1 之间的开关,决定上一段记忆保留多少。
    ftf_t
  2. 旧细胞状态上一格留下的长期档案。
    ct−1c_{t-1}
  3. 逐元素相乘⊙ 表示按位门控,不是普通的矩阵整体乘法。
    ft⊙ct−1f_t \odot c_{t-1}
  4. 新候选与输入门当前帧的新信息在输入门控制下写入,与保留的旧记忆相加。
    it⊙c~ti_t \odot \tilde{c}_t

AI 应用场景

智能客服的意图识别

阶段:序列特征建模与模型训练

解决的问题:用户消息是一串有先后顺序的词,传统词袋模型只看局部词频,容易把“退”和“先用几天”理解为互斥意图。BPTT 让 RNN 能把“退手机”这个远端信号传递到句尾,再结合门控判断真实意图。

具体案例:用户在客服对话框输入“我要退上周买的手机,但想先用几天再看看”。训练好的 RNN 不仅要识别“退”,还要识别后面的“先看看”这个转折;工程师用 LSTM 加梯度裁剪,保证长句训练不出现梯度爆炸。

金融时序交易异常检测

阶段:训练优化与梯度更新

解决的问题:信用卡交易序列中,单笔异常往往需要结合过去几十笔消费习惯才能判断。BPTT 把每一笔交易的梯度沿时间回传,但长序列容易梯度消失;门控机制让模型记得用户历史行为。

具体案例:某用户在境外突然产生一笔大额消费,序列模型需要综合“过去三个月从未出境”“前几次消费都在本地”“刚发生过密码修改”等时间跨度很长的特征。工程师在训练 LSTM 时设置梯度裁剪阈值,防止回传梯度爆炸导致模型参数飞掉。

语音唤醒词识别

阶段:序列建模与在线模型更新

解决的问题:语音信号被切成几十甚至上百帧,唤醒词的关键音节可能出现在序列开头。BPTT 负责把末尾帧的损失传回开头,门控机制则保留关键音节的上下文。

具体案例:用户对音箱说“打开客厅灯”,共 40 帧音频。声学模型需要用 RNN 记住“打开”的声学特征,并在“灯”出现时触发唤醒。训练中先使用梯度裁剪稳住数值,再引入门控让跨帧记忆更可靠。

Python 演示

python68 行
import numpy as np  def to_column(x):    return np.array(x, dtype=float).reshape(1, 1)  class TinyRNN:    def __init__(self, hidden_size=4, lr=0.05):        self.whh = np.random.randn(hidden_size, hidden_size) * 0.1        self.wxh = np.random.randn(hidden_size, 1) * 0.1        self.why = np.random.randn(1, hidden_size) * 0.1        self.bh = np.zeros((hidden_size, 1))        self.by = np.zeros((1, 1))        self.lr = lr     def forward(self, seq):        self.hs = [np.zeros((self.whh.shape[0], 1))]        self.xs = [to_column(x) for x in seq]        self.ys = []        h = self.hs[0]        for x in self.xs:            h = np.tanh(self.whh.dot(h) + self.wxh.dot(x) + self.bh)            self.ys.append(self.why.dot(h) + self.by)            self.hs.append(h)        return self.ys     def backward(self, target):        dwhh = np.zeros_like(self.whh)        dwxh = np.zeros_like(self.wxh)        dwhy = np.zeros_like(self.why)        dbh = np.zeros_like(self.bh)        dby = np.zeros_like(self.by)        dh_next = np.zeros_like(self.hs[0])        for t in range(len(self.xs) - 1, -1, -1):            dy = self.ys[t] - target[t]            dwhy += dy.dot(self.hs[t + 1].T)            dby += dy            dh = self.why.T.dot(dy) + dh_next            da = (1 - self.hs[t + 1] ** 2) * dh            dbh += da            dwhh += da.dot(self.hs[t].T)            dwxh += da.dot(self.xs[t].T)            dh_next = self.whh.T.dot(da)        return [dwhh, dwxh, dwhy, dbh, dby]     def update(self, seq, target):        self.forward(seq)        grads = self.backward(target)        for param, grad in zip(            [self.whh, self.wxh, self.why, self.bh, self.by], grads        ):            param -= self.lr * grad  def main():    np.random.seed(42)    rnn = TinyRNN(hidden_size=4, lr=0.1)    seq = [0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0]    target = [1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0]    for epoch in range(200):        rnn.update(seq, target)    out = rnn.forward(seq)    print([round(float(y[0, 0]), 3) for y in out])  if __name__ == "__main__":    main()
目的:用纯 NumPy 手写一个极简 RNN,并实现完整的 BPTT 反向传播,训练它学习“0、1 交替”的序列预测任务。
  1. self.hs = [np.zeros((self.whh.shape[0], 1))]第 0 个时间步之前的记忆初始化为零。
  2. h = np.tanh(self.whh.dot(h) + self.wxh.dot(x) + self.bh)前向展开的核心递推。
  3. self.ys.append(self.why.dot(h) + self.by)每个时间步都输出一个预测值。
  4. for t in range(len(self.xs) - 1, -1, -1):从最后一个时间步开始反向遍历,这正是 BPTT 的“倒放”。
  5. dy = self.ys[t] - target[t]计算当前时间步输出对损失的一阶导数。
  6. dwhy += dy.dot(self.hs[t + 1].T)当前时间步对输出权重的梯度投票。
  7. dh_next = self.why.T.dot(dy) + dh_next把梯度继续往前一个时间步传递。
  8. param -= self.lr * grad汇总完所有时间步的梯度后,统一更新参数。

常见误区

误区初学者看 RNN 状态传递,觉得每个时间步都在“攒记忆”,于是认为它能记住任意长度的往事,昨晚的购物记录都历历在目。

正解记忆是靠 BPTT 的梯度把“该记多久”训练出来的。展开后梯度要穿过很多时间步连乘,消失或爆炸都会让远端信息无法形成有效更新。所以关键不是“能不能存”,而是“梯度能不能把关联度送回去”。

为何常见:只看了前向状态 h_t,忽略了反向传播才是决定哪些信息能被学习到的瓶颈。

误区有人觉得 BPTT 是某种全新的并行训练算法,和传统反向传播毫无关系。

正解从数学上讲,链式法则、梯度逐层回传都和普通反向传播一致;区别仅在于 RNN 各时间步共享参数,并且要对不同时间步的同一参数梯度求和。可以说 BPTT 是“反向传播在时间维度上加了个展开按钮”。

为何常见:RNN 名字听起来特殊,加上“through time”,容易让人以为这是一套完全独立的训练技术。

误区有些人认为,梯度范数一超阈值就被改数值,方向也变了,会让模型学到错误的东西。

正解标准的全局范数裁剪只乘一个正标量,不改变梯度向量方向,只改变步长。它避免了参数被一个异常大头带飞,是一种轻量但有效的保护机制,不是瞎改方向。

为何常见:把“改数值”等同于“改方向”,忽略了缩放因子的正数属性和方向不变性。

误区觉得 LSTM/GRU 门控能打开一条常数梯度通道,所以任何超长序列都能轻松训练。

正解门控确实提供了近似恒等的切向路径,但输入门和输出门仍可能与遗忘门配合出复杂非线性;过长的序列依然有信息容量、训练延迟和数值稳定问题。门控是缓解,不是免死金牌。

为何常见:把“缓解梯度消失”误听成“彻底解决梯度问题”。

面试题

Q1. BPTT 把循环网络“按时间展开”后,网络结构变成什么?

单选题 · easy

  • A.一张多层前馈式计算图← 正确答案
  • B.一组完全独立的分类器
  • C.一个没有梯度的编码器
  • D.一张随机初始化的哈希表

答案:A。BPTT 的关键一步是把循环的时间依赖摊开成多层前馈结构,再沿用反向传播;它不是独立分类器,也没有去掉梯度。

Q2. BPTT 的核心数学工具是什么?

单选题 · easy

  • A.链式法则求偏导数← 正确答案
  • B.泰勒级数做近似
  • C.傅里叶变换算频谱
  • D.蒙特卡洛做采样

答案:A。反向传播的本质就是复合函数求导,BPTT 只是把链式法则沿时间轴使用,因此链式法则是核心。

Q3. 梯度爆炸的一个典型训练现象是什么?

单选题 · mid

  • A.损失突然变成 NaN← 正确答案
  • B.损失恒等于零
  • C.梯度全部消失
  • D.参数量不断变大

答案:A。梯度爆炸会让参数更新幅度过大,损失曲线剧烈震荡甚至溢出为 NaN;参数量不会训练中变大。

Q4. 梯度消失会让 RNN 出现哪些问题?

多选题 · mid

  • A.早期时间步学不到信号← 正确答案
  • B.权重更新永远等于零
  • C.长程依赖难以被捕捉← 正确答案
  • D.所有梯度都变成无穷大

答案:AC。远端梯度被连乘稀释后,早期时间步几乎得不到有效更新,长程依赖难以学习;但具体梯度未必严格为零,更不是无穷大。

Q5. 梯度裁剪对超过阈值的梯度做了什么?

单选题 · hard

  • A.等比缩小到安全范数← 正确答案
  • B.反向变为负梯度
  • C.随机丢弃一部分维度
  • D.换成单位范数零向量

答案:A。标准裁剪将梯度乘以 θ/∥g∥,只改变长度不改变方向;它不是反向、丢弃维度或变成零向量。

Q6. 遗忘门取值为 1 时,最接近哪种行为?

单选题 · mid

  • A.完整放行上一段记忆← 正确答案
  • B.彻底删除上一段记忆
  • C.只保留当前输入信息
  • D.让梯度方向完全翻转

答案:A。遗忘门接近 1 时,旧细胞状态几乎原样保留,梯度路径也接近恒等,这正是门控缓解梯度消失的原因。

Q7. 在展开的 RNN 中,所有时间步共享什么?

单选题 · mid

  • A.同一个权重矩阵← 正确答案
  • B.同一个损失标签
  • C.同一个输入序列
  • D.同一个输出结果

答案:A。RNN 参数共享,W、U、b 在不同时间步重复使用;输入、输出和损失则随位置变化。

Q8. 为什么长序列会让 BPTT 变得危险?

多选题 · hard

  • A.展开层数随长度线性增加← 正确答案
  • B.连乘项可能指数级放大← 正确答案
  • C.权重矩阵必然不可逆
  • D.连乘项可能指数级缩小← 正确答案

答案:ABD。序列越长,展开层数越多,反向传播连乘项既可能指数衰减也可能指数爆炸;权重矩阵是否可逆不是问题的根源。

Q9. 全局范数裁剪时,两个方向相同但范数不同的梯度都超过阈值,裁剪后两者范数会怎样变化?

单选题 · extreme

  • A.两者都被压到同一水平← 正确答案
  • B.两者差距被继续放大
  • C.较大者方向翻转成较小者
  • D.较小者被直接清零

答案:A。全局裁剪按 θ/∥g∥ 缩放,因此超过阈值的样本最终范数都等于 θ,样本间的相对大小信息会丢失;但方向不会被翻转。

Q10. 门控机制缓解长程梯度问题的原因是什么?

多选题 · easy

  • A.允许记忆沿近恒等路径传递← 正确答案
  • B.确保每个梯度都永远为零
  • C.遗忘门可以打开一条捷径← 正确答案
  • D.把网络结构改成完全前馈

答案:AC。遗忘门接近 1 时,细胞状态更新近似于 c_t ≈ c_{t-1},给梯度提供了一条近恒等捷径;它并不能保证梯度非零,也没有把 RNN 变成前馈网络。