Current: Week 12

0%

Week 12

Week 12 - End-to-End Mini GPT:从文字、学习到恢复后生成

Key question怎样沿同一批数据追踪 tokenizer、MiniGPT、loss、gradient、AdamW、checkpoint 与 target-free generation,并知道每一步改变了什么?

Learning objectives

  • 沿 [3,3]→[3,2]→[3,2,4]→两层 Block→[3,2,5]→[6,5]+[6]→loss[] 追踪完整 forward。
  • 从 scalar loss 反向追踪参数 gradients,区分 gradient buffer、parameter value 与 AdamW state。
  • 完成 100 次成功 update、eval/no-grad reference、canonical checkpoint、严格恢复与 restored-model generation。
  • 用 pipeline、same-batch learning、held-out generalization 三阶段诊断,并把 Weeks 1–12 接成一个心智模型。

130 min estimated reading time

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

最后一周不是再背一遍部件清单,而是完成三个可交付的小任务。沿用 Week 10 的同一个 MiniGPT 类;机制演示和独立文档实验各有明确的数据、配置和保存格式。

Scroll horizontally to view all columns.

Course data table
学习单元本次解决的问题
任务 A:追踪运行五词演示,记录一个输入、目标、表示、logits、loss、梯度、更新与新增 token。
任务 B:独立文档实验运行 week12_generalization.py,用随包提供的原创小文档训练和验证,保存实际 CSV、曲线与生成样例。
任务 C:控制变量先写预期,再只改变学习率、head 数或层数中的一项;保持其余数据和评估不变。
最终复述解释何时只是流程正确、何时学到训练数据、何时才有独立验证证据。

所有运行结果必须由实际命令产生。新实验不会预置“训练成功”的日志或曲线;没有改善也应如实记录。完成后应能从一个 prompt 追踪到一个新增 token,并说明小语料和短上下文的局限。

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

Week 12 核心目标:一条可追踪的完整链路

直白地说,MiniGPT 是一个条件分数函数,training controller 才把误差变成更新。固定 trace 始终使用 mini-gpt-v1:[我, 喜欢, AI, 学习, 猫] 对应 IDs 0..4;B=3、N=3、T=2、C=4、H=2、d_head=2、V=5、n_layer=2。B 在一般 API 中可变,但本训练例固定为 3。

Scroll horizontally to view all columns.

mini-gpt-v1 ordered vocabulary,V=5
IDtoken
0
1喜欢
2AI
3学习
4
Concept sequence
  1. raw text → raw IDs [B,N]=[3,3]
  2. shift → inputs/targets [B,T]=[3,2]
  3. token + position embeddings [B,T,C]=[3,2,4]
  4. two canonical pre-norm Blocks [3,2,4]
  5. bias-free LM head logits [B,T,V]=[3,2,5]
  6. reshape logits [6,5] + targets [6] → mean loss []
  7. backward → AdamW step → checkpoint → strict restore
  8. restored target-free context → last logits [B,5] → next ID [B,1]

Scroll horizontally to view all columns.

训练评分所有六个位置;generation 每轮只消费当前最后位置
监督题contexttarget
1[我]喜欢
2[我,喜欢]AI
3[猫]喜欢
4[猫,喜欢]
5[我]学习
6[我,学习]AI
textIDsN3×3(x,y)N3×2ZR3×2×5LR\mathrm{text}\to\mathrm{IDs}\in\mathbb{N}^{3\times3}\to(x,y)\in\mathbb{N}^{3\times2}\to Z\in\mathbb{R}^{3\times2\times5}\to\mathcal L\in\mathbb{R}

Knowledge check

Supervised training 在哪个对象上第一次成为一个 scalar objective?

1. 项目结构:责任先于文件名

本周不另起配置类名,也不定义第二套 MiniGPT。

Scroll horizontally to view all columns.

Course data table
module / object责任关键接口或长期 state
mini_gpt_walkthrough.pyWeek 10 canonical architectureGPTConfig、MiniGPT、stable member names、_init_weights
week11_training_and_generation.pylifecycle controllertrain_mini_gpt_step、validators、restore、stable sampling
week12_end_to_end.py组装 fixed tracebatch、100 updates、reference、save/load、generate
checkpoint file持久化兼容 stateschema、tokenizer identity、config、untied weights、AdamW、completed_updates
θ={Etoken,Eposition,θblock1,θblock2,θfinalnorm,Whead}\theta=\{E_{\mathrm{token}},E_{\mathrm{position}},\theta_{\mathrm{block\,1}},\theta_{\mathrm{block\,2}},\theta_{\mathrm{final\,norm}},W_{\mathrm{head}}\}

Scroll horizontally to view all columns.

Course data table
实际文件你可以观察到的产物
course_examples/mini_gpt_walkthrough.py520 参数、输入输出 shape、因果检查
course_examples/week11_training_and_generation.py可导入的训练/验证/恢复/生成函数
course_examples/week12_end_to_end.py100 次更新、前后概率、checkpoint 恢复一致、生成文本
course_examples/verify_learning.py数值及行为检查的结果,不是语言质量评测

先在 course_examples 目录运行 python week12_end_to_end.py。程序会在当前目录生成 mini-gpt-training.pt;它使用固定三句数据,不需要下载语料。完整依赖、环境创建和每个练习的运行顺序见同目录 README.md。

Knowledge check

哪个 owner 负责把 [B,T] 映射为 [B,T,V]?

2. 第一次运行先验证 Pipeline

先打印三条 raw IDs 和六道题,再验证 dtype、device、shapes、finite loss、gradients 与 parameter change。

week12_end_to_end.py
# excerpt: Week 12 pipeline gate
inputs, targets = make_fixed_batch(device)
assert inputs.dtype == targets.dtype == torch.long
assert inputs.shape == targets.shape == (3, 2)
before = {name: value.detach().clone() for name, value in model.named_parameters()}
loss, grad_norm, completed_updates = train_mini_gpt_step(
    model, optimizer, inputs, targets, device, completed_updates=0
)
assert completed_updates == 1
assert torch.isfinite(loss) and torch.isfinite(grad_norm)
assert any(
    not torch.equal(before[name], value.detach())
    for name, value in model.named_parameters()
)

Luniform=log(1/V)=log51.609\mathcal L_{\mathrm{uniform}}=-\log(1/V)=\log 5\approx1.609

Scroll horizontally to view all columns.

Course data table
boundary固定 evidence失败就先查
tokenizer/batch[3,3]→[3,2],long,IDs 0..4mapping 与 shift
forward[3,2]→[3,2,5],loss [] finiterank、device、mask、targets
backwardexpected .grad non-None/finitegraph 与 zero order
step至少一个 parameter value 改变,count=1optimizer binding 与 step

Knowledge check

问“文字是否好”之前至少先核对什么?

3. 从文字开始:冻结 Tokenizer 才有稳定地址

本教学 tokenizer 仅按空格切分已知词;它没有 specials、PAD 或 UNK。

Scroll horizontally to view all columns.

Course data table
IDtoken
0
1喜欢
2AI
3学习
4
week12_end_to_end.py
# excerpt from the assembled Week 12 caller
RAW_TEXTS = ("我 喜欢 AI", "猫 喜欢 我", "我 学习 AI")
EXPECTED_RAW_IDS = ((0, 1, 2), (4, 1, 0), (0, 3, 2))
FROZEN_STOI = {
    token: token_id
    for token_id, token in enumerate(CANONICAL_ORDERED_TOKENS)
}

def encode_mini_gpt_v1(text: str) -> list[int]:
    pieces = text.split()
    if not pieces:
        raise ValueError("mini-gpt-v1 text must contain a token")
    try:
        return [FROZEN_STOI[piece] for piece in pieces]
    except KeyError as error:
        raise ValueError(
            f"mini-gpt-v1 has no unknown-token fallback: {error.args[0]}"
        ) from error

encoded_rows = [encode_mini_gpt_v1(text) for text in RAW_TEXTS]
assert tuple(tuple(row) for row in encoded_rows) == EXPECTED_RAW_IDS

encode(我 喜欢 AI)=[0,1,2]N3,XrawNB×N=N3×3\operatorname{encode}(\text{我 喜欢 AI})=[0,1,2]\in\mathbb N^3,\qquad X_{\mathrm{raw}}\in\mathbb N^{B\times N}=\mathbb N^{3\times3}

Tokenizer 只决定“切成什么、编号是什么”;embedding 与 blocks 才在训练中改变连续表示。ID 4 不是“四只猫”,只是 token 猫的第 5 个 row address。

Knowledge check

把新 token 加入词表时,哪些接口必须一起变化?

4. Training Batch:一行文字变成两道监督题

raw IDs 用左两列作 inputs、右两列作 targets。Teacher forcing 提供真实前缀,而不是模型自己刚抽到的 token。

Scroll horizontally to view all columns.

Course data table
rowraw IDs [N=3]inputs [T=2]targets [T=2]两道题
b=0[0,1,2][0,1][1,2]我→喜欢;[我,喜欢]→AI
b=1[4,1,0][4,1][1,0]猫→喜欢;[猫,喜欢]→我
b=2[0,3,2][0,3][3,2]我→学习;[我,学习]→AI
python
raw = torch.tensor(
    [[0, 1, 2], [4, 1, 0], [0, 3, 2]],
    dtype=torch.long,
    device=device,
)  # [B,N] = [3,3]
inputs = raw[:, :-1]   # [[0,1],[4,1],[0,3]] [B,T]=[3,2]
targets = raw[:, 1:]   # [[1,2],[1,0],[3,2]] [B,T]=[3,2]

x=Xraw[:,0:N1],y=Xraw[:,1:N],x,yN3×2x=X_{\mathrm{raw}}[:,0:N-1],\qquad y=X_{\mathrm{raw}}[:,1:N],\qquad x,y\in\mathbb N^{3\times2}

Knowledge check

第三行 input 学习 的 target 是什么?

5. Embedding Lookup:离散地址变成四个可学习 Features

token_embedding 选 row,position_embedding 选位置;两者相加后才进入 Block。

python
B, T = inputs.shape
positions = torch.arange(T, device=inputs.device)  # [T]=[2]
token_rows = model.token_embedding(inputs)         # [B,T,C]=[3,2,4]
position_rows = model.position_embedding(positions) # [T,C]=[2,4]
x = token_rows + position_rows                     # [3,2,4]

Scroll horizontally to view all columns.

Course data table
axis含义
B=3三行三个 sequences
T=2两列每行两个 input positions
C=4四通道模型学习的 features,不是人工命名属性
X(0)=Etoken[x]+Eposition[0:T]R3×2×4X^{(0)}=E_{\mathrm{token}}[x]+E_{\mathrm{position}}[0:T]\in\mathbb R^{3\times2\times4}

Knowledge check

为什么 [3,2,4] 可以与 [2,4] 相加?

6. Transformer Block:跨位置读取,再逐位置变换

两个 canonical blocks 依次处理同一个 [3,2,4] 表示;Attention mixing T,FFN 在每个位置 mixing C。

python
# exact Week 10 member names
def forward(self, x: torch.Tensor) -> torch.Tensor:
    x = x + self.attention(self.ln1(x))       # [3,2,4] + [3,2,4]
    x = x + self.feed_forward(self.ln2(x))    # [3,2,4] + [3,2,4]
    return x                                  # [3,2,4]

X=X+Attention(LN1(X)),Xnext=X+FFN(LN2(X))X'=X+\operatorname{Attention}(\operatorname{LN}_1(X)),\qquad X^{\mathrm{next}}=X'+\operatorname{FFN}(\operatorname{LN}_2(X'))

Scroll horizontally to view all columns.

Course data table
component读取范围内部宽度external shape
causal Attention允许的当前/左侧 positionsH=2,d_head=2[3,2,4]→[3,2,4]
FFN每个 position 独立C→4C→C,即 4→16→4[3,2,4]→[3,2,4]

Knowledge check

哪个 sublayer 让位置 1 能读取位置 0?

7. Q/K/V Shape Trace:把上下文查询摊成矩阵

Query 表示目的位置在找什么,Key 表示来源位置可匹配什么,Value 表示匹配后带回什么。

Scroll horizontally to view all columns.

Course data table
operationshapeaxes
x[3,2,4][B,T,C]
qkv(x)[3,2,12][B,T,3C]
q,k,veach [3,2,4][B,T,C]
split/transposeeach [3,2,2,2][B,H,T,d_head]
q @ kᵀ[3,2,2,2][B,H,query T,key T]
masked Softmax[3,2,2,2]visible key columns sum to 1
weights @ v[3,2,2,2][B,H,T,d_head]
merge + output_projection[3,2,4][B,T,C]
S=QKT/dheadRB×H×T×T,dhead=C/H=2S=QK^{\mathsf T}/\sqrt{d_{\mathrm{head}}}\in\mathbb R^{B\times H\times T\times T},\qquad d_{\mathrm{head}}=C/H=2
A=softmax(mask(S)),Attention(Q,K,V)=AVA=\operatorname{softmax}(\operatorname{mask}(S)),\qquad \operatorname{Attention}(Q,K,V)=AV

Knowledge check

一个 batch example 的一个 head 有多大的 score matrix?

8. LM Head:每个位置给五个候选打分

两个 blocks 后仍是 [3,2,4];final_norm 保持 shape,lm_head 为每个位置产生五个 scores。

python
for block in model.blocks:
    x = block(x)                 # [3,2,4]
x = model.final_norm(x)          # [3,2,4]
logits = model.lm_head(x)         # [3,2,5], bias=False

Z=LNfinal(X(2))WheadTR3×2×5,WheadR5×4Z=\operatorname{LN}_{\mathrm{final}}(X^{(2)})W_{\mathrm{head}}^{\mathsf T}\in\mathbb R^{3\times2\times5},\qquad W_{\mathrm{head}}\in\mathbb R^{5\times4}

Scroll horizontally to view all columns.

Course data table
selected row表示的 context五个 candidate order
logits[0,0,:][我][我,喜欢,AI,学习,猫]
logits[0,1,:][我,喜欢][我,喜欢,AI,学习,猫]

Knowledge check

为什么每个 position 恰有五个 logits?

9. Cross Entropy:六个预测汇成一个 Scalar Loss

logits [3,2,5] reshape 为 [6,5],targets [3,2] reshape 为 [6];row order 同步,所以六道题仍一一对齐。

python
flat_logits = logits.reshape(6, 5)
flat_targets = targets.reshape(6)
loss = torch.nn.functional.cross_entropy(flat_logits, flat_targets)
assert loss.ndim == 0

L=1BTb=1Bt=1Tlogpθ(yb,txb,t),ZflatR6×5\mathcal L=-\frac1{BT}\sum_{b=1}^{B}\sum_{t=1}^{T}\log p_\theta(y_{b,t}\mid x_{b,\le t}),\qquad Z_{\mathrm{flat}}\in\mathbb R^{6\times5}

Knowledge check

Targets [3,2] flatten 后是什么 shape?

10. Backward Trace:从一个数回到所有参与参数

loss.backward() 反向穿过构建它的 graph,把 derivatives 累加进 participating parameters 的 .grad;此时 parameter values 还没变。

Concept sequence
  1. loss []
  2. six logit rows [6,5]
  3. lm_head.weight.grad [5,4] + final_norm grads
  4. Block 2 grads → Block 1 grads
  5. token/position embedding table grads [5,4] / [2,4]
  6. optimizer.step() 之后才产生新 parameter values
python
optimizer.zero_grad(set_to_none=True)
logits, loss = model(inputs, targets)
assert loss is not None and loss.ndim == 0
loss.backward()
assert model.lm_head.weight.grad is not None
assert model.lm_head.weight.grad.shape == (5, 4)
assert model.token_embedding.weight.grad is not None
assert model.token_embedding.weight.grad.shape == (5, 4)
# Parameters still hold pre-step values here.

gθ=θL,θnew=θold until optimizer.step() succeedsg_\theta=\nabla_\theta\mathcal L,\qquad \theta_{\mathrm{new}}=\theta_{\mathrm{old}}\ \text{until optimizer.step() succeeds}

Knowledge check

Backward 后、step 前,什么变了?

11. Embedding Rows:被查到的行才有直接 Lookup Gradient

inputs 中出现 IDs {0,1,3,4};ID 2/AI 只出现在 targets。本模型 token_embedding 与 lm_head untied。

Scroll horizontally to view all columns.

Course data table
ID/tokeninput lookup?target candidate?本 batch 的直接路径
0/我token row lookup + LM-head target/non-target scoring
1/喜欢token row lookup + LM-head scoring
2/AItoken_embedding row 2 无直接 lookup gradient;lm_head row 2 受 targets 影响
3/学习token row lookup + LM-head scoring
4/猫token row lookup;LM-head row 4 仍作为非目标候选参与 Softmax
LEtoken[i]=(b,t):xb,t=iLXb,t(0)\frac{\partial\mathcal L}{\partial E_{\mathrm{token}}[i]}=\sum_{(b,t):x_{b,t}=i}\frac{\partial\mathcal L}{\partial X^{(0)}_{b,t}}
python
row_grad_norms = model.token_embedding.weight.grad.norm(dim=1)
print(row_grad_norms)  # exact values depend on initialization/device
# Inspect finiteness and expected direct-use pattern; do not hard-code magnitudes.

“row 2 没有直接 token-embedding lookup gradient”不等于 AI 对 loss 完全没有作用:它作为 target 改变 untied lm_head 的 output-row gradients,并通过 logits/loss 影响上游已使用 representations。

Scroll horizontally to view all columns.

Course data table
观察时点row 2 的对象本 trace 能说什么
backward 后dense .grad[2]direct lookup contribution 为 0
step 前AdamW moments + weight_decay policy与当前 .grad 是不同 state
step 后token_embedding.weight[2] parameter value可能因 decay;resume 时也可能因历史 moments 而移动
python
# excerpt immediately around one fresh runner update
optimizer.zero_grad(set_to_none=True)
_, loss = model(inputs, targets)
assert loss is not None
row2_before = model.token_embedding.weight[2].detach().clone()
loss.backward()
row2_current_grad = model.token_embedding.weight.grad[2].detach().clone()
assert torch.count_nonzero(row2_current_grad).item() == 0
optimizer.step()  # AdamW uses weight_decay=1e-2 and any restored moments
row2_after = model.token_embedding.weight[2].detach().clone()
print("row2_moved_after_step=", not torch.equal(row2_before, row2_after))

Knowledge check

哪个 ID 未出现在 inputs,却出现在 targets?其直接影响在哪里?

12. Optimizer Step:读取 Gradient,更新参数与 AdamW 状态

AdamW 对每个 canonical parameter 保存 moving moments/counter;Week 11 validator 还验证唯一 group 与 object identity/order。

Scroll horizontally to view all columns.

Course data table
operationparameter.gradparameter valuesAdamW state / count
zero_grad清空不变不变
forward + loss尚未写新值不变不变
backward写入/累加不变不变
clip可能原地缩放不变不变
optimizer.step被读取更新moments/counters 更新
step 成功后仍在,等待清理已更新completed_updates += 1
mt=β1mt1+(1β1)gt,vt=β2vt1+(1β2)gt2m_t=\beta_1m_{t-1}+(1-\beta_1)g_t,\qquad v_t=\beta_2v_{t-1}+(1-\beta_2)g_t^2
m^t=mt1β1t,v^t=vt1β2t\widehat m_t=\frac{m_t}{1-\beta_1^t},\qquad \widehat v_t=\frac{v_t}{1-\beta_2^t}
θt=(1ηλ)θt1ηm^tv^t+ϵ\theta_t=(1-\eta\lambda)\theta_{t-1}-\eta\frac{\widehat m_t}{\sqrt{\widehat v_t}+\epsilon}

Plain SGD 的基本式是 θ_t=θ_{t-1}−ηg_t;AdamW 不直接把 raw g_t 当 update。即使当前 g_t 的某个 row 为零,decoupled factor (1−ηλ) 仍可改变非零参数;恢复的 m/v history 也可让 adaptive term 非零。

python
loss.backward()
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
optimizer.step()
completed_updates += 1  # only after the successful step returns

Knowledge check

第一行真正改变 model.state_dict() parameter values 的操作是什么?

13. 项目 A:固定三句的训练与保存加载

下面文件依赖 Week 10 的 mini_gpt_walkthrough.py 与 Week 11 的 week11_training_and_generation.py。它只调用冻结 API,不重定义 GPTConfig、MiniGPT、initializer、state-dict namespace、checkpoint schema 或 sampling helper。

week12_end_to_end.py
# week12_end_to_end.py
# This file is a caller. It reuses, rather than redefines, Week 10/11 APIs.
from __future__ import annotations

from pathlib import Path

import torch
from course_data import DEMO_DOCUMENTS, FIVE_WORD_TOKENIZER, configure_console

from mini_gpt_walkthrough import (
    GPTConfig,
    MiniGPT,
    save_mini_gpt_training_checkpoint,
)
from week11_training_and_generation import (
    CANONICAL_ORDERED_TOKENS,
    CANONICAL_TOKENIZER_POLICY,
    CANONICAL_TOKENIZER_VERSION,
    generate_mini_gpt_sampled,
    load_mini_gpt_training_resume,
    train_mini_gpt_step,
    validate_mini_gpt_adamw_completed_updates,
)


RAW_TEXTS = DEMO_DOCUMENTS
EXPECTED_RAW_IDS = (
    (0, 1, 2),  # 我 喜欢 AI
    (4, 1, 0),  # 猫 喜欢 我
    (0, 3, 2),  # 我 学习 AI
)
FROZEN_STOI = {
    token: token_id
    for token_id, token in enumerate(CANONICAL_ORDERED_TOKENS)
}
assert CANONICAL_ORDERED_TOKENS == FIVE_WORD_TOKENIZER.tokens
assert CANONICAL_TOKENIZER_POLICY == (
    "whitespace-delimited;no-specials;no-pad;no-unk"
)


def encode_mini_gpt_v1(text: str) -> list[int]:
    ids = FIVE_WORD_TOKENIZER.encode(text)
    if not ids:
        raise ValueError("mini-gpt-v1 text must contain a token")
    return ids


def make_fixed_batch(device: torch.device) -> tuple[torch.Tensor, torch.Tensor]:
    encoded_rows = [encode_mini_gpt_v1(text) for text in RAW_TEXTS]
    assert tuple(tuple(row) for row in encoded_rows) == EXPECTED_RAW_IDS
    raw = torch.tensor(encoded_rows, dtype=torch.long, device=device)  # [3,3]
    inputs = raw[:, :-1]   # [3,2]
    targets = raw[:, 1:]   # [3,2]
    return inputs, targets


def main() -> None:
    configure_console()
    # 极小 CPU 教学模型用单线程,减少线程调度开销。
    torch.set_num_threads(1)
    seed = 7
    torch.manual_seed(seed)
    if torch.cuda.is_available():
        torch.cuda.manual_seed_all(seed)
    device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

    model = MiniGPT(GPTConfig()).to(device)
    optimizer = torch.optim.AdamW(
        model.parameters(),
        lr=1e-3,
        weight_decay=1e-2,
    )
    inputs, targets = make_fixed_batch(device)
    assert inputs.shape == targets.shape == (3, 2)

    # Observe the labelled [我,喜欢] -> AI row before any update.
    model.eval()
    with torch.no_grad():
        before_logits, _ = model(inputs)
        before_probs = torch.softmax(before_logits[0, 1, :], dim=-1)  # [5]

    initial_parameters = [p.detach().clone() for p in model.parameters()]
    with torch.no_grad():
        _, initial_loss = model(inputs, targets)
    completed_updates = 0
    for _ in range(100):
        loss, grad_norm, completed_updates = train_mini_gpt_step(
            model,
            optimizer,
            inputs,
            targets,
            device,
            completed_updates=completed_updates,
        )
        assert torch.isfinite(loss) and torch.isfinite(grad_norm)
    assert completed_updates == 100
    assert any(not torch.equal(old, new)
               for old, new in zip(initial_parameters, model.parameters()))
    validate_mini_gpt_adamw_completed_updates(
        model,
        optimizer,
        completed_updates,
    )

    model.eval()
    with torch.no_grad():
        reference_logits, reference_loss = model(inputs, targets)
    assert reference_logits.shape == (3, 2, 5)
    assert reference_loss is not None and reference_loss.ndim == 0
    after_probs = torch.softmax(reference_logits[0, 1, :], dim=-1)  # [5]
    print("parameters=", sum(p.numel() for p in model.parameters()))
    print("completed_updates=", completed_updates)
    print("same_batch_loss_before=", initial_loss.item())
    print("same_batch_loss_after=", reference_loss.item())
    print("observed context=[我,喜欢], target=AI")
    print("vocabulary_order=", CANONICAL_ORDERED_TOKENS)
    print("before_probs=", before_probs.detach().cpu().tolist())
    print("after_probs=", after_probs.detach().cpu().tolist())

    path = Path("mini-gpt-training.pt")
    save_mini_gpt_training_checkpoint(
        str(path),
        model=model,
        optimizer=optimizer,
        completed_updates=completed_updates,
        ordered_tokens=CANONICAL_ORDERED_TOKENS,
        tokenizer_policy=CANONICAL_TOKENIZER_POLICY,
        tokenizer_version=CANONICAL_TOKENIZER_VERSION,
    )
    restored, restored_optimizer, restored_updates = (
        load_mini_gpt_training_resume(str(path), device=device)
    )
    assert restored_updates == completed_updates
    validate_mini_gpt_adamw_completed_updates(
        restored,
        restored_optimizer,
        restored_updates,
    )

    restored.eval()
    with torch.no_grad():
        restored_logits, _ = restored(inputs)
    torch.testing.assert_close(restored_logits, reference_logits)
    print("checkpoint_round_trip=PASS")

    prompt = torch.tensor(
        [[FROZEN_STOI["我"], FROZEN_STOI["喜欢"]]],
        dtype=torch.long,
        device=device,
    )
    generated_history = generate_mini_gpt_sampled(
        restored,
        prompt,
        max_new_tokens=3,
        temperature=0.8,
        top_k=3,
    )
    assert generated_history.shape == (1, 5)
    print("generated_ids=", generated_history[0].tolist())
    print("generated_text=", " ".join(
        CANONICAL_ORDERED_TOKENS[i] for i in generated_history[0].tolist()))
    print("以上是固定三句数据的机制演示,不是 held-out 泛化评测。")


if __name__ == "__main__":
    main()

P=54+24+2(8+48+20+8+80+68)+8+54=520P=5\cdot4+2\cdot4+2(8+48+20+8+80+68)+8+5\cdot4=520

Scroll horizontally to view all columns.

Course data table
checkpoint identity field固定内容
schemamini-gpt-training-checkpoint / version 1
tokenizermini-gpt-v1 ordered tokens + exact policy + SHA-256
tokenizer SHA-25638d630f4c589664c9bef567457d48764cbe2307734777e80f7d5d5c63ac88dd6
configGPTConfig(5,2,4,2,2),无 dropout
weight policytoken_embedding_lm_head=untied
training statemodel_state + exact AdamW class/state + completed_updates=100

训练 loss 的具体轨迹与生成 token 依赖初始化、device 和软件版本,因此程序只观测和验证,不在教材中捏造概率或样本。Tying 仅作概念对比:共享 5×4 table 会得到 500 parameters,却改变 gradient sharing 与 checkpoint policy;本 trace 不启用。

Scroll horizontally to view all columns.

Course data table
runtime observationlabel / shape怎样报告
before_probs[我,喜欢]→AI 的 update 前 [5]按 CANONICAL_ORDERED_TOKENS 顺序打印
after_probs同一 row 的 100 successful updates 后 [5]从 reference_logits[0,1,:] 现场计算并打印
interpretation一次特定 seed/device/version 的观测不 hard-code、不 assert 上升方向、不推广为普遍事实

Runner 先在 eval/no_grad 下记录 before_probs,再训练;随后从 post-update reference_logits 计算 after_probs。两者都明确标为 context=[我,喜欢]、target=AI,并连同 Vocabulary order 输出,因此读者能核对这次运行发生了什么,而不是把教材数字误当保证。

Knowledge check

为什么 saved completed_updates 是 100,而不是 99?

14. Autoregressive Generation:裁 Context、取最后一行、抽一个 ID

从 restored model 和 prompt [[0,1]]=[我,喜欢] 开始;不传 targets,因此 loss 为 None,也没有 update。

Concept sequence
  1. uncropped history [1,2] = [[0,1]]
  2. crop forward context to last block_size=2 IDs [1,2]
  3. restored(context), targets=None → logits [1,2,5]
  4. logits[:,-1,:] → last_logits [1,5]
  5. promote/row-center/divide by τ → optional top-k → checked Softmax [1,5]
  6. multinomial → next_id torch.long [1,1]
  7. append to uncropped history → [1,3],repeat
[B,Lhistory][B,min(Lhistory,2)][B,Tcontext,5][B,5][B,1][B,Lhistory+1][B,L_{\mathrm{history}}]\to[B,\min(L_{\mathrm{history}},2)]\to[B,T_{\mathrm{context}},5]\to[B,5]\to[B,1]\to[B,L_{\mathrm{history}}+1]

Scroll horizontally to view all columns.

Course data table
phasepositions consumedtargets?state change
trainingall B×T=6 logits rows有 [3,2]backward + step 更新 θ/AdamW
generationonly final row per prompt [B,5]只增长 caller history

Knowledge check

logits [3,2,5] 的 logits[:,-1,:] 是什么 shape?

15. 为什么朴素生成会重复计算过去

O(T²) 只是在描述增长速度:一个 head 的 T 个 query 各比较 T 个 key,有 T² 个分数。T 从 2 变 4,分数从 4 变 16。它不是“程序要运行 T² 秒”,也没有把 FFN、投影等全部成本都算进去。

当 history=[0,1,2]、block_size=2,下一轮只 forward [1,2],但这两个位置的两层 activations 都会重新计算。

Scroll horizontally to view all columns.

Course data table
strategy保存什么每轮工作trade-off
朴素 loopcaller full token IDs重算 cropped context 的全部 layers最清楚、最容易验证
KV cache每层 past keys/values + positions主要计算新 token更快但需严格管理 cache shape/device/reset
attention score work per full prefix=O(Tcontext2),t=1LO(t2)=O(L3)\mathrm{attention\ score\ work\ per\ full\ prefix}=O(T_{\mathrm{context}}^2),\qquad \sum_{t=1}^{L}O(t^2)=O(L^3)

在前缀、位置编号和计算语义一致时,KV cache 可复用过去的 keys/values,并在数值误差范围内保持相同预测。这里不能直接套用:滑窗裁剪后位置编号从 0 重新开始,保留 token 的位置表示和可见前缀都可能改变,旧 cache 不再自动等价。本教学程序保持完整重算,缓存优化留作独立项目。

Scroll horizontally to view all columns.

Course data table
计算情形每轮 attention 分数工作累计随生成长度的增长
不裁剪,前缀持续增长约 t²求和后约 L³,只计分数工作
本例固定最多看 2 个 token每 head 最多 4 个分数每轮有固定上限,累计随轮数近线性

Knowledge check

为什么 block_size=2 仍有重复计算?

16. 三阶段诊断:Pipeline、Same-batch Learning、Generalization

三个阶段回答不同问题:代码能否流通、模型能否从这批数据学到其可表达的规律、规律能否用于未见数据。

Scroll horizontally to view all columns.

Course data table
stage冻结什么evidencestop condition
1 Pipeline correctnessfixed batch/config/seed[3,2]→[3,2,5]→[];finite grads;parameter changes任一边界失败就停止
2 Same-batch learning重复同一 fixed batchloss 显著低于 ln(5),可区分 contexts 分化;[我]趋向 empirical 0.5/0.5不要要求 loss=0 或六题全 argmax
3 Generalization更大 corpus 的 frozen train/held-out spliteval + no_grad 的 held-out token-weighted loss 与 samples只有这里可讨论 generalization
Lvalidation=iheld-out valid targetsiheld-out batches#valid targets\mathcal L_{\mathrm{validation}}=\frac{\sum_{i\in\mathrm{held\text{-}out\ valid\ targets}}\ell_i}{\sum_{\mathrm{held\text{-}out\ batches}}\#\mathrm{valid\ targets}}
week12_validation_excerpt.py
@torch.no_grad()
def evaluate_token_weighted(model, held_out_batches, device, ignore_index=-100):
    was_training = model.training
    total_loss_sum = 0.0
    total_valid_targets = 0
    model.eval()
    try:
        for inputs, targets in held_out_batches:
            inputs, targets = inputs.to(device), targets.to(device)
            logits, no_loss = model(inputs)  # read-only; no training targets branch
            assert no_loss is None
            flat_targets = targets.reshape(-1)
            total_loss_sum += torch.nn.functional.cross_entropy(
                logits.reshape(-1, model.config.vocab_size),
                flat_targets,
                ignore_index=ignore_index,
                reduction="sum",
            ).item()
            total_valid_targets += int(
                flat_targets.ne(ignore_index).sum().item()
            )
    finally:
        model.train(was_training)
    if total_valid_targets == 0:
        raise ValueError("held-out evaluation has no valid targets")
    return total_loss_sum / total_valid_targets

infLfixed batch=log(1/2)log(1/2)6=ln(2)30.231\inf\mathcal L_{\mathrm{fixed\ batch}}=\frac{-\log(1/2)-\log(1/2)}{6}=\frac{\ln(2)}{3}\approx0.231
Ltrain ⇏ Lheld-out\mathcal L_{\mathrm{train}}\downarrow\ \not\Rightarrow\ \mathcal L_{\mathrm{held\text{-}out}}\downarrow

Knowledge check

哪个 stage 才能声称评估 generalization?

17. 项目 B:用独立文档完成训练、验证与结果解释

本实验使用下载包 data/documents 内的 8 篇原创英文训练短文和 3 篇独立验证短文。题材都与观察、学习、日常活动有关,降低“完全不同领域”造成的混淆,但材料很少、文风单一,不能作为语言能力 benchmark。训练文件不含验证原文,不等于已经排除所有近似重复或数据偏差。

Scroll horizontally to view all columns.

Course data table
条件本实验设置为什么明确写出
输入单位30 个固定字符:a–z、空格、句点、逗号、换行字符表独立规定,不从验证集扩词表。
模型同一 MiniGPT 类;T_max=24、C=32、H=4、2 个 block这是新配置,不再声称默认五词模型只有 520 参数。
结构手写 causal attention、Pre-Norm、GELU FFN、不共享权重、无 dropout机制与主线一致;改变数据后明确新建模型。
训练CPU、seed=7、batch=4、AdamW lr=0.003、decay=0.01、梯度范数上限 1记录实际条件;不承诺某个速度或收敛数值。
评估每 20 次更新,在全部固定训练/验证窗口上 eval + no_grad不用不同随机小批次制造误导曲线,也不改变训练抽样随机序列。

先分文档,再切窗口。T=24 意味着每道窗口要取连续 25 个字符:前 24 个作输入,后 24 个作目标。stride=24 让相邻窗口的目标位置不重叠;不足 25 个字符的末尾部分不用于本实验,报告会记录真正计分的目标数量。窗口不跨文档,更不能跨训练/验证边界。

text
示意一个文档开头(实际程序按字符切,不按单词):
原始 25 个字符  [c0,c1,...,c24]
inputs 长度 24 [c0,c1,...,c23]
targets 长度24 [c1,c2,...,c24]
下一窗从 c24 开始;它的第一个目标是 c25。

回忆 Week 6:给出目标不表示模型能看见目标。位置 t 的输出只使用本窗口中到 t 为止的输入。把不同位置都算 loss,是并行提供多道监督题,不是取消 causal mask。

bash
cd course_examples
python week12_generalization.py --steps 200 --eval-every 20 --seed 7 --output runs/first

这是可直接运行的完整程序,需要先按下载包 README 安装 PyTorch。输出目录必须不存在;重复实验请换成 runs/second,程序不会覆盖原记录。不需要下载外部语料,也不需要 GPU。

Scroll horizontally to view all columns.

Course data table
生成的文件怎样阅读它
config.json记录 Python/PyTorch、CPU、seed、模型、词表和评估设置。
data_report.json逐文档字符数、窗口数、文本校验值、完整包含检查和 48 字符重叠计数。
loss.csv / loss.svg每次记录都来自真正 forward;两条曲线都在 eval 模式下对固定窗口计算。
samples.json三个固定 prompt 的续写;可能重复、拼写混乱,不能只挑最好的一条。
inference.pt模型配置、词表、权重及明确的文件格式;只用于加载推理,不包含精确续训所需全部状态。
experiment_record.md实际首尾损失和加载差异,以及需要你自己补写的解释。

如何汇总验证 Loss?假设一批有 48 个有效目标,平均损失 2;另一批有 24 个有效目标,平均损失 1。总平均是 (48×2+24×1)/(48+24)=1.6667,不是直接平均两个 batch 得 1.5。程序累加每批平均损失乘有效目标数,再除以总数;这是 Week 11 的同一约定。

怎样解释曲线:两条都下降,表示在这个有限实验中两份资料的平均预测改善;训练下降、验证上升,先检查重复/切分、目标对齐、模式与样本量,再考虑过拟合。单个点反弹不足以定论;验证改善也不能证明事实正确性、推理能力或开放域泛化。

bash
python week12_generalization.py --generate-only runs/first/inference.pt --prompt "a " --new-tokens 80

加载时重建保存的同一模型配置和字符表。不要把这个 checkpoint 交给五词模型的严格恢复器;两种实验的格式和词表明确不同。程序会比较保存/加载前同一输入的 logits,但不宣称能逐步复现训练中断后的随机轨迹。

Scroll horizontally to view all columns.

Course data table
练习阶段只改一件事通过证据
A:同词表加句子只用五个已知词,保留每条三 token打印输入/答案;ID 和 shape 合法
B:增加真正独立资料先按来源分 train/validation数据来源可追溯;验证集未用于更新
C:增加一个新词新词表版本、新 V、新模型embedding/head 同步扩展;旧 checkpoint 明确拒绝
D:改长上下文新 block_size 和长度处理重新做 mask、shift 与因果检查

A 仍是机制练习;只有数据量和独立划分足够时,才开始讨论未见文本上的效果。新手先提交每阶段的观察记录,不需要一次把 tokenizer、模型容量和训练器全换掉。

Knowledge check

为什么这次实验比固定三句话多提供了一层证据,却仍不能证明模型具备真正的语言理解能力?

18. Character、Byte、Word 与 Subword:换单位会改变整条接口

Subword/BPE 的意义是在有限 V 下复用常见片段,平衡 sequence length 与 Vocabulary coverage;它先在 tokenizer-training 阶段学习 merges,随后 encoding 阶段冻结应用。

Scroll horizontally to view all columns.

分段仅为单位示意;确切 BPE pieces 由已学习 merges 决定
unit“我喜欢AI,AI也喜欢猫。”示意优势代价
character逐 Unicode 字符直观长词/英文片段可能很长
word按词/边界序列短未知词与切词规则困难
UTF-8 byte每个 byte 0..255任意文本可逆中文字符通常占多个 bytes
BPE/subword依 learned merges 而定coverage 与长度折中需版本化 merge rules
Pinput/output tables2VCfor untied token embedding and bias-free LM headP_{\mathrm{input/output\ tables}}\approx2VC\quad\text{for untied token embedding and bias-free LM head}

Knowledge check

V 改变时,哪两个 model layers 的边界必须改变?

19. 扩大 Model:一次只改变一个可解释 Lever

先让 fixed model 通过 pipeline 和 learning diagnostics,再改变一个变量并记录 config、seed、data split、updates、loss 和 samples。

Scroll horizontally to view all columns.

Course data table
lever接口/约束主要影响
block_size T_maxposition table 与 causal mask 同步扩展attention score memory/work 约 T²
n_embd C所有 residual paths 同宽projections/FFN 参数与 compute 大致 C²
n_head HC mod H=0,d_head=C/Hattention routing partition
n_layerModuleList 深度与 state keys 改变顺序变换、memory/compute 近线性增长
corpus/tokenizermapping、V、split 全部版本化signal coverage 与 embedding/head sizes
dhead=C/HN,#attention scores=BHT2d_{\mathrm{head}}=C/H\in\mathbb N,\qquad \#\mathrm{attention\ scores}=BHT^2

改变 block_size 会让 position_embedding 与 causal_mask shapes 和旧 checkpoint 不兼容;改变 V 会同时改变两端矩阵;改变 width/depth 会改大量 state keys/shapes。迁移必须显式设计。

Knowledge check

本 multi-head implementation 的核心整数约束是什么?

20. 项目检查清单:从输入与目标到学习证据

Scroll horizontally to view all columns.

Course data table
group必须观察的证据
Tokenizer五 token round-trip;unknown rejected;long IDs 0..4;exact SHA-256 identity
Batchexact raw/inputs/targets arrays;[3,2];六个 next-token pairs
Model[3,2,5];520 params;two registered blocks;stable member names
Gradient/updatefinite loss/expected grads;one step changes parameter;count increments after success
Checkpointstrict schema/config/tokenizer/untied/AdamW checks;restored eval logits equal reference
Causality只改变 future token,earlier-position logits 保持 close
python
model.eval()
a = torch.tensor([[0, 1]], dtype=torch.long, device=device)
b = torch.tensor([[0, 4]], dtype=torch.long, device=device)
with torch.no_grad():
    logits_a, _ = model(a)
    logits_b, _ = model(b)
torch.testing.assert_close(logits_a[:, 0, :], logits_b[:, 0, :])

pθ(xt+1xt,x>t)=pθ(xt+1xt)p_\theta(x_{t+1}\mid x_{\le t},x_{>t})=p_\theta(x_{t+1}\mid x_{\le t})
Concept sequence
  1. tokenizer/batch 失败:停止
  2. forward/loss 失败:停止,不 backward
  3. gradient/update 失败:停止,不加模型规模
  4. checkpoint round-trip 失败:拒绝 incompatible state
  5. causality 失败:先查 mask/slice/axes
  6. generation shape/probability 失败:先查 crop/last/sample/append

Knowledge check

哪个实验直接检查 position 0 没读 position 1?

21. Weeks 1–12 怎样汇合:每周负责一段因果链

Scroll horizontally to view all columns.

Course data table
Week在最终系统中的责任本 trace 的对象
1数、函数、vector/matrix 与 shape[3,2]、[3,2,4] 与 matrix multiplication
2loss 与 gradient descentscalar CE 与 update direction
3neurons/nonlinearityFFN 的 Linear→GELU→Linear
4chain rule/backproploss.backward() 到每个 parameter.grad
5tensor axes/PyTorchB、T、C、H、d_head、V 与 broadcasting
6token IDs、embedding、next-token CEV=5 corpus 与六个 shifted tasks
7causal Q/K/V[B,H,T,T] scores/mask/value aggregation
8residual、norm、FFN Block两个 pre-norm blocks,shape 保持 [3,2,4]
9tokenizer/data protocol冻结 units/IDs;隔离 w09-readable-v1
10canonical MiniGPT ownershipGPTConfig、stable members、520 params
11training/validation/checkpoint/generation lifecycleAdamW validators、strict restore、stable sampling
12组装与可观察性同一 trace 从 text 到 restored generation
Concept sequence
  1. Weeks 1–5:math 与 tensor runtime
  2. Weeks 6–9:language objective、context 与 data identity
  3. Weeks 10–11:model architecture 与 lifecycle state
  4. Week 12:同一 evidence chain 中组装、恢复与诊断
θforwardZcross entropyLchain ruleθLAdamWθ\theta\xrightarrow{\mathrm{forward}}Z\xrightarrow{\mathrm{cross\ entropy}}\mathcal L\xrightarrow{\mathrm{chain\ rule}}\nabla_\theta\mathcal L\xrightarrow{\mathrm{AdamW}}\theta'

Knowledge check

哪一周解释 Attention score 为什么有 T×T axes?

22. 最终 Mental Model:一个带状态的条件概率机器

Tokenizer 建立离散协议;embeddings 与 causal Transformer 把允许读取的左侧 context 变成 representation;LM head 打分;Softmax 把最后一行变成条件分布。Training 改 θ,generation 只增长 history。

Scroll horizontally to view all columns.

Course data table
lane完整 trace改变的长期对象
trainingtext→IDs→[3,2]→[3,2,5]→[6,5]+[6]→loss→grads→stepθ、AdamW state、completed_updates
generationprompt→crop→logits→last [B,5]→sample [B,1]→appendcaller history;θ/optimizer 不变
checkpoint restorevalidated serialized state→model/optimizer objects显式替换长期 state,但不是 learning
pθ(x1:L)=t=1Lpθ(xtx<t)p_\theta(x_{1:L})=\prod_{t=1}^{L}p_\theta(x_t\mid x_{<t})
θ=Optimizer(θ,θL,optimizer state)\theta'=\operatorname{Optimizer}(\theta,\nabla_\theta\mathcal L,\mathrm{optimizer\ state})

下面完整序列分解是一般语言模型记法。本文五词 MiniGPT 没有 BOS,forward 也拒绝空序列,因此实际运行总从一个非空 prompt 开始,只建模后续 token 在给定 prompt 下的条件概率;首词不是它从空输入预测出来的。

Knowledge check

一次 generation call 中什么固定,什么增长?

23. 最终理解测试:从症状回到出错边界

错误在 generation boundary:先取 logits[:,-1,:] [B,5],经过稳定 temperature/top-k/Softmax 后用 multinomial 得到 torch.long next_id [B,1],追加这个 ID,而不是追加五维 probabilities。

RB×VsamplingNB×1appendNB×(L+1)\mathbb R^{B\times V}\xrightarrow{\mathrm{sampling}}\mathbb N^{B\times1}\xrightarrow{\mathrm{append}}\mathbb N^{B\times(L+1)}

torch.load(..., map_location="cpu") 可以搬移 serialized tensors;它不会改变 Vocabulary meaning。Schema/version、ordered tokenizer artifact/hash/policy、exact GPTConfig、untied policy、model state keys/shapes,以及 faithful resume 的 exact AdamW class/group/order/state 与 completed_updates 仍必须匹配。

Knowledge check

Generation 为什么不能直接 append [B,5]?

24. 完成课程:用自己的话解释一次学习与生成

Learning 就是在明确 examples、模型与 loss 下寻找降低平均 next-token prediction error 的 parameters。Embedding、Q/K/V、residual、FFN 与 AdamW 都服务于“让条件分数可计算、误差可求导、参数可调整”。

Learning=finding parameters θ that minimize measured prediction loss\boxed{\mathrm{Learning}=\text{finding parameters }\theta\text{ that minimize measured prediction loss}}
θ=argminθE(x,y)D[L(fθ(x),y)]\theta^*=\arg\min_\theta\mathbb E_{(x,y)\sim\mathcal D}[\mathcal L(f_\theta(x),y)]
Concept sequence
  1. 定义 text/token/data contract
  2. 验证 tensor/causal forward contract
  3. 用 loss 与 gradients 执行 successful updates
  4. 保存并严格恢复 compatible state
  5. 在 held-out evidence 与 target-free generation 中评估用途和限制

Knowledge check

MiniGPT objective 的三个 ingredients 是什么?