AI 学习课程 / LESSON 12

第十二节:Transformer

把因果自注意力接上残差、LayerNorm 和前馈网络,组成可堆叠的模块;再看一个实际训练的字符模型如何逐字生成。

模块结构数字演算Encoder / Decoder逐字生成离线 HTML

01 / 从注意力到完整模型

注意力是部件,Transformer 是组织部件的结构

上节学了 Q/K/V、softmax、多头与因果掩码。注意力混合不同位置的信息;一个 Transformer 块还包含残差连接、LayerNorm 和逐位置前馈网络。多个块接起来,最后把每个位置的向量映射成词表中每个字符的分数。

字符 ID(B,T)
Token + 位置向量(B,T,D)
Transformer 块 × 2(B,T,D)
最终 LayerNorm(B,T,D)
输出线性层(B,T,V)

本节实际代码是 decoder-only 字符语言模型:每个位置只能读自己和左侧。B 是批次,T 是字符数,D=32 是模型宽度,V=18 是本次小语料的字符种数。

关键是跟住形状:时间轴在块内保持不变;最后的输出层只把每位置的 32 维表示转换成 18 个候选字符分数。

02 / 看清一个块的两段计算

注意力跨位置交流,FFN 在每个位置内部变换

选一个阶段,沿代码实际采用的 Pre-LN 顺序看。两次残差相加都要求分支输出与主路输入形状相同。

u = x + CausalAttention(LayerNorm₁(x)) y = u + FFN(LayerNorm₂(u)) FFN(z) = Linear₂(GELU(Linear₁(z))) (B,T,32) → FFN 内部 (B,T,64) → (B,T,32)

FFN 的两层 Linear 在每个位置使用同一套参数,只处理该位置的特征轴;它本身不让不同位置互相交流。跨位置的信息来自因果自注意力。代码用 4 个 head,每头 8 维,堆了 2 个块。

03 / 四个数的演算

残差保留原输入,LayerNorm 调整特征尺度

下面的四维向量是独立的玩具例子,不是训练模型的内部激活。残差分支也有四维,因此可以逐元素相加。LayerNorm 对一个位置的最后四个特征求均值和方差,不跨不同样本或时间位置。

残差相加

直接保留从输入到输出的一条路径,让子层可以在原有表示上作修正。

LayerNorm

实际层还有逐特征可学习的 γ 和 β;PyTorch 默认初值为 γ=1、β=0。

μ = mean(x₁,…,x_D) σ² = mean((xᵢ−μ)²) LayerNorm(xᵢ) = γᵢ · (xᵢ−μ)/√(σ²+ε) + βᵢ

本节用 Pre-LN:先归一化,再进子层,最后与未归一化的主路相加。也存在 Post-LN 排法。PyTorch LayerNorm 文档。

04 / 不同结构的信息流

Encoder、Decoder 和只用 Decoder 的模型

切换结构与输出位置,看它能读取哪些位置。四格图只表示允许读取的范围;具体注意力权重仍由 Q/K 决定。本节实际训练的只有 decoder-only。

Encoder

源序列中每个位置可以参考整个源序列;有 Padding 时仍须屏蔽填充。

Decoder-only

同一序列中的位置只能参考自己与左侧,用前缀预测下一个 Token。

Encoder–Decoder

先编码源序列;目标侧自注意力保持因果,另有交叉注意力读取源序列。

交叉注意力:Query 来自目标侧,Key / Value 来自 Encoder 输出;权重形状是 (B,H,T_target,T_source),不一定是方阵。

05 / 训练目标

输入向右错开一格,就是目标

下表来自训练语料开头。位置 t 读当前字符,目标是 t+1 的字符。训练时一次送入一段字符,同时计算所有位置的损失;因果掩码保证各位置不会从右侧输入偷看答案。

输入 x = [c₀,c₁,…,c_{T−1}] 目标 y = [c₁,c₂,…,c_T] logits.shape = (B,T,V) loss = 对所有 B×T 个目标位置的交叉熵取均值

真正生成时只能拿现有前缀预测一个新字符,再追加到前缀,重复下一步。这个实验的语料只有四句,所有训练窗口都来自这四句,没有独立测试集。

06 / 本机实际训练记录

损失下降,只能说明它学会了这个玩具语料

训练损失

配置与形状

曲线是整份训练窗口上的交叉熵。窗口高度重叠,生成前缀也来自训练语料;生成熟悉句子主要说明模型能记忆并复现局部模式。不能据此推断它理解自然语言或在未见文本上表现良好。

07 / 一次只选一个新字符

逐步查看自回归生成

这里展示这次 CPU 运行的贪心生成记录:每一步选概率最高的字。用按钮或滑块查看当前上下文、候选概率和追加后的文本。超过上下文长度时只保留最近 16 个字符。

这一步看到的上下文

下一字概率前四名
p(c_next | 当前前缀) = softmax(logits[最后一个位置]) 新前缀 = 旧前缀 + 选中的 c_next

页面展示已记录的结果,不在浏览器里训练 PyTorch。重新训练、改参数或随机种子,概率可能改变。

08 / 动手验证

本地 CPU PyTorch 练习

代码包含模型、训练窗口、因果隔离检查、训练记录、逐字生成与保存/重新加载。依赖只有项目已有的 CPU PyTorch;无需下载数据或启动服务。

uv run python lessons/lesson-12-transformer.py

阅读代码 · 查看运行结果 · 模型权重

  1. 画出 x + Attention(LayerNorm(x)) 的主路和分支,解释形状为何都是 (B,T,D)。
  2. 设 B=2,T=16,D=32,H=4,V=18,先写出每头 Q/K/V、注意力权重和 logits 的形状,再核对页面。
  3. 把 HEADS 改为 2,预测每头宽度。再设 D_MODEL=30,HEADS=4,解释为什么会报错。
  4. 给语料加入一句新短语并重新运行,比较损失与生成文本;记得泛化能力需要独立测试。
  5. 用自己的话区分“训练时并行预测所有下一字”与“生成时每次只选一个字”。

09 / 自测

五道题

1. 本节 Pre-LN 块第一段的顺序?

2. FFN 直接混合不同时间位置吗?

3. 为什么整段序列可并行算下一字损失?

4. 本节实际训练了哪种模型?

5. 训练损失低能证明对新文章泛化良好吗?

下一节:大语言模型的训练与使用。打开第十三节互动讲义。 从这个玩具模型走向预训练、指令微调、上下文窗口与检索增强生成。