从 2 类到 27 类:只需要补三个零件
V1 做的是「猫狗二分类」,V2 突然变成「预测下一个字符」—— 看起来是两件事。
实际上是同一件事。 给定当前字符,问下一个是什么 —— 答案有 27 种可能(26 个字母 + 一个结束符)。所以它就是:看到一个输入,判断它属于 27 类中的哪一类。
| 猫狗分类 | bigram | |
|---|---|---|
| 输入 | 耳朵、眼睛两个特征 | 当前字符 |
| 问题 | 属于哪一类 | 属于哪一类 |
| 类别数 | 2 | 27 |
| 网络 | 2 → 隐藏 → 2 | 27 → 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]:
| 原始 z | e 的 z 次方 | 除以总和 11.580 |
|---|---|---|
| 2.0 | 7.389 | 0.638 |
| 1.0 | 2.718 | 0.235 |
| 0.1 | 1.105 | 0.095 |
| −1.0 | 0.368 | 0.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 的概率连乘会下溢成 0,而且求导困难。靠
log(ab) = log a + log b把乘法变加法。log 单调递增,不改变最优解 - 加负号。优化框架都是往下走(最小化),翻个面
- 除以样本数,让 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 个概率 | 取出正确项 |
|---|---|---|---|---|
| 1 | e | m | [0.05, 0.40, …, 0.02, …] 和=1 | 0.02 |
| 2 | m | m | [0.01, 0.10, …, 0.30, …] 和=1 | 0.30 |
| 3 | m | a | [0.02, 0.50, …, 0.08, …] 和=1 | 0.50 |
每一行内部的 27 个数加起来是 1(softmax 作用在行内)。最右列那三个数加起来不是 1 —— 它们来自不同样本,没有任何约束关系。
softmax 沿「行内」方向操作,交叉熵沿「行间」方向求平均。
取的是「正确答案那项」,不是「最大那项」
这是最容易搞错的地方。当前字符 e,正确答案 m:
| 位置 | 字符 | 概率 |
|---|---|---|
| 1 | a | 0.40 ← 最大 |
| 13 | m | 0.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 |
|---|---|---|---|---|
| 0 | 0.126 | 0.874 | 1.0000 | 2.073 |
| 1 | 0.341 | 0.659 | 1.0000 | 1.075 |
| 3 | 0.704 | 0.296 | 1.0000 | 0.351 |
| 5 | 0.828 | 0.172 | 1.0000 | 0.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.9 | 0.01 | 0.105 |
| 0.5 | 0.25 | 0.693 |
| 0.1 | 0.81 | 2.303 |
| 0.01 | 0.98 | 4.605 |
| 0.0001 | 1.00 | 9.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ˣ,导数会多出一个碍事的系数。
从图上能直接看出三件事:
| 图上现象 | 含义 |
|---|---|
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.30 | 27 |
| bigram | 约 2.45 | 11.6 |
| 下一步的 MLP | 约 2.2 | 9 |
| Transformer | 更低 | 更少 |
这就是「看得更远 → 更确定」的量化体现。 后面会看着这个数一路降下去。
串起来
a ──one-hot──→ [0,1,0,…](27 维)──×W──→ 27 个 logits ──softmax──→ 27 个概率(和为 1)──交叉熵──→ loss就是一个 27→27 的单层网络,结构和 MLP(27, [27]) 一样,只是:
| 猫狗版 | bigram 版 | |
|---|---|---|
| 输入 | 天然是数字 | one-hot |
| 激活函数 | tanh | softmax |
| 损失函数 | 平方误差 | 交叉熵 |
其余全部相同 —— 前向、反向、梯度下降,一个字不用改。
一句话概括这一篇:猫狗二分类 →(类别数 2 变 27)→ bigram →(上下文 1 变几千)→ GPT。沿着这条线,没有任何一步是跳跃的。
下一篇进正片:下载 32033 个真实人名,先完全不用神经网络,只靠数频次做出一个完整的语言模型。