上一篇我们知道了:序列数据不能随便打乱,因为顺序本身就包含信息。

一句话、一道语音、一段股价曲线,都不是孤立点的集合,而是一条按时间或逻辑展开的链。为了处理这种数据,我们需要模型具备一种能力:

读到当前位置时,还能记得前面发生过什么。

这就是循环神经网络(Recurrent Neural Network,RNN)的核心思想。

这一篇,我们先从语言模型讲起,再一步步拆开 RNN 的“记忆”到底是什么。


一、语言模型:预测下一个 token

当你用手机输入法打字:

今天晚上吃

输入法可能会推荐:

火锅、烧烤、面条

它在做的事情就是语言模型的典型任务:

根据前面的内容,预测下一个 token 的概率分布。

也就是说,它不是只给一个答案,而是给一组可能性:

P(火锅 | 今天 晚上 吃) = 0.35
P(烧烤 | 今天 晚上 吃) = 0.20
P(面条 | 今天 晚上 吃) = 0.15
...

概率越高,说明模型越觉得这个 token 合理。

生活化理解:

  • 你输入“今天晚上吃”,输入法给出三四个候选词。
  • 其实它内部在做“打分”,只是把分数最高的几个展示出来。
  • 这就是“概率分布”的直观形态:每个候选都有一个可能性。

下面用一个小图把过程串起来:

前文: 今天 晚上 吃

语言模型打分

火锅 0.35

烧烤 0.20

面条 0.15

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 都会做四件事:

  1. 看当前输入 XtX_tXt
  2. 参考上一时刻的记忆 Ht−1H_{t-1}Ht1
  3. 写出新的记忆 HtH_tHt
  4. 根据新的记忆做输出 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+Ht1Whh+bh)

Ot=HtWhq+bq O_t = H_t W_{hq} + b_q Ot=HtWhq+bq

各个符号的含义:

  • XtX_tXt:第 ttt 个时间步的输入向量(当前 token 的表示)。
  • Ht−1H_{t-1}Ht1:上一时刻的隐状态(上一步的记忆)。
  • 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}Ht1Whh:过去记忆带来的旧信息;
  • 两者加起来,再经过激活函数,得到新的记忆 HtH_tHt
  • 最后用 HtH_tHt 预测输出。

RNN 真正厉害的地方就在这里:

它不是只看当前输入,而是把当前输入和过去记忆一起考虑。

再用一个展开图感受“循环”的含义:

X1

H1

H0

X2

H2

X3

H3

这个图在表达三件事:

  • 每个时间步都会把“当前输入 XtX_tXt”和“上一时刻记忆 Ht−1H_{t-1}Ht1”合并,得到新的记忆 HtH_tHt
  • 同一套参数在不同时间步反复使用,所以图里是“同一个结构在时间上展开”。
  • 记忆会一路向后传递:H0→H1→H2→H3H_0 \rightarrow H_1 \rightarrow H_2 \rightarrow H_3H0H1H2H3,让后面的步骤能“记住”前面的信息。

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+H2H1H2H3

把参数写出来,其实每一步都在做同一件事:

Ht=tanh⁡(XtWxh+Ht−1Whh+bh) H_t = \tanh(X_t W_{xh} + H_{t-1} W_{hh} + b_h) Ht=tanh(XtWxh+Ht1Whh+bh)

这里的参数 WxhW_{xh}WxhWhhW_{hh}WhhWhqW_{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 的核心思想:

  1. 语言模型的目标是根据前文预测下一个 token。
  2. N-gram 方法简单,但记忆短,且容易遇到数据稀疏。
  3. 普通神经网络没有自然保存历史的机制。
  4. RNN 通过隐状态 HtH_tHt 把过去信息传到下一步。
  5. 隐状态是压缩后的记忆,不是完整复制前文。
  6. 困惑度可以衡量语言模型预测时有多不确定。

如果用一句话收尾:

RNN 的本质,就是让神经网络沿着时间一步步读,并把读过的信息压缩进隐状态里。

下一篇,我们开始写代码:从 one-hot 编码,到手写 RNN 前向传播,再到理解通过时间反向传播和梯度裁剪。

(注:文档部分内容参考《动手学深度学习》)
《动手学深度学习》循环神经网络:https://zh.d2l.ai/chapter_recurrent-neural-networks/rnn.html#id2

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐