Current: Week 8

0%

Week 8

Week 8 - Transformer:把 Attention 组装成可训练的网络

Key question怎样把 token 和位置表示、causal multi-head Attention、FFN、LayerNorm 与 residual paths 组装成可堆叠的 GPT block?

Learning objectives

  • 沿同一批 [我,喜欢] / [猫,喜欢] 的 [2,2,4] tensors 追踪一个 pre-norm Transformer block。
  • 区分跨 token-position mixing 的 Attention 与逐 token channel mixing 的 FFN。
  • 审计每个 residual add、two-head output projection、4→16→4 FFN 与最终 [2,2,5] vocabulary logits。
  • 把 decoder-only 的因果可见性接回 Week 6 的 loss / generation interface,并为 tokenizer/data pipeline 铺垫。

135 min estimated reading time

Week progress: 0 of 20 sections (0%)Course progress: 0 of 321 sections (0%)

Attention 已经能读上下文,但不是完整 GPT。本周始终区分“原始表示仍从旁路传递”与“子层算出的更新量”。前置是 Week 7 的一次 causal attention,以及均值、平方和平方根。

Scroll horizontally to view all columns.

Course data table
学习单元本次解决的问题
一:补上位置同一个 token 的查表向量相同;位置向量告诉模型它出现在哪个位置。
二:加工与旁路FFN 在一个位置内组合特征;残差把子层更新加回原始表示。
三:统一尺度与顺序手算 LayerNorm;沿 x+attention(LN1(x))、再加 FFN(LN2(...)) 追踪两条旁路。
四:把表示交给输出层运行过渡实验,观察 block 输出和词表 logits 的区别,再对照完整两头手算。

运行 python week08_bridge.py。先复现上一周单头,再逐项加部件;下方完整 block 使用另行注明的固定两头参数,不把它当成上一周训练产物。Encoder/Decoder 分类、GELU 特殊函数和新位置方案属于选读。

建议阅读、手算、改代码交替进行,每个单元可拆成几次完成。章节编号保留用于旧链接和回查;按页面从上到下的新顺序学习,不需要按旧编号来回跳转。

Week 8 核心目标:把 Attention 组装成一个可重复的 Block

Scroll horizontally to view all columns.

Course data table
后文简称先用中文理解
Block可反复堆叠的一块计算
per-token / per position对每个位置分别做同一个操作
residual原表示保留,再加上本次修改量
pre-norm先规范分支输入,再计算并加回原表示
didactic weights为了手算指定的教学权重
identity / I保持输入不变的恒等变换,不是 token 身份

第一遍追踪“输入 → 本次新增信息 → 输出”以及 shape;GELU 的误差函数、残差的矩阵导数可以第二遍再读。先会解释组件为什么存在,比同时背下所有公式更有用。

固定教学 batch:Vocabulary 为 0=我、1=喜欢、2=AI、3=学习、4=猫,V_vocab=5;prompt A IDs=[[0,1]](我 喜欢),prompt B IDs=[[4,1]](猫 喜欢);B=2、T=2、C=4、n_head=2、head_size=D=2。

Concept sequence
  1. ids [B,T] = [2,2]
  2. token embeddings [B,T,C] = [2,2,4] + position embeddings [T,C] = [2,4]
  3. residual_0 [2,2,4]
  4. residual_0 + Attention(LN1(residual_0)) → residual_after_attention [2,2,4]
  5. residual_after_attention + FFN(LN2(residual_after_attention)) → residual_after_ffn [2,2,4]
  6. contextual representation [2,2,4]
  7. final LayerNorm + LM head → logits [B,T,V_vocab] = [2,2,5]

两条 Prompt 的 喜欢@1 在 Token+Position 后起点相同,但 Attention 能读取不同的第 0 行(我@0 或 猫@0),因此 residual_after_ffn[:,1,:] 可以不同;未经训练不能据此承诺某个续写。

r0=Etoken(ids)+Epos(0:T),rA=r0+Attention(LN1(r0)),rF=rA+FFN(LN2(rA))r_0=E_{\mathrm{token}}(\mathrm{ids})+E_{\mathrm{pos}}(0{:}T),\quad r_A=r_0+\operatorname{Attention}(\operatorname{LN}_1(r_0)),\quad r_F=r_A+\operatorname{FFN}(\operatorname{LN}_2(r_A))

Knowledge check

哪一步把 我/猫 混入第二位置,哪一步只变换该位置已有的 features?

1. Attention 还缺什么

Scroll horizontally to view all columns.

Course data table
缺少的能力补上的组件喜欢@1 的作用
token 位置/距离position embeddings P让相同 token 的不同 location 有不同输入表示
跨位置通信causal multi-head Attention可读 position 0 的 我 或 猫
每行的非线性特征计算FFN + GELU只处理已 contextualized 的四个 channels
保留与 gradient routeresidual + x把旧表示与 branch update 相加
稳定 branch inputsLayerNorm逐 token row 归一化后再送入 branch
Block:RB×T×CRB×T×C\operatorname{Block}:\mathbb{R}^{B\times T\times C}\to\mathbb{R}^{B\times T\times C}

Knowledge check

本课为 Attention 补上的四类能力是什么?

2. Token Embedding 不包含位置

固定教学 batch:Vocabulary 为 0=我、1=喜欢、2=AI、3=学习、4=猫,V_vocab=5;prompt A IDs=[[0,1]](我 喜欢),prompt B IDs=[[4,1]](猫 喜欢);B=2、T=2、C=4、n_head=2、head_size=D=2。

Scroll horizontally to view all columns.

Course data table
tokenE[token]position 0position 1
喜欢[0.60, 0.30, -0.20, 0.10]同一 lookup row同一 lookup row

没有 positional input 的 unmasked self-attention 对 permutation 是等变的。GPT 的固定 causal mask 绑定了 sequence indices,让 t=0 与 t=1 的可见前缀不同,因此不会对任意 token permutation 保持等变;它仍没有给模型一个可学习的“这是第 t 个位置”坐标。position embedding(或之后的 RoPE)显式补上这件事。

Etoken(i)=Etoken(i) at every position,token_embedding(ids):[B,T,C]=[2,2,4]E_{\mathrm{token}}(i)=E_{\mathrm{token}}(i)\ \text{at every position},\qquad \operatorname{token\_embedding}(\mathrm{ids}):[B,T,C]=[2,2,4]

Knowledge check

为什么 喜欢 的 lookup 本身不能告诉模型它在 position 1?

3. Position Embedding

下表是本周固定的 didactic initial values,只用于展示 shape 与信息路线;训练从 loss 学习参数,坐标没有人工命名的“第一词意义”。

Scroll horizontally to view all columns.

didactic initial position table [T,C]=[2,4]
position tP[t]
0[0.05, 0.10, -0.05, 0.00]
1[-0.10, 0.00, 0.05, 0.10]

Scroll horizontally to view all columns.

Course data table
prompt / tokenE[token]P[t]x=E+P
A: 我@0[0.20,-0.10,0.70,0.30][0.05,0.10,-0.05,0.00][0.25,0.00,0.65,0.30]
A: 喜欢@1[0.60,0.30,-0.20,0.10][-0.10,0.00,0.05,0.10][0.50,0.30,-0.15,0.20]
B: 猫@0[-0.70,0.40,0.30,0.60][0.05,0.10,-0.05,0.00][-0.65,0.50,0.25,0.60]
B: 喜欢@1[0.60,0.30,-0.20,0.10][-0.10,0.00,0.05,0.10][0.50,0.30,-0.15,0.20]
x[b,t,:]=Etoken[ids[b,t]]+Epos[t]x[b,t,:]=E_{\mathrm{token}}[\mathrm{ids}[b,t]]+E_{\mathrm{pos}}[t]

position rows [T,C]=[2,4] 在 Batch Axis 上 Broadcast,故 Token Rows [2,2,4] + Position Rows [2,4] = residual_0 [2,2,4]。Learned Absolute Table 只为 configured context length 建行;out-of-range t 没有可查的 Row。RoPE 是之后的 relative-position alternative,不是省略基本问题的理由。

Knowledge check

为什么 [T,C]=[2,4] 的 P 可以加到 [B,T,C]=[2,2,4]?

4. Transformer Block 的两个主要计算单元

Attention 是 cross-position mixing:query t 的 output 从允许的 j≤t 的 value_states 汇总。FFN 是 per-token channel mixing:同一个 nonlinear function 分别作用于 x[b,t,:],绝不直接读取 x[b,j,:](j≠t)。“communication / local feature transformation”只是帮助记忆的比喻,不是 literal thought。

Attention(x)b,t,:=jtαb,t,jvalue_statesb,j,:,FFN(x)b,t,:=f(xb,t,:)\operatorname{Attention}(x)_{b,t,:}=\sum_{j\le t}\alpha_{b,t,j}\,\mathrm{value\_states}_{b,j,:},\qquad \operatorname{FFN}(x)_{b,t,:}=f(x_{b,t,:})
Concept sequence
  1. z = LN1(x) [2,2,4]
  2. Q, K, value_states [2,2,4] each
  3. reshape + transpose each → [B,H,T,D] = [2,2,2,2]
  4. scores = Q @ K.transpose(-2,-1) [B,H,T,T] = [2,2,2,2]
  5. causal mask + Softmax over key axis [2,2,2,2]
  6. head outputs = weights @ value_states [2,2,2,2]
  7. transpose + concatenate heads [B,T,H·D] = [2,2,4]
  8. output projection [B,T,C] = [2,2,4]

Knowledge check

FFN 在 喜欢@1 能否直接查看 猫@0?

5. Feed-Forward Network

若看到 I₄,把它想成一个四通道的直通开关:主对角线为 1,其余为 0,xI₄=x。用它构造 FFN 只是为了能手算每个数,不表示真实网络应该把权重固定成 I。FFN 第一层把 4 个特征变成 16 个中间特征,经过非线性,第二层再合成 4 个,才能与原表示相加。

attention 已让 A/B 的 喜欢@1 可以带有不同 context;之后同一组 FFN weights 分别处理 batch 中四个 token rows。输入数值不同会得到不同 output,但每个位置没有自己的 FFN 参数,也不会借此直接读另一个 row。

FFN(z)=GELU(zW1+b1)W2+b2\operatorname{FFN}(z)=\operatorname{GELU}(zW_1+b_1)W_2+b_2

Scroll horizontally to view all columns.

Course data table
stagerow-vector parameter / tensor shapevisible batch shape
first LinearW1:[C,4C]=[4,16][2,2,4] → [2,2,16]
GELUno learned shape change[2,2,16] → [2,2,16]
second LinearW2:[4C,C]=[16,4][2,2,16] → [2,2,4]

准确地说,Attention 整体也可以是非线性的,因为读取权重由输入经过 Q/K 和 Softmax 决定。FFN 提供的是另一条明确的、逐位置的通道变换;“Attention 负责沟通、FFN 负责加工”是职责对照,不是说前者整体只有线性运算。

Knowledge check

为什么第二个 Linear 必须从 16 回到 4?

6. 为什么需要 GELU

(zW1+b1)W2+b2=z(W1W2)+(b1W2+b2)(zW_1+b_1)W_2+b_2=z(W_1W_2)+(b_1W_2+b_2)
GELU(x)=xΦ(x)=x2(1+erf ⁣(x2))\operatorname{GELU}(x)=x\Phi(x)=\frac{x}{2}\left(1+\operatorname{erf}\!\left(\frac{x}{\sqrt2}\right)\right)

Scroll horizontally to view all columns.

GELU 是连续的平滑非线性函数,不是一个二值开关。
输入 xGELU(x) 约等于与 ReLU 的直观差别
−1−0.159负值没有被强制归零
00原点仍映射到零
10.841正值被平滑保留
21.955较大的正值接近原值
Concept sequence
  1. one token row z [4]
  2. Linear W1 → hidden [16]
  3. GELU → hidden [16]
  4. Linear W2 → output [4]
  5. applied independently to every [B,T] location → [2,2,4]

GELU 的响应是连续、平滑的;它不是二值 if-statement,也不负责 token mixing。不同 architecture 也可能使用 ReLU、SwiGLU 等 choices,本 block 的重点是 nonlinear middle step。

text
Prompt A 最终位置在第二个 LayerNorm 后:
z = [1.2476, 0.2644, -1.5404, 0.0284]

教学 W1 的前四个 Hidden Pre-activations:
[1.2476, 0.2644, -1.5404, 0.0284]

GELU 后:
[1.1152, 0.1596, -0.0952, 0.0144]

再经教学 W2=0.25I:
FFN update = [0.2788, 0.0399, -0.0238, 0.0036]

Knowledge check

删除 GELU 的精确失败是什么?

7. Residual Connection

先只看一个数:输出 y=x+F(x)。若 F(x)=0.1x,那么 y=1.1x;输入多 0.01,输出多 0.011。反向影响来自两条路:直接的 x 给斜率 1,F 分支给斜率 0.1,合计 1.1。后面矩阵公式里的 I 就是多维版本的“直接这条路贡献 1”。残差提供路径,但不保证所有梯度永远稳定。

对 Prompt A,令 a=Attention(LN1(residual_0)),则 residual_after_attention=residual_0+a;对 B,相同规则保留由 猫@0 带来的不同信息,同时加上 Contextual Update。分支处 Gradients 相加,呼应“paths multiply; branches add”,但这不是所有 Optimization Problems 的保证。

y=x+F(x),yx=I+F(x)xy=x+F(x),\qquad \frac{\partial y}{\partial x}=I+\frac{\partial F(x)}{\partial x}

Scroll horizontally to view all columns.

Course data table
termshape in this lesson
residual_0[B,T,C]=[2,2,4]
Attention(LN1(residual_0))[B,T,C]=[2,2,4]
residual_after_attention[B,T,C]=[2,2,4]
FFN(LN2(residual_after_attention))[B,T,C]=[2,2,4]
residual_after_ffn[B,T,C]=[2,2,4]
text
Prompt A 的最终“喜欢”位置:
residual_0      = [ 0.5000, 0.3000, -0.1500,  0.2000]
attention_update = [ 0.9944, 0.1092, -1.4326, -0.0512]

逐元素相加:
residual_after_attention
= [1.4944, 0.4092, -1.5826, 0.1488]

这里的 Attention Update 将在第 10 节从 LN1、两个 Heads、Softmax Weights 和 Values 完整推导。Residual 不是覆盖旧向量,也不是 Concatenation;它把旧状态与分支提出的修改逐元素合并。

Knowledge check

Residual Output 的 Forward 中有哪两条贡献路径?

8. Residual 对 Shape 的要求

text
residual_0                                      [2,2,4]
Attention(LN1(residual_0))                    [2,2,4]
residual_after_attention                      [2,2,4]

FFN(LN2(residual_after_attention))            [2,2,4]
residual_after_ffn                            [2,2,4]

causal multi-head Attention 负责 cross-position mixing:每 head 是 [B,T,D]=[2,2,2],H=2 个 heads concatenate 成 [B,T,H·D]=[2,2,4],再经 output projection [2,2,4]→[2,2,4]。FFN 只对每个 token row 做 channel mixing,暂时到 [2,2,16],再由 W2 回到 [2,2,4]。

shape(x)=shape(F(x))=[B,T,C],HD=22=C=4\operatorname{shape}(x)=\operatorname{shape}(F(x))=[B,T,C],\qquad H\cdot D=2\cdot2=C=4

Knowledge check

为什么 [2,2,16] FFN hidden state 不能直接加到 [2,2,4]?

9. Layer Normalization 的直觉

对于 didactic row [1,2,3,4],μ=2.5;减去 μ、除以该 row 的 variance+ε 的平方根,再应用 γ、β。A 的 我@0、喜欢@1 和 B 的两个 rows 都分别计算自己的 statistics。

μb,t=1Cc=1Cxb,t,c,σb,t2=1Cc=1C(xb,t,cμb,t)2,LN(x)b,t,c=γcxb,t,cμb,tσb,t2+ε+βc\mu_{b,t}=\frac{1}{C}\sum_{c=1}^{C}x_{b,t,c},\quad \sigma^2_{b,t}=\frac{1}{C}\sum_{c=1}^{C}(x_{b,t,c}-\mu_{b,t})^2,\quad \operatorname{LN}(x)_{b,t,c}=\gamma_c\frac{x_{b,t,c}-\mu_{b,t}}{\sqrt{\sigma^2_{b,t}+\varepsilon}}+\beta_c
text
先令 gamma=[1,1,1,1]、beta=[0,0,0,0],忽略极小 epsilon 的显示误差:

x        = [1, 2, 3, 4]
mean     = (1+2+3+4) / 4 = 2.5
deviation = [-1.5, -0.5, 0.5, 1.5]
variance = (2.25+0.25+0.25+2.25) / 4 = 1.25
std      = sqrt(1.25) ≈ 1.118

normalized
= deviation / std
≈ [-1.342, -0.447, 0.447, 1.342]

Scroll horizontally to view all columns.

Course data table
normalizationstatistics are computed overtrain / eval behavior
LayerNorm hereeach x[b,t,:] row over its C=4 featuressame per-row rule for one generation prompt or a batch
BatchNorm (typical)per channel over batch and often spatial/time exampleskeeps running statistics for evaluation

BatchNorm 并非普遍错误;LayerNorm 更贴合 variable-length/autoregressive transformer:它不需要把一个 token row 的 normalization 依赖其他 batch examples。input/output 都是 [2,2,4],γ、β 是 [4]。

Knowledge check

x[1,0,:] 的 LayerNorm mean 由哪些值决定?

10. Pre-Norm Transformer Block

Concept sequence
  1. residual_0 [2,2,4] → LN1 → normalized_for_attention [2,2,4]
  2. causal MHA → attention_update [2,2,4]
  3. residual_0 + attention_update → residual_after_attention [2,2,4]
  4. LN2 → normalized_for_ffn [2,2,4]
  5. FFN 4→16→4 → ffn_update [2,2,4]
  6. residual_after_attention + ffn_update → residual_after_ffn [2,2,4]
  7. Final LayerNorm → h [2,2,4]
rA=r0+Attention(LN1(r0)),rF=rA+FFN(LN2(rA))r_A=r_0+\operatorname{Attention}(\operatorname{LN}_1(r_0)),\qquad r_F=r_A+\operatorname{FFN}(\operatorname{LN}_2(r_A))

教学 Attention 使用最容易审计的参数:Head 1 选择 normalized row 的 Channels 0–1,Head 2 选择 Channels 2–3;每个 Head 内 Q=K=V;W_O 使用 Identity。它仍执行真实的 QKᵀ、Scale、Mask、Softmax 与 Weighted Values,只是把 Projection 简化为 Channel Selection。

q(1)=k(1)=v(1)=[z0,z1],q(2)=k(2)=v(2)=[z2,z3],WO=I4q^{(1)}=k^{(1)}=v^{(1)}=[z_0,z_1],\qquad q^{(2)}=k^{(2)}=v^{(2)}=[z_2,z_3],\qquad W_O=I_4

Scroll horizontally to view all columns.

最终“喜欢”的 LN1 Row 相同;两个 Prompt 的 Position 0 不同。
LN1 RowPrompt APrompt B
Position 0[-0.2156,-1.2939,1.5095,0.0000][-1.6731,0.6591,0.1521,0.8619]
Position 1: 喜欢[1.2206,0.3715,-1.5390,-0.0531][1.2206,0.3715,-1.5390,-0.0531]
text
Prompt A、Head 1、最终 Query 的两次打分:
q = [1.2206, 0.3715]
k_我 = [-0.2156, -1.2939]
k_喜欢 = [1.2206, 0.3715]

score_我
= (1.2206×-0.2156 + 0.3715×-1.2939) / sqrt(2)
≈ -0.5260

score_喜欢
= (1.2206×1.2206 + 0.3715×0.3715) / sqrt(2)
≈ 1.1511

Softmax([-0.5260, 1.1511])
≈ [0.1575, 0.8425]

Scroll horizontally to view all columns.

每个 Head 对两条可见 Positions 分别做 Row Softmax。不同 Position-0 Rows 使最终 Attention Weights 不同。
Final QueryScaled Scores Head 1Weights Head 1Scaled Scores Head 2Weights Head 2
Prompt A[-0.5260,1.1511][0.1575,0.8425][-1.6427,1.6768][0.0349,0.9651]
Prompt B[-1.2709,1.1511][0.0815,0.9185][-0.1979,1.6768][0.1330,0.8670]
text
Prompt A 的 Values 就是各 Head 选出的 LN1 Channels:

Head 1 output
= 0.1575×[-0.2156,-1.2939] + 0.8425×[1.2206,0.3715]
≈ [0.9944, 0.1092]

Head 2 output
= 0.0349×[1.5095,0.0000] + 0.9651×[-1.5390,-0.0531]
≈ [-1.4326, -0.0512]

Concat 两个 Heads;教学 W_O=I_4:
attention_update ≈ [0.9944,0.1092,-1.4326,-0.0512]

text
Prompt A final row
attention_update         = [ 0.9944, 0.1092, -1.4326, -0.0512]
residual_0               = [ 0.5000, 0.3000, -0.1500,  0.2000]
residual_after_attention = [ 1.4944, 0.4092, -1.5826,  0.1488]
LN2                      = [ 1.2476, 0.2644, -1.5404,  0.0284]
FFN update               = [ 0.2788, 0.0399, -0.0238,  0.0036]
residual_after_ffn       = [ 1.7732, 0.4492, -1.6064,  0.1524]
final LayerNorm h        = [ 1.3128, 0.2134, -1.4933, -0.0330]

Prompt B final row
attention_update         = [ 0.9847, 0.3949, -1.3141,  0.0686]
residual_0               = [ 0.5000, 0.3000, -0.1500,  0.2000]
residual_after_attention = [ 1.4847, 0.6949, -1.4641,  0.2686]
LN2                      = [ 1.1475, 0.4158, -1.5843,  0.0209]
FFN update               = [ 0.2508, 0.0687, -0.0224,  0.0027]
residual_after_ffn       = [ 1.7356, 0.7637, -1.4865,  0.2713]
final LayerNorm h        = [ 1.2100, 0.3787, -1.5462, -0.0425]

两条 Prompt 的 residual_0 final row 起点完全相同;差异第一次出现在 Attention 读取不同 Position 0 之后,并继续穿过 Residual、LN2、FFN 与 Final LayerNorm。Pre-Norm 的直觉是 Branch 总能接收按 Row 校准的输入,而 Direct Residual Route 仍保留未被本次 LayerNorm 改写的状态和 Identity Gradient Path。

Knowledge check

r_A=r_0+Attention(LN1(r_0)) 中 Direct Residual Path 传递什么?

11. 完整 Transformer Block

python
import math

import torch
import torch.nn.functional as F


torch.set_printoptions(precision=4, sci_mode=False)

ids = torch.tensor([
    [0, 1],  # 我 喜欢
    [4, 1],  # 猫 喜欢
])

token_table = torch.tensor([
    [ 0.20, -0.10,  0.70,  0.30],  # 我
    [ 0.60,  0.30, -0.20,  0.10],  # 喜欢
    [-0.40,  0.80,  0.50, -0.30],  # AI
    [ 0.10,  0.20,  0.90,  0.40],  # 学习
    [-0.70,  0.40,  0.30,  0.60],  # 猫
])

position_table = torch.tensor([
    [ 0.05, 0.10, -0.05, 0.00],
    [-0.10, 0.00,  0.05, 0.10],
])

B, T = ids.shape
C, num_heads = 4, 2
head_size = C // num_heads

residual_0 = F.embedding(ids, token_table) + position_table[:T]

# Teaching LayerNorm: gamma=1, beta=0, epsilon=1e-5.
normalized_for_attention = F.layer_norm(
    residual_0,
    normalized_shape=(C,),
    eps=1e-5,
)

# Teaching Q/K/V projections:
# Head 1 selects channels 0:2; Head 2 selects channels 2:4.
heads = normalized_for_attention.view(B, T, num_heads, head_size)
heads = heads.transpose(1, 2)  # [B,H,T,D]
q = heads
k = heads
value_states = heads

scores = (q @ k.transpose(-2, -1)) / math.sqrt(head_size)
causal_mask = torch.tril(torch.ones(T, T, dtype=torch.bool))
masked_scores = scores.masked_fill(~causal_mask, float("-inf"))
attention_probs = F.softmax(masked_scores, dim=-1)

head_outputs = attention_probs @ value_states
attention_update = head_outputs.transpose(1, 2).contiguous().view(B, T, C)
# Teaching W_O is I_4, so the projection leaves attention_update unchanged.

residual_after_attention = residual_0 + attention_update
normalized_for_ffn = F.layer_norm(
    residual_after_attention,
    normalized_shape=(C,),
    eps=1e-5,
)

# Sparse teaching FFN:
# W1 copies four channels into the first four of sixteen hidden units.
hidden = torch.zeros(B, T, 4 * C)
hidden[..., :C] = normalized_for_ffn
hidden = F.gelu(hidden, approximate="none")

# W2 selects those four units and multiplies them by 0.25.
ffn_update = 0.25 * hidden[..., :C]
residual_after_ffn = residual_after_attention + ffn_update

final_hidden = F.layer_norm(
    residual_after_ffn,
    normalized_shape=(C,),
    eps=1e-5,
)

# Reuse Week 6's five candidate scoring rules.
W_out = torch.tensor([
    [1.0, 0.0,  0.0, 0.0],
    [0.0, 1.0,  2.0, 2.0],
    [0.0, 0.0,  1.0, 1.0],
    [0.0, 3.0, -1.0, 0.0],
    [1.0, 2.0,  0.0, 0.0],
])
bias = torch.tensor([-0.2, 0.1, 0.0, 0.0, 0.0])

logits = final_hidden @ W_out.T + bias
vocabulary_probs = F.softmax(logits, dim=-1)

print("final attention probabilities:", attention_probs[:, :, -1, :])
print("attention update:", attention_update[:, -1, :])
print("residual after attention:", residual_after_attention[:, -1, :])
print("FFN update:", ffn_update[:, -1, :])
print("final hidden:", final_hidden[:, -1, :])
print("final logits:", logits[:, -1, :])
print("final vocabulary probabilities:", vocabulary_probs[:, -1, :])

python
import math

import torch
import torch.nn as nn
import torch.nn.functional as F


class CausalSelfAttention(nn.Module):
    def __init__(self, model_dim: int, num_heads: int, dropout: float):
        super().__init__()
        if model_dim % num_heads != 0:
            raise ValueError("model_dim must be evenly divisible by num_heads")
        self.num_heads = num_heads
        self.head_size = model_dim // num_heads
        self.qkv = nn.Linear(model_dim, 3 * model_dim, bias=False)
        self.output = nn.Linear(model_dim, model_dim, bias=False)
        self.attention_dropout = nn.Dropout(dropout)
        self.output_dropout = nn.Dropout(dropout)

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        batch_size, time_steps, model_dim = x.shape
        q, k, value_states = self.qkv(x).chunk(3, dim=-1)
        q = q.view(
            batch_size, time_steps, self.num_heads, self.head_size
        ).transpose(1, 2)
        k = k.view(
            batch_size, time_steps, self.num_heads, self.head_size
        ).transpose(1, 2)
        value_states = value_states.view(
            batch_size, time_steps, self.num_heads, self.head_size
        ).transpose(1, 2)

        scores = (q @ k.transpose(-2, -1)) / math.sqrt(self.head_size)
        causal = torch.ones(time_steps, time_steps, device=x.device, dtype=torch.bool).tril()
        scores = scores.masked_fill(~causal, float("-inf"))

        attention_probs = F.softmax(scores, dim=-1)
        dropped_probs = self.attention_dropout(attention_probs)
        heads = dropped_probs @ value_states

        merged = heads.transpose(1, 2).contiguous().view(batch_size, time_steps, model_dim)
        return self.output_dropout(self.output(merged))


class TransformerBlock(nn.Module):
    def __init__(self, model_dim: int, num_heads: int, dropout: float):
        super().__init__()
        self.ln1 = nn.LayerNorm(model_dim)
        self.attention = CausalSelfAttention(model_dim, num_heads, dropout)
        self.ln2 = nn.LayerNorm(model_dim)
        self.ffn = nn.Sequential(
            nn.Linear(model_dim, 4 * model_dim),
            nn.GELU(),
            nn.Linear(4 * model_dim, model_dim),
            nn.Dropout(dropout),
        )

    def forward(self, residual: torch.Tensor) -> torch.Tensor:
        attention_update = self.attention(self.ln1(residual))
        residual_after_attention = residual + attention_update

        ffn_update = self.ffn(self.ln2(residual_after_attention))
        residual_after_ffn = residual_after_attention + ffn_update
        return residual_after_ffn


sample = torch.randn(2, 2, 4)
block = TransformerBlock(model_dim=4, num_heads=2, dropout=0.1)
block.eval()
output = block(sample)
print(output.shape)  # torch.Size([2, 2, 4])

Concept sequence
  1. z = LN1(x) [2,2,4]
  2. Q, K, value_states [2,2,4] each
  3. reshape + transpose each → [B,H,T,D] = [2,2,2,2]
  4. scores = Q @ K.transpose(-2,-1) [B,H,T,T] = [2,2,2,2]
  5. causal mask + Softmax over key axis [2,2,2,2]
  6. head outputs = weights @ value_states [2,2,2,2]
  7. transpose + concatenate heads [B,T,H·D] = [2,2,4]
  8. output projection [B,T,C] = [2,2,4]

ln1/ln2 是 Per-Token [2,2,4] Normalization;Attention 跨 Positions Mixing 后回到 [2,2,4];FFN 是 Per-Token 4→16→4。两个 Residual Add 都合并两份 [2,2,4],第二次结果就是 Block Output。model.eval() 会关闭 Dropout,因此除 Sampling 外,同一输入的 Inference 是确定的。

Knowledge check

哪两行含 residual add,它们输出什么 shape?

12. 堆叠多个 Blocks

Concept sequence
  1. x^(0) = token + position embeddings [2,2,4]
  2. Block^(0)(x^(0)) → x^(1) contextual states [2,2,4]
  3. Block^(1)(x^(1)) → x^(2) richer contextual states [2,2,4]
  4. Final LayerNorm(x^(2)) → h [2,2,4]
x(+1)=Block()(x()),=0,,L1x^{(\ell+1)}=\operatorname{Block}^{(\ell)}(x^{(\ell)}),\qquad \ell=0,\ldots,L-1

每个 block 有自己的 LayerNorm、Q/K/V/output projections 和 FFN parameters,除非 architecture 明确 weight sharing。dropout 在 training 中可按 block 配置使用但不改变 shape。n_layer 是 blocks 数量,不是 n_head。

Pre-Norm Block 的每个 Branch Input 都被归一化,但两条 Direct Residual Routes 保留 Raw Residual Stream;堆叠结束后通常再使用 Final LayerNorm,为共享 LM Head 提供按 Row 校准的最终输入。Final LayerNorm 不是第三个 Residual Branch。

Knowledge check

Block 2 消费什么输入?

13. 从 Transformer Output 到 Vocabulary Logits

Final LayerNorm 后,每个 Vocabulary Candidate 都有自己的一组四维 Scoring Weights。一个 Logit 使用当前 h Row 的全部四个 Features 做 Dot Product,再加该候选的 Bias;不是“四个 Features 分别对应五个 Scores”。

zv=wvh+bv=c=1Cwv,chc+bvz_v=w_v\cdot h+b_v=\sum_{c=1}^{C}w_{v,c}h_c+b_v

Scroll horizontally to view all columns.

同一组教学 Output Head 同时处理 Prompt A 与 Prompt B 的每个位置。
候选 vWeek 6 沿用的 w_vb_v
[1,0,0,0]−0.2
喜欢[0,1,2,2]0.1
AI[0,0,1,1]0
学习[0,3,−1,0]0
[1,2,0,0]0
text
Prompt A final hidden
h_A = [1.3128, 0.2134, -1.4933, -0.0330]

例如“学习”的 Logit:
z_学习 = [0,3,-1,0] · h_A + 0
       = 3(0.2134) - (-1.4933)
       ≈ 2.1336

全部 Logits [我,喜欢,AI,学习,猫]
z_A = [1.1128, -2.7390, -1.5262, 2.1336, 1.7397]

Prompt B final hidden
h_B = [1.2100, 0.3787, -1.5462, -0.0425]
z_B = [1.0100, -2.6987, -1.5887, 2.6821, 1.9674]

WoutRVvocab×C=[5,4],logits=HWoutT+bW_{\mathrm{out}}\in\mathbb{R}^{V_{\mathrm{vocab}}\times C}=[5,4],\qquad \mathrm{logits}=H W_{\mathrm{out}}^{\mathsf T}+b

Scroll horizontally to view all columns.

不同 Context 可以产生不同分布,即使这一次 Argmax 碰巧仍是同一个 Token。
Prompt final rowVocabulary Probabilities [我,喜欢,AI,学习,猫]最高候选
A[0.1742,0.0037,0.0124,0.4835,0.3261]学习
B[0.1108,0.0027,0.0082,0.5897,0.2886]学习

Scroll horizontally to view all columns.

Course data table
quantityshape
h[B,T,C]=[2,2,4]
W_out[V_vocab,C]=[5,4]
b_vocab[V_vocab]=[5]
logits[B,T,V_vocab]=[2,2,5]

训练时 LM Head 对所有 Positions 运行,以便同时形成 B×T 道下一词分类题;生成当前轮只消费 logits[:,−1,:]。Logits 是 Raw Scores,Vocabulary Softmax 才把它们转成概率。

Knowledge check

为什么 [2,2,4] 会成为 [2,2,5]?

14. GPT Forward Pass 的完整 Shape

text
ids A/B                                      [2,2]
token embeddings                             [2,2,4]
position embeddings                          [2,4] (broadcast across B)
residual_0 = token + position                 [2,2,4]
one or more Transformer blocks               [2,2,4]
final LayerNorm                              [2,2,4]
LM head                                      [2,2,5]

training: logits.reshape(B*T,V_vocab)        [4,5]
          targets.reshape(B*T)                [4]
          cross entropy                       scalar

generation: logits[:, -1, :]                 [2,5]
            sampled/argmax next_id            [2,1]
            appended ids                       [2,3]

python
class MiniGPT(nn.Module):
    def __init__(
        self,
        vocab_size: int,
        context_length: int,
        model_dim: int,
        num_heads: int,
        num_layers: int,
        dropout: float,
    ):
        super().__init__()
        self.context_length = context_length
        self.token_embedding = nn.Embedding(vocab_size, model_dim)
        self.position_embedding = nn.Embedding(context_length, model_dim)
        self.blocks = nn.ModuleList([
            TransformerBlock(model_dim, num_heads, dropout)
            for _ in range(num_layers)
        ])
        self.final_norm = nn.LayerNorm(model_dim)
        self.lm_head = nn.Linear(model_dim, vocab_size)

    def forward(
        self,
        token_ids: torch.Tensor,
        targets: torch.Tensor | None = None,
    ) -> tuple[torch.Tensor, torch.Tensor | None]:
        B, T = token_ids.shape
        if T > self.context_length:
            raise ValueError("Sequence length exceeds context_length")

        positions = torch.arange(T, device=token_ids.device)
        residual = (
            self.token_embedding(token_ids)
            + self.position_embedding(positions)
        )

        for block in self.blocks:
            residual = block(residual)

        final_hidden = self.final_norm(residual)
        logits = self.lm_head(final_hidden)

        if targets is None:
            return logits, None

        loss = F.cross_entropy(
            logits.reshape(B * T, logits.size(-1)),
            targets.reshape(B * T),
        )
        return logits, loss


model = MiniGPT(
    vocab_size=5,
    context_length=8,
    model_dim=4,
    num_heads=2,
    num_layers=2,
    dropout=0.1,
)

inputs = torch.tensor([
    [0, 1],  # 我 喜欢
    [4, 1],  # 猫 喜欢
])
targets = torch.tensor([
    [1, 2],  # 喜欢 AI
    [1, 3],  # 喜欢 学习
])

logits, loss = model(inputs, targets)
print(logits.shape)  # torch.Size([2, 2, 5])
print(loss.shape)    # torch.Size([])

L=CrossEntropy(reshape(logits,[BT,Vvocab]),reshape(targets,[BT]))\mathcal{L}=\operatorname{CrossEntropy}(\operatorname{reshape}(\mathrm{logits},[BT,V_{\mathrm{vocab}}]),\operatorname{reshape}(\mathrm{targets},[BT]))

literal T=2 是教学 window。真实 generation 每轮先将一个 [B,1] ID append 到已保存 sequence,下一次 forward 前再把过长 prompt crop 到 configured context size。

Knowledge check

B=2、T=2、V_vocab=5 时,什么 shapes 进入 cross entropy?

15. Encoder、Decoder 和 Decoder-only

先问这次任务允许看哪些输入:给整句话分类,可以让词互相看全句;逐词续写,只能看已经出现的前缀;翻译时,生成端可以看完整原文以及已经生成的译文。这三种可见范围帮助区分 encoder、decoder 与 decoder-only,而不是把“decoder”误当 tokenizer.decode。

Scroll horizontally to view all columns.

Course data table
familypermitted contexttypical role / interface
encoder-onlytoken 可读 input sequence 的双向 contextrepresentation / understanding tasks
encoder-decoderencoder 双向读 source;decoder 因果读 generated target prefix,并 cross-attend sourceinput sequence → generated output sequence
decoder-only GPTposition t 只读 same prompt 的 j≤t[B,T,V_vocab] next-token logits;last-position generation
decoder-only:P(x1:T)=t=1TP(xtx<t)\mathrm{decoder\text{-}only}:\qquad P(x_{1:T})=\prod_{t=1}^{T}P(x_t\mid x_{<t})

对 [我,喜欢],decoder-only 的第二位置可读 positions 0 和 1,不能读未来。每 head 的 causal score mask 在本课为 [B,H,T,T]=[2,2,2,2]。decoder-only 是 architecture label,不表示它不能形成 prompt representation,也不表示它只是机械 decoding。

Knowledge check

哪个 family 匹配 Mini GPT,为什么?

16. Transformer 的能力来自哪里

Concept sequence
  1. data / tokenizer produce IDs
  2. embeddings + positions create [B,T,C]
  3. causal Attention exchanges allowed token information
  4. FFN + GELU transform local channels; residual/norm support depth
  5. LM head exposes vocabulary logits
  6. loss + backprop + optimizer update parameters
θθηθL\theta\leftarrow\theta-\eta\nabla_{\theta}\mathcal{L}

θ 包括 token/position tables、Q/K/value/output projections、FFN、LayerNorm 与 LM-head parameters。回到 A/B:Attention 给 我/猫 影响 喜欢@1 的 route;positions 标记 locations;FFN/stacking transform state;data、loss 与 optimization 决定参数最终学到什么。

Knowledge check

为什么 Attention 在本课中是必要但不充分的?

17. 需要知道但暂不展开的概念

Scroll horizontally to view all columns.

Course data table
advanced concept它改变什么,不改变什么
RoPE / relative positions改变 position 如何进入 Q/K;不取消 position problem
weight tying共享 token embedding 与 LM-head weights;不改变 logits interface
Flash Attention改变 efficient implementation/resource usage;不改 attention mathematics
KV cachegeneration 重用 past K/V;不改变 causal permissions
padding / masking details处理 variable-length batches;不等于 causal mask 的目的
gated FFNs / other norms替换 local branch variant;仍需回到 [B,T,C]
cachet={Kt,Vt}\mathrm{cache}_t=\{K_{\le t},V_{\le t}\}

cache 下一步是在每 layer 追加一个 position,而不重新算 earlier K/V;逻辑上的 causal attention output 仍只可读取 allowed prefix。RoPE 与 learned absolute table 是 alternative position strategies,不要求同时使用。

Knowledge check

KV cache 会改变 causal mask 允许哪些 prior tokens 吗?

18. Week 8 最应该理解的 7 件事

  1. Token embedding 给 token identity;position embedding 给 location;两者相加后仍是 [B,T,C]。
  2. Causal multi-head Attention 是基础 block 中唯一跨 token positions 混合信息的操作。
  3. H=2、D=2 时,heads 是 [B,H,T,D],concat 为 [B,T,4],output projection 保持 [B,T,C]。
  4. FFN 将每个 [4] row 独立做同一 nonlinear [4]→[16]→[4] transformation。
  5. 每个 residual add 合并两份 equal [B,T,C] tensors,并保留 identity information/gradient route。
  6. Pre-Norm 是 LN → branch → residual add,做两次;LayerNorm 统计一个 token row 的 features,不统计 batch rows。
  7. Blocks 以 [B,T,C]→[B,T,C] 堆叠;最终 LM head 得到 [B,T,V_vocab],训练用 all positions,生成用 last position。
Concat(head1,head2)RB×T×(HD)=R2×2×4\operatorname{Concat}(\mathrm{head}_1,\mathrm{head}_2)\in\mathbb{R}^{B\times T\times(H D)}=\mathbb{R}^{2\times2\times4}

Knowledge check

把 Attention、FFN、LayerNorm、residual add 和 LM head 分别归类。

19. Week 8 → Week 9

Concept sequence
  1. raw text
  2. tokenizer-selected tokens
  3. vocabulary IDs [B,T]
  4. token + position embeddings [B,T,C]
  5. Transformer
  6. logits [B,T,V_vocab]
texttokenizerids{0,,Vvocab1}B×T\mathrm{text}\xrightarrow{\mathrm{tokenizer}}\mathrm{ids}\in\{0,\ldots,V_{\mathrm{vocab}}-1\}^{B\times T}

本周 [我,喜欢] 与 [猫,喜欢] 假定 tokenizer 已把 我、喜欢、猫 纳入同一 vocabulary。Week 9 会比较不同 segmentation choices,并区分 tokenizer training 与 encoding。保存的 model 与 tokenizer 必须共享 ID-to-token mapping;否则一个数值合法的 ID 也会指向错误 token。

Knowledge check

保存的 model 与 tokenizer 必须匹配什么?