01 / 从注意力到完整模型
注意力是部件,Transformer 是组织部件的结构
上节学了 Q/K/V、softmax、多头与因果掩码。注意力混合不同位置的信息;一个 Transformer 块还包含残差连接、LayerNorm 和逐位置前馈网络。多个块接起来,最后把每个位置的向量映射成词表中每个字符的分数。
本节实际代码是 decoder-only 字符语言模型:每个位置只能读自己和左侧。B 是批次,T 是字符数,D=32 是模型宽度,V=18 是本次小语料的字符种数。
02 / 看清一个块的两段计算
注意力跨位置交流,FFN 在每个位置内部变换
选一个阶段,沿代码实际采用的 Pre-LN 顺序看。两次残差相加都要求分支输出与主路输入形状相同。
FFN 的两层 Linear 在每个位置使用同一套参数,只处理该位置的特征轴;它本身不让不同位置互相交流。跨位置的信息来自因果自注意力。代码用 4 个 head,每头 8 维,堆了 2 个块。
03 / 四个数的演算
残差保留原输入,LayerNorm 调整特征尺度
下面的四维向量是独立的玩具例子,不是训练模型的内部激活。残差分支也有四维,因此可以逐元素相加。LayerNorm 对一个位置的最后四个特征求均值和方差,不跨不同样本或时间位置。
直接保留从输入到输出的一条路径,让子层可以在原有表示上作修正。
实际层还有逐特征可学习的 γ 和 β;PyTorch 默认初值为 γ=1、β=0。
本节用 Pre-LN:先归一化,再进子层,最后与未归一化的主路相加。也存在 Post-LN 排法。PyTorch LayerNorm 文档。
04 / 不同结构的信息流
Encoder、Decoder 和只用 Decoder 的模型
切换结构与输出位置,看它能读取哪些位置。四格图只表示允许读取的范围;具体注意力权重仍由 Q/K 决定。本节实际训练的只有 decoder-only。
源序列中每个位置可以参考整个源序列;有 Padding 时仍须屏蔽填充。
同一序列中的位置只能参考自己与左侧,用前缀预测下一个 Token。
先编码源序列;目标侧自注意力保持因果,另有交叉注意力读取源序列。
交叉注意力:Query 来自目标侧,Key / Value 来自 Encoder 输出;权重形状是 (B,H,T_target,T_source),不一定是方阵。
05 / 训练目标
输入向右错开一格,就是目标
下表来自训练语料开头。位置 t 读当前字符,目标是 t+1 的字符。训练时一次送入一段字符,同时计算所有位置的损失;因果掩码保证各位置不会从右侧输入偷看答案。
真正生成时只能拿现有前缀预测一个新字符,再追加到前缀,重复下一步。这个实验的语料只有四句,所有训练窗口都来自这四句,没有独立测试集。
06 / 本机实际训练记录
损失下降,只能说明它学会了这个玩具语料
曲线是整份训练窗口上的交叉熵。窗口高度重叠,生成前缀也来自训练语料;生成熟悉句子主要说明模型能记忆并复现局部模式。不能据此推断它理解自然语言或在未见文本上表现良好。
07 / 一次只选一个新字符
逐步查看自回归生成
这里展示这次 CPU 运行的贪心生成记录:每一步选概率最高的字。用按钮或滑块查看当前上下文、候选概率和追加后的文本。超过上下文长度时只保留最近 16 个字符。
页面展示已记录的结果,不在浏览器里训练 PyTorch。重新训练、改参数或随机种子,概率可能改变。
08 / 动手验证
本地 CPU PyTorch 练习
代码包含模型、训练窗口、因果隔离检查、训练记录、逐字生成与保存/重新加载。依赖只有项目已有的 CPU PyTorch;无需下载数据或启动服务。
uv run python lessons/lesson-12-transformer.py- 画出
x + Attention(LayerNorm(x))的主路和分支,解释形状为何都是(B,T,D)。 - 设
B=2,T=16,D=32,H=4,V=18,先写出每头 Q/K/V、注意力权重和 logits 的形状,再核对页面。 - 把
HEADS改为 2,预测每头宽度。再设D_MODEL=30,HEADS=4,解释为什么会报错。 - 给语料加入一句新短语并重新运行,比较损失与生成文本;记得泛化能力需要独立测试。
- 用自己的话区分“训练时并行预测所有下一字”与“生成时每次只选一个字”。
09 / 自测