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 10 的同一个 MiniGPT 类;机制演示和独立文档实验各有明确的数据、配置和保存格式。
Scroll horizontally to view all columns.
| 学习单元 | 本次解决的问题 |
|---|---|
| 任务 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.
| ID | token |
|---|---|
| 0 | 我 |
| 1 | 喜欢 |
| 2 | AI |
| 3 | 学习 |
| 4 | 猫 |
- raw text → raw IDs [B,N]=[3,3]
- shift → inputs/targets [B,T]=[3,2]
- token + position embeddings [B,T,C]=[3,2,4]
- two canonical pre-norm Blocks [3,2,4]
- bias-free LM head logits [B,T,V]=[3,2,5]
- reshape logits [6,5] + targets [6] → mean loss []
- backward → AdamW step → checkpoint → strict restore
- restored target-free context → last logits [B,5] → next ID [B,1]
Scroll horizontally to view all columns.
| 监督题 | context | target |
|---|---|---|
| 1 | [我] | 喜欢 |
| 2 | [我,喜欢] | AI |
| 3 | [猫] | 喜欢 |
| 4 | [猫,喜欢] | 我 |
| 5 | [我] | 学习 |
| 6 | [我,学习] | AI |
Knowledge check
Supervised training 在哪个对象上第一次成为一个 scalar objective?
1. 项目结构:责任先于文件名
本周不另起配置类名,也不定义第二套 MiniGPT。
Scroll horizontally to view all columns.
| module / object | 责任 | 关键接口或长期 state |
|---|---|---|
| mini_gpt_walkthrough.py | Week 10 canonical architecture | GPTConfig、MiniGPT、stable member names、_init_weights |
| week11_training_and_generation.py | lifecycle controller | train_mini_gpt_step、validators、restore、stable sampling |
| week12_end_to_end.py | 组装 fixed trace | batch、100 updates、reference、save/load、generate |
| checkpoint file | 持久化兼容 state | schema、tokenizer identity、config、untied weights、AdamW、completed_updates |
Scroll horizontally to view all columns.
| 实际文件 | 你可以观察到的产物 |
|---|---|
| course_examples/mini_gpt_walkthrough.py | 520 参数、输入输出 shape、因果检查 |
| course_examples/week11_training_and_generation.py | 可导入的训练/验证/恢复/生成函数 |
| course_examples/week12_end_to_end.py | 100 次更新、前后概率、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。
# 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()
)Scroll horizontally to view all columns.
| boundary | 固定 evidence | 失败就先查 |
|---|---|---|
| tokenizer/batch | [3,3]→[3,2],long,IDs 0..4 | mapping 与 shift |
| forward | [3,2]→[3,2,5],loss [] finite | rank、device、mask、targets |
| backward | expected .grad non-None/finite | graph 与 zero order |
| step | 至少一个 parameter value 改变,count=1 | optimizer binding 与 step |
Knowledge check
问“文字是否好”之前至少先核对什么?
3. 从文字开始:冻结 Tokenizer 才有稳定地址
本教学 tokenizer 仅按空格切分已知词;它没有 specials、PAD 或 UNK。
Scroll horizontally to view all columns.
| ID | token |
|---|---|
| 0 | 我 |
| 1 | 喜欢 |
| 2 | AI |
| 3 | 学习 |
| 4 | 猫 |
# 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_IDSTokenizer 只决定“切成什么、编号是什么”;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.
| row | raw 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 |
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]Knowledge check
第三行 input 学习 的 target 是什么?
5. Embedding Lookup:离散地址变成四个可学习 Features
token_embedding 选 row,position_embedding 选位置;两者相加后才进入 Block。
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.
| axis | 值 | 含义 |
|---|---|---|
| B=3 | 三行 | 三个 sequences |
| T=2 | 两列 | 每行两个 input positions |
| C=4 | 四通道 | 模型学习的 features,不是人工命名属性 |
Knowledge check
为什么 [3,2,4] 可以与 [2,4] 相加?
6. Transformer Block:跨位置读取,再逐位置变换
两个 canonical blocks 依次处理同一个 [3,2,4] 表示;Attention mixing T,FFN 在每个位置 mixing C。
# 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]Scroll horizontally to view all columns.
| component | 读取范围 | 内部宽度 | external shape |
|---|---|---|---|
| causal Attention | 允许的当前/左侧 positions | H=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.
| operation | shape | axes |
|---|---|---|
| x | [3,2,4] | [B,T,C] |
| qkv(x) | [3,2,12] | [B,T,3C] |
| q,k,v | each [3,2,4] | [B,T,C] |
| split/transpose | each [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] |
Knowledge check
一个 batch example 的一个 head 有多大的 score matrix?
8. LM Head:每个位置给五个候选打分
两个 blocks 后仍是 [3,2,4];final_norm 保持 shape,lm_head 为每个位置产生五个 scores。
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=FalseScroll horizontally to view all columns.
| 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 同步,所以六道题仍一一对齐。
flat_logits = logits.reshape(6, 5)
flat_targets = targets.reshape(6)
loss = torch.nn.functional.cross_entropy(flat_logits, flat_targets)
assert loss.ndim == 0Knowledge check
Targets [3,2] flatten 后是什么 shape?
10. Backward Trace:从一个数回到所有参与参数
loss.backward() 反向穿过构建它的 graph,把 derivatives 累加进 participating parameters 的 .grad;此时 parameter values 还没变。
- loss []
- six logit rows [6,5]
- lm_head.weight.grad [5,4] + final_norm grads
- Block 2 grads → Block 1 grads
- token/position embedding table grads [5,4] / [2,4]
- optimizer.step() 之后才产生新 parameter values
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.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.
| ID/token | input lookup? | target candidate? | 本 batch 的直接路径 |
|---|---|---|---|
| 0/我 | 是 | 是 | token row lookup + LM-head target/non-target scoring |
| 1/喜欢 | 是 | 是 | token row lookup + LM-head scoring |
| 2/AI | 否 | 是 | token_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 |
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.
| 观察时点 | 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 而移动 |
# 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.
| operation | parameter.grad | parameter values | AdamW state / count |
|---|---|---|---|
| zero_grad | 清空 | 不变 | 不变 |
| forward + loss | 尚未写新值 | 不变 | 不变 |
| backward | 写入/累加 | 不变 | 不变 |
| clip | 可能原地缩放 | 不变 | 不变 |
| optimizer.step | 被读取 | 更新 | moments/counters 更新 |
| step 成功后 | 仍在,等待清理 | 已更新 | completed_updates += 1 |
Plain SGD 的基本式是 θ_t=θ_{t-1}−ηg_t;AdamW 不直接把 raw g_t 当 update。即使当前 g_t 的某个 row 为零,decoupled factor (1−ηλ) 仍可改变非零参数;恢复的 m/v history 也可让 adaptive term 非零。
loss.backward()
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
optimizer.step()
completed_updates += 1 # only after the successful step returnsKnowledge 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
# 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()Scroll horizontally to view all columns.
| checkpoint identity field | 固定内容 |
|---|---|
| schema | mini-gpt-training-checkpoint / version 1 |
| tokenizer | mini-gpt-v1 ordered tokens + exact policy + SHA-256 |
| tokenizer SHA-256 | 38d630f4c589664c9bef567457d48764cbe2307734777e80f7d5d5c63ac88dd6 |
| config | GPTConfig(5,2,4,2,2),无 dropout |
| weight policy | token_embedding_lm_head=untied |
| training state | model_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.
| runtime observation | label / 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。
- uncropped history [1,2] = [[0,1]]
- crop forward context to last block_size=2 IDs [1,2]
- restored(context), targets=None → logits [1,2,5]
- logits[:,-1,:] → last_logits [1,5]
- promote/row-center/divide by τ → optional top-k → checked Softmax [1,5]
- multinomial → next_id torch.long [1,1]
- append to uncropped history → [1,3],repeat
Scroll horizontally to view all columns.
| phase | positions consumed | targets? | state change |
|---|---|---|---|
| training | all B×T=6 logits rows | 有 [3,2] | backward + step 更新 θ/AdamW |
| generation | only 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.
| strategy | 保存什么 | 每轮工作 | trade-off |
|---|---|---|---|
| 朴素 loop | caller full token IDs | 重算 cropped context 的全部 layers | 最清楚、最容易验证 |
| KV cache | 每层 past keys/values + positions | 主要计算新 token | 更快但需严格管理 cache shape/device/reset |
在前缀、位置编号和计算语义一致时,KV cache 可复用过去的 keys/values,并在数值误差范围内保持相同预测。这里不能直接套用:滑窗裁剪后位置编号从 0 重新开始,保留 token 的位置表示和可见前缀都可能改变,旧 cache 不再自动等价。本教学程序保持完整重算,缓存优化留作独立项目。
Scroll horizontally to view all columns.
| 计算情形 | 每轮 attention 分数工作 | 累计随生成长度的增长 |
|---|---|---|
| 不裁剪,前缀持续增长 | 约 t² | 求和后约 L³,只计分数工作 |
| 本例固定最多看 2 个 token | 每 head 最多 4 个分数 | 每轮有固定上限,累计随轮数近线性 |
Knowledge check
为什么 block_size=2 仍有重复计算?
16. 三阶段诊断:Pipeline、Same-batch Learning、Generalization
三个阶段回答不同问题:代码能否流通、模型能否从这批数据学到其可表达的规律、规律能否用于未见数据。
Scroll horizontally to view all columns.
| stage | 冻结什么 | evidence | stop condition |
|---|---|---|---|
| 1 Pipeline correctness | fixed batch/config/seed | [3,2]→[3,2,5]→[];finite grads;parameter changes | 任一边界失败就停止 |
| 2 Same-batch learning | 重复同一 fixed batch | loss 显著低于 ln(5),可区分 contexts 分化;[我]趋向 empirical 0.5/0.5 | 不要要求 loss=0 或六题全 argmax |
| 3 Generalization | 更大 corpus 的 frozen train/held-out split | eval + no_grad 的 held-out token-weighted loss 与 samples | 只有这里可讨论 generalization |
@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_targetsKnowledge check
哪个 stage 才能声称评估 generalization?
17. 项目 B:用独立文档完成训练、验证与结果解释
本实验使用下载包 data/documents 内的 8 篇原创英文训练短文和 3 篇独立验证短文。题材都与观察、学习、日常活动有关,降低“完全不同领域”造成的混淆,但材料很少、文风单一,不能作为语言能力 benchmark。训练文件不含验证原文,不等于已经排除所有近似重复或数据偏差。
Scroll horizontally to view all columns.
| 条件 | 本实验设置 | 为什么明确写出 |
|---|---|---|
| 输入单位 | 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 个字符的末尾部分不用于本实验,报告会记录真正计分的目标数量。窗口不跨文档,更不能跨训练/验证边界。
示意一个文档开头(实际程序按字符切,不按单词):
原始 25 个字符 [c0,c1,...,c24]
inputs 长度 24 [c0,c1,...,c23]
targets 长度24 [c1,c2,...,c24]
下一窗从 c24 开始;它的第一个目标是 c25。回忆 Week 6:给出目标不表示模型能看见目标。位置 t 的输出只使用本窗口中到 t 为止的输入。把不同位置都算 loss,是并行提供多道监督题,不是取消 causal mask。
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.
| 生成的文件 | 怎样阅读它 |
|---|---|
| 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 的同一约定。
怎样解释曲线:两条都下降,表示在这个有限实验中两份资料的平均预测改善;训练下降、验证上升,先检查重复/切分、目标对齐、模式与样本量,再考虑过拟合。单个点反弹不足以定论;验证改善也不能证明事实正确性、推理能力或开放域泛化。
python week12_generalization.py --generate-only runs/first/inference.pt --prompt "a " --new-tokens 80加载时重建保存的同一模型配置和字符表。不要把这个 checkpoint 交给五词模型的严格恢复器;两种实验的格式和词表明确不同。程序会比较保存/加载前同一输入的 logits,但不宣称能逐步复现训练中断后的随机轨迹。
Scroll horizontally to view all columns.
| 练习阶段 | 只改一件事 | 通过证据 |
|---|---|---|
| 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.
| unit | “我喜欢AI,AI也喜欢猫。”示意 | 优势 | 代价 |
|---|---|---|---|
| character | 逐 Unicode 字符 | 直观 | 长词/英文片段可能很长 |
| word | 按词/边界 | 序列短 | 未知词与切词规则困难 |
| UTF-8 byte | 每个 byte 0..255 | 任意文本可逆 | 中文字符通常占多个 bytes |
| BPE/subword | 依 learned merges 而定 | coverage 与长度折中 | 需版本化 merge rules |
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.
| lever | 接口/约束 | 主要影响 |
|---|---|---|
| block_size T_max | position table 与 causal mask 同步扩展 | attention score memory/work 约 T² |
| n_embd C | 所有 residual paths 同宽 | projections/FFN 参数与 compute 大致 C² |
| n_head H | C mod H=0,d_head=C/H | attention routing partition |
| n_layer | ModuleList 深度与 state keys 改变 | 顺序变换、memory/compute 近线性增长 |
| corpus/tokenizer | mapping、V、split 全部版本化 | signal coverage 与 embedding/head sizes |
改变 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.
| group | 必须观察的证据 |
|---|---|
| Tokenizer | 五 token round-trip;unknown rejected;long IDs 0..4;exact SHA-256 identity |
| Batch | exact raw/inputs/targets arrays;[3,2];六个 next-token pairs |
| Model | [3,2,5];520 params;two registered blocks;stable member names |
| Gradient/update | finite loss/expected grads;one step changes parameter;count increments after success |
| Checkpoint | strict schema/config/tokenizer/untied/AdamW checks;restored eval logits equal reference |
| Causality | 只改变 future token,earlier-position logits 保持 close |
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, :])- tokenizer/batch 失败:停止
- forward/loss 失败:停止,不 backward
- gradient/update 失败:停止,不加模型规模
- checkpoint round-trip 失败:拒绝 incompatible state
- causality 失败:先查 mask/slice/axes
- generation shape/probability 失败:先查 crop/last/sample/append
Knowledge check
哪个实验直接检查 position 0 没读 position 1?
21. Weeks 1–12 怎样汇合:每周负责一段因果链
Scroll horizontally to view all columns.
| Week | 在最终系统中的责任 | 本 trace 的对象 |
|---|---|---|
| 1 | 数、函数、vector/matrix 与 shape | [3,2]、[3,2,4] 与 matrix multiplication |
| 2 | loss 与 gradient descent | scalar CE 与 update direction |
| 3 | neurons/nonlinearity | FFN 的 Linear→GELU→Linear |
| 4 | chain rule/backprop | loss.backward() 到每个 parameter.grad |
| 5 | tensor axes/PyTorch | B、T、C、H、d_head、V 与 broadcasting |
| 6 | token IDs、embedding、next-token CE | V=5 corpus 与六个 shifted tasks |
| 7 | causal Q/K/V | [B,H,T,T] scores/mask/value aggregation |
| 8 | residual、norm、FFN Block | 两个 pre-norm blocks,shape 保持 [3,2,4] |
| 9 | tokenizer/data protocol | 冻结 units/IDs;隔离 w09-readable-v1 |
| 10 | canonical MiniGPT ownership | GPTConfig、stable members、520 params |
| 11 | training/validation/checkpoint/generation lifecycle | AdamW validators、strict restore、stable sampling |
| 12 | 组装与可观察性 | 同一 trace 从 text 到 restored generation |
- Weeks 1–5:math 与 tensor runtime
- Weeks 6–9:language objective、context 与 data identity
- Weeks 10–11:model architecture 与 lifecycle state
- Week 12:同一 evidence chain 中组装、恢复与诊断
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.
| lane | 完整 trace | 改变的长期对象 |
|---|---|---|
| training | text→IDs→[3,2]→[3,2,5]→[6,5]+[6]→loss→grads→step | θ、AdamW state、completed_updates |
| generation | prompt→crop→logits→last [B,5]→sample [B,1]→append | caller history;θ/optimizer 不变 |
| checkpoint restore | validated serialized state→model/optimizer objects | 显式替换长期 state,但不是 learning |
下面完整序列分解是一般语言模型记法。本文五词 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。
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 都服务于“让条件分数可计算、误差可求导、参数可调整”。
- 定义 text/token/data contract
- 验证 tensor/causal forward contract
- 用 loss 与 gradients 执行 successful updates
- 保存并严格恢复 compatible state
- 在 held-out evidence 与 target-free generation 中评估用途和限制
Knowledge check
MiniGPT objective 的三个 ingredients 是什么?