2.1 为什么需要序列模型
第 1 讲我们一直在和定长向量打交道:一张 的图摊平成 维,一组特征写成 维向量,然后交给 MLP。这个设定有一个隐含前提——输入的形状是固定的,所以我们可以为每个输入维度配一个权重,把网络「焊」在形状上。
序列模型的第一句话就是:这个前提不成立。一句话可以是 3 个词,也可以是 300 个词;一段音频可以是 2 秒,也可以是 2 分钟;一只机械臂的传感器读数会一直流下去,没有终点。更麻烦的是,顺序本身携带信息:「狗咬人」和「人咬狗」用的是同一堆词,含义却相反;把时间序列的采样点打乱,物理意义直接消失。
于是问题变成:给定一个长度可变的、顺序有意义的符号序列,怎么设计一个参数数量不随长度变化的函数? 这一节要回答的就是这个问题——先把它写成精确的定义,再给出任务的分类学,然后从历史脉络(n-gram → 神经语言模型)说明为什么必须引入「表示学习」,最后画出本讲的三条技术主线:递推、卷积、注意力。后面七节都是在这三条主线上展开。
设序列 ,其中 ,长度 可变。序列建模(sequence modeling)是学习一个由参数 指定的映射 ,使它对任意 都有定义,且
- 参数不变性: 的规模与 无关(,更不允许 );
- 顺序敏感: 对非平凡的置换 一般成立;
- 一致性:同一组 能作用于训练时未出现过的长度 。
按输出形态,序列任务分成四类(见 定义 2.1.2)。
把三条要求翻译成工程语言:参数不变性排除「给每个位置单独一套权重」——那正是朴素 MLP 的做法, 一长就崩;顺序敏感排除「把序列当成一个无序集合(如对所有词向量做平均或求和)」——词袋模型就是这么干的,它对「狗咬人 / 人咬狗」没有任何区分能力;一致性排除「pad 到训练时的最大长度再硬接 MLP」——不是不能跑,而是永远无法处理更长的输入,参数量还随最长长度线性膨胀。三条要求合起来,只剩一条路:让同一套函数在不同位置反复作用,也就是「沿序列方向的权值共享」。这正是 RNN、CNN 与注意力三种设计共同遵守的公约。
2.1.1 序列数据的三个性质与它们的代价
把上面三条要求具体化成数据侧的三个性质,并各自标注「如果不管它会付出什么」。
| 性质 | 含义 | 如果不管它 | 对应方案 |
|---|---|---|---|
| 变长 | 不固定,且测试时可能比训练时更长 | 权重形状固定 ⇒ 只能截断或 pad;截断丢信息,pad 引入无效计算与错误统计量 | 权值共享 + 掩码 / 动态图长度 |
| 保序 | 位置 的语义依赖前文上下文 | 词袋式平均 ⇒ 无法区分语序与搭配 | 递推状态、位置编码、因果掩码 |
| 参数共享 | 同一模式出现在任意位置都该由同一组参数处理 | 每个位置一套权重 ⇒ 参数量 、样本效率崩塌、无法外推 | 循环权重矩阵、卷积核、注意力算子 |
值得强调的是第三条既是要求也是红利:权值共享让一个在位置 1 学到「主谓一致」的检测器自动适用于位置 50,这是参数效率与泛化能力的共同来源。RNN 的 、卷积核的 、注意力的 ,本质上都是同一份「跨位置复用的函数」。
还有一处容易被忽略的代价:变长带来的「无效位置」必须显式剔除。把一批长度不同的序列凑成一个张量,只能 pad 到批内最长,而 pad 位置既不是数据、也不该产生梯度。如果只在模型内部跳过 pad、却忘了在损失端屏蔽它们,模型就会花一部分容量去学「预测 pad」,同时把损失均值拉向「容易的位置」——这也是序列任务里最常见的隐性偏差来源(2.1.6 给出屏蔽写法)。
2.1.2 任务的分类学:序列 → 什么
按「输入是序列 / 输出是序列」以及「对齐方式」,序列任务分为四类:
| 形态 | 目标函数 | 输出与输入的长度关系 | 典型例子 |
|---|---|---|---|
| 序列 → 标签 | 输出是单个标签,与 无关 | 情感分类、音频事件识别、心电图异常判别 | |
| 同步对齐(序列 → 等长序列) | ,逐时刻输出 | 词性标注、命名实体识别、逐帧语音识别、逐时刻动作分类 | |
| 序列 → 变长序列(异步) | , 一般成立 | 长度由任务决定 | 机器翻译、语音转写、摘要 |
| 自回归生成 | 建模 ,把上一步的输出接回输入 | 逐步生成,长度自定 | 语言模型、音乐生成、代码补全 |
四类的差别直接决定结构的选择:同步对齐只需要「看一眼过去」,可以用因果(单向)结构;序列 → 标签常常允许看完整句子,所以编码器可以双向;序列 → 变长序列需要一个「编码器 + 解码器」的组合(2.4 的动机);自回归生成则强制因果——第 步只能看 的位置,任何「偷看未来」的结构在推理时都不成立。
这四类任务最实用的区别不是输出形状,而是因果约束。同步对齐与自回归生成是因果的:位置 的输出不许依赖 的输入;序列 → 标签(句级分类)与编码器部分则是非因果的:整句话在手上,双向看没问题。搞混这一条会出现两类经典事故:给语言模型用了双向编码器(训练指标极好、生成时无从下手),或者反过来给「实时逐帧预测」的任务用了双向结构(离线评测漂亮,上线必然失败——因为在线时未来还没发生)。本书后面凡是提到「双向」,都会同时点明它是否可用。
2.1.3 与 MLP / CNN 的根本差别
三种结构处理「空间(位置)方向」的方式完全不同,把它们并排看一次,后面的取舍就都能解释了。
| 结构 | 跨位置的信息通路 | 感受野 | 参数量随 的变化 | 能否并行(沿位置) | 长程依赖能力 |
|---|---|---|---|---|---|
| MLP(把序列摊平) | 无共享,位置各配权重 | 全部位置(但不是共享的) | (甚至 ) | 能 | 理论上有,实际不可训练 / 不可外推 |
| 因果膨胀卷积 | 局部核 + 空洞(dilation)堆叠 | 随层数指数扩张 | (核大小固定,与 无关) | 能 | 需要足够多堆叠层;路径长度 级 |
| 递推网络(RNN 族) | 状态沿时间串行传递 | 无界(原则上) | 不能(依赖前一步) | 形而上无界,实际被梯度消失截断在几十步 | |
| 注意力 | 任意两位置直接相连 | 全局 | (与 无关) | 能 | 路径长度 ,但与 的算力换 |
读这张表要抓住两处对比。第一处是「并行」这一列:MLP 与卷积天然并行,因为它们对每个位置的处理互不依赖;RNN 不能并行,因为 依赖 ——这是它的效率原罪,门控(2.3)改善的是记忆而不是并行。第二处是「路径长度」:在递推结构里,位置 1 的信息要经过 次状态更新才能影响位置 ,梯度同样要走 步(2.2 的主题);而注意力里任意两点之间只有一步。
既然卷积「并行 + 参数与长度无关」,为什么不直接把一维序列当成 的图像,用卷积一路堆下去?答案在因果性与感受野的性价比上。因果卷积必须只用左边的点(否则就是偷看未来),于是要么把核开得很大(参数量与计算量随之上涨),要么用空洞卷积堆很多层(层数 ,深了以后训练与前向延迟都变贵)。更要紧的是,逐帧在线预测要求「每来一个新采样点就能立刻给出输出」,卷积需要每步重新算一个窗口,递推只需要更新一次状态——这正是 RNN 在流式场景里至今仍有位置的原因(见 2.2 的陷阱框)。本章不把卷积当作序列建模的第三条主线展开,但请记住:「用局部窗口 + 膨胀」与「用状态递推」是同一种取舍的两种写法。
考虑因果膨胀卷积(causal dilated convolution):核宽 、第 层空洞率 ()。
(1) 推导堆 层后的感受野(能回看多远)。 (2) 设目标上下文长度 与 ,分别需要多少层? (3) 位置 1 的信息传到位置 的路径长度是多少?与 RNN 相比如何?
(1) 第 层的核跨越 个间隔、 个点,因此把感受野扩大 :
(2) 由 得 : 需 层; 需 层。也就是说层数只需随 对数增长——这是卷积式方案的核心优势(RNN 要覆盖 步需要 步串行,注意力要 次比较)。
(3) 因果卷积里位置 1 与位置 之间的信息路径长度为 (每一层跨一个更大的间隔)。对比:RNN 是 的串行依赖链,注意力是 但每对位置都要算一次。三者的「路径长度—每步成本」组合正好构成 method 2.1.1 那张表的三行。
一个必须补的限定:感受野大不等于信息能无损传过去。卷积层数一多,梯度同样要穿过 个非线性层,而且卷积核本身可视为一种稀疏矩阵——卷积式方案的长程能力同样依赖残差连接与归一化来维持。「参数量与长度无关」只是入场券,不是长程建模能力的保证。
∎2.1.4 历史脉络:从 n-gram 到神经语言模型
在神经网络之前,序列建模有一套成熟的统计做法:n-gram 语言模型。它清晰、可解释、可枚举,也正好暴露了「不做表示学习」的代价。
给定词表大小 与序列 ,-gram 模型用马尔可夫假设截断历史:
退化为按词频采样(完全丢掉顺序), 只看前一个词(bigram), 看前两个词(trigram)。参数表就是所有 -gram 的条件概率,规模约 。
是中等词表的量级。于是:bigram 表约 个计数单元;trigram 约 ;5-gram 约 ——后者已经超过地球上可用的存储能力好几个数量级。更糟的是即使表能建起来也填不满:一个 词级别的语料里,绝大多数 5-gram 组合从未出现,概率会被估成 0(于是整句概率为 0)。经典的做法是各种平滑(把一部分概率质量分给未见组合,如 Kneser–Ney 类方法),但平滑只能在「已经离散化」的计数上做修补,它无法回答一个更本质的问题:「猫」和「狗」没见过同一堆上下文,但它们本该相似——因为它们作为词本身是相似的。要让模型利用这种相似性,必须先把词变成向量,而这就是表示学习的入口(第 3 讲与第 6 讲会正式展开词嵌入与大模型训练)。
设词表 ,语料规模 个 token。
(1) 分别估算 时参数表(计数单元)的规模,并与 比较。 (2) 假设我们要建模「相隔 100 个词的主谓一致」, 需要取多大?给出参数表规模。 (3) 用上面的数字说明:为什么 n-gram 与神经网络必须解决的是两个不同的问题。
(1) 表规模约 (更精确地说是 个可能的 -gram 组合):
| 组合数 | 与语料 之比 | |
|---|---|---|
| 1 | (远小于语料,可估准) | |
| 2 | (每个二元组平均只见到 4 次) | |
| 3 | (绝大多数单元没数据) | |
| 5 | (完全不可行) |
(2) 马尔可夫假设的阶数 决定了「看多远」:要覆盖 100 步之前的依赖,至少需要 ,则表规模约 ,任何存储与估计算法都无意义。这说明 n-gram 的长程依赖不是「算得慢」,而是「结构上就不支持」。
(3) 两个问题的分工是:n-gram 的问题在如何用有限数据估计一个巨大的离散概率表(统计估计、平滑、回退);神经网络的问题在如何用一个参数量可控的连续函数压缩历史(表示学习、梯度优化)。前者无论怎么改进都受限于 ,后者把参数从「所有组合」变成「一个共享的函数」——这正是 定义 2.1.1 第 1 条「参数量与 无关」的意义。
∎2.1.5 三条主线:递推 / 卷积 / 注意力
把历史与结构合起来看,处理序列的现代方案只有三类:把历史压进状态(递推),用局部窗口扫(卷积),或者直接对所有位置加权求和(注意力)。三者可以互相嵌套——今天的主流模型往往是「注意力为主干 + 卷积做局部 + 递推做流式」的混合体(见 2.7)。
| 递推(RNN / LSTM / GRU / SSM) | 卷积(因果膨胀卷积) | 注意力(Transformer / 线性注意力) | |
|---|---|---|---|
| 状态 / 记忆 | 固定维度向量 (有损摘要) | 无显式状态,靠层叠感受野 | 全部位置的键值对(无损但昂贵) |
| 每步计算量 | (单步) | (窗口 ) | 训练 ;增量解码 |
| 沿位置并行 | 否(必须串行) | 是 | 是 |
| 任意两点的路径长度 | 级(多层膨胀) | ||
| 推理时的内存 | (只留状态) | 需要窗口缓冲 | 通常 (KV cache) |
| 最适合 | 流式 / 超长序列 / 小数据 | 局部结构强的信号(音频波形、传感器) | 需要长程精确检索的大规模训练 |
| 代表节点 | 2.2 RNN、2.3 门控、2.7 SSM | 因果卷积体系(本讲不展开) | 2.4–2.5 Transformer |
一句话记住这张表:递推省内存、卷积省结构、注意力省路径——三者的代价分别是「不能并行」「感受野要堆」「平方开销」。
真正的结构选择不是「哪个模型最强」,而是按约束逐层排除:
- 因果约束:输出必须在读到当前输入后立刻给出吗?是 ⇒ 只能用因果结构(递推 / 因果卷积 / 带掩码的注意力);否 ⇒ 可以先做双向编码(2.1.2)。
- 序列有多长:几百以内 ⇒ 注意力最省心;上万 ⇒ 必须处理 (滑窗 / 稀疏 / 状态空间 / 分块,见 2.6)。
- 数据有多少:几千条以内 ⇒ 带强归纳偏置的递推或卷积通常更稳;数据足够大 ⇒ 注意力类结构的收益才会显现(这条是工程经验,不是严格定理)。
- 部署形态:流式 / 端侧 ⇒ 递推与状态空间(每步 内存);离线批处理 ⇒ 注意力与卷积(并行度高)。
四条的顺序有意义:因果约束先于长度,长度先于数据量,数据量先于部署形态——越靠前的条件越不可协商,先按它砍掉一半选项,再比较剩下的。
工程上真正被采用的方案几乎都是混合体:注意力负责全局检索(「我需要哪个位置的信息」),卷积负责局部模式(相邻 token 的搭配、音素的短时结构),递推或状态空间负责流式与超长上下文(每步 的内存)。2.7 会把这类混合架构(如按比例交错注意力层与状态空间层)单列一节。理解本讲的方式不要是「记住三个名字」,而是「记住三笔账」——每引入一种机制,就问它把哪一笔账变便宜了、又把哪一笔变贵了。
2.1.6 代码:变长序列怎么进网络(批处理、掩码与形状)
序列代码里最容易出错的地方不是模型,而是批处理:同一个 batch 里的序列长度不同,必须 pad 到等长,而 pad 出来的位置不是数据。下面这段代码把三件事一次说清:形状约定、打包(packing)、以及掩码该加在哪里。
import torch, torch.nn as nn
B, T_max, d_x, d_h = 4, 6, 8, 16
x = torch.randn(B, T_max, d_x) # 批优先(batch-first):(B, T, d)
lengths = torch.tensor([6, 4, 2, 3]) # 每个样本的真实长度(注意 T_max=6)
# ① 掩码:True 表示「是 pad,不算」
idx = torch.arange(T_max).unsqueeze(0) # (1, T)
mask = idx >= lengths.unsqueeze(1) # (B, T) bool
# ② 让循环网络跳过 pad:入参必须是长度降序(或 enforce_sorted=False)
packed = nn.utils.rnn.pack_padded_sequence(
x, lengths, batch_first=True, enforce_sorted=False)
gru = nn.GRU(d_x, d_h, batch_first=True)
out_packed, h_n = gru(packed)
out, _ = nn.utils.rnn.pad_packed_sequence(out_packed, batch_first=True)
print(out.shape) # (4, ?, 16):packed 后按各样本真实长度还原,T 维随样本而变
print(h_n.shape) # (1, 4, 16):num_layers*num_directions = 1, 批, 隐维
# ③ 损失端:pad 位置的 logits 必须被屏蔽(否则模型在学「预测 pad」)
logits = torch.randn(B, T_max, 10) # 例如 10 类 token
target = torch.randint(0, 10, (B, T_max))
loss = nn.functional.cross_entropy(
logits.reshape(-1, 10), target.reshape(-1), reduction="none")
loss = loss.view(B, T_max)
loss = (loss * (~mask)).sum() / (~mask).sum() # 只对真实位置求均值
print(loss.item())
三个必须记住的点:
- 形状约定是批优先: 中第 0 维永远是批、第 1 维是时间。这与第 1 讲的「批维不变」一脉相承,所有序列算子(RNN、注意力、卷积)都遵守它。
h_n不是「每个样本最后一个真实时刻的状态」——如果不做 packing,循环网络会把 pad 也走一遍,返回的第 步状态对应的是 pad 之后的状态。用pack_padded_sequence才能拿到「真实末尾」的状态;这也是「分类任务用最后一个隐状态」这类写法最常见的翻车点。- 掩码要出现在损失里:只在模型里跳过 pad 是不够的,输出端的交叉熵同样要被屏蔽,否则 pad 位置会持续贡献梯度。
| 量 | 形状 | 在代码里是谁 | 说明 |
|---|---|---|---|
| 输入(已 pad) | x | 是批内最长长度,不是全局最长 | |
| 真实长度 | lengths | 每个样本的有效时间步数 | |
| 掩码 | mask | True 表示 pad;损失端用它按元素清零 | |
| 循环网络输出 | out | 每个时间步的隐状态(pad 位置无意义) | |
| 末状态 | h_n | 不打包时它对应 pad 之后的状态,这是最常见的隐性 bug | |
| logits | logits | 为词表 / 类别数;拼接成 后交给交叉熵 |
- 「序列任务就是把序列 pad 到定长再用 MLP」:能跑,但违反 定义 2.1.1 的两条要求——参数量随最长长度增长,且测试时更长的序列无从处理。pad + MLP 只适合样本长度高度集中(如固定窗口振动信号)的窄场景。
- 「词袋 + MLP 与 RNN 差不多,反正最后都做分类」:词袋丢弃了顺序(定义 2.1.1 第 2 条)。对「否定词、语序、修饰关系」敏感的任务(情感分类里的「不」、命名实体里的搭配)会系统性变差,且差距随句子变长扩大。
- 「序列 → 标签任务里,用双向结构总比单向好」:只有在「整段输入在推理时已经全部可获取」时才成立。逐帧在线任务(实时语音、在线动作识别)不能双向,因为未来数据还没产生——这是任务约束而不是模型偏好问题。
- 「n-gram 完全没有价值,只是历史」:n-gram 至今仍在工程里充当基线、语音识别系统的浅融合特征与拼写纠错的一环;它的价值在于快且可解释。本书强调它的不足,是为了说清「神经网络换了什么问题来解」,而不是宣告它无用。
Q1 用一句话写出序列建模的三条要求,并各举一个「违反它」的做法。
Q2 词表 、语料 :trigram 的条件概率表有多少个单元?平均每个单元能分到多少计数?这会导致什么问题?
Q3 判断以下两个任务是否允许使用双向编码器,并说明理由:① 句级情感分类;② 在线实时手语逐帧翻译。
Q4 因果膨胀卷积与 RNN 都能让「参数量与 无关」,它们在在线流式推理上的关键差别是什么?
参考要点
Q1 ① 参数不变性——违反者:给每个位置配一套权重的摊平 MLP;② 顺序敏感——违反者:对所有词向量取平均的词袋模型;③ 一致性(可外推到未见长度)——违反者:把序列截断 / pad 到固定长度再交给固定形状的网络。
Q2 个单元,而语料只有 个 token,平均每个单元 次——也就是说绝大多数 trigram 的计数为 0,直接做最大似然估计会得到大量零概率。需要平滑 / 回退把概率质量分给未观测组合,但平滑无法解决「没见过同一上下文的相似词应该共享统计强度」这件事。
Q3 ① 允许:整句话在推理时一次给全,双向可以同时利用左右上下文,通常更准;② 不允许(至少不能直接用双向):第 帧的输出在推理时必须立刻给出,此时 之后的数据尚不存在,双向结构会造成训练与部署的不一致(离线指标虚高);若一定要用双向模型,需要引入固定的延迟窗口并明确标注它是「带延迟」的在线系统。
Q4 关键差别在每个新采样点到达时的增量计算量:RNN 只需更新一次状态(),卷积需要重新算一个窗口的卷积(, 为窗口大小),且需要维护窗口缓冲。因此超低延迟的流式场景更偏向递推类结构(含 2.7 的状态空间模型),而离线或允许批处理的场景更偏向并行度高的卷积与注意力。
- MIT 6.S191《Introduction to Deep Learning》第 2 讲 Deep Sequence Modeling(官网
introtodeeplearning.com)——母本对「为什么需要序列模型」的原始讲法,配合它的音乐生成实验一起看。 - 本书 Lab 1 · 深度学习入门与音乐生成——把本节的三条要求落到「旋律 = token 序列」这个具体任务上。
- 经典统计语言模型与平滑(n-gram、Kneser–Ney 类方法)——任何一本统计自然语言处理教材的相关章节;本书不引具体编号,只要求你理解「 表 + 平滑」和「共享函数 + 梯度」是两种不同的世界观。
- 关于「长依赖为什么难学」的经典分析,见 2.2 的延伸阅读(Bengio 等 1994;Pascanu 等 2013)。
本节小结:序列建模的三条要求(变长 / 保序 / 参数不变)把「给每个位置配一套权重」和「把序列当无序集合」两条捷径都堵死了,只剩「沿序列方向共享同一套函数」这条路。任务的四种形态(序列→标签、同步对齐、异步序列→序列、自回归生成)之间最重要的差别是因果约束,它决定了结构可选单向还是双向。n-gram 的历史告诉我们:不做表示学习,参数会按 爆炸且数据永远稀疏。下一节从最朴素的那条共享之路开始——让网络自带状态,读一步更新一步,并看看梯度沿时间传回去时会发生什么。