信息论与交叉熵:损失函数从哪来

配套视频

  • B站 · 3B1B《深度学习》系列 · 第 2~4 集 —— 梯度下降 / 反向传播,理解"损失函数到底在优化什么"

  • B站 · 搜索:StatQuest 交叉熵 / 信息熵 —— 选播放量最高的中文字幕版即可

⭐ 重点

  • 信息量 = 惊讶程度:事件越不可能发生,它携带的信息量越大

  • 熵 = 平均惊讶程度 = 不确定度:分布越平均,熵越大

  • 交叉熵 = 用模型分布去"编码"真实数据的平均代价,这就是训练损失

  • KL 散度 = 交叉熵 − 熵:衡量两个分布差多远,最小化交叉熵等价于最小化 KL

  • 困惑度 PPL = exp(交叉熵):LLM 评测最常用的指标,"模型平均在几个词之间犹豫"

⚡ 为什么这一篇是"核心中的核心":很多人学完 Transformer 还是说不清"训练到底在优化什么"。答案是:最小化交叉熵

4.1 信息量:用"惊讶程度"度量信息

先建立一个直觉:信息是用来消除不确定性的。太阳明天从东边升起——这句话没有信息量,因为你知道它一定发生。而"明天下雪"在七月就是大新闻。

于是信息量应该满足:

  • 概率越小 → 信息量越大

  • 两个独立事件的信息量应该相加(同时知道两件事 = 两份信息之和)

唯一(在连续、单调等条件下)满足这两条的构造是取对数:

自信息(self-information)

I(x) = -\log p(x)

负号是为了让结果为正(因为 p ≤ 1,log p ≤ 0)。底数决定单位:底 2 是 比特(bit),底 e 是 奈特(nat)。深度学习默认用 ln(奈特),因为求导干净。

4.2 熵:平均惊讶程度 = 不确定度

有了单个事件的信息量,我们把整个分布的"平均信息量"算出来,这就是香农熵:

熵(Entropy)

H(p) = -\sum_x p(x)\, \log p(x) = \mathbb{E}_{x \sim p}[-\log p(x)]

注意 H(p) 只跟分布 p 自己有关,它是这个分布本身的不确定度,是"数据固有的难度"。它不依赖任何模型。

几个典型例子(用 ln,单位 nat):

分布 p

熵 H(p)

含义

[1, 0, 0, 0]

0

完全确定,没有任何不确定度

[0.7, 0.1, 0.1, 0.1]

0.94

比较确定

[0.4, 0.3, 0.2, 0.1]

1.28

中等不确定

[0.25, 0.25, 0.25, 0.25]

1.386 = ln 4

完全均匀,熵最大

训练就是要把模型的输出分布"推尖"——让正确词的 p 尽量大、其他词的 p 尽量小,从而降低交叉熵。

4.3 交叉熵:用模型分布去编码真实数据的代价

熵是"数据自己"的难度。但我们手上没有真实分布,只有模型预测的分布 q。用 q 去衡量真实分布 p 的数据,得到的就是交叉熵:

交叉熵(Cross-Entropy)

H(p, q) = -\sum_x p(x)\, \log q(x)

🔍 一个符号的差别,含义天差地别:熵是 −Σ p·log p,交叉熵是 −Σ p·log q。区别在于:期望是按真实分布 p 求的(数据说了算),而对数里装的是模型分布 q(模型说了算)。训练时 p 是固定的(数据不变),我们只能动 q——所以交叉熵对 q 是可优化的。

在监督训练的单个样本上,观测到的目标通常表示为 one-hot 标签(正确 token 为 1,其余为 0)。它是对未知真实条件分布的一次观测;代进经验交叉熵,只有目标 token 那一项留下:

语言模型的交叉熵损失(单个 token)

\mathcal{L} = -\log q(w^*)

(w* = 正确词)

就是这么简单的一句话:损失 = 负的"模型给正确词的概率"的对数。模型给正确词 0.9 的概率 → 损失 0.105;只给 0.1 → 损失 2.303。给得越低,罚得越狠(对数曲线在接近 0 时爆炸)。

模型给正确词的概率 q

损失 −ln q

直观

0.9

0.105

很有信心且答对了,几乎不罚

0.5

0.683

一半把握

0.1

2.303

答对了但很不自信,重罚

0.01

4.605

几乎认为不可能,罚得极重

4.4 KL 散度:交叉熵比熵多出来的那部分

把交叉熵拆开,会得到一个漂亮的结构:

第 1 步 · 写出交叉熵

H(p, q) = -\sum_x p(x)\, \log q(x)

第 2 步 · 分子分母同乘 p,拆成两项

= -\sum_x p(x)\, \log\!\Big[\frac{q(x)}{p(x)} \cdot p(x)\Big] = -\sum_x p(x)\, \log p(x) \;-\; \sum_x p(x)\, \log\!\Big[\frac{q(x)}{p(x)}\Big]

第 3 步 · 认出第一项就是熵,第二项定义为 KL 散度

H(p, q) = H(p) + D_{\mathrm{KL}}(p \,\|\, q)

KL 散度(相对熵)

D_{\mathrm{KL}}(p \,\|\, q) = \sum_x p(x)\, \log\frac{p(x)}{q(x)} \;\geq\; 0

KL 散度衡量"用 q 代替 p 会多付出多少代价",它永远非负,当且仅当 p = q 时取 0(这就是吉布斯不等式)。

⭐ 这一步是理解训练目标的关键

因为 H(p) 只跟数据有关、跟模型无关,是个常数,所以:

minq H(p, q) ⟺ minq DKL(p ‖ q) ⟺ 让模型分布 q 尽量逼近真实分布 p

4.5 从最大似然到交叉熵

训练目标是最大似然。现在把它推完,你会看到交叉熵是必然结果:

第 1 步 · 最大似然:让整份语料出现的概率最大

\theta^* = \arg\max_\theta \prod_t q_\theta(w_t \mid w_{<t})

第 2 步 · 连乘不好优化,取对数变成求和(对数单调,不改变极值点)

\theta^* = \arg\max_\theta \sum_t \log q_\theta(w_t \mid w_{<t})

第 3 步 · 优化器只会最小化,加负号翻转

\theta^* = \arg\min_\theta \big(-\sum_t \log q_\theta(w_t \mid w_{<t})\big)

第 4 步 · 除以 token 数 T 取平均,得到最终损失函数

\mathcal{L}(\theta) = -\tfrac{1}{T}\sum_t \log q_\theta(w_t \mid w_{<t}) \quad = \text{交叉熵损失}

交叉熵损失

⚠️ 一句话记住:最大似然 ⟺ 最小化负对数似然(NLL)⟺ 最小化交叉熵。三者是同一件事的三种说法。面试里被问到"语言模型的损失函数是什么",能把这句串起来讲,比背公式有用得多。

4.6 困惑度 PPL:交叉熵的"人话版"

交叉熵的单位是 nat,不好直观理解。取个指数就变成"平均在几个词之间犹豫":

困惑度(Perplexity)

\mathrm{PPL} = \exp(\mathcal{L})

(L 是平均交叉熵,单位 nat)

交叉熵 L(nat)

困惑度 PPL

含义

0

1

完美预测,从不犹豫

0.693

2

平均在 2 个词之间犹豫

2.303

10

平均在 10 个词之间犹豫

3.0

≈ 20

早期 GPT-2 量级的水平

💎 工程上的意义:PPL 不需要人工任务标签,只要有带上下文的文本就能计算,常用于语言建模评估。但注意:PPL 低 ≠ 对话好用,而且只有在相同 tokenization、相同数据集和相同计算约定下才适合直接比较。较低 PPL 表示模型更擅长预测评测文本,并不直接衡量指令遵循、事实性或安全性。

用 NumPy 把四个指标全算一遍

import numpy as np

def entropy(p):
    # 熵:只依赖 p 自己。0*log0 约定为 0,所以加个掩码
    p = np.asarray(p, dtype=float)
    m = p > 0
    return -(p[m] * np.log(p[m])).sum()

def cross_entropy(p, q):
    # 交叉熵:期望按 p 求,log 里装 q
    p, q = np.asarray(p, float), np.asarray(q, float)
    q = np.clip(q, 1e-12, 1.0)   # 防止 log(0) 爆炸
    return -(p * np.log(q)).sum()

def kl(p, q):
    # KL = 交叉熵 - 熵
    return cross_entropy(p, q) - entropy(p)

# ---- 语言模型的真实场景:正确答案只有一个词(one-hot)----
p_true = [1.0, 0.0, 0.0, 0.0]

for name, q in [
    ("模型很自信", [0.90, 0.05, 0.03, 0.02]),
    ("模型还行",   [0.50, 0.20, 0.20, 0.10]),
    ("模型答错了", [0.05, 0.45, 0.30, 0.20]),
]:
    ce = cross_entropy(p_true, q)
    print(f"{name}: 交叉熵={ce:.3f} nat, 困惑度={np.exp(ce):.2f}")

# 输出:
#   模型很自信: 交叉熵=0.105 nat, 困惑度=1.11
#   模型还行:   交叉熵=0.693 nat, 困惑度=2.00
#   模型答错了: 交叉熵=2.996 nat, 困惑度=20.00

# ---- 验证 KL >= 0 ----
p = np.array([0.7, 0.2, 0.1])
q = np.array([0.5, 0.3, 0.2])
print(f"KL(p||q) = {kl(p, q):.4f}")   # 0.0513
print(f"KL(q||p) = {kl(q, p):.4f}")   # 0.0578  ← 注意不对称!

⚠️ KL 不是距离:上面的输出里 KL(p‖q) ≠ KL(q‖p),所以 KL 散度不满足对称性,不是数学意义上的"距离"。这也是为什么 RLHF 里的 KL 惩罚项写 kl(πθ ‖ πref) 而不是反过来——方向不同,含义不同。