【transformer前置篇-02】深入理解RNN的底层原理
深入理解RNN的底层原理
如需转载,请附上链接:https://zhenghuisheng.blog.csdn.net/article/details/161878085
一、深入理解RNN的底层原理
上一篇我们讲完了《神经网络入门篇》,知道了神经网络的核心价值在哪儿:它不是靠程序员一条条写规则,而是通过大量数据,自动学习输入和输出之间的规律。从神经元、权重、偏置、激活函数,到前向传播、反向传播、梯度下降,整条链路都串起来了。

这一篇继续往下走。在 Transformer 出现之前,AI 圈有一个非常重要的阶段性答案,用来处理"一句话"、“一段语音”、"一串用户行为"这种带顺序的数据,它就是今天的主角:RNN(循环神经网络)。
1. 回顾上篇前馈神经网络
上一篇的结论一句话就能概括:前馈神经网络,**本质上就是一个通过大量数据、不断微调参数,最终学会正确预测的过程。核心就两件事:微调和大量训练数据。**他的一个的计算公式长这样:
输出 = 激活函数(x1·w1 + x2·w2 + ... + xn·wn + 偏置)
假设我们在招一个AI 算法工程师,想用一个神经网络来给每个候选人打分。一开始权重是拍脑袋定的(真实训练里是随机初始化,思路一样)
| 特征 | 初始权重 |
|---|---|
| 会 Python | 10 |
| 会机器学习 | 8 |
| 会前端 | 4 |
| 会唱歌 | 4 |
| 会跳舞 | 4 |
| 整体门槛(偏置) | -10 |
按这个权重算一遍,会唱歌跳舞会前端但是不会python和机器学习的的候选人也能拿到不低的分数。显然不合理——但模型一开始并不知道。然后我们给模型喂一批真实数据:过去招进来的人里,谁表现好、谁表现一般。模型跑一轮后会发现:
- 真正干得好的,几乎都是机器学习基础扎实的;
- 会不会唱跳,和工作表现完全没关系。
于是模型通过大量的样本,通过预测值和真实值的对比,以及正向反馈和逆向反馈,将自己把权重往这个方向调:
| 特征 | 调整后权重 |
|---|---|
| 会 Python | 15 |
| 会机器学习 | 20 |
| 会前端 | 2 |
| 会唱歌 | 接近 0 |
| 会跳舞 | 接近 0 |
| 偏置 | -15 |
整个过程,没有程序员手动写过一句"唱歌不重要"的规则。模型是从数据里自己学出来的。这样会唱歌跳舞会前端的人只能拿到2分,远小于15,肯定是预测不能通过面试,减小误差。其核心实现如下图

其训练本质就是预测 → 对比 → 反向传播,输入进来 → 一次性算完 → 输出结果 → 完事。每次预测都是从零开始,它不会记得上一次看过什么,也分不清"哪个信息先发生、哪个后发生"。
比如判断判断一张图片是不是猫,判断一个用户是不是高风险,判断一封邮件是不是垃圾等,基本一次性读完就行;但是遇到词和词有先后顺序等需要按顺序展开的,就会出现问题。比如以下例子
- “我不觉得这个产品好” 和 “我觉得这个产品不好”——一个"不"字的位置不同,语气从"有保留"变成"直接差评";
- 小明把球给小刚,因为他跑得快"——这个"他"是谁?要回头看前半句才知道。
其缺陷主要就是两个,一个是没有记忆,一个是分不清顺序
2. RNN 的核心原理
2.1 RNN 是什么?
RNN,全称 Recurrent Neural Network,中文叫循环神经网络。它和普通前馈网络最大的区别,可以用一句话概括:普通前馈网络只把输入往前传, RNN 会把上一步形成的“记忆”继续传给下一步。。
前馈网络是看一眼快照,就下结论;RNN 是按顺序看一段录像,边看边理解。你可以把 RNN 想象成一个正在读书的人:他不是把整本书拍成照片一次性塞进脑子,而是一页一页读,读的时候脑子里会持续更新对剧情的理解。
如上图所示,每一不执行前会形成一个初始记忆h1,携带记忆h1一直往下一步走,每走完一步,都会将记忆不断的更新,直到更新成hn,然后形成最终的理解ht。

2.2 RNN 为什么叫"循环"?
第一次听到"循环神经网络"这个名字,写代码出身的同学很容易下意识联想到 while(true) 那种死循环。但 RNN 这里的"循环"完全不是这个意思。它真正想表达的是:同一个 RNN 单元被沿着时间轴展开,每一个时间步都复用同一套参数。
我们用一句话拆开来看:我/喜欢/你。它在 RNN 里的处理过程是这样的(忽略权重版):
- 第1步:输入
x1 = "我",由于前面什么都还没读过,通常会先给一个初始隐藏状态h0,这一步得到隐藏状态h1 - 第2步:输入
x2 = "喜欢",同时把上一步的h1一起送进同一个 RNN 单元,得到新的h2 - 第3步:输入
x3 = "你",同时把上一步的h2送进去,得到h3
抽象成统一的形式,每一步其实都在做同一件事(暂时先忽略权重):
x1 → h1
x2 + h1 → h2
x3 + h2 → h3
...
x_t + h_{t-1} → h_t
关键点在于:每一步用的都是同一个 RNN 单元、同一套权重 W_x、W_h、b。不是每个时间步都新建一个网络,而是同一个网络在时间维度上被反复跑了 N 次。
类似于 Java,可以这样类比:RNN 单元就像一个有状态的对象,处理序列就是不停调用它的 step(x_t, h_{t-1}) 方法。每次调用都把上一次的输出 h_{t-1} 继续传进来;对象本身(参数 W_x、W_h、b)从头到尾只有一份。

就是每一步都会又一个输入值x,然后会将这一步的输入值x的总结h记忆携带给下一步,然后重复携带,最终通过计算得出结论
// w1和w2两个权重值不相等
f(x) = w1x + w2h + b
// 最终计算公式如下
h_t = tanh(W_x x_t + W_h h_{t-1} + b_h)
2.3 隐藏状态(记忆h)是什么?
隐藏状态(hidden state) 的严谨说法是:它是 RNN 在当前时间步对前文信息形成的一份压缩记忆。它会尽量保留对后续判断有用的信息,但并不保证完整记住所有历史内容。
它更像是一份不断被覆盖、融合、更新的上下文摘要:每来一个新输入,旧记忆会被 W_h 重新塑形一次,和新信息相加后再过一次激活函数,压回同样大小的向量里继续往下传。
把这几点合起来看,RNN 的本质其实可以浓缩成一句话:
状态在时间上流动。
h1传给h2,h2传给h3……整句话的理解,就是这条隐藏状态链一路被新输入推着往前走、不断更新的结果。模型对一句话的最终判断,并不是某一步突然"看懂"的,而是这条状态流在最后一步呈现出来的样子。

但流动是有代价的,固定大小的隐藏状态要承担越来越多的历史信息,越早出现的内容,被覆盖和稀释的次数就越多。在短序列里这套机制工作得相当不错;一旦序列拉长到几十步、上百步,原始 RNN 很难稳定记住长距离信息——前面提到的内容会在隐藏状态里慢慢被稀释掉。这就是 RNN 后面会遇到的"长距离依赖问题",也是 LSTM 引入门控机制的直接原因,再往后 Transformer 干脆放弃这条状态链、改用注意力机制,思路也是从这里一路演进过来的。

2.4 RNN 的基本计算流程(了解即可)
这一节不堆推导,但要把 RNN 一步的计算讲到"看得见、摸得着"的程度。先看最抽象的形式:
h_t = f(x_t, h_{t-1}) //每一步的记忆值
y_t = g(h_t) //每一步的输出值
它本质上是一整套神经网络计算——里面有权重矩阵(压缩记忆)、偏置项(阈值)、激活函数,和我们上一篇讲的前馈网络一层做的事情是同一类。换句话说,RNN 没有发明全新的运算,它仍然是神经网络计算,只是在原有的"加权求和 + 激活"之外,额外把上一步的隐藏状态 h_{t-1} 也作为输入塞了进来。
符号一个个解释:
x_t:当前时间步的输入向量。如果是 NLP 任务,这一步是先把词通过 embedding(粗略理解为"把一个词查表换成一个固定长度的向量",比如 256 维浮点数)转成向量,再交给 RNN——模型从头到尾处理的都是向量,而不是汉字本身;h_{t-1}:上一步留下的隐藏状态,也就是"前文压缩出来的记忆";h_t:当前时间步更新后的隐藏状态;y_t:当前时间步的输出(不是每一步都要输出,看任务,比如句子分类只在最后一步输出);f、g:本质上各自就是一小段神经网络计算(权重 + 偏置 + 激活函数)。
抽象形式看完,下面是**经典 RNN(vanilla RNN)**实际用的具体形式:
h_t = tanh(W_x · x_t + W_h · h_{t-1} + b)
这一行就是 RNN 的"心脏"。逐项拆开:
W_x:作用在当前输入上的权重矩阵(输入参数的权重值);W_h:作用在上一步隐藏状态上的权重矩阵(当前步骤的权重值);b:偏置向量(阈值);tanh:激活函数,把数值压到 -1 到 1 之间,给整个变换加上非线性;h_t:经过这一轮加权 + 激活后,当前时间步的新隐藏状态。
h_{t-1} 不是被"原样"带过来的,它会先经过 W_h 这个权重矩阵重新加权一次,得到 W_h · h_{t-1}——也就是说,上一步的隐藏状态并不会被直接丢弃,但也不会被原样保留,而是被 W_h 重新塑形后,再参与当前时间步的计算。这一点后面讲梯度消失时会反复用到,先记下:信号在时间步之间穿越,每过一步都要被 W_h 乘一次。
把整个加权和拆开看,更直观一点:
W_x · x_t这一项代表"当前输入对新状态的影响"——这一步读到的新信息,给h_t注入新的成分,就是在上一次的记忆中注入当前文本的记忆,最后形成新的h_t;W_h · h_{t-1}这一项代表"历史上下文对新状态的影响"——之前积累的记忆,经过重新加权后继续作用在当前判断上;- 把这两项相加,再加上偏置
b,最后过一次tanh,就得到新的隐藏状态h_t。
注意它们是相加,不是拼接、不是相乘。一加就把"当前输入"和"历史记忆"放进了同一个加权和里,相当于让两路信号在同一个空间里融合,然后经过非线性激活,再压回固定大小的向量继续往下传。
也就是说,RNN 的核心其实不是"存档",而是"不断改写状态"。h_t 不是把旧记忆原样拷一份留下来,而是在旧记忆的基础上重新整理一次当前理解——每读一个新词,模型都会把"已经知道的"和"刚刚读到的"重新揉一遍,得到一份新的上下文。换句话就是:每一步的输入除了本身的输入值和权重之外,还会携带对前一步记忆的总结一起计算,然后二者结合之后再将总结的记忆带给下一步,不会将前文具体的往下带,而是将总结的记忆往下携带

2.5 RNN电商场景案例
前面讲了 RNN 的概念和流程,但全是定性描述。这一节我们用真实数字把一个最简化的 RNN 跑一遍,让你看到每一步到底在算什么。还是和之前一样在电商系统中预测用户是否会下单,假设我们有一个极简版 RNN,用来判断用户这次会话的购买意图。用户在电商 App 上产生了 3 步行为:
浏览商品 → 加入购物车 → 领取优惠券
模型读完这 3 步之后,需要输出一个"购买意图分数",分数越高代表越可能下单。为了让计算可以手算跑通,做几个简化设定:
| 项目 | 设定 |
|---|---|
| 输入向量维度 | 长度为 2(真实模型一般是几十到几百维) |
| 隐藏状态维度 | 长度为 2 |
| 激活函数 | tanh(把数值压到 -1 ~ 1) |
3 个行为先编码成数字向量。在真实系统里,行为通常会先映射成 embedding 向量,这些 embedding 参数会随着训练不断更新(原理和前馈网络里调权重一样,只不过那边调的是 W 矩阵,这边调的是每个行为对应的向量)。这里为了让你能手算把 RNN 的计算流程跑清楚,直接人为指定三个 2 维向量,不代表真实的业务含义:
| 时间步 | 行为 | 输入向量 |
|---|---|---|
| t1 | 浏览商品 | x1 = [1.0, 0.0] |
| t2 | 加入购物车 | x2 = [0.0, 1.0] |
| t3 | 领取优惠券 | x3 = [1.0, 1.0] |
RNN 每一步的计算公式如下,上面有详细的讲解每个值的意义:
h_t = tanh(W_x · x_t + W_h · h_{t-1} + b)
公式里出现了三个参数 W_x、W_h、b,它们各自分工不同:
| 参数 | 形状 | 作用 |
|---|---|---|
W_x |
2 × 2 矩阵 | 当前输入 x_t 如何影响隐藏状态 |
W_h |
2 × 2 矩阵 | 历史记忆 h_{t-1} 如何影响当前状态 |
b |
长度 2 向量 | 整体偏移量 |
假设这套 RNN 已经训练好了,参数如下:
输入权重矩阵 W_x
W_x = [[0.5, 0.0],
[0.0, 0.5]]
隐藏状态权重矩阵 W_h
W_h = [[0.8, 0.0],
[0.0, 0.8]]
偏置向量 b
b = [0.0, 0.0]
偏置直接设成 0,是为了让手算更清晰,不影响理解流程。同时
W_x、W_h都被刻意设成对角矩阵——两个维度互不交叉,每一维独立计算。真实训练出来的矩阵一般是稠密的,但那样手算会变成噩梦。b这里可以先粗略理解成前馈神经网络里的偏置项,作用是给加权和做一次整体偏移或门槛修正(数学上等价于"负阈值",但今天严谨地说就叫偏置)。
初始隐藏状态 h0
h0 = [0.0, 0.0]
序列还没开始读,记忆里什么都没有。
第 1 步:读"浏览商品"
当前输入
x1 = [1.0, 0.0]
上一步记忆
h0 = [0.0, 0.0]
当前输入贡献:W_x · x1
W_x · x1 = [0.5×1.0 + 0.0×0.0,
0.0×1.0 + 0.5×0.0]
= [0.5, 0.0]
历史记忆贡献:W_h · h0
W_h · h0 = [0.8×0.0 + 0.0×0.0,
0.0×0.0 + 0.8×0.0]
= [0.0, 0.0]
合并结果(加上偏置)
[0.5, 0.0] + [0.0, 0.0] + [0.0, 0.0] = [0.5, 0.0]
过激活函数 tanh
h1 = [tanh(0.5), tanh(0.0)] = [0.46, 0.0]
得到新隐藏状态
h1 = [0.46, 0.0]
这一步发生了什么?
- 历史记忆为空,
W_h · h0全是 0,对当前结果没有贡献; - 当前输入
x1通过W_x直接写入隐藏状态的第一维; - 第二维仍然为 0,因为
x1在第二维上没有信号; - 隐藏状态从
[0, 0]演化成[0.46, 0],购买意图开始形成。
其详细的计算流程如下,关于矩阵的算法页、也贴在旁边

第 2 步:读"加入购物车"
当前输入
x2 = [0.0, 1.0]
上一步记忆
h1 = [0.46, 0.0]
当前输入贡献:W_x · x2
W_x · x2 = [0.5×0.0 + 0.0×1.0,
0.0×0.0 + 0.5×1.0]
= [0.0, 0.5]
历史记忆贡献:W_h · h1
W_h · h1 = [0.8×0.46 + 0.0×0.0,
0.0×0.46 + 0.8×0.0]
= [0.37, 0.0]
合并结果(加上偏置)
[0.0, 0.5] + [0.37, 0.0] + [0.0, 0.0] = [0.37, 0.5]
过激活函数 tanh
h2 = [tanh(0.37), tanh(0.5)]
= [0.35, 0.46]
得到新隐藏状态
h2 = [0.35, 0.46]
这一步发生了什么?
- 上一步的"浏览"信号没有被丢弃:
h1经过W_h加权,再过 tanh 重新塑形,留下 0.35 在第一维; - 当前的"加购"信号通过
W_x写入第二维,得到 0.46; - 历史记忆和当前输入在同一个隐藏状态里融合。这里因为
W_x、W_h都是对角矩阵,看起来像是不同信号分别落在不同维度上;真实模型里各维度通常是混合的,不能简单理解为"某一维就等于某个行为"; - 可以近似理解为:
h2同时带有"浏览过"和"加购了"两条上下文。
其详细流程可以看下图

第 3 步:读"领取优惠券"
当前输入
x3 = [1.0, 1.0]
上一步记忆
h2 = [0.35, 0.46]
当前输入贡献:W_x · x3
W_x · x3 = [0.5×1.0 + 0.0×1.0,
0.0×1.0 + 0.5×1.0]
= [0.5, 0.5]
历史记忆贡献:W_h · h2
W_h · h2 = [0.8×0.35 + 0.0×0.46,
0.0×0.35 + 0.8×0.46]
= [0.28, 0.37]
合并结果(加上偏置)
[0.5, 0.5] + [0.28, 0.37] + [0.0, 0.0] = [0.78, 0.87]
过激活函数 tanh
h3 = [tanh(0.78), tanh(0.87)]
= [0.65, 0.70]
得到新隐藏状态
h3 = [0.65, 0.70]
这一步发生了什么?
h2继续通过W_h向后传递,把"浏览 + 加购"的历史信息带到h3里;- 当前输入
x3 = [1, 1]在两个维度上都注入了信号; - 历史信号和当前信号在每个维度上分别叠加,再过 tanh;
h3的两个维度都被推到了较高水平,形成了一份完整的上下文摘要。
最终输出:购买意图分数
读完整条序列后,用最后一步的隐藏状态 h3 来做最终判断。这里我们把输出层做到最简,就是把 h3 的两个维度加权求和:
购买意图分数 = 0.6 × h3[0] + 0.4 × h3[1]
= 0.6 × 0.65 + 0.4 × 0.70
= 0.39 + 0.28
= 0.67
分数 0.67,比较高,模型判断:这个用户购买意图较强。
真实任务里,输出层通常不会这么裸。回归任务一般会再过一次线性层,二分类会接
sigmoid把分数压到 0~1,多分类会接softmax输出每个类别的概率。这里为了让你看清"隐藏状态怎么变成最终结果",先简化处理。
对比:如果用户只浏览了就走了呢?
假设另一个用户的行为序列只有一步:
浏览商品 → 离开
那模型只跑到 h1 = [0.46, 0.0] 就结束了:
购买意图分数 = 0.6 × 0.46 + 0.4 × 0.0
= 0.28
分数 0.28,明显低很多。模型判断:这个用户购买意图不强。
把整个过程串起来看
| 时间步 | 行为 | 输入 | 上一步记忆 | 当前记忆 | 购买意图趋势 |
|---|---|---|---|---|---|
| t1 | 浏览商品 | [1.0, 0.0] |
[0.0, 0.0] |
[0.46, 0.0] |
弱 |
| t2 | 加入购物车 | [0.0, 1.0] |
[0.46, 0.0] |
[0.35, 0.46] |
中等 |
| t3 | 领取优惠券 | [1.0, 1.0] |
[0.35, 0.46] |
[0.65, 0.70] |
较强 |
画成图:
x1=[1,0] x2=[0,1] x3=[1,1]
"浏览" "加购" "领券"
│ │ │
▼ ▼ ▼
┌────┐ h1 ┌────┐ h2 ┌────┐ h3
│RNN │──────▶ │RNN │──────▶ │RNN │──────▶ 购买意图=0.67
└────┘[.46,0] └────┘[.35,.46]└────┘[.65,.70]
▲ ▲ ▲
h0=[0,0] h1=[.46,0] h2=[.35,.46]
最终购买意图分数聚合流程可以参考下图

通过这个例子可以得到以下结论:
- 隐藏状态确实在传递:
h1 → h2 → h3一路向后流动,每一步都基于前一步的结果继续计算,不是各自孤立地在算; - 历史信息确实参与了后续计算:
h2的算式里出现了W_h · h1,h3的算式里出现了W_h · h2,前文信息会以压缩后的形式继续参与后续计算,但不会原样完整保留; - 同一套参数被反复复用:从 t1 到 t3,三步用的都是同一份
W_x、W_h、b,没有为某个时间步单独建一份网络; - 当前输入和历史状态共同决定新状态:每一步的
h_t都是W_x · x_t和W_h · h_{t-1}这两路信号融合后再过 tanh 得到的,缺一不可; - RNN 的本质是状态流,不是状态存档:模型并没有把每一步的输入原样存下来;它做的是不断在旧状态上叠加新输入、再压回固定大小的向量继续往下传。
2.6 同样的行为,顺序不同,含义就不同
上面已经用数字把电商场景跑过一遍了,这一节我们换个角度:抛开具体计算,看看"顺序"本身在用户行为建模里有多重要。
先看两条用户的会话:
- 用户 A:
浏览商品 → 加入购物车 → 领取优惠券 → 下单 - 用户 B:
领取优惠券 → 浏览商品 → 离开页面
我们从"是否发生过"这个维度看一下:
| 维度 | 用户 A | 用户 B |
|---|---|---|
| 是否浏览过商品 | ✅ | ✅ |
| 是否领过券 | ✅ | ✅ |
如果用前馈网络来建模,通常会把会话压扁成这种"是否发生过"的字段:是否浏览、是否加购、是否领券……特征工程做到这一步,模型看到的就只剩"什么动作出现过"。
但这两条序列描述的根本不是同一种用户:
- 用户 A 是典型的购买路径:被商品吸引 → 决定下单 → 顺手领个券再付款;
- 用户 B 更像羊毛党或纯薅券型用户:先冲着券来 → 看了一眼商品发现不感兴趣 → 直接走人。
把"先后"这个维度抹掉以后,前馈网络看这两条会话长得几乎一样,自然没法区分。RNN 不一样。它是按时间步一步步读的,隐藏状态会随着行为顺序一路演化下来:
- 用户 A 的隐藏状态序列大致演化成"浏览 → 浏览+加购 → 浏览+加购+领券 → 完整购买路径",最终的
h_t承载的是一段比较强的购买意图; - 用户 B 的隐藏状态序列则是"先领券 → 领券+浏览 → 领券+浏览+离开",最终的
h_t承载的更像是一段"低意图、薅券型"的会话。
用户做了同样的几件事,但因为做的顺序不一样,RNN 在最后一步给出的隐藏状态可以差得很远,下游任务(比如预测下单概率、识别羊毛党)也就能给出不同的结论。
也就是说:
RNN 读到的不是"发生过哪些动作",而是**“这些动作以什么顺序、什么节奏发生”**。在很多任务里,顺序本身就是信号。
文本里词的先后、语音里帧的先后、系统日志里事件的先后、时间序列里数值的先后,本质上都是同一回事——RNN 把"顺序"这件事第一次系统性地纳入了神经网络。

3. RNN 是怎么训练的?
讲完 RNN “怎么前向算”,自然要问一句:这一套 W_x、W_h、b 是怎么训练出来的? 别担心,思路和上一篇几乎一模一样,只是多了一点点"时间"的味道。
3.1 核心训练流程
上一篇讲过,神经网络训练的核心循环就四步:
| 步骤 | 名字 | 在 RNN 里做的事 |
|---|---|---|
| ① | 前向传播 | 用当前参数把整条序列从头跑到尾,得到最终输出 |
| ② | 计算损失 | 把模型输出和真实标签对比,算出误差大小 |
| ③ | 反向传播 | 误差沿计算图一路回传,给每个参数算一份"应该怎么调"的梯度 |
| ④ | 梯度下降 | 沿着让损失变小的方向,把参数挪一小步 |
RNN 本质上还是神经网络,训练流程完全一样:
预测 → 计算损失 → 反向传播 → 调整参数 → 换下一条样本 → 继续循环。
唯一的不同在于:RNN 处理的是一整条序列,而不是一个孤立的样本。所以反向传播时,除了要在"层与层"之间回传,还要在"时间步与时间步"之间回传——多了一条"沿时间往回看"的轴。这条轴上发生的事情,就是下一节要讲的 BPTT。
3.2 BPTT沿着时间展开的反向传播
RNN 的反向传播有个专门的名字:
Backpropagation Through Time(BPTT),中文叫"沿时间的反向传播"。
听起来唬人,但本质并不神秘:
BPTT 不是一种全新的训练方法。它只是把 RNN 沿时间展开成一个很长的网络,然后在这个展开后的网络上做普通的反向传播。
普通神经网络的反向传播是"从输出层往输入层,一路回头看哪里错了";RNN 的反向传播则是"从最后一个时间步往前,一路回头看前面哪些词、哪些行为对这次预测产生了多大影响"。前者沿"层"展开,后者沿"时间步"展开——用的还是同一套链式法则,没有引入任何新的训练范式。
一个电商例子:误差是怎么往前追的
沿用前面的电商场景,假设输入序列是:
浏览商品 → 加入购物车 → 领取优惠券 → 下单
模型读完整条序列后预测"不会下单",但真实标签是"下单"。这时候 BPTT 做的事情大概是:
- 最后一步算出误差:预测和真实标签对不上,得到一个 loss;
- 往前一步:检查"下单"这个时间步的隐藏状态有没有充分保留前面累积的购买意图信号,给当前步的参数算一份梯度建议;
- 再往前:检查在读"领券"时,前一步累积到隐藏状态里的"加购"信息有没有被有效带过来,再给参数算一份梯度建议;
- 再往前:检查在读"加购"时,隐藏状态里"浏览"留下的信号是否被有效保留;
- 一直追溯到序列开头:每一个时间步都参与一次梯度计算。
注意上面所有步骤里都没有"模型意识到 / 模型觉得"这种说法——BPTT 做的事其实非常机械:沿计算图一路回传,按链式法则算偏导。所谓"前面的信号是否被保留",物理上对应的就是隐藏状态在这一步累积了哪些数值,以及这些数值经过 W_h 重新加权后还剩多少。
关键性质:参数是共享的,梯度要"汇总"
这里有一个最容易被读者误解的点,必须单独强调:
RNN 每个时间步用的是同一套参数
W_x、W_h、b。 所以 BPTT 回传时,每个时间步会各自给这同一组参数贡献一份梯度,这些梯度最后累加到一起,再统一更新一次。
也就是说,并不是每个时间步各自更新一份参数,更不是给每个时间步单独建一套独立权重。这一点和普通深层网络区别很大——普通深层网络每一层有自己独立的 W 和 b,每层的梯度只更新自己那一份;RNN 是同一组参数被反复用了 T 次,对应的 T 份梯度要先求和再下降。
为什么必须共享参数? 因为序列的长度是不固定的:一句话可能 5 个词,也可能 50 个词;一次会话可能有 3 个行为,也可能有 30 个。如果每一步都用一套独立参数,模型就只能处理固定长度的序列,而且每加长一步就要多学一组权重——既不现实,也学不动。参数共享是 RNN 能处理变长序列的根本前提,也是"循环"二字最实在的含义。
把整条流程画出来
可以这样想象 BPTT 的工作方式(建议在脑子里画一张时间展开图:横轴是时间步,纵轴上每一格是一个 RNN 单元,所有格子共享同一套 W_x、W_h、b):
前向:x1 ──▶ h1 ──▶ h2 ──▶ h3 ──▶ y ──▶ loss
│
反向: ▼
▲ ▲ ▲ ▲ 误差
│ │ │ │
梯度₁ 梯度₂ 梯度₃ 梯度₄
└──────────┴─────────┴─────────┘
│
▼
梯度₁ + 梯度₂ + 梯度₃ + 梯度₄
│
▼
更新同一组 W_x / W_h / b
每个时间步都给同一组参数贡献一份梯度,最后求和、统一下降——这就是 BPTT 区别于普通反向传播的全部"特殊性"。
用一张表总结:RNN 训练 vs 前馈网络训练
| 对比项 | 前馈网络训练 | RNN 训练(BPTT) |
|---|---|---|
| 反向传播方向 | 沿"层"从输出层往输入层 | 沿"时间步"从最后一步往前 |
| 参数是否共享 | 每一层各有各的参数 | 所有时间步共享同一组 W_x、W_h、b |
| 梯度怎么处理 | 每层梯度更新自己的参数 | 各时间步梯度求和后统一更新共享参数 |
| 处理的样本 | 单条样本(一个特征向量) | 一整条序列(多个时间步) |
| 用的算法 | 链式法则 + 反向传播 | 同样的链式法则,只是沿时间展开 |
一句话总结 BPTT:
把 RNN 按时间展开成一个很长的网络,再用上一篇那套反向传播跑一遍;不同时间步算出来的梯度,会汇总到共享参数上一次性更新。
具体的偏导推导这里就不展开了,记住这个直觉就够用。
不过这里其实埋着一个隐患:误差从最后一步往前回传时,每跨一个时间步都要乘一次 W_h。序列短的时候没事,可一旦时间步拉到几十、上百,这条乘法链上的数值会变得非常不稳定——这正是后面 RNN "记不住远距离信息"的根源所在,留到第 6 章再展开。
4. RNN 解决了什么?
把前面讲的东西拎出来,RNN 相对于普通前馈网络,带来了三件事。

4.1 它让神经网络可以按顺序读数据
前馈网络更适合处理已经整理好的固定长度特征——如果不额外设计结构,它并不天然理解时间顺序。RNN 则是"一步一步把序列读完,每读一步都基于前一步的隐藏状态继续往下算"。这意味着:
- 文本可以按"词的先后"来处理;
- 语音可以按"帧的先后"来处理;
- 用户行为可以按"动作发生的时间"来处理;
- 股价、日志、传感器数据,也都能顺着时间看。
不能说 RNN 就此完美解决了序列问题,但至少在结构上,它第一次让神经网络具备了按时间步处理序列的能力——这是结构层面的一次大升级。
4.2 它引入了隐藏状态,让模型拥有了短期记忆
隐藏状态 h_t 是 RNN 最核心的创新。它像一个随时间滚动更新的小本本,把前面读过的信息压缩成一份摘要,传给下一步。和前面 2.3 节强调过的一样:它不是完整记录,而是一份不断被覆盖、融合、更新的压缩摘要。
因此:
- 读"他"的时候,能带着前文里"小明"和"小刚"的信息;
- 读"下单"的时候,能带着前面"浏览、加购、领券"的上下文;
- 读一段语音的第 100 帧时,能带着前 99 帧积累下来的语境。
这份"短期记忆",就是 RNN 真正让神经网络具备"理解序列"的关键。
4.3 它拓宽了神经网络的应用场景
有了"顺序"和"记忆"这两个能力,RNN 在一段时间里,几乎是序列任务的事实标配:
- 文本分类:情感分析、意图识别;
- 语言模型:根据前文预测下一个词;
- 机器翻译:把一句中文变成一句英文;
- 语音识别:把一段波形识别成一串文字;
- 时间序列预测:股价、销量、温度走势;
- 用户行为预测:下一步会不会点击、会不会下单、会不会流失。
可以毫不夸张地说:
在 Transformer 大规模流行之前,RNN 及其改进版 LSTM、GRU,长期是序列建模任务里的主流方案。
不过,这份"顺序 + 记忆"的能力是有代价的。
RNN 必须按时间步一步一步串行处理:h2 要等 h1 算完,h3 要等 h2 算完,没法跳过、没法并行。同时,所有历史信息都要被压进同一个固定大小的隐藏状态里——序列短的时候没事,序列一长,早期的信息就容易被一层层覆盖、稀释掉。
这两条限制——串行计算 + 长距离信息容易被冲淡——也就自然引出了后面的故事:
- LSTM、GRU 在隐藏状态这条链上加门控,让模型自己决定"什么该记住、什么该忘掉";
- 再后来 Transformer 干脆放弃这条状态链,改用注意力机制,把"按时间串行"也一起拆掉了。
至于这两条限制具体是怎么发作的、为什么 LSTM 能缓解、Transformer 又是怎么彻底换思路的,留到第 6 章和后面几篇专门展开。
5. RNN 和普通前馈网络的区别
最后用一张表把两者对比清楚:
| 对比项 | 前馈神经网络 | RNN |
|---|---|---|
| 数据流向 | 输入层 → 输出层,单向走完一次 | 沿时间步串行展开,每步带着上一步的隐藏状态 |
| 是否保留历史 | 不天然保留,每次预测都从零开始 | 通过隐藏状态把前文上下文持续传递下去 |
| 是否理解顺序 | 不天然理解,"先后"信息要靠人工特征塞进去 | 顺序本身就是输入的一部分,模型天然能感知 |
| 参数使用 | 每一层各有各的参数 | 同一套参数 W_x、W_h、b 在所有时间步上反复复用 |
| 适合数据 | 固定长度特征、结构化数据 | 长度可变的序列数据 |
| 典型任务 | 图片分类、结构化预测、风控打分 | 文本、语音、系统日志、时间序列、用户行为 |
| 类比 | 看一张快照 | 看一段录像 |

不过,正因为 RNN 必须按时间步一步一步算,它天然就是串行的:h2 必须等 h1 算完,h3 必须等 h2 算完,没法跳过、没法并行。这条性质同时带来了两件事:
- 好的一面:让 RNN 拥有了"顺序感"和"上下文记忆",这是它处理序列数据的根本;
- 坏的一面:训练慢、难以利用 GPU 并行,并且时间步一拉长,早期信息很容易在隐藏状态里被一层层覆盖掉,造成长距离依赖建模困难。
6. RNN 的问题痛点
RNN 很强,但它不是终点。前面几节我们埋了好几次伏笔——长序列遗忘、梯度沿时间反复相乘、串行没法并行——这一章集中收一收。这里不展开数学,也不深入 LSTM、GRU、Transformer 的细节,只把"RNN 为什么不是终点"讲清楚,给下一篇做铺垫。
6.1 长序列容易忘
RNN 的隐藏状态是固定大小的(比如 128 维)。每读一步新输入,旧记忆都要被 W_h 重新塑形,再和新信息融合一次,最后压回同样大小的向量继续往下传。
这就带来一个直接后果:越早出现的内容,被覆盖和稀释的次数就越多。
- 序列只有 3-5 步(比如一次电商会话的"浏览 → 加购 → 领券"),RNN 通常能把上下文有效带到最后一步;
- 序列拉到几百字的长文,模型就经常"忘掉开头说过什么";
- 序列拉到几千字,就更吃力了。
这不是工程实现的问题,而是原始 RNN 结构本身的限制:固定大小的容器要装下不断增长的历史信息,前面的信号必然会被反复压缩、稀释。

6.2 训练时梯度容易出问题
这个问题本质上是 6.1 的"数学版本",根源就藏在 BPTT 里。
3.2 节我们提过:误差从最后一步沿时间往回传时,每跨一个时间步,梯度都要乘一次 W_h。一条几十步、上百步的长序列,就等于把同一组 W_h 反复连乘几十、上百次。连续相乘会带来两种极端:
- 如果整条乘法链上的尺度不断缩小,梯度回传到前面的时间步时几乎为 0 → 梯度消失:早期时间步的参数收不到有效更新,模型学不到长距离依赖;
- 如果整条乘法链上的尺度不断放大,梯度会变成非常大的数值 → 梯度爆炸:一次更新就能把训练崩掉。
工程上对付梯度爆炸有梯度裁剪这种现成的招;但梯度消失更难治——它是结构层面的问题,不是补丁能彻底解决的。

6.3 串行计算效率低
RNN 的计算天然是串行的:
必须先算完 h1,才能算 h2;
必须先算完 h2,才能算 h3;
......
这条依赖关系是硬的,没法跳过。结果是:
- 一条长序列,只能从头到尾一步一步跑;
- GPU 擅长的"大规模并行矩阵运算"在序列内部基本用不上;
- 模型一大、序列一长,训练时间就被这条串行链路拖住,没法跟 GPU 数量线性扩展。
模型规模小、序列短的时候这还能忍;一旦想做"更长上下文 + 更大模型",这条短板就变成硬伤。

更多推荐

所有评论(0)