AI 学习课程 / LESSON 10

第十节:文本与序列建模

从一句短文本出发,逐步看到 Token、整数 ID、向量、批次和“预测下一个 Token”的训练目标,再理解 RNN 如何沿时间传递信息。

分词互动Embedding 查表文本批次与掩码真实 CPU 训练离线 HTML

01 / 从图像来到文本

词有顺序,长度也不一定相同

图像张量常用 (B,C,H,W);文本先被切成一个个 Token,再把每个 Token 变成整数 ID。一批文本的 ID 常排成 (B,T):B 是句子条数,T 是批次中的序列长度。顺序很重要:“小猫 追 小狗”和“小狗 追 小猫”包含相同词,意思却不同。

B批次内句子数
T每句占用的位置数
V词表大小
D每个 Token 的向量长度

本节从七句人工编写的玩具语料学习机制。它不是一个能理解自然语言的大模型;训练结果仅说明代码确实执行了序列预测。

02 / 先决定什么算一个 Token

分词规则决定 ID 的含义

“小猫 喜欢 小鱼”按空格切成 3 个词级 Token;如果去掉空格并逐字切,就有 6 个字级 Token。这里的空格是我们事先标注的词边界,不是自动中文分词。实际系统还常用子词 Token,边界由其分词器和词表决定。

注意:Token ID 只是词表行号,不表示“谁比谁大”。相同数字在不同词表中也未必对应同一个 Token;遇到词表外内容,本玩具示例会用 <UNK>,真实分词器可能用子词拆开。

03 / 查表,而不是对整数做算术

Embedding 把一个 ID 变成一行向量

词表有 V=15 行。为了容易看清查表,本页用 D=3 的固定演示表;运行模型另外使用可训练的 D=8 向量。点击表中的行,观察选中 ID 如何索引出向量。演示表里的 0.1、0.2 等数字是人为设定的,没有语义。

E.shape = (V, D) = (15, 3) id = 4 → E[4] = [1.2, 1.3, 1.4] input_ids.shape = (B, T) → E[input_ids].shape = (B, T, D)
词表与演示向量
当前选中的 Token

从数学上看,取一行也等价于一个 one-hot 行向量乘矩阵 E;实际使用时可直接按 ID 查表,不必造出稀疏的 one-hot 数组。

同一个 Token 的 ID 每次都相同;训练会调整这一行的向量值,让它更有利于任务。<PAD> 的演示向量为全零,模型也用 padding_idx=0。

PyTorch Embedding 文档。

04 / 长短句如何放在一批

补齐、移位和有效位置

输入前放 <BOS> 表示开始,末尾放 <EOS> 表示结束。短句少一个位置,用 <PAD> 补齐。训练下一个 Token 时,输入和目标错开一格;点击任意目标位置。

完整序列 = [<BOS>, 小猫, 喜欢, 小鱼, <EOS>] 输入 x = [<BOS>, 小猫, 喜欢, 小鱼] 目标 y = [小猫, 喜欢, 小鱼, <EOS>]
输入 x:模型已经看见
目标 y:下一位置的答案

PyTorch 批次实际是 x.shape=y.shape=(7,4),共 28 个目标格,其中 27 个有效,1 个是 <PAD>。损失设 ignore_index=PAD_ID,跳过这个目标格;只是把 Embedding 的 PAD 向量置零,不会自动让损失忽略 PAD。

PyTorch CrossEntropyLoss 文档。

05 / 语言模型究竟在学什么

给定前文,预测下一个 Token

在第 t 个位置,模型只能根据已输入的前文输出对 V 个候选 Token 的 logits,softmax 后得到一个分布。训练目标提高真实下一个 Token 的概率;同一句的每个有效位置都贡献损失。

p(x₁,…,xₜ) = ∏ₖ p(xₖ | x₁,…,xₖ₋₁) 位置 t 的损失 = −log p(真实下一个 Token | 已看到的前文) 批次损失 = 有效目标位置损失的平均值

例如只看到 <BOS> 时,还不知道主角是谁;看到 小猫 后,下一词在玩具语料里可能是“喜欢”或“追”。模型学的是条件分布,不要求每个位置都有唯一必然的答案。

下面的 RNN 是单向的,每一步只能利用当前及之前的输入。本节的“移位”让标签变成下一词;不能把当前位置的目标词提前塞进同位置输入,否则就泄漏答案。

06 / 顺序从哪里进入模型

RNN 带着状态逐步读入

同一个 RNN 单元依次处理向量 eₜ 与上一时刻的状态 hₜ₋₁。输出 hₜ 再预测下一词。这里的状态有 16 个数;点击下一步看实际训练模型在第一句各位置的状态和预测。

hₜ = tanh(Wₓ eₜ + Wₕ hₜ₋₁ + b) logitsₜ = Wₒ hₜ + c 输入 (B,T,D=8) → RNN 输出 (B,T,H=16) 最终状态 h_last.shape = (1,B,16),这里是单层单向。

展示的是训练后的实际数值;每个时刻 16 维状态只列前 4 维。即使第一句真实是“小猫…”,只看到 <BOS> 时,模型也可能更偏向语料中出现次数多的“小鸟”。这不是未来信息泄漏。

PyTorch RNN 形状与递推定义。

07 / 为什么还出现 LSTM

普通 RNN 的长距离记忆困难

普通 RNN 把前面的信息压进一个不断更新的状态;训练时梯度也要沿时间反向传递。序列很长时,早期信息可能难以保留,梯度也可能变得很小或很大。LSTM 增加细胞状态 cₜ以及门,学习什么时候保留、写入或输出信息。

遗忘门

决定旧细胞状态的哪些部分继续保留。

输入门

决定本步候选信息写入多少。

输出门

决定细胞状态的哪些部分形成对外状态 hₜ。

cₜ = fₜ ⊙ cₜ₋₁ + iₜ ⊙ gₜ hₜ = oₜ ⊙ tanh(cₜ) 其中门值来自 sigmoid;⊙ 表示逐元素乘法。

LSTM 有助于处理较长依赖,但不保证无限记忆;RNN/LSTM 的时间步仍有先后依赖,难以把整条长序列的所有位置完全并行计算。下一节会看注意力怎样直接建立不同位置之间的联系。PyTorch LSTMCell 定义。

08 / 确认代码真的跑过

小语料训练曲线与形状核对

代码在 7 句玩具语料上训练一个 Embedding(15,8) → RNN(8,16) → Linear(16,15)。下面是实际 CPU PyTorch 运行产生的训练损失;这里只是在训练语料上检查机制,没有独立测试集,不能称作泛化能力。

输入 ID(7,4)
Embedding(7,4,8)
RNN 隐状态(7,4,16)
每位置 logits(7,4,15)

训练准确率不可能简单要求 100%:同一个前文在这些句子里可能对应不同后续词。模型的输出是分布;非零损失也有合理的不可预测部分。

09 / 自己核对张量

本地 PyTorch 练习

程序只用当前项目的 CPU PyTorch,语料直接写在代码中,不下载数据或模型。HTML 已嵌入一次真实运行的结果,直接打开不会重新训练,也无需服务。

uv run python lessons/lesson-10-text.py

阅读代码 · 查看结果 JSON · 训练权重

  1. 切换词级与字级 Token,记录“词表大小”和同一句的 Token 数为何变化。
  2. 手动写出两句话的 <BOS>、<EOS>、x、y 和 <PAD>,与图解核对。
  3. 查一个 Token ID 的演示向量,说明为什么查表输出会增加一个 D 轴。
  4. 把代码的 Embedding 维度从 8 改为 12,同时调整 RNN 的 input_size;预测哪些张量形状改变、哪些不变,再运行验证。
  5. 解释模型在 <BOS> 后不能确定第一词;不要把本节训练集成绩当成真实语言理解成绩。

10 / 自测

五道题

1. 输入 ID 形状为 (7,4),Embedding 维度 8,输出形状?

2. 词级词表的 ID=4 与字级词表的 ID=4 必定同义吗?

3. 预测下一个 Token 时,输入和目标如何配对?

4. Embedding 的 PAD 行为零,损失会自动忽略 PAD 吗?

5. 本节的训练准确率能证明模型会理解陌生文章吗?

下一节:注意力机制。我们会让序列中某个位置直接参考其他位置,并推导 Query、Key、Value 的形状。