高中觉得最没用的 log,其实是大模型判断"自己错得有多离谱"的唯一标尺

2026-07-05 · Steve Chan

对数、换底公式、lgln、对数函数图像……高中背了一整章,考完只剩一句"log 到底有什么用"。 因为老师只教你"查表、算 pH、比大小",没告诉你两件事:

第一,log 的灵魂不是"一种难算的函数",是一台"把连乘变成连加"的翻译机。 第二,今天每一个大模型(LLaMA、Qwen、GPT、DeepSeek)训练时判断"自己这一步错得有多离谱", 用的唯一标尺就是 log——它的名字叫交叉熵(cross-entropy)。

这篇文章不背一个公式、不做一道换底题,全部用能跑起来的 PyTorch 代码, 把对数最核心的"连乘变连加"本质,一路接到大模型的损失函数。 并且顺手回答四个灵魂拷问:

  • log 明明是算 pH、算地震级数的,怎么会和"大模型训练"扯上关系?
  • 一个句子的概率为什么要"连乘"?为什么连乘一下就出事了?
  • 交叉熵损失里那个 -log(p) 到底在衡量什么?为什么非得套个 log?
  • 训练日志里天天见的"困惑度 perplexity"又是什么?为什么它等于 exp(loss)

0. 一句话主线

如果只能留一句话,那就是这句:

log 是一台"连乘 → 连加"的翻译机:log(a×b) = log a + log b

一堆概率乘在一起会小到电脑存不下(下溢成 0),可只要各自取个 log, "连乘"就变成了"连加",天塌下来的数值问题瞬间消失。 大模型算一个句子有多"可能"、算自己错得有多离谱,靠的全是这台翻译机。

import torch

# log 就是指数的逆问题:"e 要自乘多少次,才能得到 x"
for x in [1.0, 2.718, 7.389, 20.09]:
    print(f"ln({x:6.3f}) = {torch.log(torch.tensor(x)):.3f}")
# ln(1)=0, ln(e)=1, ln(e^2)=2, ln(e^3)=3 —— 它问的永远是"e 的几次方"

记住这个画面:log 是在数"要乘多少次"。而"乘多少次"这件事,天生就能把乘法拆成加法。 下面所有东西都挂在这一个性质上。


1. log 的真身:一台"连乘变连加"的翻译机

高中把 log 塞进一堆"换底、对数不等式"的题里,把你锁死在计算技巧中。 但 log 真正有用的,只有一条黄金性质——乘积的 log,等于 log 的和

\[\log(a \times b) = \log a + \log b\]

这条性质平平无奇,却是整篇文章的地基。代码验证一下,顺便看它把"四个数连乘"拆成了"四个 log 连加":

import torch

a = torch.tensor([0.3, 0.1, 0.05, 0.2])   # 四个概率
prod = a.prod()                            # 先把它们连乘起来
sum_of_logs = a.log().sum()               # 各自取 log 再相加

print("连乘再取 log :", prod.log())        # log(0.3 * 0.1 * 0.05 * 0.2)
print("取 log 再连加 :", sum_of_logs)       # log0.3 + log0.1 + log0.05 + log0.2
print("两者相等吗?", torch.allclose(prod.log(), sum_of_logs))   # True

True"把一串数乘起来再取 log" = "各自取 log 再加起来"。 你可能觉得这只是个代数小把戏。但下一节你会看到:正是这个把戏, 救回了大模型里一个本来会彻底崩掉的计算。


🤔 疑惑点一:一个句子的概率为什么要"连乘"?为什么连乘一下就出事了?

因为语言模型是逐词预测的:它先猜第 1 个词、再在第 1 个词的基础上猜第 2 个……整句话的概率就是每一步概率的连乘。而一句话动辄几十上百个词,每个概率都小于 1,几百个小于 1 的数乘在一起,会小到浮点数存不下,直接变成 0——信息全丢了。

这是最反直觉、也最致命的一点。模型给每个位置的"下一个词"打一个概率, 整句话出现的概率,就是这些概率的乘积(这叫链式法则)。用代码看看灾难现场:

import torch

# 一句话的概率 = 每个词在前文下的条件概率,全部连乘
# 假设这句话有 200 个词,模型给每个词的概率都还不错(0.1 上下)
probs = torch.full((200,), 0.1)

print("直接连乘 200 个 0.1 :", probs.prod())       # 0.0 —— 下溢!信息全没了
print("换成 log 再相加     :", probs.log().sum())   # -460.5 —— 完好无损

probs.prod() 得到 0.0。不是这句话概率真的是 0,而是 0.1²⁰⁰ = 10⁻²⁰⁰ 这个数, 比 32 位浮点能表示的最小正数还小得多,电脑只能把它约等于 0。 一旦变成 0,梯度就是 0,模型再也学不到任何东西——训练直接死掉。

而右边 probs.log().sum() 稳稳给出 -460.5。这就是第 1 节那台翻译机的价值: 连乘会下溢,连加不会。 把每个概率取 log 再相加,10⁻²⁰⁰ 这种吓人的小数, 就变成了 -460.5 这种电脑最擅长处理的普通数字。所以大模型从头到尾不碰原始概率,只在 log 空间里算账。


2. log-likelihood:模型永远在 log 空间里给句子打分

既然不能连乘,那就连加。一个句子的"对数似然(log-likelihood)", 就是把每个正确词的 log 概率加起来。代码里,模型输出的是每个位置在整个词表上的分数(logits), 我们用 log_softmax 一步到位算出 log 概率,全程不碰会下溢的原始概率:

import torch
import torch.nn.functional as F

torch.manual_seed(0)
V = 1000                                    # 词表大小:1000 个候选词
logits = torch.randn(5, V)                  # 模型对 5 个位置的原始打分
target = torch.tensor([12, 7, 900, 3, 55])  # 这 5 个位置真正的下一个词

logp = F.log_softmax(logits, dim=-1)        # 直接得到 log 概率(不碰原始概率)
picked = logp[torch.arange(5), target]      # 取出每个"正确词"的 log 概率

print("每个正确词的 log 概率:", picked.round(decimals=2))
print("整句 log-likelihood  :", picked.sum())     # 连加,绝不连乘
print("负对数似然(要最小化):", -picked.sum())     # 加个负号,就成了 loss

log_softmax 而不是"先 softmax 再 log",是工程上专门为了数值稳定设计的——又是 log 在背后兜底。log-likelihood 取个负号,就得到了"负对数似然(NLL)":模型越可能说对这句话,NLL 越小。 训练的目标,就是把这个 log 空间里的数字压到最低。 而它,正是交叉熵的真身。


3. log 就是"意外":概率越低,-log(p) 越大

为什么偏偏用 log 来当损失,而不是用"1 − 概率"之类更朴素的东西? 因为 -log(p) 有一个无可替代的性质:它精确地度量了"意外程度"。 一件几乎必然的事发生,你毫不意外;一件几乎不可能的事发生,你惊掉下巴。看代码里这条曲线的陡峭程度:

import torch

# 意外程度 = -log(概率)。越不可能发生的事,一旦发生就越"意外"
for p in [0.9, 0.5, 0.1, 0.01, 1e-4]:
    surprise = -torch.log(torch.tensor(p))
    print(f"概率 {p:7.4f}  ->  意外 = {surprise:.2f}")
# 0.9 -> 0.11(几乎不意外)   0.01 -> 4.61   1e-4 -> 9.21(意外爆炸)

看这个增长:概率从 0.9 掉到 1e-4,意外值从 0.11 暴涨到 9.21-log(p) 在概率趋近 0 时会冲向无穷大——这正是我们想要的性质: 模型对一件真实发生的事给出"几乎不可能"的判断,就该受到近乎无穷的惩罚。 1 − p 做不到这种"错得越离谱、罚得越狠到失控"的效果,只有 log 可以。


4. 交叉熵:把"平均意外"当成损失,log 专治"自信的错误"

把第 3 节的"意外"用到每个正确词上,再取平均,就是交叉熵损失

\[\text{loss} = \frac{1}{N}\sum_{i=1}^{N} -\log p(\text{正确词}_i)\]

一句话:交叉熵 = 模型对"本该说出口的那个词"平均有多意外。 越意外,说明错得越离谱,loss 越大。 log 在这里最狠的一手,是重罚"笃定的错误"——你越自信地答错,惩罚越是指数级爆炸:

import torch
import torch.nn.functional as F

# 两个模型,对同一道题(正确答案是类别 2)给出不同的自信程度
confident_right = torch.tensor([[0.5, 0.5, 5.0]])   # 笃定选【对】
confident_wrong = torch.tensor([[5.0, 0.5, 0.5]])   # 笃定选【错】
target = torch.tensor([2])

for name, logits in [("笃定答对", confident_right), ("笃定答错", confident_wrong)]:
    loss = F.cross_entropy(logits, target)          # 内部就是 -log(正确类的概率)
    print(f"{name}: 交叉熵 = {loss:.3f}")
# 笃定答对 -> loss≈0.02(几乎不罚)   笃定答错 -> loss≈4.52(往死里罚)

同样是"很自信",答对的几乎零惩罚,答错的被罚到 4.5这种"温柔奖对、严惩自信之错"的手感,完全来自 -log——它逼着模型"没把握就别太笃定", 正是这条 log 曲线,塑造了今天大模型那种"该确定时确定、该含糊时含糊"的分寸感。 F.cross_entropy 这一个在每份训练代码里出现无数次的函数,内核就是你高中那章 log。


🤔 疑惑点二:训练日志里的"困惑度 perplexity"是什么?为什么它等于 exp(loss)

因为交叉熵是 log 空间里的"意外值",人脑对它没有直觉;把它用 exp 送回原空间,就变成一个能读懂的数——"模型现在平均在几个词之间纠结"。loss 是 log,困惑度就是把 log 脱掉。

交叉熵虽然好算,但 2.3 这个数字对人没有意义。于是我们用 log 的逆运算 exp 把它翻译回来:

import torch

# 困惑度 = e^(交叉熵),把 log 里的"意外"翻译回"在几个词之间纠结"
for ce in [0.0, 0.693, 2.303]:
    ppl = torch.exp(torch.tensor(ce))
    print(f"交叉熵 {ce:.3f}  ->  困惑度 = {ppl:.1f}")
# 0 -> 1(毫不犹豫,闭眼选对)
# 0.693(=ln2) -> 2(在 2 个词之间五五开地纠结)
# 2.303(=ln10) -> 10(在 10 个词之间抓瞎)

困惑度 = exp(交叉熵) 之所以成立,就是因为 explog 互为逆运算——它把 log 脱掉,还原成"有效选项数"。 一个 GPT 说困惑度是 8,意思就是"它预测下一个词时,平均像在 8 个词里犹豫"; 困惑度掉到 3,就是选择范围收窄到 3 个。这就是为什么所有语言模型的战报都盯着 perplexity—— 它是那把 log 尺子翻译给人看的、唯一说人话的版本。


🎬 动手:训练一个最小语言模型,看 log-loss 与困惑度一起跳水

把上面所有画面缝进一个能跑的最小 bigram 语言模型:用当前字符预测下一个字符, 一边训练,一边看交叉熵(log 空间的意外)和困惑度(人话版)如何同步下滑:

import torch
import torch.nn as nn
import torch.nn.functional as F

text = "hello world " * 50                 # 玩具语料:一段不断重复的字符串
chars = sorted(set(text))
stoi = {c: i for i, c in enumerate(chars)}
data = torch.tensor([stoi[c] for c in text])
x, y = data[:-1], data[1:]                 # 输入=当前字符,目标=下一个字符

V = len(chars)
model = nn.Embedding(V, V)                 # 最简 bigram:每个字符直接查出一行 logits
opt = torch.optim.Adam(model.parameters(), lr=0.1)

for step in range(301):
    logits = model(x)
    loss = F.cross_entropy(logits, y)      # 平均 -log(正确字符的概率)
    opt.zero_grad(); loss.backward(); opt.step()
    if step % 60 == 0:
        ppl = torch.exp(loss)              # 困惑度 = exp(交叉熵)
        print(f"step {step:3d}  交叉熵={loss:.3f}  困惑度={ppl:.2f}")
# 交叉熵一路下滑;困惑度从"在好几个字符间纠结"逼近 1(几乎笃定下一个字符)

你会看到交叉熵从一个偏大的值一路跌下去,困惑度也跟着从"好几"逼近"1"—— 模型正在学会"闭着眼都知道下一个字符是什么"。 残留的那点困惑度,来自语料里真正的歧义 (比如 l 后面既可能是 l 又可能是 o),这恰恰说明困惑度诚实地反映了"还剩多少不确定"。

仓库里的动画脚本把这根"log 尺子"画了出来:

python log_likelihood_visualization.py

左边画的是那条 意外 = -log(p) 的曲线,一个小球代表"模型对正确词的把握", 训练时它从左边(概率低、意外高、贴着那道陡峭的墙)一路滑向右边(概率高、意外趋近 0); 右边则是交叉熵与困惑度随训练步数同步跳水。 你会亲眼看到:概率每往 0 靠一点,意外就沿着那道 log 的陡墙暴涨——这就是大模型不敢"自信答错"的原因。


缝合:把所有画面接起来

回到开头,现在每个概念都有了画面和代码出处:

对数概念 高中怎么讲 这篇文章怎么看(画面) 在大模型里是什么
log 的定义 logₐN、换底公式 数"e 的几次方"(第 0 节) 一切损失计算的底层空间
log(ab)=log a+log b 一条要背的性质 连乘变连加的翻译机(第 1 节) 句子概率不下溢的救命稻草
对数函数图像 一条过 (1,0) 的曲线 -log(p) 那道陡峭的"意外墙"(第 3 节) 严惩"自信错误"的手感
logexp 互逆 指对互化 脱掉 log,还原"有效选项数"(疑惑点二) 困惑度 = exp(loss)
对数似然 课本几乎没讲 正确词 log 概率的连加(第 2 节) 交叉熵损失的真身

三句话总结这篇文章:

log 的灵魂是"连乘变连加"——正是它让几百个概率相乘不再下溢成 0(第 1、2 节); -log(p) 精确地度量"意外",概率趋 0 时意外冲向无穷,于是"自信的错误"被指数级重罚(第 3、4 节); 交叉熵就是平均意外、困惑度就是把 log 脱掉的人话版,这就是今天所有大模型训练时唯一的那把尺子(第 4 节、疑惑点二)。

当年对数背得那么苦,不是因为它难,是因为没人告诉你: 那条你觉得最没用、过 (1,0) 的 log 曲线,最后长成了每一个大模型的损失函数, 成了它衡量"自己错得有多离谱"的唯一标尺。 现在把上面每段代码跑一遍,比高中刷十套对数题都值。


备注(选题/标题): 这篇走的是"高中知识其实是 AI 基石"的钩子(和《sin 和 cos》《求根公式》同一路子)。 若想并入"大学4年"主系列,标题可换成: 《大学4年没让你真正搞懂的对数,被一段交叉熵代码讲透了》。 与《信息论》那篇是姊妹篇:信息论那篇讲"熵是平均意外",这篇从 log 本身切入、落到损失函数,可互相引流。