← 返回首页
01_bigram_crossentropy_viz.html
logits 摊平 & 交叉熵到底在算什么
逐步拆解 nanoGPT bigram forward 的最后两行:
loss = F.cross_entropy(logits.view(B*T, C), targets.view(B*T))。
为了画面清晰,这里用 B=2, T=3(共 6 道题)的小例子,词表只画 6 个示意字符。
STEP 3
单题 cross_entropy 拆解
超参速查
batch_size 32 句子数
block_size 8 位置数 T
vocab_size 65 词表大小
⚠ 本页为看清楚,用 B=2 / T=3 的小例子演示(真实 32 / 8)
Step 1 · 一个 batch 长什么样:x 和 y 错开一格
取字符串 "To be or" 切成 B=2 个序列、每个长度 T=3。
规则:看到 x[t] 就要预测它的下一个字符 y[t] —— 所以 y 是 x 整体右移一位。
x:输入字符(喂给模型)
y:目标字符(正确答案)
↓ 看到上面 → 该预测下面
一个 batch 的"题目数" = batch_size × block_size = 2 × 3 = 6 道独立的分类题。
真实代码里是 32batch × 8block = 256 道。每道题都是:在 65vocab 个字符里选出正确的下一个。
Step 2 · 把三维 logits 摊平成二维分类题
模型对每个位置都吐出一个长度 65 的打分向量,所以 logits 形状是
(2, 3, 65) —— B 行 × T 列,每格里藏着 65 个数。
view(B*T, C) 把这个 2×3 网格 拉直成一列,得到 6 道并排的题。
摊平 不改变任何数据,只是换了个排列方式:从"2×3 的二维结构"拉直成"6 道独立分类题"。
C=65 这一维(每格里的 65 个打分)始终原封不动地跟着走。真实代码是 256 × 65。
Step 3 · 单道题里 cross_entropy 的三步
点一道题,看 cross_entropy 内部发生什么:① softmax 把打分变成概率 →
② 找出正确答案那一类 → ③ 取它概率的 -log。词表用 6 个示意字符。
cross_entropy(logits, target) = 内部先做 softmax 得到概率分布,
再取 正确类别的概率 p,最后 loss = -ln(p)。
(实现上用 log_softmax 更数值稳定,但意义等价。)
Step 4 · 核心直觉:loss = -ln(p)
拖动滑块改变"模型给真实下一字符的概率 p",观察 loss 怎么变。
模型越确信正确答案(p→1),loss→0;越瞎猜,loss 越大。
p≈1/65=0.015 · 随机初始 loss≈4.17
p=0.5 · loss≈0.69
p=0.9 · loss≈0.11
p=1.0 · loss=0(完美)
训练就是不断把"模型给真实下一字符的概率"往 1 推,从而把 loss 从随机的
ln(65)≈4.17 一路压向 0。这就是为什么 nanoGPT 一开始 loss 接近 4.17 不是 bug。
最终的标量 loss = 6 道题各自 -ln(p) 的平均(reduction='mean')
⚠️ 这个平均(上面那个数)只是这 6 道示意题的 loss —— 它们的 logits 是为演示故意调得偏准的(正确答案概率偏高),所以数字偏小。
它不是真实 bigram 的训练 loss(真实起步 ≈4.17、收敛 ~2.5);这里只为看清"多道题的 loss 怎么平均成一个标量"这件事。
Step 5 · 训练循环:5 行代码 = 一个不断转的闭环
前 4 步只讲清了"怎么给一批题打一个分(loss)"。真正的训练,是把下面这
5 行放进一个 for 里,转 3000 圈。每转一圈,模型的参数就被
梯度推着扭一小步,loss 随之下降一点点。
A这个闭环每一步在做什么
5 个动作沿圆环顺时针依次执行;点「单步执行」像调试器一样一步步走,或「自动循环」让高亮自动流动。
注意第 5 步之后,箭头会回到第 1 步 —— 这就是"闭环重复"。
训练循环
把这 5 步重复 3000 次。每一圈,模型都更聪明一点点。
代码就是 for it in range(max_iters): get_batch → forward → cross_entropy → backward → step。
5 个动作首尾相接,转一圈叫一次 iteration,转 3000 圈就训练完了。
B参数是"开关",loss 是"成绩单"——点训练,旋钮 / 表 / 预测一起变
别把 loss 和参数搞混:参数(旋钮 / 下面这张表)是因,loss(成绩单)是果。
旋钮只是拟物;参数真正的样子是一张 vocab×vocab 的得分表(第 i 行第 j 列 = "看到字符 i 后,下一个是 j 的得分"),
形状由词表大小决定(真实 65×65,和 batch 的 2×3 无关)。下面用 6 字符示意画 6×6。
① 6 个示意旋钮(参数 = 开关)与 loss 成绩单
② 参数真身:6×6 得分表(真实 65×65)· 点一行看预测 ↓
出生时:随机雪花表(颜色杂乱)。点上方「执行一轮训练」看它逐渐变出结构。
低分
高分
训练 = 把这 36(真实 4225)个数字,从随机雪花,调成字符接龙的统计规律。
每个旋钮(W[当前→下一个])就是表里的一个格子(橘色描边标出),旋钮一转,格子数字同步变,loss 随之下降。
转动幅度为确定性演示值(非真实梯度,越接近天花板扭动越小)。
点某一行 = 取该行 6 个 logit 过 softmax,就是模型看到该字符时对"下一个字符"的概率猜测。
C3000 圈下来,loss 怎么走
点「训练!」,看曲线从左到右画出来。起点 ≈ 4.17 是随机瞎猜(=ln(65)),
迅速掉到 ~2.6,最后卡在 ~2.5 下不去。
两个关键点:① 起点 4.17 = ln(vocab_size),是完全随机时的 loss,不是 bug;
② 卡在 2.5 下不去,是因为 bigram 只看前 1 个字符 —— 这是它的能力天花板,
也正是下一课要加 注意力(attention) 的理由。
为什么每轮要先 optimizer.zero_grad()?
PyTorch 的
梯度是
累加的:
loss.backward() 会把新梯度
加到旧梯度上,而不是覆盖。
若不清零,这一轮就会带着上一轮的"陈旧方向",越走越偏。所以每圈开头必须先清零。
zero_grad 清零
→
backward 算新梯度
→
step 扭开关
· 顺序不能乱