Current: Week 6

0%

Week 6

Week 6 - 从 Token 到最小语言模型:用一个例子理解预测

Key question模型怎样把“我 喜欢 AI”变成数字、根据上下文为每个候选打分,再通过训练提高正确 Token 的概率?

Learning objectives

  • 区分 Token、Tokenizer、Vocabulary、Token ID、Embedding、Context Representation、Logit 与 Probability。
  • 解释 Output Head 怎样通过 Dot Product 和 Bias 产生原始 Logits,并手算 Softmax 与 Cross Entropy。
  • 沿同一批数据追踪 [3,3] → [3,2] → [3,2,4] → [3,2,5] → [6,5] → Scalar Loss。
  • 用 Bigram 跑通训练与生成,再说明它为什么需要 Attention 才能读取更早的上下文。

100 min estimated reading time

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

本周不从概率符号开始,而从三句有答案的文字开始。前置是 Week 5 的 Tensor、矩阵乘法和一次训练步骤。主线词表固定为 [我,喜欢,AI,学习,猫],以后讲 Attention 时也不改这些编号。

Scroll horizontally to view all columns.

Course data table
学习单元本次解决的问题
一:把文字变成题先 Token,再 Input/Target,再 Batch;说明 B 是几行、T 是每行几个输入位置,为什么需要 T+1 个原始 token。
二:表示、评分、计错解释 C 个表示特征和 V 个候选的区别;Embedding → 给定 h 的输出层 → logits → Softmax → CE。
三:连起来并学习用串联练习核对六道题,再运行 Bigram 的完整一步和重复训练。
四:生成与局限先明确如何选择和追加,再解释同一末尾 token 无法区分不同前缀;引出 Attention。

完成标准:手算 p(喜欢)≈0.5923 和 CE≈0.5237,解释 g=p−one-hot;运行 python week06_bigram.py。自然对数的求导与 Perplexity 可第二遍读;输出层的实际乘加、概率归一化及 target 对齐不能跳过。

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

Week 6 核心目标:把文字变成六道预测题

本周始终使用同一个 tokenizer 规则:按空格切分。固定词表、语料和维度不随章节改变。训练时模型同时学习六个“当前 token → 真实下一 token”问题;生成时则一次只追加一个 token。

Scroll horizontally to view all columns.

固定 Vocabulary(V=5)
IDToken
0
1喜欢
2AI
3学习
4
text
Corpus:
我 喜欢 AI
猫 喜欢 我
我 学习 AI

raw IDs = [[0,1,2], [4,1,0], [0,3,2]]
inputs  = [[0,1],   [4,1],   [0,3]]
targets = [[1,2],   [1,0],   [3,2]]

Scroll horizontally to view all columns.

Course data table
符号固定值含义
B3batch 中的三条 sequence
N3shift 前每条 source sequence 的 token 数
T2右移后每条 sequence 的 next-token 训练位置
C4每个 token embedding 的连续 features
V5Vocabulary candidates / 行数
Concept sequence
  1. raw IDs [3,3]
  2. shifted inputs [3,2]
  3. embeddings [3,2,4]
  4. logits [3,2,5]
  5. flattened logits [6,5]
  6. flattened targets [6]
  7. scalar mean cross-entropy loss

Knowledge check

本周固定的六道预测题是什么?

1. Token、Tokenizer、Vocabulary 与 Token ID

Token 是 Tokenizer 交给模型处理的基本单位,不一定是完整单词,也可能是字符、subword 或 byte。本周为了能手算,Tokenizer 按空格切分。Vocabulary 是它允许使用的有限 token 清单;Token ID 是某个 token 在这份清单中的整数地址。

同一段文字在不同 Tokenizer 下可能产生不同数量的 Token。例如 learning 可能是一个 Token,也可能拆成 learn 与 ing。模型一次读取和生成的是 Token;切分方式会影响 sequence length、context 能容纳多少文字,以及训练和推理需要多少计算。真实 GPT 常用 subword 或 byte-level 方法,Week 9 再详细展开。

Scroll horizontally to view all columns.

同一 Tokenizer 下的完整编码
原文TokensIDs
我 喜欢 AI[我, 喜欢, AI][0,1,2]
猫 喜欢 我[猫, 喜欢, 我][4,1,0]
我 学习 AI[我, 学习, AI][0,3,2]

Knowledge check

“猫 喜欢 我”为何编码为 [4,1,0]?ID 4 表达什么?

8. Input 与 Target:用同一段文字自动出题,答案右移一位

Language Model 的训练任务不是“把输入原样复读”,而是“看到到目前为止的文字,猜紧接着的下一 Token”。因此一段原始文字既提供题目,也提供标准答案:每个 Token 左边的位置负责预测它。这个做法叫 self-supervised,因为答案来自文本本身,而不是人工逐句标注。

X=R[:,0:N1],Y=R[:,1:N],T=N1X=R[:,0:N-1],\qquad Y=R[:,1:N],\qquad T=N-1
text
raw IDs [B,N] = [[0,1,2], [4,1,0], [0,3,2]]  # [3,3]
inputs  [B,T] = [[0,1],   [4,1],   [0,3]]    # [3,2]
targets [B,T] = [[1,2],   [1,0],   [3,2]]    # [3,2]

T = N - 1 = 2

现在才把三条独立原始序列放进 Batch:第 0 条是“我 喜欢 AI”,第 1 条是“猫 喜欢 我”,第 2 条是“我 学习 AI”。B=3 只表示一次并行处理三条样本;它们不会彼此拼接,也不会互相提供上下文。每条有 T=2 个预测位置,所以本次一共是 3×2=6 道题。

Scroll horizontally to view all columns.

模型输出 p 的 shape 是 `[B,T,V]=[3,2,5]`:每一题都有五个候选概率,Target ID 指出该从最后一轴取哪一个。
题目位置 [b,t]该条原始文字一般 causal LM 此处可见的上下文正确 TargetCross Entropy 会读取哪一项?
[0,0]我 喜欢 AI喜欢 / ID 1p[0,0,1]
[0,1]我 喜欢 AI我 喜欢AI / ID 2p[0,1,2]
[1,0]猫 喜欢 我喜欢 / ID 1p[1,0,1]
[1,1]猫 喜欢 我猫 喜欢我 / ID 0p[1,1,0]
[2,0]我 学习 AI学习 / ID 3p[2,0,3]
[2,1]我 学习 AI我 学习AI / ID 2p[2,1,2]
Concept sequence
  1. 长文本切出 raw IDs R [B,N]
  2. 右移形成 inputs X [B,T] 与 targets Y [B,T],其中 T=N−1
  3. Transformer 用 causal mask 计算每个位置仅依赖左侧的 h [B,T,C]
  4. Output Head 得到每题五个 Logits / Probabilities [B,T,V]
  5. Target ID 指向每题正确概率 p[b,t,target_id]
  6. Cross Entropy 对全部 B×T 道题计算并平均 Loss

Knowledge check

原始序列“我 喜欢 AI”为什么只产生两题?第 0 题的 Target 与可见上下文各是什么?

4. 先看一个 Batch:B、T、C、V 分别在做什么

先不要把 `[B,T,C]` 当作一条需要背诵的公式。它只是一个三层表格:先选第几条训练样本,再选这条样本中的第几个预测位置,最后取出该位置的一组连续数字。我们先从只有一条样本的情况开始。

text
一条语料:我 喜欢 AI

输入 IDs  = [0, 1]    shape [T] = [2]
目标 IDs  = [1, 2]    shape [T] = [2]

第 0 个位置:看到 我,预测 喜欢
第 1 个位置:看到 喜欢,预测 AI

这里 T=2,表示这条样本当前提供两个 next-token 预测位置。原句有 N=3 个 Token;右移一位后才得到 T=N−1=2 个训练问题。T 不是固定常数,而是当前每条序列参加训练的位置数。

Scroll horizontally to view all columns.

先只有一条样本时,shape 只需读成 [T]。
把一条样本放到 Batch 前它回答的问题
inputs[0] = 我喜欢
inputs[1] = 喜欢AI

Batch 的意思是:把多条独立样本放在同一个张量中,让 GPU/CPU 可以并行计算,再把所有预测的 Loss 取平均。它不是把三句话接成一长句话,也不表示第 0 条会把信息传给第 1 条;在本课的 Transformer 中,不同 batch 样本之间不会互相 Attention。

text
B = 3:这一次同时处理三条独立语料

inputs [B,T] = [[0,1],   [4,1],   [0,3]]    # shape [3,2]
targets[B,T] = [[1,2],   [1,0],   [3,2]]    # shape [3,2]

b=0:我 → 喜欢;喜欢 → AI
b=1:猫 → 喜欢;喜欢 → 我
b=2:我 → 学习;学习 → AI

Scroll horizontally to view all columns.

所以 `[B,T]=[3,2]` 应读成“三条样本 × 每条两个预测位置”,不是“六个 Token 串成一句话”。
符号本例数值它在现实中数什么此时的读法
B3Batch 中有几条独立样本先选哪一条句子
T2每条样本有几个预测位置再选这条句子的哪个位置
B×T6本次训练一共有几道 next-token 题三个样本各两题

现在才把 Embedding 接上。`inputs[b,t]` 仍然只是一个整数地址;Embedding 为每一个地址查出长度为 C 的浮点向量。B 和 T 都保留,因为我们仍要知道“哪条样本、哪个位置”;只是在最后新增一个 C 轴,记录这个位置的内部表示。

text
Embedding table E 的 shape 是 [V,C] = [5,4]

inputs[0,1] = 1                  # 第 0 条样本、第 1 个位置是 “喜欢”
x[0,1,:] = E[1]                 # 取出它的全部 C=4 个数
         = [0.60, 0.30,-0.20,0.10]

[B,T] --Embedding--> [B,T,C]
[3,2]  ------------> [3,2,4]

`x[b,t,:]` 中的冒号表示“这个位置的所有 C 个特征”。例如 `x[1,0,:]` 是第 1 条样本里的“猫”所对应的四个数。C 可以理解为模型给每个位置留出的表示宽度;这里故意设为 4 便于手算,真实模型通常会大得多。单个坐标本身没有预先命名的词义,训练会调整它们。

text
x.shape       = [3,2,4]   # 三个 axis,rank=3
x[1].shape    = [2,4]     # 取第 1 条样本,还剩两个位置、每处四个数
x[1,0].shape  = [4]       # 再取第 0 个位置,得到“猫”的完整表示
x[1,0,2]      = 0.30      # 再取第 2 个 feature,最终得到一个标量

b:选择哪条样本
t:选择样本中的哪个位置
c:选择该位置内部表示的哪个坐标

axis 可以理解为“沿哪个问题继续索引”,axis size 则是这个问题有多少个选择。`[3,2,4]` 的 rank 是 3,因为需要回答 b、t、c 三层问题;它有 3×2×4=24 个标量。rank=3 不表示只有三个数。

Scroll horizontally to view all columns.

Course data table
符号本例它的职责出现在哪个 shape 中
B3一次并行处理几条独立样本[B,T]、[B,T,C]、[B,T,V]
T2每条样本当前有几个预测位置[B,T]、[B,T,C]、[B,T,V]
C4每个位置的内部连续表示有多宽Embedding [V,C] 与表示 [B,T,C]
V5词表中有几个候选 Token;它不是输入 x 的额外特征Embedding [V,C] 的 row 数;之后的 Logits [B,T,V]

Scroll horizontally to view all columns.

B 是本次装入多少样本;T、C 与 V 则分别约束位置长度、模型表示宽度和词表接口。
如果改变直观变化主要影响
B:3 → 1每一步只装入一条样本模型结构不变;并行量、内存和一次 Gradient 使用的样本数改变
T:2 → 4每条样本保留四个有顺序的位置能处理更长的当前窗口;Attention 的位置间计算也会增加
C:4 → 8每个位置从四个内部数扩为八个表示宽度、相关权重矩阵、参数量和计算量改变
V:5 → 10词表有十个 Token 候选Embedding row 数和每个位置的输出 Logit 数都变为十个
text
Token IDs                 [B,T] = [3,2]  每格是一个整数地址
Embedding / context state  [B,T,C] = [3,2,4]  每格是一组 4 个内部特征
Output logits              [B,T,V] = [3,2,5]  每格是对 5 个 Token 的分数

Knowledge check

本例中 `x[1,0,:]` 是什么?为什么一次训练有六道题,而不是一条六个 Token 的句子?

2. 为什么 Token ID 还要变成 Embedding

Embedding 把离散 ID 查成一个可学习的连续向量。这里的输入表是 nn.Embedding(V,C) = nn.Embedding(5,4):五个 Vocabulary rows,每行四个 learned features。下面是明确标注的教学初始值,不是人工写好的词义。

如果直接把 ID 当普通数字,模型会被迫接受“猫=4 比 AI=2 大两倍”之类不存在的关系。ID 只回答去哪里查;Embedding 才提供后续神经网络可以组合、比较并通过 Gradient 微调的浮点特征。

Scroll horizontally to view all columns.

didactic initial value:E 的形状 [V,C]=[5,4]
ID / Tokenc₀c₁c₂c₃
0 / 我0.20-0.100.700.30
1 / 喜欢0.600.30-0.200.10
2 / AI-0.400.800.50-0.30
3 / 学习0.100.200.900.40
4 / 猫-0.700.400.300.60
E[token_id]R4E[\mathrm{token\_id}]\in\mathbb{R}^{4}

例如 我 选择 E[0]=[0.20,-0.10,0.70,0.30];猫 选择 E[4]=[-0.70,0.40,0.30,0.60]。训练会经由 Loss、Backpropagation 和 Optimizer 修改这些坐标。

Knowledge check

输入 ID 4 后得到标量还是向量?

3. One-hot 等价与 [V,C] Embedding Matrix

ERV×C=R5×4E\in\mathbb{R}^{V\times C}=\mathbb{R}^{5\times4}

Scroll horizontally to view all columns.

与第 2 节相同的 didactic initial value
ID / Tokenc₀c₁c₂c₃
0 / 我0.20-0.100.700.30
1 / 喜欢0.600.30-0.200.10
2 / AI-0.400.800.50-0.30
3 / 学习0.100.200.900.40
4 / 猫-0.700.400.300.60
text
one_hot(4) = [0,0,0,0,1]

one_hot(4) @ E
= E[4]
= [-0.70,0.40,0.30,0.60]

nn.Embedding(5,4).weight.shape == [5,4]

One-hot 是选择机制:它只把第 4 row 选出来。实际实现不会创建长度 V 的巨大 one-hot vector,而是直接读取 E[id];两者前向结果相同。

Knowledge check

AI 的 one-hot 与 lookup 结果是什么?

9. Logits:完整手算五个候选的原始分数

现在预测上下文“我”之后的 Token。假设经过 Input Embedding 和 Context Model 后,当前位置得到 h=[0.20,-0.10,0.70,0.30]。为了保持手算简单,本教学步骤暂时让 h 与“我”的输入 Embedding 相同;真实 Transformer 中的 h 通常已经融合上下文并被多层网络改变。

Scroll horizontally to view all columns.

这些 feature 没有预先指定的人类语义名称
Context featureh value怎样理解
h₀0.20第 0 个 learned context feature
h₁-0.10第 1 个 learned context feature
h₂0.70第 2 个 learned context feature
h₃0.30第 3 个 learned context feature

输出层为 Vocabulary 中的每个候选保存一行 Output Weights 和一个 Bias。每一行都像一条可学习的评分规则:把同一个 h 的四项证据按该候选自己的方式加权,然后求和。

zi=wih+bi=h0wi,0+h1wi,1+h2wi,2+h3wi,3+biz_i=w_i\cdot h+b_i=h_0w_{i,0}+h_1w_{i,1}+h_2w_{i,2}+h_3w_{i,3}+b_i

Scroll horizontally to view all columns.

教学用 Output Head 参数;真实参数由训练得到
候选Output Weight wᵢBias bᵢ代入计算Logit
[1,0,0,0]-0.21(0.2)−0.20
喜欢[0,1,2,2]0.1−0.1+2(0.7)+2(0.3)+0.12
AI[0,0,1,1]00.7+0.31
学习[0,3,−1,0]03(−0.1)−0.7−1
[1,2,0,0]00.2+2(−0.1)0

例如“喜欢”的评分完整展开为 0×0.20 + 1×(−0.10) + 2×0.70 + 2×0.30 + 0.10 = 2。这里没有任何一步在直接查“正确答案”;只是当前 h 与“喜欢”的 learned scoring weights 组合后得到较高总分。

z=[z,z喜欢,zAI,z学习,z]=[0,2,1,1,0]z=[z_{\text{我}},z_{\text{喜欢}},z_{\text{AI}},z_{\text{学习}},z_{\text{猫}}]=[0,2,1,-1,0]
python
import torch

# 当前上下文的四个内部特征
h = torch.tensor([
    0.20,
    -0.10,
    0.70,
    0.30,
])  # [C] = [4]

# 每一行是一个候选 Token 的 scoring weights
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],  # AI
    [0.0, 3.0, -1.0, 0.0],  # 学习
    [1.0, 2.0,  0.0, 0.0],  # 猫
])  # [V,C] = [5,4]

bias = torch.tensor([
    -0.2,
     0.1,
     0.0,
     0.0,
     0.0,
])  # [V] = [5]

logits = W_out @ h + bias
print(logits)
# tensor([0., 2., 1., -1., 0.])

手算时分别完成五个 Dot Products;矩阵 W_out 把五行评分规则排在一起,所以 W_out @ h + bias 可以一次产生五个候选分数。Bias [5] 为每个候选提供一个与当前 h 无关的可学习基础偏移。

WoutRV×C,hRC,z=Wouth+bRVW_{\mathrm{out}}\in\mathbb{R}^{V\times C},\quad h\in\mathbb{R}^{C},\quad z=W_{\mathrm{out}}h+b\in\mathbb{R}^{V}
H:[B,T,C]HWoutT+b:[B,T,V]H:[B,T,C]\quad\to\quad H W_{\mathrm{out}}^{\mathsf T}+b:[B,T,V]
python
# H: [B,T,C] = [3,2,4]
# W_out.T: [C,V] = [4,5]
logits = H @ W_out.T + bias
# logits: [B,T,V] = [3,2,5]

Logit 对 Softmax 重要,是因为 Softmax 使用候选之间的差距。若两个 Logits 相差 1,它们在 Softmax 后的概率比是 e¹≈2.718;如果只把“喜欢”的 Logit 提高,它相对其他候选的概率会增加。若给全部 Logits 同时加 100,差距不变,Probability 也不变。

pipj=ezizj\frac{p_i}{p_j}=e^{z_i-z_j}

Knowledge check

本例“喜欢”的 Logit=2 是怎样产生的?它是不是 200%?

10. Softmax:把 Logits 变成 Probability

可以先只看两个候选分数 [0,1]:指数后变成 [1,2.718],总和为 3.718,除以总和得到约 [0.269,0.731]。每一项都非负,并且加总为 1。再回到五候选表,只是同样的计算多做三项。

上一节已经从 h、W_out 和 bias 真正算出 logits=[0,2,1,-1,0]。这些数可以比较高低,却不是 Probability:2 不代表 200%,−1 也不代表负概率。Sampling 和 Cross Entropy 需要一组全部非负、总和等于 1 的合法分布。

pi=ezij=0V1ezjp_i=\frac{e^{z_i}}{\sum_{j=0}^{V-1}e^{z_j}}

第一步是指数化。e⁰=1、e²≈7.389、e¹≈2.718、e⁻¹≈0.368。负 Logit 经过指数后仍是正数,因此对应候选仍有机会,只是相对权重较小。

Scroll horizontally to view all columns.

固定顺序 [我, 喜欢, AI, 学习, 猫];logits [0,2,1,-1,0]
candidate orderlogitexponentialprobability
01.0000.080
喜欢27.3890.592
AI12.7180.218
学习-10.3680.029
01.0000.080

第二步是归一化。五个指数权重之和为 12.475;每个候选除以同一个总和后,得到 [0.080,0.592,0.218,0.029,0.080],全部相加约等于 1。

text
exponentials = [1.000,7.389,2.718,0.368,1.000]
sum = 12.475
probabilities = [0.080,0.592,0.218,0.029,0.080]
0.080 + 0.592 + 0.218 + 0.029 + 0.080 ≈ 1

  1. 指数化:所有结果都大于 0,所以负 Logit 也能成为合法正权重。
  2. 保持顺序:若 zᵢ > zⱼ,则 eᶻⁱ > eᶻʲ,原本更高的候选仍然更高。
  3. 转成相对比:pᵢ/pⱼ = e^(zᵢ−zⱼ),Logit 差距直接控制候选之间的概率比。
  4. 归一化:除以全部候选权重的总和,使结果相加为 1。
zizj=1pipj=e12.718z_i-z_j=1\quad\Rightarrow\quad\frac{p_i}{p_j}=e^1\approx2.718
python
import torch
import torch.nn.functional as F

logits = torch.tensor([0.0, 2.0, 1.0, -1.0, 0.0])
probabilities = F.softmax(logits, dim=-1)

print(probabilities)
# tensor([0.0802, 0.5923, 0.2179, 0.0295, 0.0802])

print(probabilities.sum())
# tensor(1.)

若 logits 的 Shape 是 [B,T,V]=[3,2,5],dim=-1 表示对每一个 [b,t] 位置自己的五个 Vocabulary candidates 做 Softmax。它不会把不同句子或不同时间位置混在同一个分母中。

把所有分数同加 100 得到 [100,101],概率不变,因为候选之间仍差 1。实际计算常先减最大分数,例如 [0,1] 变 [-1,0],避免指数过大。Softmax 需要的是分数之间的差距,不是分数的绝对零点。

Knowledge check

Softmax 为什么先使用指数函数,再除以全部指数之和?

11. Cross Entropy:从正确答案概率到 Loss,再到 Gradient

Softmax 已得到五个候选概率。假设这道题的真实下一 Token 是“喜欢”,训练首先读取模型分给正确答案的概率 p_correct=0.592。我们希望:正确答案概率越接近 1,Loss 越接近 0;概率越接近 0,惩罚越大。

L=ln(py)L=-\ln(p_y)

Scroll horizontally to view all columns.

正确答案 Probability 与 single-position Cross Entropy
p(correct)−ln(p)训练直觉
0.990.010几乎确信正确,惩罚接近 0
0.900.105正确且有把握
0.600.511倾向正确,但仍有明显不确定性
0.500.693只有一半概率给正确答案
0.102.303正确答案概率很低
0.014.605非常自信地偏离正确答案,惩罚很大

Scroll horizontally to view all columns.

Course data table
上下文targetp(correct)loss
喜欢(ID 1)0.592-ln(0.592) ≈ 0.524
我(若 target 改为)AI0.218约 1.524
我(若 target 改为)0.080约 2.524

Cross Entropy 不只检查 argmax 是否正确:若正确答案同样是喜欢,概率从 0.51 升到 0.90,loss 仍会下降。对多个位置,平均 loss 是每个正确 target 负对数概率的平均。

text
Vocabulary order        = [我,    喜欢,  AI,    学习,  猫]
predicted probabilities = [0.080, 0.592, 0.218, 0.029, 0.080]
one_hot(target=喜欢)     = [0,     1,     0,     0,     0]

L = -Σ yᵢ ln(pᵢ)
  = -(0 ln 0.080 + 1 ln 0.592 + 0 ln 0.218
      + 0 ln 0.029 + 0 ln 0.080)
  = -ln(0.592)
  ≈ 0.524

L=i=0V1yiln(pi)=ln(pcorrect)L=-\sum_{i=0}^{V-1}y_i\ln(p_i)=-\ln(p_{\mathrm{correct}})

Scroll horizontally to view all columns.

候选顺序为 [我, 喜欢, AI, 学习, 猫];显示值经四舍五入。五个 Gradient 相加约为 0,说明 Softmax 主要重新分配候选之间的相对分数。
候选pᵢTarget yᵢgᵢ=pᵢ−yᵢGradient Descent 的方向
0.08020+0.0802z_我 向下调一点
喜欢(正确)0.59231−0.4077z_喜欢 向上调一点
AI0.21790+0.2179z_AI 向下调一点
学习0.02950+0.0295z_学习 向下调一点
0.08020+0.0802z_猫 向下调一点
Lzi=pione_hot(y)i\frac{\partial L}{\partial z_i}=p_i-\mathrm{one\_hot}(y)_i
text
p                     = [0.080, 0.592, 0.218, 0.029, 0.080]
one_hot(target=喜欢)   = [0,     1,     0,     0,     0]
p - one_hot(target)   = [0.080,-0.408, 0.218, 0.029, 0.080]

Gradient Descent subtracts this gradient:
喜欢 has a negative gradient, so its relative logit is pushed up;
the other candidates have positive gradients, so their relative logits are pushed down.

记号提醒:y=1 表示正确类别的一个整数 ID;公式里的 yᵢ 表示该 ID 对应 one-hot 向量的第 i 项:只有 i=1 时为 1,其余为 0。它们不是把整数 1 拆成许多个 1。

Knowledge check

Target 是“喜欢”且 p_喜欢=0.5923 时,g_喜欢 与 g_AI 分别是多少?它们告诉 Gradient Descent 做什么?

12. 为什么 PyTorch F.cross_entropy 接收 Logits

F.cross_entropy 在内部稳定地结合 LogSoftmax 与 negative log likelihood。因此它的输入是 raw logits;本课使用硬标签:targets 是范围 0 到 V−1 的 torch.long IDs,每题一个正确类别。

python
import torch
import torch.nn.functional as F

# 一道题,五个候选 raw logits
logits_one = torch.tensor(
    [[0.0, 2.0, 1.0, -1.0, 0.0]],
    requires_grad=True,
)  # [1,5]

# 正确答案是“喜欢”,ID=1
target_one = torch.tensor([1], dtype=torch.long)  # [1]

loss_one = F.cross_entropy(logits_one, target_one)
loss_one.backward()

print(loss_one.item())
# approximately 0.524

print(logits_one.grad)
# approximately [[0.080, -0.408, 0.218, 0.029, 0.080]]

这段代码与上一节手算完全对应:[1,5] 表示一题有五个候选分数,[1] 表示这一题只需一个正确类别 ID。确认单题后,再把 batch 中的六个位置排成六题。

Scroll horizontally to view all columns.

reshape 保持 row 对齐:flattened logits 的每行与 flattened targets 的同一索引是一道题
flattened rowbatch positioninput / contexttarget tokentarget ID
0[0,0]我 / 我喜欢1
1[0,1]喜欢 / 我 喜欢AI2
2[1,0]猫 / 猫喜欢1
3[1,1]喜欢 / 猫 喜欢0
4[2,0]我 / 我学习3
5[2,1]学习 / 我 学习AI2
python
import torch
import torch.nn.functional as F

# logits: [B,T,V] = [3,2,5]; targets: [B,T] = [3,2]
B, T, V = logits.shape
logits_2d = logits.reshape(B * T, V)       # [6,5]
targets_1d = targets.reshape(B * T)        # [6], dtype=torch.long

loss = F.cross_entropy(
    logits_2d,
    targets_1d,
)                                           # scalar mean loss

# Incorrect: F.cross_entropy expects logits, not already-softmaxed probabilities.
# probabilities = F.softmax(logits_2d, dim=-1)
# loss = F.cross_entropy(probabilities, targets_1d)

[3,2,5][6,5]and[3,2][6][3,2,5]\to[6,5]\quad\text{and}\quad[3,2]\to[6]

API 边界补充:PyTorch 的交叉熵也支持与 logits 同 shape 的浮点类别概率作为 targets,例如软标签;所以“交叉熵永远不接受 one-hot”并不准确。本课只采用整数硬标签,便于看清六道题和六个答案的对应关系;所有示例继续使用 [BT,V] logits 与 [BT] long targets。

Knowledge check

为什么 logits_2d 是 [6,5] 而 targets_1d 是 [6]?

6. Language Model:这五个 Probability 到底在回答什么?

上一节追踪了 [0,0] 的一题:可见文字是“我”,正确下一 Token 是“喜欢”。该位置先有一个上下文表示 h,再经过 Output Head 得到 Logits z=[0,2,1,−1,0],最后经过 Softmax 得到 p=[0.0802,0.5923,0.2179,0.0295,0.0802]。这一节不重复计算点积或指数;现在要回答的是:这五个数字在语言上各自是什么意思?

P(xt+1xt)P(x_{t+1}\mid x_{\le t})

Scroll horizontally to view all columns.

这里默认 causal language model:当前位置只能使用左侧(含当前位置)信息,不能偷看右侧的 Target。
公式部分这里的意思用“我 喜欢 AI”举例
x一串 Token(实现时通常是一串 Token IDs)“我”“喜欢”“AI”依次是 x₀、x₁、x₂。
t当前正在给哪一个位置出题若 t=1,模型已经看到了位置 0 和 1。
x≤t从开始到 t 的可见前缀x≤1 就是“我 喜欢”。
“在已知……的条件下”不是除法;读成“已知我喜欢之后”。
P模型输出的概率规则P(AI|我喜欢) 是模型给“AI”作为下一 Token 的概率。

所以 P(xₜ₊₁|x≤ₜ) 读成:“已经看过前缀 x≤ₜ 后,下一个 Token xₜ₊₁ 的概率是多少?”模型实际一次不会只返回一个数字;它会返回对整个 Vocabulary 的分布 p。我们只从这个分布中取出正确 Target 对应的 pᵢ,交给 Cross Entropy 打分。

现在可以把主链严格地读成:先由 Context Model 得到 h;再由 Output Head 计算 z=W_out h+b_out;最后由 p=Softmax(z) 得到每个候选的条件概率。h 是“根据上下文整理出的证据”,z 是“尚未归一化的候选分数”,p 才是“可用于回答下一 Token 概率”的分布。

Knowledge check

P(AI|我 喜欢) 在问什么?它与 p、z、h 分别怎样相连?

5. 串联练习:六道题怎样汇成一个 Loss

这是前面各小节的综合练习。章节保留原编号和链接,阅读顺序已调整:先完成 Input/Target、Batch、Embedding、Logits、Softmax 和 Cross Entropy,再到这里串联。不要把第一次见到的名词都压在一张总图里背。

Scroll horizontally to view all columns.

Course data table
对象具体值或 shape由谁产生、用来做什么
原文我 喜欢 AI;猫 喜欢 我;我 学习 AI人为准备训练资料,不是模型生成。
raw IDs[[0,1,2],[4,1,0],[0,3,2]]固定词表编码,每行保留三个连续 token。
inputs / targets[[0,1],[4,1],[0,3]] / [[1,2],[1,0],[3,2]]同一行错开一个位置,得到六道有答案的题。
embedding / h[3,2,4] / [3,2,4]前者查表;后者由所选模型处理可见上下文后得到。
logits[3,2,5]每道题用同一个输出层,对五个候选分别打分。
mean loss一个数各行按对应 target 取负对数,再平均六项。

先只认一道题:[0,0] 的输入是“我”,target_id=1 即“喜欢”。为了核对输出层,评分手算示例给定 h=[0.20,−0.10,0.70,0.30],并用固定输出权重得到 z=[0,2,1,−1,0]。h 是本次数值演示的已知输入,不表示完整 Transformer 永远令 h 等于 embedding。Week 7–8 会打开“怎样计算 h”这个尚未展开的部件。

沿词表做 Softmax 后,“喜欢”的概率约 0.5923,因此这一题 Loss≈0.5237。若另一题也只看到同位置的“我”,在同一确定性模型下分布必须相同;当它的 target 是“学习”时,应从同一分布取出约 0.02949,得到 Loss≈3.5237。Target 不进入预测分支。

text
同一个 [b,t] 的追踪:
inputs[b,t] → embedding[b,t,:] → h[b,t,:]
           → logits[b,t,:] → p[b,t,:]
targets[b,t] ───────────────→ 选正确项计算 L[b,t]
最后才做 L.mean();真实 CE 接口直接接收 logits。

展平只改变摆放方式:原来的 [0,0]、[0,1]、[1,0]、[1,1]、[2,0]、[2,1] 依次成为第 0–5 行。它不会把不同句子的上下文拼接起来;Attention 已在展平前独立处理每个 batch 行。

python
# loss 计算片段:logits、targets 由上面的 forward/数据入口提供。
flat_logits = logits.reshape(6, 5)
flat_targets = targets.reshape(6)  # [1,2,1,0,3,2]
loss = F.cross_entropy(flat_logits, flat_targets)

EX06-串联:不看答案写出三项:flat_logits[4] 对应哪道题?为什么 h 与 embedding shape 相同却不一定数值相同?backward 后哪些长期参数会收到梯度、哪些整数不会?

Knowledge check

完成串联题后核对答案。

13. Bigram:只看当前 Token 的最小 Language Model

Bigram 的名字来自 bi-gram(两个相邻 Token)。它只学一件事:看见当前 Token x_t 时,历史数据里紧接着它的 Token 通常是什么。比如它可以学习“猫”后常出现“喜欢”;但它不会问“猫喜欢”前面是否还有别的文字。它不是完整 GPT 的替代品,而是一个可完全看透的最小基线。

P(xt+1x1,,xt)=P(xt+1xt)P(x_{t+1}\mid x_1,\ldots,x_t)=P(x_{t+1}\mid x_t)

Scroll horizontally to view all columns.

Course data table
tableshaperow meaningcolumn / feature meaning
input embedding E[V,C]=[5,4]当前 token四个 learned features
Bigram W_bigram[V,V]=[5,5]当前 token五个 candidate next-token logits
python
import torch
import torch.nn as nn
import torch.nn.functional as F


class BigramLanguageModel(nn.Module):
    def __init__(self, vocab_size: int):
        super().__init__()
        # 每个 ID 查出一整行下一-token logits,而非语义 embedding。
        self.token_table = nn.Embedding(vocab_size, vocab_size)

    def forward(self, token_ids, targets=None):
        # token_ids: [B, T]
        logits = self.token_table(token_ids)
        # logits: [B, T, V_vocab]

        if targets is None:
            return logits, None

        B, T, V_vocab = logits.shape
        loss = F.cross_entropy(
            logits.reshape(B * T, V_vocab),
            targets.reshape(B * T),
        )
        return logits, loss

logits[b,t,:]=Wbigram[token_ids[b,t],:]\mathrm{logits}[b,t,:]=W_{\mathrm{bigram}}[\mathrm{token\_ids}[b,t],:]

Knowledge check

Bigram 是做什么的?为什么它无法区分“我 喜欢”与“猫 喜欢”?

14. 亲手走完一次 Bigram 训练:出题、算错多少、修改评分表

上一节的 Bigram 已经可以查表预测,但一张刚创建的表还不知道“我”后面常出现什么。训练的目的就是调整这张表:反复用文本中的下一 Token 作为答案,告诉模型哪些候选应该获得更多概率。Loop 只是“重复执行这些步骤”的程序循环。

下面是训练脚本主体:放在第 13 节的 BigramLanguageModel 类定义之后运行。每一步开头清除旧梯度,然后使用同一批题目、当前参数做一次新预测。模型和 Optimizer 只在循环外创建一次。

python
# 接在第 13 节的 imports 与 BigramLanguageModel 定义之后。
inputs = torch.tensor([
    [0, 1],  # 我→喜欢,喜欢→AI
    [4, 1],  # 猫→喜欢,喜欢→我
    [0, 3],  # 我→学习,学习→AI
], dtype=torch.long)

targets = torch.tensor([
    [1, 2],
    [1, 0],
    [3, 2],
], dtype=torch.long)

torch.manual_seed(7)
model = BigramLanguageModel(vocab_size=5)
with torch.no_grad():
    model.token_table.weight.zero_()  # 与上面的全零手算一致
optimizer = torch.optim.SGD(model.parameters(), lr=0.1)

model.train()
for step in range(500):
    optimizer.zero_grad(set_to_none=True)       # 清掉上一步的梯度
    logits, loss = model(inputs, targets)         # [3,2,5], scalar
    loss.backward()                            # 计算当前参数处的梯度
    optimizer.step()                           # 修改评分表

    if step % 100 == 0:
        print("step", step, "loss_before_update", loss.item())

# 需要更新后的 Loss 时,必须用最后的参数重新 forward。
with torch.no_grad():
    _, final_loss = model(inputs, targets)
print("loss_after_500_updates", final_loss.item())

L=1B×Tbtlogsoftmax(logits[b,t,:])[targets[b,t]]L=-\frac{1}{B\times T}\sum_b\sum_t\log\operatorname{softmax}(\mathrm{logits}[b,t,:])[\mathrm{targets}[b,t]]

Scroll horizontally to view all columns.

Course data table
步骤改变的状态
forward用真实 inputs 得到全部 [3,2,5] logits 和 mean loss
zero_grad清掉上一轮累积的 gradient
backward从六个 targets 的 loss 计算每个参数的 gradient
stepoptimizer 用 gradient 更新 [5,5] table

Knowledge check

为什么 “喜欢” row 会收到来自不同 target 的训练信号?

7. Autoregressive:选一个 Token,把它追加后再预测

先把生成理解成一个很小的循环:模型看当前的 prompt,给 Vocabulary 中每个候选一个概率;生成器从中选出一个 Token;再把这个 Token 放到 prompt 的末尾。因为下一轮 prompt 变了,模型也必须重新计算下一轮的 h、Logits 和概率。这个“自己的输出成为下一轮输入”的过程叫 autoregressive(自回归)。

现在才看训练与生成的区别。训练文本已经给出真实后续 Token,所以可以一次准备好许多 Input/Target 位置并行打分;生成时并没有真实的下一 Token,必须先选出第 1 个 next_id、append,才能知道第 2 轮要输入什么。

Scroll horizontally to view all columns.

Course data table
对比项训练(teacher forcing)生成(autoregressive)
下一 Token 从哪里来原始训练文本已经给出正确 Target由 argmax 或 sampling 从本轮 p 中选出 next_id
下一轮输入所有右移 inputs 一开始就可构造好必须先 append 本轮 chosen next_id 才产生
一次处理的位置并行处理全部 B×T 个已知位置当前轮只从最后位置选择一个 Token
Parameters 会改变吗Loss → backward → optimizer.step 会更新它们不会;这里只是在固定模型下选择并追加 IDs

Knowledge check

从“我”生成出“喜欢”后,下一轮模型的输入是什么?为什么不能仍用“我”?

15. 从最后位置选择下一个 Token:Argmax 与 Sampling

text
prompt: 我 喜欢 -> [[0,1]], shape [1,2]
logits: [1,2,5]
logits[:, 0, :]: “我”后面是什么
logits[:, 1, :]: “喜欢”后面是什么
last_logits = logits[:, -1, :]  # [1,5],用于本轮追加

Scroll horizontally to view all columns.

Course data table
strategyruleresult characteristic
Argmax永远选择最大 probability同一 state 可重复,较确定但可能单一
Sampling按 Softmax probability 抽样高概率更常出现,低概率仍可能被选,更多样
python
@torch.no_grad()
def generate(model, token_ids, max_new_tokens, temperature=1.0):
    if temperature <= 0:
        raise ValueError("temperature 必须大于 0")

    model.eval()
    for _ in range(max_new_tokens):
        logits, _ = model(token_ids)              # [B, T, V_vocab]
        last_logits = logits[:, -1, :]            # [B, V_vocab]
        probabilities = F.softmax(
            last_logits / temperature,
            dim=-1,
        )                                         # [B, V_vocab]
        next_id = torch.multinomial(
            probabilities,
            num_samples=1,
        )                                         # [B, 1]
        token_ids = torch.cat(
            (token_ids, next_id),
            dim=1,
        )                                         # [B, T + 1]

    return token_ids

# Greedy alternative:
# next_id = torch.argmax(last_logits, dim=-1, keepdim=True)  # [B,1]

温度 τ>0 使用 softmax(last_logits / τ):τ=1 保持原分布,τ<1 更尖锐,τ>1 更平坦。本教学词表没有 <EOS>,所以必须用 max_new_tokens 限定循环。

Knowledge check

为什么 next_id 必须是 [B,1] 而不是 [B]?

16. Bigram 的关键局限:只记得最后一个词

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

logits_a, _ = model(prompt_a)
logits_b, _ = model(prompt_b)

last_a = logits_a[:, -1, :]
last_b = logits_b[:, -1, :]

assert torch.equal(last_a, last_b)

两个 prompt 最后的 ID 都是 1,所以 Bigram 都只查 W_bigram[1,:]。这不是偶然的训练结果:只要表仍是 [V_vocab,V_vocab] 并且输入接口只提供当前 token,更早的 我 或 猫 不可能影响最后位置输出。

alast=blastWbigram[alast,:]=Wbigram[blast,:]P(nexta)=P(nextb)a_{\mathrm{last}}=b_{\mathrm{last}}\Rightarrow W_{\mathrm{bigram}}[a_{\mathrm{last}},:]=W_{\mathrm{bigram}}[b_{\mathrm{last}},:]\Rightarrow P(\mathrm{next}\mid a)=P(\mathrm{next}\mid b)

固定词表的表只有 5×5=25 个 logits;它可以记住一步转移,是教学与数据管线 baseline,却不能表示同一当前词在不同长上下文中的不同续写。

Scroll horizontally to view all columns.

Course data table
Bigram 只看最后一个词六道题中的答案最优概率分配
我,出现两次喜欢 / 学习各 1/2
喜欢,出现两次AI / 我各 1/2
猫,出现一次喜欢可趋近 1
学习,出现一次AI可趋近 1

因此理想 Bigram 在这六道题上的平均损失下确界是 4×ln(2)/6≈0.462,而不是 0。有限 logits 只能接近确定概率 1。Week 11 的 causal MiniGPT 能区分 [我,喜欢] 与 [猫,喜欢],所以只剩两个相同 [我] 前缀的冲突,理想下界降为 2×ln(2)/6≈0.231。这是模型可见上下文不同造成的差别,不是算术不一致,也不保证小模型一定训练到下界。

Knowledge check

为何 [我,喜欢] 与 [猫,喜欢] 的 last logits 必然相同?

17. Perplexity:模型对真实下一词有多“困惑”

mean_NLL=1NilogP(xix<i),PPL=exp(mean_NLL)\mathrm{mean\_NLL}=-\frac{1}{N}\sum_i\log P(x_i\mid x_{<i}),\qquad \mathrm{PPL}=\exp(\mathrm{mean\_NLL})

Scroll horizontally to view all columns.

Course data table
每个真实 token 的 probabilitymean_NLLPPL直觉
1/5(五个候选均匀)ln(5) ≈ 1.6095像在五个候选间同样犹豫
1/2ln(2) ≈ 0.6932平均像在两个候选间犹豫
python
import math


@torch.no_grad()
def evaluate_perplexity(model, data_loader):
    model.eval()
    total_nll = 0.0
    total_tokens = 0

    for inputs, targets in data_loader:
        logits, _ = model(inputs)                 # [B, T, V_vocab]
        V_vocab = logits.size(-1)
        nll_sum = F.cross_entropy(
            logits.reshape(-1, V_vocab),
            targets.reshape(-1),
            reduction="sum",
        )
        total_nll += float(nll_sum)
        total_tokens += targets.numel()

    if total_tokens == 0:
        raise ValueError("没有可用于评估的 token")

    return math.exp(total_nll / total_tokens)

PPL=exp(平均 token loss),可以读成“正确词概率的几何平均倒数”。两题概率 0.5、0.125:平均 loss=[ln2+ln8]/2=ln4,所以 PPL=4;不是把倒数 2 和 8 算术平均成 5。它不是“模型真的在四个同概率词中犹豫”。换 tokenizer 后 token 单位会变,不能仅凭两个 PPL 数值比较不同分词系统的语言能力。

Knowledge check

什么时候可以写 perplexity = torch.exp(loss)?

18. Week 6 总结:一条完整的最小语言模型流水线

Concept sequence
  1. 文本按 Tokenizer 编码成 raw IDs [3,3]
  2. 右移得到 inputs / targets [3,2]
  3. 一般 LM:input embedding [3,2,4] → context model / output head → logits [3,2,5]
  4. Bigram:IDs [3,2] → 直接查 [5,5] logit table → logits [3,2,5]
  5. 展平为 logits [6,5] 与 targets [6]
  6. F.cross_entropy 得到 scalar mean loss
  7. backward → optimizer 更新;生成则取最后位置并追加
text
Week 6 fixed batch:
raw IDs                              [3,3]
shifted inputs                       [3,2]

General language model path:
IDs                                  [3,2]
input embedding                      [3,2,4]
context model + output head          [3,2,5] vocabulary logits

Bigram shortcut path:
IDs                                  [3,2]
direct W_bigram [5,5] lookup         [3,2,5] vocabulary logits

flattened logits                     [6,5]
flattened targets                    [6]
mean cross-entropy loss              scalar

Generation from prompt [[0,1]]:
prompt IDs                           [1,2]
model logits                         [1,2,5]
last logits[:, -1, :]                [1,5]
next_id                              [1,1]
appended prompt                      [1,3]

Scroll horizontally to view all columns.

Course data table
训练生成
文本 → IDs → shifted inputs/targets → [B,T,V] logits → flatten → cross-entropy → backward → updateprompt IDs → [B,T,V] logits → logits[:, -1, :] [B,V] → Argmax/Sampling → next_id [B,1] → append → repeat

Knowledge check

为何 Week 7 加入 Attention 后仍保留 logits、cross-entropy 和 logits[:, -1, :]?

19. 从 Bigram 过渡到 Attention:让当前位置回看整个前缀

Self-Attention 让每个位置动态汇总允许看到的历史位置。Query 是当前位置正在寻找什么信息,Key 是每个候选位置提供的匹配线索,Value state 是匹配后真正汇总的内容。它们通常由同一 hidden representation 的不同可学习投影得到,不是三个额外 token。

Scroll horizontally to view all columns.

Course data table
quantitytypical shapemeaning
query, key[B,T,d_k]用于位置间匹配
value_states[B,T,d_v]被权重汇总的内容
attention scores / weights[B,T,T]每个 Query 对每个 Key position 的分数 / 归一化权重
context[B,T,d_v]加权 Values
final vocabulary logits[B,T,V_vocab]仍是 Week 6 的 next-token 接口
python
import math

# query, key:    [B, T, d_k]
# value_states:  [B, T, d_v]
scores = query @ key.transpose(-2, -1)             # [B, T, T]
scores = scores / math.sqrt(d_k)

# causal_mask 可广播到 [B,T,T];True 表示允许查看的位置。
scores = scores.masked_fill(~causal_mask, float("-inf"))
weights = F.softmax(scores, dim=-1)                # [B, T, T]
context = weights @ value_states                   # [B, T, d_v]

scoret,j=qtkjdk,αt,j=softmaxj(scoret,j),contextt=jtαt,jvaluej\mathrm{score}_{t,j}=\frac{q_t\cdot k_j}{\sqrt{d_k}},\qquad \alpha_{t,j}=\operatorname{softmax}_j(\mathrm{score}_{t,j}),\qquad \mathrm{context}_t=\sum_{j\le t}\alpha_{t,j}\,\mathrm{value}_j

对 prompt A=[我,喜欢] 与 B=[猫,喜欢],第二位置的当前 token 虽都为 喜欢,但 Attention 可读取不同的第 0 位置,因此最后 logits 可能不同。它只是提供使用更长上下文的路径;未经训练并不保证模型会给某个特定答案,更不保证“理解”。

Knowledge check

Attention 如何让两个都以“喜欢”结尾的 prompt 可能产生不同 last logits?