bobofamilyNIU CHUN
07/36

从 2 类到 27 类:只需要补三个零件

2026-09-039 分钟—阅读

V1 做的是「猫狗二分类」,V2 突然变成「预测下一个字符」—— 看起来是两件事。

实际上是同一件事。 给定当前字符,问下一个是什么 —— 答案有 27 种可能(26 个字母 + 一个结束符)。所以它就是:看到一个输入,判断它属于 27 类中的哪一类。

猫狗分类bigram
输入耳朵、眼睛两个特征当前字符
问题属于哪一类属于哪一类
类别数227
网络2 → 隐藏 → 227 → 27

结构完全相同,只是类别数从 2 变成 27。 前向传播、反向传播、梯度下降一个字不用改。

第三篇那个 2→2→2 的双输出网络(猫 / 狗)已经是雏形,bigram 就是把输出层从 2 个神经元扩到 27 个。

真正需要新学的只有三件事,而且都是因为「类别多了」才需要。

one-hot:字符怎么变成数字

网络只能吃数字,a 不是数字。

为什么不能直接编号(a=1、b=2、…、z=26):因为数字自带大小关系,网络会认为 z 比 a「大 25 倍」,而字母之间根本没有这种关系。

one-hot 的做法是用 27 维向量,只有对应位置是 1,其余全 0:

a = [0, 1, 0, 0, …, 0]
b = [0, 0, 1, 0, …, 0]

(第 0 位留给起止符。)所有字符两两之间的「距离」都相同,没有虚假的大小关系。

代价是维度大 —— 词表 5 万时就是 5 万维,真实模型不可接受。后面会用 embedding 解决,bigram 阶段 27 维完全够用。

猫狗任务里输入天然是数字(0.9、0.8),不需要这一步。one-hot 是「离散符号」进入网络的通行证。

softmax:27 个输出怎么变成概率

网络最后一层输出 27 个数,叫 logits,有正有负、大小随意。这不是概率 —— 概率必须非负,而且加起来等于 1。

softmax(zᵢ) = e^zᵢ / Σⱼ e^zⱼ

分子是这一项取指数,分母是所有项取指数后求和。

和 tanh / sigmoid 不同:那些是一个数进、一个数出;softmax 是一组数进、一组数出,因为它必须知道所有项才能算分母。

走一遍具体数字,设 logits 为 [2.0, 1.0, 0.1, −1.0]:

原始 ze 的 z 次方除以总和 11.580
2.07.3890.638
1.02.7180.235
0.11.1050.095
−1.00.3680.032

加起来正好 1.000。注意 −1.0 变成了 0.368 —— 负数变正了。

两个性质:

  • 保持排序。原来最大的还是最大的
  • 放大差距。z 里 2.0 是 1.0 的两倍,概率却是 2.7 倍 —— 指数把差距拉开了

第四篇留下的那个问题在这里有了答案:eˣ 为什么不能单独当激活函数,却又到处都是。 因为它从不单独出现,永远被除法约束着 —— softmax 的分母就是那个约束。第四篇补的 exp 算子就是为它准备的。

猫狗两类时用 sigmoid 就够了(一个概率 p 定了,另一个自动是 1−p)。27 类时必须让它们互相制约,softmax 就是干这个的。

交叉熵:为什么换掉平方误差

不是「更高级」,是同一原则的不同形式

平方误差也有理论依据。两者是同一套原则在不同假设下的产物:

任务分布假设最大似然推出
回归(预测连续值)高斯平方误差
分类多项分布交叉熵

公式是怎么来的

问:什么样的参数算好参数?答:让真实观测到的数据发生的概率最大。

所有 bigram 同时发生的概率是连乘,目标是让它最大。接下来三步纯技术转换:

  1. 取对数。二十几万个小于 1 的概率连乘会下溢成 0,而且求导困难。靠 log(ab) = log a + log b 把乘法变加法。log 单调递增,不改变最优解
  2. 加负号。优化框架都是往下走(最小化),翻个面
  3. 除以样本数,让 loss 不随数据量变化
max ∏ Pᵢ → max Σ log Pᵢ → min ( −(1/N) Σ log Pᵢ )

全程没有任何「我们决定这样设计」的步骤。

两个公式里的 i 不是一回事

这是个符号冲突,很容易看混:

  • softmax 里的 i 遍历「类别」:27 个数,属于同一个样本,加起来等于 1
  • 交叉熵求和里的 i 遍历「样本」:N 个数,来自不同样本,加起来不等于 1

写清楚应该是 loss = −(1/N) Σᵢ log P(i, tᵢ),i 是样本编号,tᵢ 是第 i 个样本的正确类别。

样本输入正确答案模型输出的 27 个概率取出正确项
1em[0.05, 0.40, …, 0.02, …] 和=10.02
2mm[0.01, 0.10, …, 0.30, …] 和=10.30
3ma[0.02, 0.50, …, 0.08, …] 和=10.50

每一行内部的 27 个数加起来是 1(softmax 作用在行内)。最右列那三个数加起来不是 1 —— 它们来自不同样本,没有任何约束关系。

softmax 沿「行内」方向操作,交叉熵沿「行间」方向求平均。

取的是「正确答案那项」,不是「最大那项」

这是最容易搞错的地方。当前字符 e,正确答案 m:

位置字符概率
1a0.40 ← 最大
13m0.02 ← 正确答案,用这个

loss = −log(0.02) = 3.91。

如果取最大值,loss 会是 −log(0.40) = 0.92 —— 模型自信满满地猜错也不被惩罚,永远学不到东西。

正因为取的是正确项,训练才有方向:正确项概率低 → loss 高 → 梯度大 → 推高正确项。

其余 26 项去哪了

完整公式是对所有类别求和,但真实标签是 one-hot:

loss = −Σⱼ yⱼ log Pⱼ = −(0·log P₀ + … + 1·log P₁₃ + …) = −log P₁₃

26 项被 0 乘掉了,不是主观决定不看。 而 softmax 保证总和为 1,推高正确项时其余项会自动被挤下去,不需要额外惩罚。

实际代码里直接按索引取(probs[target]),而不是真的构造 one-hot 再相乘 —— 省掉 26 次乘 0。PyTorch 的 F.cross_entropy 接受的也是索引而非 one-hot。

「总和为 1 / 推高 / 压下去 / 惩罚」具体是什么意思

四个词逐个落实。用 4 类简化演示:起始 logits [0, 1, −0.5, −0.7],正确答案是第 3 类,梯度就是 P − y,学习率 1.0,走 5 步:

步正确项其余合计总和loss
00.1260.8741.00002.073
10.3410.6591.00001.075
30.7040.2961.00000.351
50.8280.1721.00000.189

总和为 1 是 softmax 分母造成的结构约束,恒成立。看「总和」那列:权重一直在变,它始终是 1.0000。这不是训练出来的 —— Σ eᶻⁱ / Σ eᶻʲ,分子分母本来就是同一个数。

推高是梯度下降在做。loss 只跟正确项有关,要降 loss 唯一的办法就是增大正确项概率 —— 第二列在往上走,0.126 → 0.828。

压下去是自动发生的。第二列和第三列永远加起来是 1,所以正确项涨到 0.828 时,其余合计必然从 0.874 掉到 0.172。没有任何代码去压它们,是被「总和必须是 1」挤下去的 —— 像挤气球,一头鼓起来另一头必然瘪下去。

训练前: [0.207, 0.564, 0.126, 0.103]
训练后: [0.057, 0.077, 0.828, 0.038]
↓ ↓ ↑ ↓

惩罚指的是 loss 里专门为某件事加的一项。假如没有 softmax 约束,模型可以输出 [0.9, 0.9, 0.9, 0.9] —— 正确项 0.9,loss 很低,但它对每一类都说「很可能」,等于什么都没学会。那样就得手动加一项:

loss = -log(P_正确) + sum(P_错误) # 第二项就是「额外惩罚」

有了 softmax,这一项完全不需要写 —— 概率是零和的,给正确项多了,别人自动就少。

为什么平方误差在分类上不行

正确项概率平方误差交叉熵
0.90.010.105
0.50.250.693
0.10.812.303
0.010.984.605
0.00011.009.21

看最后两行。 平方误差有上限 —— 概率被限制在 0~1,单项误差最多是 1,所以 0.98 和 1.00 几乎没差别。梯度趋近于零,模型错得越离谱反而越学不动,完全反了。

交叉熵没有上限:越自信地犯错,惩罚上升越急,梯度越大,逼着模型赶紧改。

梯度形式也更干净。softmax + 交叉熵的梯度化简后是:

∂loss/∂zᵢ = Pᵢ − yᵢ

预测概率减去真实标签,一行搞定。 e 和 log 互相抵消了 —— 这不是巧合,正因为它们是配套设计的。

顺带:e 和 log

e ≈ 2.718281828… e = lim (1 + 1/n)ⁿ

它来自复利问题:1 块钱、年利率 100%,计息越频繁收益越高,但不会无限增长,收敛到 e。

为什么数学里到处是它:只有 eˣ 的导数等于它自己。换成 2ˣ,导数会多出一个碍事的系数。

左:eˣ 输出恒为正。右:log x 在 0 附近垂直下坠 —— 概率为 0 时 loss 会爆炸。

从图上能直接看出三件事:

图上现象含义
log 在 0<x<1 时是负的概率总在这区间 → loss 要加负号才变正
x 趋近 0 时 log 趋于负无穷概率为 0 时 loss 爆炸(下一篇数 bigram 会碰到一堆 0)
靠近 0 处陡峭、靠近 1 处平缓错得越离谱,惩罚增长越快

log(a×b) = log a + log b 的来历:设 a = eᵐ、b = eⁿ,则 ab = e^(m+n),取对数得 m+n。本质是指数相乘则指数相加。

困惑度:把 loss 翻译成人能感受的数

perplexity = e^loss

含义是:模型相当于在多少个选项里瞎猜。

模型loss相当于在几个里猜
完全随机(27 类)3.3027
bigram约 2.4511.6
下一步的 MLP约 2.29
Transformer更低更少

这就是「看得更远 → 更确定」的量化体现。 后面会看着这个数一路降下去。

串起来

a ──one-hot──→ [0,1,0,…](27 维)──×W──→ 27 个 logits
──softmax──→ 27 个概率(和为 1)──交叉熵──→ loss

就是一个 27→27 的单层网络,结构和 MLP(27, [27]) 一样,只是:

猫狗版bigram 版
输入天然是数字one-hot
激活函数tanhsoftmax
损失函数平方误差交叉熵

其余全部相同 —— 前向、反向、梯度下降,一个字不用改。


一句话概括这一篇:猫狗二分类 →(类别数 2 变 27)→ bigram →(上下文 1 变几千)→ GPT。沿着这条线,没有任何一步是跳跃的。

下一篇进正片:下载 32033 个真实人名,先完全不用神经网络,只靠数频次做出一个完整的语言模型。