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
Attention 已经能读上下文,但不是完整 GPT。本周始终区分“原始表示仍从旁路传递”与“子层算出的更新量”。前置是 Week 7 的一次 causal attention,以及均值、平方和平方根。
Scroll horizontally to view all columns.
| 学习单元 | 本次解决的问题 |
|---|---|
| 一:补上位置 | 同一个 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.
| 后文简称 | 先用中文理解 |
|---|---|
| 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。
- ids [B,T] = [2,2]
- token embeddings [B,T,C] = [2,2,4] + position embeddings [T,C] = [2,4]
- residual_0 [2,2,4]
- residual_0 + Attention(LN1(residual_0)) → residual_after_attention [2,2,4]
- residual_after_attention + FFN(LN2(residual_after_attention)) → residual_after_ffn [2,2,4]
- contextual representation [2,2,4]
- final LayerNorm + LM head → logits [B,T,V_vocab] = [2,2,5]
两条 Prompt 的 喜欢@1 在 Token+Position 后起点相同,但 Attention 能读取不同的第 0 行(我@0 或 猫@0),因此 residual_after_ffn[:,1,:] 可以不同;未经训练不能据此承诺某个续写。
Knowledge check
哪一步把 我/猫 混入第二位置,哪一步只变换该位置已有的 features?
1. Attention 还缺什么
Scroll horizontally to view all columns.
| 缺少的能力 | 补上的组件 | 喜欢@1 的作用 |
|---|---|---|
| token 位置/距离 | position embeddings P | 让相同 token 的不同 location 有不同输入表示 |
| 跨位置通信 | causal multi-head Attention | 可读 position 0 的 我 或 猫 |
| 每行的非线性特征计算 | FFN + GELU | 只处理已 contextualized 的四个 channels |
| 保留与 gradient route | residual + x | 把旧表示与 branch update 相加 |
| 稳定 branch inputs | LayerNorm | 逐 token row 归一化后再送入 branch |
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.
| token | E[token] | position 0 | position 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)显式补上这件事。
Knowledge check
为什么 喜欢 的 lookup 本身不能告诉模型它在 position 1?
3. Position Embedding
下表是本周固定的 didactic initial values,只用于展示 shape 与信息路线;训练从 loss 学习参数,坐标没有人工命名的“第一词意义”。
Scroll horizontally to view all columns.
| position t | P[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.
| prompt / token | E[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] |
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。
- z = LN1(x) [2,2,4]
- Q, K, value_states [2,2,4] each
- reshape + transpose each → [B,H,T,D] = [2,2,2,2]
- scores = Q @ K.transpose(-2,-1) [B,H,T,T] = [2,2,2,2]
- causal mask + Softmax over key axis [2,2,2,2]
- head outputs = weights @ value_states [2,2,2,2]
- transpose + concatenate heads [B,T,H·D] = [2,2,4]
- 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。
Scroll horizontally to view all columns.
| stage | row-vector parameter / tensor shape | visible batch shape |
|---|---|---|
| first Linear | W1:[C,4C]=[4,16] | [2,2,4] → [2,2,16] |
| GELU | no learned shape change | [2,2,16] → [2,2,16] |
| second Linear | W2:[4C,C]=[16,4] | [2,2,16] → [2,2,4] |
准确地说,Attention 整体也可以是非线性的,因为读取权重由输入经过 Q/K 和 Softmax 决定。FFN 提供的是另一条明确的、逐位置的通道变换;“Attention 负责沟通、FFN 负责加工”是职责对照,不是说前者整体只有线性运算。
Knowledge check
为什么第二个 Linear 必须从 16 回到 4?
6. 为什么需要 GELU
Scroll horizontally to view all columns.
| 输入 x | GELU(x) 约等于 | 与 ReLU 的直观差别 |
|---|---|---|
| −1 | −0.159 | 负值没有被强制归零 |
| 0 | 0 | 原点仍映射到零 |
| 1 | 0.841 | 正值被平滑保留 |
| 2 | 1.955 | 较大的正值接近原值 |
- one token row z [4]
- Linear W1 → hidden [16]
- GELU → hidden [16]
- Linear W2 → output [4]
- applied independently to every [B,T] location → [2,2,4]
GELU 的响应是连续、平滑的;它不是二值 if-statement,也不负责 token mixing。不同 architecture 也可能使用 ReLU、SwiGLU 等 choices,本 block 的重点是 nonlinear middle step。
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 的保证。
Scroll horizontally to view all columns.
| term | shape 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] |
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 的要求
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]。
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。
先令 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.
| normalization | statistics are computed over | train / eval behavior |
|---|---|---|
| LayerNorm here | each x[b,t,:] row over its C=4 features | same per-row rule for one generation prompt or a batch |
| BatchNorm (typical) | per channel over batch and often spatial/time examples | keeps 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
- residual_0 [2,2,4] → LN1 → normalized_for_attention [2,2,4]
- causal MHA → attention_update [2,2,4]
- residual_0 + attention_update → residual_after_attention [2,2,4]
- LN2 → normalized_for_ffn [2,2,4]
- FFN 4→16→4 → ffn_update [2,2,4]
- residual_after_attention + ffn_update → residual_after_ffn [2,2,4]
- Final LayerNorm → h [2,2,4]
教学 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。
Scroll horizontally to view all columns.
| LN1 Row | Prompt A | Prompt 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] |
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.
| Final Query | Scaled Scores Head 1 | Weights Head 1 | Scaled Scores Head 2 | Weights 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] |
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]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
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, :])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])- z = LN1(x) [2,2,4]
- Q, K, value_states [2,2,4] each
- reshape + transpose each → [B,H,T,D] = [2,2,2,2]
- scores = Q @ K.transpose(-2,-1) [B,H,T,T] = [2,2,2,2]
- causal mask + Softmax over key axis [2,2,2,2]
- head outputs = weights @ value_states [2,2,2,2]
- transpose + concatenate heads [B,T,H·D] = [2,2,4]
- 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
- x^(0) = token + position embeddings [2,2,4]
- Block^(0)(x^(0)) → x^(1) contextual states [2,2,4]
- Block^(1)(x^(1)) → x^(2) richer contextual states [2,2,4]
- Final LayerNorm(x^(2)) → h [2,2,4]
每个 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”。
Scroll horizontally to view all columns.
| 候选 v | Week 6 沿用的 w_v | b_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 |
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]Scroll horizontally to view all columns.
| Prompt final row | Vocabulary 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.
| quantity | shape |
|---|---|
| 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
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]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([])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.
| family | permitted context | typical role / interface |
|---|---|---|
| encoder-only | token 可读 input sequence 的双向 context | representation / understanding tasks |
| encoder-decoder | encoder 双向读 source;decoder 因果读 generated target prefix,并 cross-attend source | input sequence → generated output sequence |
| decoder-only GPT | position t 只读 same prompt 的 j≤t | [B,T,V_vocab] next-token logits;last-position generation |
对 [我,喜欢],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 的能力来自哪里
- data / tokenizer produce IDs
- embeddings + positions create [B,T,C]
- causal Attention exchanges allowed token information
- FFN + GELU transform local channels; residual/norm support depth
- LM head exposes vocabulary logits
- loss + backprop + optimizer update parameters
θ 包括 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.
| 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 cache | generation 重用 past K/V;不改变 causal permissions |
| padding / masking details | 处理 variable-length batches;不等于 causal mask 的目的 |
| gated FFNs / other norms | 替换 local branch variant;仍需回到 [B,T,C] |
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 件事
- Token embedding 给 token identity;position embedding 给 location;两者相加后仍是 [B,T,C]。
- Causal multi-head Attention 是基础 block 中唯一跨 token positions 混合信息的操作。
- H=2、D=2 时,heads 是 [B,H,T,D],concat 为 [B,T,4],output projection 保持 [B,T,C]。
- FFN 将每个 [4] row 独立做同一 nonlinear [4]→[16]→[4] transformation。
- 每个 residual add 合并两份 equal [B,T,C] tensors,并保留 identity information/gradient route。
- Pre-Norm 是 LN → branch → residual add,做两次;LayerNorm 统计一个 token row 的 features,不统计 batch rows。
- Blocks 以 [B,T,C]→[B,T,C] 堆叠;最终 LM head 得到 [B,T,V_vocab],训练用 all positions,生成用 last position。
Knowledge check
把 Attention、FFN、LayerNorm、residual add 和 LM head 分别归类。
19. Week 8 → Week 9
- raw text
- tokenizer-selected tokens
- vocabulary IDs [B,T]
- token + position embeddings [B,T,C]
- Transformer
- logits [B,T,V_vocab]
本周 [我,喜欢] 与 [猫,喜欢] 假定 tokenizer 已把 我、喜欢、猫 纳入同一 vocabulary。Week 9 会比较不同 segmentation choices,并区分 tokenizer training 与 encoding。保存的 model 与 tokenizer 必须共享 ID-to-token mapping;否则一个数值合法的 ID 也会指向错误 token。
Knowledge check
保存的 model 与 tokenizer 必须匹配什么?