循环神经网络(二):让机器拥有“记忆”,揭开RNN与语言模型的神秘面纱
上一篇我们知道了:序列数据不能随便打乱,因为顺序本身就包含信息。
一句话、一道语音、一段股价曲线,都不是孤立点的集合,而是一条按时间或逻辑展开的链。为了处理这种数据,我们需要模型具备一种能力:
读到当前位置时,还能记得前面发生过什么。
这就是循环神经网络(Recurrent Neural Network,RNN)的核心思想。
这一篇,我们先从语言模型讲起,再一步步拆开 RNN 的“记忆”到底是什么。
一、语言模型:预测下一个 token
当你用手机输入法打字:
今天晚上吃
输入法可能会推荐:
火锅、烧烤、面条
它在做的事情就是语言模型的典型任务:
根据前面的内容,预测下一个 token 的概率分布。
也就是说,它不是只给一个答案,而是给一组可能性:
P(火锅 | 今天 晚上 吃) = 0.35
P(烧烤 | 今天 晚上 吃) = 0.20
P(面条 | 今天 晚上 吃) = 0.15
...
概率越高,说明模型越觉得这个 token 合理。
生活化理解:
- 你输入“今天晚上吃”,输入法给出三四个候选词。
- 其实它内部在做“打分”,只是把分数最高的几个展示出来。
- 这就是“概率分布”的直观形态:每个候选都有一个可能性。
下面用一个小图把过程串起来:
1. 一句话的概率怎么理解?
一句话可以看成多个条件概率连乘:
P(我 爱 深度 学习)
= P(我) x P(爱 | 我) x P(深度 | 我 爱) x P(学习 | 我 爱 深度)
再直观一点:
- “我 爱 深度 学习”这句话看起来顺,是因为每一步都“接得上”。
- 如果变成“我 爱 深度 面包”,前面虽然通顺,但最后一步突然不符合上下文,整体就别扭了。
一个句子是否自然,取决于接下来每一步的词是否符合前文。
所以语言模型既可以用来评估一句话是否像人话,也可以用来做文本生成。
二、旧方法:N-gram 为什么不够?
N-gram 是一种“只看最近 N-1 个词”的语言模型。它把句子拆成连续的词组片段(gram),用这些片段在语料中出现的频率来估计概率。
它的思想很简单:只看前面固定数量的词。
- 一元语法(Unigram):不看上下文,只看某个词本身常不常见。
- 二元语法(Bigram):只看前 1 个词。
- 三元语法(Trigram):只看前 2 个词。
比如用 bigram 时:
P(饭 | 吃)
模型只看“吃”后面经常接什么。
1. N-gram 的问题
N-gram 很直观,但问题明显。
问题一:记忆太短
如果只看前 2 个词,很多长距离信息会丢失。
比如:
我昨天在朋友推荐下买的那本书,今天终于读完了,感觉非常
预测最后一个词时,前面很长一段内容都有帮助。比如这里的“感觉非常”,通常需要结合前面的“读完了这本书”才能猜到“感动/精彩/满意”。
但 bigram 只看最近 1 个词,相当于只看“非常”前面的那一个词,几乎抓不到“这本书”“读完了”这些更早的信息,所以容易猜错。
补充一点:即使加上平滑(比如加一法),也只是缓解“没见过的组合”问题,并不能解决“记不住长距离信息”的问题。要捕捉这种长依赖,就需要更强的模型(如 RNN/LSTM/GRU)。
问题二:组合太多
如果把 N 设得很大,比如看前 10 个词,组合数量会爆炸。很多组合在训练数据里根本没出现过,概率就很难估计。
这叫 数据稀疏。
更直白地说:
- N 越大,“连续词组”的种类就越多。
- 训练语料再大,也很难把这些组合都见一遍。
- 没见过的组合就没法可靠统计概率,只能乱猜或用很粗糙的近似。
所以 N-gram 的问题不是“算不过来”,而是“数据不够覆盖所有可能组合”。
举个小例子:
假设词表只有 1000 个词,
- 用 bigram(N=2),组合数大约是 10002=1,000,0001000^2 = 1,000,00010002=1,000,000。
- 用 5-gram(N=5),组合数大约是 10005=10151000^5 = 10^{15}10005=1015。
你的语料可能只有几百万句话,远远不可能覆盖 101510^{15}1015 种组合,
于是大量 5-gram 在数据里从没出现过,这就叫“数据稀疏”。
不死记所有词组组合,而是把历史信息压缩成一个连续的向量。
这里说“连续的向量”,是因为模型不会把过去每个词原样存下来,而是用一组实数把“上下文的要点”编码进去。
这种表示是连续值(不是离散的词编号),好处是:
- 维度固定,不会随着句子变长而爆炸。
- 相似的上下文会得到相近的向量,模型更容易泛化。
- 向量可以参与矩阵运算,适合神经网络训练和优化。
这就是 RNN 的切入点。
三、普通神经网络为什么没有记忆?
先看普通神经网络。
对于每个时间步 ttt,它只处理当前输入 XtX_tXt:
Xt → 神经网络 → Ot X_t \;\rightarrow\; \text{神经网络} \;\rightarrow\; O_t Xt→神经网络→Ot
如果下一步来了 Xt+1X_{t+1}Xt+1,它就重新计算:
Xt+1 → 神经网络 → Ot+1 X_{t+1} \;\rightarrow\; \text{神经网络} \;\rightarrow\; O_{t+1} Xt+1→神经网络→Ot+1
这里的 OtO_tOt 表示第 ttt 个时间步的输出(output)。
在语言模型里,它通常是一组“下一个词”的预测分数;
如果是分类任务,它就是这一时刻的分类结果或分数。
这就像一个人每读一个字都失忆一次。读到“吃”时,他不知道前面是“今天晚上”还是“这台电脑正在”。
对序列任务来说,这显然不够用。
换句话说,普通前馈网络更像“看一帧图片做判断”,而序列任务需要“看一段录像做判断”。
四、RNN 的核心:隐状态
RNN 多出来的关键东西叫 隐状态(Hidden State),通常记作 HtH_tHt。
你可以把它理解成模型的“记忆本”。
更具体一点:
- HtH_tHt 是一个向量,长度由你设置(比如 128、256)。
- 向量里每一个数都不是“具体词”,而是“语义线索的压缩表示”。
- 起始时刻的 H0H_0H0 通常是全 0,表示“还没读任何内容”。
每读一个新 token,RNN 都会做四件事:
- 看当前输入 XtX_tXt;
- 参考上一时刻的记忆 Ht−1H_{t-1}Ht−1;
- 写出新的记忆 HtH_tHt;
- 根据新的记忆做输出 OtO_tOt。
公式是:
Ht=tanh(XtWxh+Ht−1Whh+bh) H_t = \tanh(X_t W_{xh} + H_{t-1} W_{hh} + b_h) Ht=tanh(XtWxh+Ht−1Whh+bh)
Ot=HtWhq+bq O_t = H_t W_{hq} + b_q Ot=HtWhq+bq
各个符号的含义:
- XtX_tXt:第 ttt 个时间步的输入向量(当前 token 的表示)。
- Ht−1H_{t-1}Ht−1:上一时刻的隐状态(上一步的记忆)。
- HtH_tHt:当前时刻的隐状态(更新后的记忆)。
- OtO_tOt:当前时刻的输出(预测分数或分类分数)。
- WxhW_{xh}Wxh:输入到隐状态的权重矩阵。
- WhhW_{hh}Whh:隐状态到隐状态的权重矩阵。
- WhqW_{hq}Whq:隐状态到输出的权重矩阵。
- bhb_hbh:隐状态的偏置。
- bqb_qbq:输出层的偏置。
看不懂公式也没关系,我们把它翻译成人话:
- XtWxhX_t W_{xh}XtWxh:当前输入带来的新信息;
- Ht−1WhhH_{t-1} W_{hh}Ht−1Whh:过去记忆带来的旧信息;
- 两者加起来,再经过激活函数,得到新的记忆 HtH_tHt;
- 最后用 HtH_tHt 预测输出。
RNN 真正厉害的地方就在这里:
它不是只看当前输入,而是把当前输入和过去记忆一起考虑。
再用一个展开图感受“循环”的含义:
这个图在表达三件事:
- 每个时间步都会把“当前输入 XtX_tXt”和“上一时刻记忆 Ht−1H_{t-1}Ht−1”合并,得到新的记忆 HtH_tHt。
- 同一套参数在不同时间步反复使用,所以图里是“同一个结构在时间上展开”。
- 记忆会一路向后传递:H0→H1→H2→H3H_0 \rightarrow H_1 \rightarrow H_2 \rightarrow H_3H0→H1→H2→H3,让后面的步骤能“记住”前面的信息。
1. 为什么叫“循环”?
因为同一个网络结构会在每个时间步重复使用。
X1+H0 →H1X2+H1 →H2X3+H2 →H3… \begin{aligned} X_1 + H_0 &\;\rightarrow H_1 \\ X_2 + H_1 &\;\rightarrow H_2 \\ X_3 + H_2 &\;\rightarrow H_3 \\ \ldots \end{aligned} X1+H0X2+H1X3+H2…→H1→H2→H3
把参数写出来,其实每一步都在做同一件事:
Ht=tanh(XtWxh+Ht−1Whh+bh) H_t = \tanh(X_t W_{xh} + H_{t-1} W_{hh} + b_h) Ht=tanh(XtWxh+Ht−1Whh+bh)
这里的参数 WxhW_{xh}Wxh、WhhW_{hh}Whh、WhqW_{hq}Whq 是共享的。无论句子有多长,RNN 都用同一套参数一遍遍处理序列。
这叫 参数共享。
参数共享的好处是:
- 不管句子多长,参数量都不随长度增长。
- 学到的“语言规律”可以被反复复用。
2. 隐状态不是完整记忆
需要特别注意:隐状态不是把前面所有 token 原封不动存下来。
RNN 每读一步,就把目前为止的重要信息压缩进一个向量里。
这也是 RNN 的优点和缺点:
- 优点:不需要保存无限长的历史;
- 缺点:太久远的信息可能被压缩丢失。
后面学习 LSTM、GRU 时,你会看到它们正是在改进 RNN 的记忆能力。
五、RNN 怎样做语言模型?
假设我们有一句话:
深 度 学 习
训练时可以构造:
输入:深 度 学
标签:度 学 习
模型的任务是:
- 看到“深”,预测“度”;
- 看到“度”,预测“学”;
- 看到“学”,预测“习”。
每个时间步都会输出一个对词表中所有 token 的预测分数。经过 softmax 后,就变成概率分布。
比如词表大小是 5000,那么每一步输出就是 5000 个分数,表示下一个 token 是词表中每个 token 的可能性。
你可以把它想成“选择题”:
- 词表里有 5000 个候选。
- 模型给每个候选打分,分数越高越可能。
- softmax 把分数变成“概率”,方便比较和训练。
一个非常小的“数据对齐”示例:
# 原始序列(比如由词表编号得到)
tokens = [1, 2, 3, 4, 5]
# 输入是前 n-1 个,标签是向后错一位
inputs = tokens[:-1] # [1, 2, 3, 4]
labels = tokens[1:] # [2, 3, 4, 5]
六、困惑度:语言模型有多纠结?
分类任务常用准确率,语言模型常用一个指标叫 困惑度(Perplexity)。
它可以通俗理解为:
模型每次预测下一个 token 时,平均在多少个候选答案之间纠结。
如果困惑度是 1,说明模型几乎完全确定答案。
如果词表有 10000 个 token,而模型完全乱猜,困惑度会接近 10000。
困惑度越低,语言模型通常越好。
它和交叉熵损失有关:
困惑度 = exp(平均交叉熵损失)
一个小例子:
- 如果模型平均每一步只在 2 个词之间犹豫,困惑度大约是 2。
- 如果它总是在 1000 个词之间乱猜,困惑度就会很大。
小白阶段不用推导,只要记住:
困惑度越小,模型越不迷茫。
七、小结
这一篇我们正式认识了 RNN 的核心思想:
- 语言模型的目标是根据前文预测下一个 token。
- N-gram 方法简单,但记忆短,且容易遇到数据稀疏。
- 普通神经网络没有自然保存历史的机制。
- RNN 通过隐状态 HtH_tHt 把过去信息传到下一步。
- 隐状态是压缩后的记忆,不是完整复制前文。
- 困惑度可以衡量语言模型预测时有多不确定。
如果用一句话收尾:
RNN 的本质,就是让神经网络沿着时间一步步读,并把读过的信息压缩进隐状态里。
下一篇,我们开始写代码:从 one-hot 编码,到手写 RNN 前向传播,再到理解通过时间反向传播和梯度裁剪。
(注:文档部分内容参考《动手学深度学习》)
《动手学深度学习》循环神经网络:https://zh.d2l.ai/chapter_recurrent-neural-networks/rnn.html#id2
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)