序列模型

序列模型(Sequence Model)是指:专门用来处理和建模序列数据的模型。

如果我们有一个长度为 $T$ 的序列:

$$x_1, x_2, x_3, \dots, x_T$$

那么序列模型的目标通常是学习这个序列的规律

序列数据和普通独立样本最大的不同在于:样本之间不是独立同分布的。

常见序列模型包括:

  • 自回归模型
  • 马尔可夫模型
  • 隐马尔可夫模型
  • 条件随机场
  • RNN
  • LSTM
  • GRU
  • Transformer
  • 状态空间模型
  • 潜变量序列模型

自回归模型

自回归模型(Autoregressive Model, AR)的核心思想是:当前时刻的值由前面已经出现的值来预测。

在概率建模中,一个序列的联合概率可以写成链式法则:
$$p(x_1, x_2, \dots, x_T) = \prod_{t=1}^{T} p(x_t \mid x_1, x_2, \dots, x_{t-1})$$

这就是最标准的自回归分解。
它的含义是:

  • 先生成 $x_1$
  • 再根据 $x_1$ 生成 $x_2$
  • 再根据 $x_1, x_2$ 生成 $x_3$
  • 一直生成到 $x_T$
    所以自回归模型本质上是在建模:
    每一个位置在给定历史条件下的条件概率

**优点

  • 概率解释清晰
  • 很适合生成任务
  • 联合分布可分解为条件分布,训练容易定义
  • 语言建模中非常自然
    **缺点
  • 生成速度慢,因为通常要一步一步生成
  • 长距离依赖可能难建模
  • 误差会逐步累积

马尔可夫模型

马尔可夫模型(Markov Model)的核心思想是:未来只依赖当前状态,而与更久远的过去无关。

$k$ 阶马尔可夫模型写成:
$$p(x_t \mid x_1, \dots, x_{t-1}) = p(x_t \mid x_{t-1}, \dots, x_{t-k})$$
所以高阶马尔可夫模型和有限阶自回归模型很接近。

**优点

  • 结构简单
  • 计算方便
  • 参数较少
  • 概率解释很强
    **缺点
  • 马尔可夫假设通常过于强
  • 很难刻画长距离依赖
  • 对复杂现实序列表达能力有限

潜变量模型

潜变量模型(Latent Variable Model)的核心思想是:观测到的数据背后,存在一些没有直接观测到的隐藏变量,这些隐藏变量决定了数据的生成方式。

潜变量模型的基本思想是:
表面上看到的数据很复杂,但如果引入一个隐藏原因,数据就更容易解释。

一个典型生成式潜变量模型包含两个过程:
**1)先采样潜变量
$$
z∼p(z)$$

**2)再根据潜变量生成观测
$$
x∼p(x∣z)$$

于是联合分布为:
$$
p(x, z) = p(z)p(x \mid z)$$

这类模型强调:
观测数据是由隐藏结构生成出来的。

prefix(前缀)

在序列建模里,prefix 通常指:
一个序列从开头到当前位置之前的那一段。
给定 prefix,预测下一个 token。

Token(词元)

自然语言处理中最基础、也最重要的一条处理链:
$$
原始文本 \rightarrow 分词/切分 \rightarrow token(词元) \rightarrow vocab(词表) \rightarrow token_id(索引)$$

在自然语言处理里,文本不能直接送进模型,因为模型本质上只能处理数字张量,而不能直接理解字符串。因此,必须先把文本转换成数字。

这个过程通常分成几步:

  1. 把文本切分成若干基本单位
  2. 给每个单位建立词表编号
  3. 把文本映射成整数序列
  4. 再把整数序列送入嵌入层或模型

token(词元)表示:文本经过切分之后得到的基本处理单元。
token 是切分后的单位,不一定等于自然语言中的“词”。

vocab(词表)

它表示:模型允许识别的所有 token 的集合,以及每个 token 对应的编号规则。

词表的作用是:把文本单位映射成整数编号。
因为神经网络最终处理的是数值,而不是字符串,所以每个 token 都必须转换成 index。

token index 或 token id 指的是:某个 token 在 vocab 中对应的整数编号。

为什么要转成 index
因为神经网络不能直接处理:[“the”, “cat”, “sat”]
它需要的是:[0, 1, 2]
然后这些整数再进一步通过嵌入层映射成向量
$$\text{token id} \rightarrow \text{embedding vector}$$

如何构建vocab
构建 vocab 时,通常会先统计语料中每个 token 出现的频率。然后通常会按频率从高到低排序,过滤低频词、加入特殊 token,再依次编号分配 index。

为什么 vocab 常按出现次数排序
为什么高频词放前面?

  1. 方便保留最重要的高频 token。
  2. 用较小的 vocab 就能覆盖大部分实际输入。
  3. 按频率排序并截断前 $K$ 个高频词,可以显著减少模型参数量和内存占用。
  4. 提高训练效率,较小且高频优先的词表意味着:
    • embedding 表更小
    • softmax 层更小
    • 训练和推理更快
    • 数据稀疏性更低
      因此这确实会“提高性能”,但这里的“性能”更准确应理解为:
    • 提高训练效率
    • 提高内存利用率
    • 提高词表覆盖效率
    • 有时也能提升泛化效果
  5. 词表一般是哈希表或字典结构:查找通常本来就是近似 $O(1)$,所以“把高频词放前面”并不主要是为了字典查找速度

通常 vocab 至少包含两个映射。

  1. token 到 index
    token_to_idx = {
    “I”: 4,
    “love”: 5,
    “NLP”: 8
    }

  2. index 到 token
    idx_to_token = {
    4: “I”,
    5: “love”,
    8: “NLP”
    }
    这样就能实现双向转换:
    $$\text{text} \leftrightarrow \text{token} \leftrightarrow \text{id}$$

未知词 <unk>
现实中测试文本里可能出现训练时没见过的 token。—–词表外(OOV, Out-of-Vocabulary)问题。
把词拆成更小的子词 token。
这样做后:

  • 词表更小
  • OOV 更少
  • 更适合开放词汇场景

语言模型

总结
语言模型的核心任务是估计文本序列的联合概率,即 $P(x_1, x_2, \dots, x_T)$。由于直接建模整个序列的联合概率非常困难,通常使用链式法则将其分解为一系列条件概率的乘积,即 $P(x_1, \dots, x_T)=\prod_{t=1}^{T} P(x_t \mid x_{<t})$。
计数模型通过统计语料中词和词串的出现次数来估计这些条件概率,
而 N 元语法模型进一步假设当前词只依赖前面最近的 $n-1$ 个词,从而将复杂的历史依赖简化为局部上下文依赖。
然而,当词表大小为 $V$ 时,可能的 $n$ 元组数量最多为 $V^n$,因此随着 $n$ 增大,参数空间会指数级增长,带来严重的数据稀疏和存储问题。
在神经语言模型训练中,通常将长文本序列切成固定长度的小片段,并采用 mini-batch 方式组织训练数据。
常见的抽取方法包括随机采样和顺序分区采样,两者的核心都是构造“输入序列”和“右移一位的目标序列”,从而训练模型根据前文预测下一个词。

从概率角度看,语言模型的任务就是:估计文本序列的联合概率。

  1. 判断句子是否自然
  2. 预测下一个词
  3. 生成文本
  4. 用于机器翻译、语音识别、输入法等

语言模型的定义可以写成:对一个文本序列出现的概率分布进行建模的模型。

概率的链式法则(Chain Rule):
$$P(x_1, x_2, \dots, x_T) = P(x_1) P(x_2 \mid x_1) P(x_3 \mid x_1, x_2) \cdots P(x_T \mid x_1, \dots, x_{T-1})$$
一般写成:
$$P(x_1, x_2, \dots, x_T) = \prod_{t=1}^{T} P(x_t \mid x_1, x_2, \dots, x_{t-1})$$

计数模型(Count-based Language Model)

计数模型的基本思想非常直观:通过统计语料中词或词串出现的次数,来估计概率。

一元模型(Unigram)
把每个词看成彼此独立:
$$P(x_1, x_2, \dots, x_T) \approx \prod_{t=1}^{T} P(x_t)$$

其中:
$$P(x_t) = \frac{\text{count}(x_t)}{\sum_w \text{count}(w)}$$
也就是某个词的出现次数除以总词数。
缺点
这种做法忽略了词序和上下文。

二元模型(Bigram)
为了考虑前一个词对当前词的影响,可以使用 bigram 模型:
$$P(x_1, x_2, \dots, x_T) \approx P(x_1) \prod_{t=2}^{T} P(x_t \mid x_{t-1})$$

其中条件概率用计数来估计:
$$P(x_t \mid x_{t-1}) = \frac{\text{count}(x_{t-1}, x_t)}{\text{count}(x_{t-1})}$$
$$P(\text{学习} \mid \text{机器})$$
表示在语料中,“机器”后面接“学习”的频率有多高。

三元模型(Trigram)
$$P(x_1, x_2, \dots, x_T) \approx P(x_1)P(x_2 \mid x_1)\prod_{t=3}^{T} P(x_t \mid x_{t-2}, x_{t-1})$$

其中:
$$P(x_t \mid x_{t-2}, x_{t-1}) = \frac{\text{count}(x_{t-2}, x_{t-1}, x_t)} {\text{count}(x_{t-2}, x_{t-1})}$$
这表示当前词依赖前两个词。

N 元语法(N-gram)

N 元语法模型(N-gram model)就是对语言模型的一种近似方法。它的核心假设是:

当前词只依赖前面最近的 $n-1$ 个词,而不依赖更早的历史。
即:
$$P(x_t \mid x_1, x_2, \dots, x_{t-1}) \approx P(x_t \mid x_{t-n+1}, \dots, x_{t-1})$$
这就是 N 元语法的马尔可夫近似。

设词表中一共有 $V$ 个不同词。
1-gram 数量可能的一元组最多有:V种。
2-gram 数量可能的二元组最多有:V^2种。
3-gram 数量可能的三元组最多有:V^3种。
n-gram 数量可能的n元组最多有:V^n种。

因为每个位置都可以取 $V$ 个不同词,而一个 $n$ 元组有 $n$ 个位置.

缺点

  1. 即使语料很大,也不可能覆盖所有可能的 $n$ 元组。因此很多合法但没见过的序列,其概率会被估计成 0。
  2. 句尾的信息可能与句首有关,但 bigram、trigram 只能看很短上下文,建模不了这种长程关系。
  3. 随着 $n$ 增大,参数空间迅速膨胀。

mini-batch

每次不是只用一个样本更新参数,也不是一次用全部样本,而是使用一小批样本共同计算梯度。

抽取 mini-batch 的两种常见方法

随机采样(Random Sampling)
把整个长序列看成一个很长的 token 序列,然后随机选取长度为 num_steps 的片段作为输入。
优点

  • 样本之间更随机
  • mini-batch 更接近独立同分布
  • 适合打乱训练数据
    缺点
  • 不容易保留跨 batch 的上下文连续性
  • RNN 的隐藏状态一般不能直接在不同 batch 间传递

顺序分区采样(Sequential Partitioning)
把整个长序列按顺序切成若干段,再按 batch size 分配成多行,然后每次从每行中取一小段组成 batch。
优点

  • 序列连续
  • 相邻 batch 之间上下文能延续
  • 适合 RNN 隐状态跨 batch 传递
    缺点
  • 随机性较弱
  • 相邻样本高度相关

RNN(Recurrent Neural Network,循环神经网络)

RNN 的核心思想可以概括为一句话:
在处理序列时,模型在每个时刻不仅接收当前输入,还会接收上一个时刻的隐藏状态,从而把历史信息传递下去。

最经典的 RNN 更新公式为:
$$h_t = \phi(W_{xh} x_t + W_{hh} h_{t-1} + b_h)$$
其中:

  • $x_t$ 是当前输入
  • $h_{t-1}$ 是上一时刻隐藏状态
  • $W_{xh}$ 是输入到隐藏层的权重
  • $W_{hh}$ 是隐藏状态到隐藏状态的权重
  • $b_h$ 是偏置
  • $\phi$ 是非线性激活函数,比如 $\tanh$ 或 ReLU
    如果还要输出预测值,则常写成:
    $$y_t = W_{hy} h_t + b_y$$

或者做分类时:
$$\hat{y}t = \text{softmax}(W{hy} h_t + b_y)$$

虽然名字叫“循环”神经网络,但在理解和训练时,我们通常把它沿时间轴展开。
例如长度为 4 的序列,可以展开为:$$x_1 \rightarrow h_1 \rightarrow y_1$$$$x_2, h_1 \rightarrow h_2 \rightarrow y_2$$$$x_3, h_2 \rightarrow h_3 \rightarrow y_3$$$$x_4, h_3 \rightarrow h_4 \rightarrow y_4$$
这里看起来像是 4 个网络连在一起,但它们其实共享同一套参数

  • 同一个 $W_{xh}$
  • 同一个 $W_{hh}$
  • 同一个 $W_{hy}$
    这也是 RNN 的重要特点:同一个单元在不同时间步重复使用。

优点

  • 能够处理变长序列
  • 能够建模顺序关系
  • 不同时间步共享参数,模型规模不会随着序列长度线性膨胀。
    缺点
  • 尤其在长序列上,容易出现:梯度消失、梯度爆炸
  • 虽然理论上 $h_t$ 包含全部历史,但实际训练中,较早的信息常常难以有效保留。
  • RNN 必须按时间步一个一个计算,不像 Transformer 那样高度并行。

梯度爆炸(Gradient Explosion)

梯度爆炸指的是:在训练神经网络时,反向传播得到的梯度值变得非常大,导致参数更新过猛,训练不稳定甚至直接数值溢出
例如参数更新公式:
$$\theta \leftarrow \theta - \eta \nabla_\theta $$

如果梯度 $\nabla_\theta L$ 非常大,那么即使学习率 $\eta$ 不大,更新步长也会非常夸张,导致:

  • loss 突然变大
  • 参数变成极端值
  • 出现 NaN
  • 训练发散

为什么 RNN 特别容易出现梯度爆炸
RNN 的训练需要使用 BPTT(Backpropagation Through Time,时间反向传播)
在 BPTT 中,梯度要沿时间步不断往回传。由于隐藏状态是递推定义的:
$$h_t = \phi(W_{hh} h_{t-1} + W_{xh} x_t + b)$$
因此,某个较早时刻的梯度会包含很多个雅可比矩阵连乘。
粗略地说,会出现类似这样的项:
$$\frac{\partial h_t}{\partial h_k} = \prod_{i=k+1}^{t} \frac{\partial h_i}{\partial h_{i-1}}$$
如果这些矩阵的范数大于 1,连乘后就可能迅速变大,于是梯度爆炸。

梯度剪裁(Gradient Clipping)

既然 RNN 训练中可能出现梯度爆炸,那么最直接的思路就是:当梯度太大时,不让它继续无限增大,而是把它“截住”。
这就是梯度剪裁。
梯度剪裁不是从根本上消除梯度爆炸的来源,但它是一个非常有效、非常常用的工程技巧。

假设当前参数的整体梯度为:$\mathbf{g}$
如果它的范数太大,例如:
$$|\mathbf{g}| > \theta$$

其中 $\theta$ 是预设阈值,那么就把梯度缩放到范数等于 $\theta$:
$$
\mathbf{g} \leftarrow \frac{\theta}{|\mathbf{g}|}\mathbf{g}$$

如果梯度本来不大:
$$|\mathbf{g}| \le \theta$$

则不做处理。

数学形式
最常见的是按全局范数剪裁(clip by global norm)
$$
g\mathbf{g} \leftarrow \min\left(1, \frac{\theta}{|\mathbf{g}|}\right)\mathbf{g}$$

这个公式很好理解:

  • 当 $|\mathbf{g}| \le \theta$ 时,前面的系数是 1,不变
  • 当 $|\mathbf{g}| > \theta$ 时,前面的系数小于 1,把梯度整体缩小
    注意这里是整体按比例缩放,而不是简单把每个分量硬砍掉。

**它能做的

  • 缓解梯度爆炸
  • 提高训练稳定性
    **它不能彻底解决的
  • 长距离依赖问题
  • 梯度消失问题
  • RNN 结构本身的表达局限
    所以后来才出现了:
  • LSTM
  • GRU
  • LSTM:更完整、更复杂
  • GRU:更简洁、更轻量
    这些结构通过门控机制更系统地解决长序列训练问题。

GRU(Gated Recurrent Unit,门控循环单元)

GRU 的核心思想是:
用更少的门、更紧凑的状态结构,达到与 LSTM 相近的效果。

GRU 主要有两个门:

  1. 更新门(update gate)
  2. 重置门(reset gate)

给定输入 $x_t$ 和上一时刻隐藏状态 $h_{t-1}$:

1. 更新门

$$z_t = \sigma(W_z [h_{t-1}, x_t] + b_z)$$

更新门控制:

当前时刻保留多少旧状态、写入多少新状态。

可以把它理解成同时扮演了 LSTM 中“遗忘门 + 输入门”的综合角色。


2. 重置门

$$r_t = \sigma(W_r [h_{t-1}, x_t] + b_r)$$

重置门控制:

在生成候选隐藏状态时,过去信息应参与多少。

如果 $r_t$ 很小,说明模型希望“重置”过去状态,让当前候选更多依赖当前输入。


3. 候选隐藏状态

$$\tilde{h}t = \tanh(W_h [r_t \odot h{t-1}, x_t] + b_h)$$

这里表示:

  • 先用重置门筛选旧状态
  • 再和当前输入一起生成候选状态

4. 最终隐藏状态更新

$$h_t = z_t \odot h_{t-1} + (1 - z_t) \odot \tilde{h}_t$$

这个公式非常关键。

它表示:

  • 如果 $z_t$ 大,更多保留旧状态 $h_{t-1}$
  • 如果 $z_t$ 小,更多采用新候选状态 $\tilde{h}_t$

LSTM(Long Short-Term Memory,长短期记忆网络)

它是为了解决普通 RNN 长期依赖难学的问题而提出的。LSTM 的核心思想可以概括为一句话:
通过门控机制控制信息的写入、保留、遗忘和输出,使网络能够更稳定地记住长期信息。

LSTM 相比普通 RNN,最大的变化是它引入了一个额外的状态:

  • 隐藏状态 $h_t$
  • 细胞状态 $c_t$
    其中:
  • $h_t$ 更像当前时刻对外输出的状态
  • $c_t$ 更像沿时间轴流动的“长期记忆通道”
    这个 $c_t$ 是 LSTM 最关键的设计。

LSTM 主要有三个门:

  1. 遗忘门(forget gate)
  2. 输入门(input gate)
  3. 输出门(output gate)
    此外还有一个候选记忆向量。

给定当前输入 $x_t$ 和上一时刻状态 $(h_{t-1}, c_{t-1})$,LSTM 的计算通常写成:

1. 遗忘门

$$f_t = \sigma(W_f [h_{t-1}, x_t] + b_f)$$

遗忘门决定:
上一时刻的细胞状态 $c_{t-1}$ 中哪些信息要保留,哪些要遗忘。
这里:

  • $\sigma$ 是 sigmoid 函数
  • 输出范围在 $(0,1)$
  • 越接近 1,表示越保留
  • 越接近 0,表示越遗忘

2. 输入门

$$i_t = \sigma(W_i [h_{t-1}, x_t] + b_i)$$

输入门决定:
当前候选信息中哪些部分应该写入细胞状态。
控制新信息写入多少。

  • 大:当前新信息重要,要写入
  • 小:当前信息不重要,不写入

3. 候选记忆

$$\tilde{c}t = \tanh(W_c [h{t-1}, x_t] + b_c)$$

这表示当前时刻根据输入和旧隐藏状态生成的“候选记忆内容”。

它不是直接写入,而是要经过输入门筛选。


4. 更新细胞状态

$$c_t = f_t \odot c_{t-1} + i_t \odot \tilde{c}_t$$

这是 LSTM 最关键的一步。
它表示:

  • 先把旧记忆 $c_{t-1}$ 按遗忘门 $f_t$ 进行筛选
  • 再把新候选记忆 $\tilde{c}_t$ 按输入门 $i_t$ 写入
  • 二者相加,得到新的细胞状态 $c_t$
    其中 $\odot$ 表示按元素乘法。

5. 输出门

$$o_t = \sigma(W_o [h_{t-1}, x_t] + b_o)$$

输出门决定:

当前细胞状态中哪些信息可以输出成隐藏状态。
控制当前记忆对外输出多少。

  • 大:让记忆更多体现在 $h_t$ 中
  • 小:少输出

6. 更新隐藏状态

$$h_t = o_t \odot \tanh(c_t)$$

这表示:

  • 先对细胞状态做一个 $\tanh$
  • 再用输出门控制暴露多少信息

深层循环神经网络(Deep Recurrent Neural Network)

Deep RNN

它的核心思想是:
不仅沿时间维度展开,还沿层数维度堆叠多个循环层,从而提升模型表达能力。

在普通单层 RNN 中,每个时间步只有一个隐藏状态:$h_t$
而在深层 RNN 中,每个时间步会有多层隐藏状态,例如 3 层:

  • 第一层:$h_t^{(1)}$
  • 第二层:$h_t^{(2)}$
  • 第三层:$h_t^{(3)}$
    这意味着每个时间步都不是只做一次变换,而是要经过多层循环处理。

假设是三层 RNN,则每个时间步的计算可以写成:
**第一层
$$h_t^{(1)} = f^{(1)}(x_t, h_{t-1}^{(1)})$$

**第二层
$$h_t^{(2)} = f^{(2)}(h_t^{(1)}, h_{t-1}^{(2)})$$

**第三层
$$h_t^{(3)} = f^{(3)}(h_t^{(2)}, h_{t-1}^{(3)})$$

这里可以看到:

  • 同一层在时间上递推
  • 上一层在同一时间步把输出传给下一层
    所以深层循环网络同时包含两种依赖:
  1. 时间依赖:$h_{t-1}^{(l)} \rightarrow h_t^{(l)}$
  2. 层间依赖:$h_t^{(l-1)} \rightarrow h_t^{(l)}$

深层循环神经网络则是在时间递归之外,再沿层数方向堆叠多个 RNN、LSTM 或 GRU 层,使模型能够提取更高层次的时序特征并增强表达能力(非线性性)。

双向循环神经网络(Bidirectional RNN, BiRNN)

它的核心思想是:不仅利用当前位置之前的历史信息,还同时利用当前位置之后的未来信息。

普通 RNN 在处理序列时,通常只能从左到右读:$$x_1 \rightarrow x_2 \rightarrow x_3 \rightarrow \cdots \rightarrow x_T$$
因此,在第 $t$ 个位置,模型只能利用前缀信息,也就是:
$$x_1, x_2, \dots, x_t$$
但很多任务中,一个位置的含义不仅取决于前文,也取决于后文。于是就有了双向循环神经网络。

双向循环神经网络的核心结构是:

  • 一个正向 RNN:从左到右处理序列
  • 一个反向 RNN:从右到左处理序列
    对于输入序列:
    $$x_1, x_2, \dots, x_T$$
    双向 RNN 会同时计算两组隐藏状态:
    **1. 正向隐藏状态
    $$\overrightarrow{h_t} = f(\overrightarrow{h_{t-1}}, x_t)$$

它表示从左到右读取到第 $t$ 个位置时的状态。


**2. 反向隐藏状态
$$\overleftarrow{h_t} = f(\overleftarrow{h_{t+1}}, x_t)$$

它表示从右到左读取到第 $t$ 个位置时的状态。


然后,把两个方向的状态合并,得到当前位置的最终表示:
$$h_t = [\overrightarrow{h_t}; \overleftarrow{h_t}]$$
其中:

  • $[\cdot ; \cdot]$ 表示**拼接
  • 也有些实现会用求和或平均,但最常见的是拼接
    所以,$h_t$ 同时包含:
  • 左边上下文信息
  • 右边上下文信息

数学表示

设输入序列为:
$$X = (x_1, x_2, \dots, x_T)$$

**正向隐藏状态
$$\overrightarrow{h_t} = \phi(W_x^{(f)}x_t + W_h^{(f)}\overrightarrow{h_{t-1}} + b^{(f)})$$
反向隐藏状态
$$\overleftarrow{h_t} = \phi(W_x^{(b)}x_t + W_h^{(b)}\overleftarrow{h_{t+1}} + b^{(b)})$$

然后进行拼接:
$$h_t = [\overrightarrow{h_t}; \overleftarrow{h_t}]$$

若用于分类或标注,则进一步输出:
$$y_t = W_y h_t + b_y$$
或者:
$$\hat{y}_t = \text{softmax}(W_y h_t + b_y)$$

适合哪些任务

双向 RNN 特别适合理解型任务,也就是:当你在处理当前位置时,可以访问整个输入序列。

  1. 序列标注
  2. 文本分类中的编码器
  3. 语音识别中的离线场景
  4. 机器翻译中的编码器

它不能直接用于“严格在线”的预测场景:因为反向 RNN 需要看到未来输入。

  1. 实时语音流预测
  2. 自回归文本生成
  • 理解任务适合双向 RNN
  • 生成任务通常不适合直接用双向 RNN 做解码器

Seq2Seq(Sequence to Sequence,序列到序列模型)

它的核心目标是:把一个输入序列映射成另一个输出序列。
也就是说,输入和输出都不是单个值,而是一个有顺序的序列。

可以写成:

$$x_1, x_2, \dots, x_{T_x} \rightarrow y_1, y_2, \dots, y_{T_y}$$

其中:

  • 输入长度 $T_x$ 可以和输出长度 $T_y$ 不同
  • 输入和输出的含义也可以不同
  • 模型要学的是“如何从一个序列生成另一个序列”
    Seq2Seq 是深度学习处理中序列问题的一个非常重要的框架,在机器翻译、文本摘要、对话系统、语音识别等任务中都非常经典。

Seq2Seq 的核心思想可以概括成两步:

  1. 先把输入序列编码成一个内部表示,把输入序列编码成一个能够表示其语义的内部向量。
  2. 再根据这个表示逐步解码出输出序列,根据输入序列的编码结果和之前已经生成的输出,逐步生成当前输出 token。
    因此,Seq2Seq 最经典的结构叫:
  • Encoder-Decoder
  • 编码器—解码器结构
    即:
    $$
    \text{输入序列} \rightarrow \text{Encoder} \rightarrow \text{上下文表示} \rightarrow \text{Decoder} \rightarrow \text{输出序列}
    $$
    这就是 Seq2Seq 的基本框架。

其概率建模形式为:
$$P(y_{1:T_y}\mid x_{1:T_x}) = \prod_{t=1}^{T_y} P(y_t \mid y_{<t}, x_{1:T_x})$$

训练时,我们通常知道输入序列和正确输出序列,因此可以监督训练(准确输入作为输入)。
训练时,Decoder 在每一步不一定使用“自己刚刚预测出来的词”,而是通常使用真实标签作为下一步输入,这叫:Teacher Forcing(教师强制)

  • 训练时喂真实前一个词
  • 测试时喂模型自己上一步生成的词
    这样做的优点是:
  • 训练更稳定
  • 收敛更快
  • 误差不容易在训练中不断累积

最早的 Seq2Seq 有一个非常重要的问题:Encoder 必须把整个输入序列压缩到一个固定长度向量里。

即:
$$x_1, x_2, \dots, x_{T_x} \rightarrow c$$

这里 $c$ 是单个 context vector。
这在短句上可能还行,但在长句上会有明显问题:

  • 信息压缩过度
  • 长句重要细节容易丢失
  • Decoder 很难仅靠一个固定向量恢复整个输入
    这就是经典 Seq2Seq 的瓶颈。

Seq2Seq 中的注意力机制

在 Seq2Seq 里,注意力机制是一个非常关键的改进。它几乎可以看成是:把“编码器把整句压缩成一个固定向量”这种做法,改成“解码器在每一步生成时,都可以动态去查看输入序列中最相关的部分”。

为了解决“固定长度上下文向量瓶颈”,后来引入了 Attention(注意力机制)。核心思想是:
**Decoder 在生成每个输出词时,不必只看一个固定向量,而是可以动态查看 Encoder 各个位置的隐藏状态。

Pasted image 20260411112020

当 Decoder 准备生成输出 $y_t$ 时,它会:

  1. 看看自己当前的解码状态
  2. 用这个状态去和 Encoder 的每个隐藏状态比较相关性
  3. 给每个输入位置分配一个注意力权重
  4. 对 Encoder 的隐藏状态做加权求和
  5. 得到当前步专属的上下文向量 $c_t$
  6. 再用 $c_t$ 辅助生成当前输出

也就是说,不再只有一个 context vector,而是每一步都有自己的上下文向量:
$$c_t = \sum_{i=1}^{T_x} \alpha_{t,i} h_i$$
其中:

  • $h_i$ 是 Encoder 第 $i$ 个位置的隐藏状态
  • $\alpha_{t,i}$ 是第 $t$ 个解码步对第 $i$ 个输入位置的注意力权重
    这样 Decoder 就可以在不同生成时刻,重点关注输入序列的不同部分。

贪心搜索(Greedy Search)

每一步都直接选择当前概率最大的 token:
$$y_t=\arg\max P(y_t\mid y_{<t},x)$$

例如:

  • 第一步选当前最可能的词

  • 第二步在这个前缀下继续选最可能的词

  • 一直这样生成下去

  • 简单

  • 速度快

  • 实现容易
    但是局部最优不一定导致全局最优。

束搜索(Beam Search)

束搜索(Beam Search)是序列生成任务中非常经典的一种近似搜索算法一种启发式近似搜索算法。
它要解决的核心问题是:当模型需要一步一步生成序列时,如何在巨大的候选空间中找到一个“比较好”的输出序列。

每一步都有很多候选 token,所有可能序列的数量会指数级增长。
如果词表大小为 $V$,序列长度为 $T$,那么可能的输出序列数大约是:V^T
这个数量通常极其庞大,无法暴力枚举。

束搜索的核心思想可以概括为一句话:每一步不只保留一个最优前缀,而是保留概率最高的 $k$ 个候选前缀,再继续扩展。
每一步只保留前B个候选

束搜索常用的序列分数是:
$$\text{score}(y_{1:t})=\sum_{i=1}^{t}\log P(y_i\mid y_{<i},x)$$

这样做有两个好处:

  1. 乘法变加法,计算更稳定
  2. 避免数值过小下溢–条件概率通常小于 1,多个概率连乘后会变得非常小,log更稳定
    问题–长度偏置(length bias)
    每一项 $\log P(\cdot)$ 通常是负数,所以序列越长,和就越小。
    这意味着:
  • 短序列更容易得到较高分
  • 长序列容易吃亏

为了缓解束搜索偏向短句的问题,常使用长度归一化(Length Normalization)
例如把分数改成:

$$\text{score}(y)=\frac{1}{T^\alpha}\sum_{t=1}^{T}\log P(y_t\mid y_{<t},x)$$

或者更简单地:
$$\text{score}(y)=\frac{1}{T}\sum_{t=1}^{T}\log P(y_t\mid y_{<t},x)$$

这样做后:

  • 不再单纯偏向短句
  • 更公平地比较不同长度序列
    这里 $\alpha$ 是长度惩罚超参数,常在 0 到 1 之间调节。

注意力机制(Attention)

它的核心思想可以先用一句话概括:当模型处理某个位置的信息时,不必平均地看待所有输入,而是应该有选择地“重点关注”那些最相关的部分。

这说明:不同输出位置,对输入中不同位置的依赖强度是不一样的。

最经典的注意力机制通常包含三个概念:

  • Query(查询)—–当前我想找什么
  • Key(键)———-每个候选元素有哪些特征,可以拿来和 Query 匹配
  • Value(值)——–每个候选元素真正提供的信息内容
    通常记作:
    $$Q,\ K,\ V$$

注意力机制的基本计算流程

设有一个 Query:
以及一组 Key 和 Value:
$$k_1, k_2, \dots, k_n v_1, v_2, \dots, v_n$$
注意力机制通常分为三步。
注意力本质上需要一个“相似度”指标

第一步:计算相似度分数

先计算 Query 与每个 Key 的匹配程度:

$$s_i = \text{score}(q, k_i)$$

这里的 $\text{score}$ 可以有很多实现方式,比如点积、加性打分等。


第二步:归一化成权重

把这些分数通过 softmax 转成概率权重:

$$\alpha_i = \frac{\exp(s_i)}{\sum_j \exp(s_j)}$$
这样:

  • 每个 $\alpha_i \ge 0$
  • 所有 $\alpha_i$ 之和等于 1
    所以 $\alpha_i$ 可以理解为模型对第 $i$ 个位置分配的注意力权重。

第三步:加权求和

用这些权重对 Value 加权求和:

$$\text{Attention}(q, K, V)=\sum_{i=1}^{n} \alpha_i v_i$$

这个结果就是注意力输出。

相似度分数

点积注意力(Dot-Product Attention)

最简单的方法:

$$\text{score}(q,k)=q^\top k$$

向量点积

点积可以理解成“方向一致程度 + 长度影响”。
如果两个向量:

  • 方向相近
  • 对应维度上的大值出现在相同位置
    那么点积通常会更大。所以点积大,通常表示:query 和 key 更相关。

优点是:

  • 简单
  • 高效
  • 适合矩阵并行计算

缩放点积注意力(Scaled Dot-Product Attention)

Transformer 中最经典的方法:

$$\text{score}(q,k)=\frac{q^\top k}{\sqrt{d_k}}$$

其中 $d_k$ 是 Key 的维度。

为什么要除以 $\sqrt{d_k}$?

因为当维度较大时,点积值可能变得过大,导致 softmax 进入过于尖锐的区域,影响训练稳定性。除以 $\sqrt{d_k}$ 可以让数值更平稳。


加性注意力(Additive Attention)

也叫 Bahdanau Attention,常写成:

$$\text{score}(q,k)=v^\top \tanh(W_q q + W_k k)$$
其中:

  • $W_q$:query 的线性变换矩阵
  • $W_k$:key 的线性变换矩阵
  • $b$:偏置
  • $v$:一个可学习向量
    它通过一个小型前馈网络来计算分数。

点积打分比较像:直接拿两个向量比相似度。

而加性打分更像:先把 query 和 key 映射到一个共同空间,再让一个小神经网络判断它们是否匹配。所以加性打分通常更灵活。

优点是:

  • 灵活
  • 在早期 Seq2Seq 中非常经典
    缺点是:
  • 计算不如点积高效

关键细节

  1. softmax 前最好做数值稳定处理
  2. 批量维度要处理清楚真实训练里常有: batch 维、序列长度维、特征维,shape 一定要想清楚。
  3. 如果有 padding 或者因果约束,打分后要对某些位置 mask。
  4. 点积注意力常用缩放版本,尤其维度大时

注意力与其他区别

**RNN

  • 依赖隐藏状态递推
  • 信息一步一步传递
  • 长距离依赖难
  • 并行性差

**注意力机制

  • 当前位置可直接查看其他位置
  • 更灵活
  • 长距离依赖更容易
  • 并行性更好

**卷积

  • 用固定大小局部感受野
  • 关注邻近区域
  • 权重共享

**注意力

  • 动态决定关注谁
  • 可以跨很远距离
  • 权重由当前输入决定

自注意力(Self-Attention)和位置编码(Positional Encoding)

自注意力(Self-Attention)和位置编码(Positional Encoding)是 Transformer 的两个核心组成部分。可以先用一句话概括它们的关系:
自注意力负责让序列中每个位置动态地查看其他位置,建模词与词之间的依赖关系;位置编码负责告诉模型“谁在前、谁在后、相对远近如何”,补上顺序信息。

自注意力(Self-Attention)

它的核心思想是:序列中的每个位置,都可以对同一个序列中的所有位置计算注意力。

也就是说:

  • Query 来自当前序列
  • Key 来自当前序列
  • Value 也来自当前序列
    因此叫做 self-attention

设输入序列表示为矩阵:

$$X \in \mathbb{R}^{n \times d}$$

通过线性变换得到:

$$Q = XW_Q,\quad K = XW_K,\quad V = XW_V$$
然后计算注意力:
$$
\text{Attention}(Q,K,V)=\text{softmax}\left(\frac{QK^\top}{\sqrt{d_k}}\right)V$$

这里:

  • $QK^\top$ 得到每个位置对所有位置的相关性分数
  • softmax 后得到注意力权重矩阵
  • 再乘以 $V$ 得到输出

这是 Transformer 的核心公式之一

**普通 Encoder-Decoder Attention

  • Query 来自 Decoder
  • Key / Value 来自 Encoder
    即“输出看输入”。

**Self-Attention

  • Query、Key、Value 都来自同一个序列
    即“序列内部自己看自己”。

纯自注意力对输入顺序是置换等变的。
也就是说,如果你把输入位置重新排列,输出也会跟着重新排列,但模型本身没有额外机制区分“这个顺序”和“那个顺序”谁更合理。所以必须显式加入顺序信息

位置编码(Positional Encoding)

位置编码(Positional Encoding)的核心作用是:
为每个位置注入顺序信息,让模型知道 token 处在序列中的哪个位置。

Transformer 中没有:

  • RNN 的递归结构
  • CNN 的局部卷积窗口
    所以它不像 RNN 那样天然按顺序处理,也不像 CNN 那样有局部相邻关系偏置。
    因此,Transformer 里的自注意力机制需要外部告诉它:
  • 哪个 token 在前
  • 哪个 token 在后
  • 哪两个 token 离得近
  • 哪些模式和位置有关
    否则模型只能看到一组“无序的 token 向量集合”。

最常见做法是:
$$\text{Input to Transformer} = \text{Token Embedding} + \text{Positional Encoding}$$

也就是:

  1. 先把 token id 映射成 embedding
  2. 再加上同维度的位置编码
  3. 然后送入自注意力层

最经典入门内容通常先讲绝对位置编码,尤其是 Transformer 原论文中的正弦-余弦位置编码

1. 绝对位置编码(Absolute Positional Encoding)

直接告诉模型:

  • 这是第 1 个位置
  • 这是第 2 个位置
  • 这是第 3 个位置
正弦-余弦位置编码

Transformer 原论文提出了一种固定的、无须学习的位置编码方式(正弦-余弦位置编码):
对于位置 $pos$ 和维度索引 $i$,定义:
$$PE(pos, 2i) = \sin\left(\frac{pos}{10000^{2i/d_{\text{model}}}}\right) $$$$PE(pos, 2i+1) = \cos\left(\frac{pos}{10000^{2i/d_{\text{model}}}}\right)$$
其中:

  • $pos$ 是位置编号
  • $i$ 是维度索引
  • $d_{\text{model}}$ 是模型维度

可以把它理解成:每个位置都被映射成一组不同频率波形的取值组合。
类似于:

  • 一个位置向量像是一串“位置指纹”
  • 这个指纹既唯一,又具有平滑变化规律
    所以模型既能区分位置,也能感受到位置之间的相对关系。
学习式位置编码

直接把位置当作离散 id,然后学习一个位置 embedding 表。
例如:

  • 第 0 个位置有向量 $p_0$
  • 第 1 个位置有向量 $p_1$
  • 第 2 个位置有向量 $p_2$
    一直到最大长度。
    写成矩阵形式,就是一个:
    $$
    P \in \mathbb{R}^{L \times d_{\text{model}}}$$
    其中:
  • $L$ 是最大序列长度
  • 每一行对应一个位置向量
    这种方法和 token embedding 很像,只不过对象不是词,而是位置。

2. 相对位置编码(Relative Positional Encoding)

强调:

  • 当前位置和另一个位置相差多少
  • 两个位置相对远近关系如何

让注意力分数直接感知位置之间的相对距离。
例如在计算第 $i$ 个位置对第 $j$ 个位置的注意力时,不只看内容匹配,还加上与 $(i-j)$ 有关的位置偏置。

Transformer

Transformer 是一种以注意力机制为核心、完全摆脱循环结构、能够高效并行建模序列的神经网络架构。

在 Transformer 出现之前,处理序列的主流模型主要是:

  • RNN
  • LSTM
  • GRU
  • 带注意力的 Seq2Seq
    这些模型虽然有效,但有几个明显问题。
  1. 计算串行,难并行(RNN时间上递推)
  2. 长距离依赖路径太长
  3. Seq2Seq 虽然有注意力,但主干还是 RNN
    能不能完全抛弃循环结构,只靠注意力机制来建模序列?Transformer 就是这个问题的答案。

Transformer 的核心思想是:

  1. 用自注意力机制替代 RNN 的递归
  2. 让序列中任意两个位置都能直接交互
  3. 通过位置编码补充顺序信息
  4. 通过多头注意力从多个角度建模关系
  5. 通过前馈网络进一步变换表示
    Embedded image
    Pasted image 20260411114035

Encoder

的任务是:把输入序列编码成一组上下文化表示。

一层 Encoder 可以总结为:
**第一步:多头自注意力
$$
H’ = \text{LayerNorm}(X + \text{MultiHeadSelfAttention}(X))$$

**第二步:前馈网络
$$
H = \text{LayerNorm}(H’ + \text{FFN}(H’))$$

然后把 $H$ 传给下一层。

Decoder

的任务是:根据已经生成的前缀和 Encoder 的输出,一步一步生成目标序列。

一层 Decoder 的流程更长:
**第一步:Masked Multi-Head Self-Attention
$$
H_1 = \text{LayerNorm}(X + \text{MaskedSelfAttention}(X))$$

**第二步:Encoder-Decoder Attention
$$H_2 = \text{LayerNorm}(H_1 + \text{CrossAttention}(H_1, E))$$

其中 $E$ 是 Encoder 输出。
**第三步:前馈网络
$$
H_3 = \text{LayerNorm}(H_2 + \text{FFN}(H_2))$$

残差连接

例如:
$$\text{Output} = x + \text{Sublayer}(x)$$

作用是:

  • 缓解深层网络训练困难
  • 改善梯度传播

Layer Normalization

作用是:

  • 稳定训练
  • 保持数值分布更平滑
    所以 Transformer 每层通常写成:
    $$
    \text{LayerNorm}(x + \text{Sublayer}(x))$$

前馈网络(FFN)

Position-wise Feed Forward Network
公式通常写成:
$$\text{FFN}(x)=W_2\ \sigma(W_1x+b_1)+b_2$$
其中 $\sigma$ 可以是:

  • ReLU
  • GELU
    这个网络的特点是:
  • 对每个位置独立应用
  • 不和其他位置交互
  • 主要负责非线性变换和特征提炼
    可以理解为:
  • 注意力负责“位置间信息交换”
  • FFN 负责“每个位置内部特征加工”

掩码多头注意力(Masked Attention)

因为 Decoder 是自回归生成的。在第 $t$ 步预测时,只能使用:
$$y_1, y_2, \dots, y_{t-1}$$
不能偷看未来的 $y_t, y_{t+1}, \dots$
所以在 Decoder 的自注意力里,要把未来位置 mask 掉。
常见做法是给未来位置一个很小的分数,例如:
$$
-\infty$$
softmax 后这些位置权重就变成 0。

多头注意力(Multi-Head Attention)

单头注意力只学习一种关注模式,但现实中一个位置可能同时与其他位置存在多种不同关系,例如:

  • 语法关系
  • 语义关系
  • 位置关系
  • 指代关系
    所以 Transformer 引入了多头注意力。

核心思想是:把注意力拆成多个“头”,每个头在不同子空间中独立学习注意力模式,然后再把结果拼接起来

假设有 $h$ 个头,则对每个头分别计算:
$$
\text{head}_i = \text{Attention}(Q_i, K_i, V_i)$$

然后拼接:
$$
\text{MultiHead}(Q,K,V)=\text{Concat}(\text{head}_1,\dots,\text{head}_h)W_O$$

这样做的好处是:

  • 不同头可以关注不同关系
  • 提高表达能力
  • 模型更灵活

BERT

BERT = Bidirectional ==Encoder== Representations from Transformers
中文通常翻译为:
基于 Transformer 的双向编码表示模型

如果先用一句话概括 BERT:
BERT 是一个基于 Transformer 编码器的预训练语言表示模型,它通过大规模无监督预训练学到通用语言知识,再通过微调适配各种下游 NLP 任务。
它的重要性在于:
BERT 让自然语言处理从“针对每个任务单独设计模型”,转向了“先大规模预训练,再针对具体任务微调”的范式。

BERT 的主干是 多层 Transformer Encoder 堆叠
输入经过:

  1. Token Embedding
  2. Segment Embedding
  3. Position Embedding
    三者相加后,进入多层 Transformer Encoder。
    输出是每个位置的上下文化表示。
    整体可以写成:
    $$
    \text{Input Embedding} \rightarrow \text{Transformer Encoder Layers} \rightarrow \text{Contextual Representations}$$
    Pasted image 20260411115901

**1. Token Embedding

表示每个 token 本身是什么词或子词。

例如:

我 喜欢 NLP

先经过 tokenizer 切成 token,再映射成 token embedding。


**2. Position Embedding

因为 Transformer 本身没有顺序感,所以需要位置编码告诉模型:

  • 哪个 token 在第 1 个位置
  • 哪个 token 在第 2 个位置

BERT 使用的是可学习的位置嵌入


**3. Segment Embedding

BERT 原始设计支持句对输入,例如:

  • 句子 A
  • 句子 B

它会给不同句子分配不同的 segment id:

  • 句子 A 的 token 加上 segment A embedding
  • 句子 B 的 token 加上 segment B embedding

这在句子关系判断任务中很有用。

BERT 是 Encoder-only 结构,并且 MLM 会遮住部分词进行预测。
因此它非常擅长:

  • 表示学习
  • 文本理解
  • 分类
  • 抽取式任务
    但它不适合直接做自然流畅的自回归生成。