系列回顾:上一篇我们推导了 MLE——最大似然估计统一了分类任务的交叉熵损失和回归任务的 MSE 损失。但交叉熵本身来自哪里?为什么叫"交叉"熵?"熵"又是什么?这一篇从信息论的角度重新理解训练损失,把熵、交叉熵、KL 散度这三个概念的关系讲清楚,再联系到 DeepSeek V3 的训练和 R1 的强化学习。
1948 年,Claude Shannon 在贝尔实验室发表了一篇论文,奠定了信息论的基础。他解决了一个看似哲学的问题:信息量是什么,怎么度量?
Shannon 的答案出人意料地简洁:
一件事的信息量,等于得知它发生后,你的不确定性减少了多少。
越不可能发生的事,一旦发生,提供的信息量越大。
这个直觉,被 Shannon 量化为一个精确的数学公式。这个公式,是交叉熵损失的最终来源。
事件 $x$ 发生的自信息量(Self-Information):
$$I(x) = -\log P(x)$$
通常取以 2 为底(单位是比特 bit)或以 $e$ 为底(单位是奈特 nat)。深度学习里通常用自然对数(奈特)。
性质一:概率越小,信息量越大
$$P(x) \to 1 \Rightarrow I(x) = -\log 1 = 0$$ $$P(x) \to 0 \Rightarrow I(x) = -\log 0 = +\infty$$
必然事件信息量为 0,不可能事件信息量无穷大。
性质二:独立事件的信息量可加
若 $x$ 和 $y$ 独立:$P(x,y) = P(x)P(y)$
$$I(x,y) = -\log P(x,y) = -\log P(x) - \log P(y) = I(x) + I(y)$$
两个独立事件同时发生的信息量,等于各自信息量之和。这个可加性是选择对数函数的根本原因——对数是唯一能把乘法变成加法的函数。
掷一枚公平骰子($P = \frac{1}{6}$):
$$I(\text{出现3点}) = -\log_2 \frac{1}{6} = \log_2 6 \approx 2.585 \text{ bits}$$
抛公平硬币($P = \frac{1}{2}$):
$$I(\text{正面}) = -\log_2 \frac{1}{2} = 1 \text{ bit}$$
这正是"比特"的由来——1 比特是"从两个等可能结果中确定一个"的信息量。
语言模型预测下一个词(词表 128K 个词,均匀时 $P = \frac{1}{128000}$):
$$I(\text{某个词}) = -\log_2 \frac{1}{128000} = \log_2 128000 \approx 16.96 \text{ bits}$$
单个事件的信息量 $I(x)$ 是随机的($x$ 取不同值时不同)。
信息熵(Shannon Entropy) $H(P)$ 是信息量的期望值——平均信息量:
$$H(P) = \mathbb{E}_{x \sim P}[I(x)] = -\sum_x P(x) \log P(x) = -\mathbb{E}[\log P(x)]$$
连续分布(微分熵):
$$H(P) = -\int p(x) \log p(x) \, dx$$
约定 $0 \log 0 = 0$(因为 $\lim_{p \to 0} p \log p = 0$)。
熵越高,分布越"不确定"、越"均匀";熵越低,分布越"集中"、越"确定"。
例1:公平硬币($p=0.5$)
$$H = -0.5\log 0.5 - 0.5\log 0.5 = \log 2 \approx 0.693 \text{ nats} = 1 \text{ bit}$$
例2:偏心硬币($p=0.9$)
$$H = -0.9\log 0.9 - 0.1\log 0.1 \approx 0.095 + 0.230 = 0.325 \text{ nats}$$
例3:确定的硬币($p=1$)
$$H = -1 \cdot \log 1 = 0$$
结果完全确定,没有不确定性,熵为 0。
离散均匀分布的熵最大:对 $K$ 个等可能结果,$H = \log K$。
证明:对 $P(x) = \frac{1}{K}$ 时,$H = -\sum_{k=1}^K \frac{1}{K} \log \frac{1}{K} = \log K$。
用 Jensen 不等式(log 是凹函数)可以证明这是所有满足 $\sum_k p_k = 1$ 的分布中熵的最大值。
启示:语言模型在训练初期(参数随机初始化),对所有词几乎均匀分布,熵接近最大值 $\log 128000 \approx 11.76$ nats(即困惑度 ≈ 128000)。随着训练进行,模型对"下一个词是什么"越来越有把握,熵持续降低,困惑度下降。
Shannon 曾估算英语文本的熵约为每字母 1.0~1.3 bits。这意味着英语有大量冗余——不是每个字母都完全不可预测,上下文能帮助预测后续内容。
现代语言模型对英语文本的困惑度(以词为单位)已经降到个位数,说明模型捕捉到了语言的大量统计规律,接近了语言本身的信息论下界。
信息熵 $H(P) = -\mathbb{E}_{x \sim P}[\log P(x)]$ 中,对数里的 $P$ 和期望里的 $P$ 是同一个分布——真实数据分布。
但实际情况是:我们不知道真实分布 $P$,模型只是一个近似 $Q_\theta$。
如果实际数据来自 $P$,但我们用 $Q_\theta$ 来"编码"(即用 $Q_\theta$ 计算信息量),平均每个事件需要多少信息量?
$$H(P, Q) = -\mathbb{E}_{x \sim P}[\log Q(x)] = -\sum_x P(x) \log Q(x)$$
这就是交叉熵(Cross-Entropy):期望用真实分布 $P$ 算,但对数用近似分布 $Q$。
差值 $H(P, Q) - H(P)$ 是"用错分布付出的额外代价",这就是 KL 散度(下一篇详细讲):
$$D_{KL}(P \| Q) = H(P, Q) - H(P) \geq 0$$
训练集的标签分布 $P$(one-hot):真实类别的概率为 1,其余为 0。
模型预测分布 $Q_\theta$:Softmax 输出的概率向量。
交叉熵损失:
$$H(P, Q_\theta) = -\sum_k P(k) \log Q_\theta(k) = -\log Q_\theta(y^*)$$
由于 $P$ 是 one-hot,求和只有真实类别 $y^*$ 处的项非零,直接等于 $-\log Q_\theta(y^*)$。
最小化交叉熵 = 最小化"用模型分布替代真实分布的代价" = 让模型分布尽量接近真实分布。
这和第14篇的 MLE 视角完全一致:$-\log Q_\theta(y^*)$ 就是真实类别的负对数概率(NLL)。
$$H(P, Q) = H(P) + D_{KL}(P \| Q)$$
最小化交叉熵 = 最小化 KL 散度(因为 $H(P)$ 和模型参数无关,是常数)= 让模型分布 $Q_\theta$ 尽量接近真实分布 $P$。
对于分类任务,$P$ 是 one-hot,$H(P) = 0$(确定性分布,熵为 0),所以交叉熵完全等于 KL 散度:
$$H(P_{one-hot}, Q_\theta) = D_{KL}(P_{one-hot} \| Q_\theta)$$
语言模型在位置 $t$ 的交叉熵损失:
$$L_t = -\log P_\theta(x_t \mid x_{ 这是模型对真实 token $x_t$ 的"惊讶程度"——模型越确定,$P_\theta(x_t \mid x_{ 整个序列的平均交叉熵: $$L = \frac{1}{T}\sum_{t=1}^T (-\log P_\theta(x_t \mid x_{ 这正是 DeepSeek V3 的预训练损失。每一步梯度更新,都在减少模型对训练数据的"平均惊讶程度"。 困惑度再理解 $$\text{PPL} = e^{L} = \exp\left(\frac{1}{T}\sum_t -\log P_\theta(x_t \mid x_{ PPL 是所有条件概率之积的几何平均的倒数——平均每步模型的"有效候选数量"。 信息论解释:PPL 等于模型平均在"多少个等可能选项里猜",即模型用这个文本的"等效均匀词表大小"。PPL=10 意味着模型平均只需从 10 个同等可能的词里选,远好于随机的 128000。 训练过程中的熵变化 训练阶段 模型状态 典型PPL 熵(nats) 随机初始化 均匀猜测 ~128000 ~11.76 预训练初期 开始学词频 ~1000 ~6.9 预训练中期 掌握语法结构 ~50 ~3.9 预训练后期 理解语义 ~10 ~2.3 SOTA模型 深度理解 ~3-8 ~1.1-2.1 第五部分:二元交叉熵——二分类的特殊情形 推导 二分类:标签 $y \in \{0, 1\}$,模型预测正例概率 $\hat{p} = \sigma(z) \in (0,1)$。 真实分布 $P$:$P(1) = y$,$P(0) = 1-y$。 模型分布 $Q$:$Q(1) = \hat{p}$,$Q(0) = 1-\hat{p}$。 交叉熵: $$H(P, Q) = -P(1)\log Q(1) - P(0)\log Q(0)$$ $$= -y \log \hat{p} - (1-y)\log(1-\hat{p})$$ 这就是二元交叉熵损失(Binary Cross-Entropy),也叫 BCE Loss。 两种极端情形 $y=1$,$\hat{p} \to 1$(正确且自信):损失 $= -\log 1 = 0$ ✓ $y=1$,$\hat{p} \to 0$(预测错误且自信):损失 $= -\log 0 = +\infty$ ← 对自信错误惩罚极大 $y=0$,$\hat{p} \to 0$(正确且自信):损失 $= -\log 1 = 0$ ✓ $y=0$,$\hat{p} \to 1$(预测错误且自信):损失 $= -\log 0 = +\infty$ ← 同样的极大惩罚 交叉熵损失有一个良好的性质:对"自信地犯错"的惩罚是无穷大,迫使模型在不确定时保持谦逊,不要给出极端的概率预测。 第六部分:熵的其他形态 条件熵 条件熵(Conditional Entropy) $H(Y \mid X)$:已知 $X$ 的条件下,$Y$ 的平均不确定性: $$H(Y \mid X) = -\mathbb{E}_{x,y}[\log P(y \mid x)] = \sum_x P(x) H(Y \mid X=x)$$ 链式法则: $$H(X, Y) = H(X) + H(Y \mid X)$$ 联合不确定性 = $X$ 的不确定性 + 知道 $X$ 后 $Y$ 的不确定性。 语言模型的训练损失就是条件熵的估计: $$L = H(X_t \mid X_{ 目标是最小化"给定前文条件下,下一个词的不确定性"。 联合熵与互信息 互信息(Mutual Information) $I(X; Y)$:$X$ 和 $Y$ 之间共享的信息量: $$I(X; Y) = H(X) - H(X \mid Y) = H(Y) - H(Y \mid X)$$ $$= H(X) + H(Y) - H(X,Y)$$ 互信息衡量两个变量的相关程度: $I(X;Y) = 0$:完全独立,知道 $Y$ 不帮助预测 $X$ $I(X;Y) = H(X)$:$X$ 完全由 $Y$ 决定,知道 $Y$ 就知道 $X$ 在深度学习中的应用: 互信息最大化(Mutual Information Maximization)是无监督表示学习的一种范式——让编码器的输出(表示)和输入数据之间的互信息最大化。直觉是:好的表示应该保留输入的最多信息。 对比学习(Contrastive Learning)可以理解为最大化正样本对之间的互信息,最小化负样本对之间的互信息。CLIP、SimCLR 等模型的训练目标,本质上都是互信息最大化。 第七部分:温度、熵与采样策略 温度控制熵 语言模型的输出分布,用温度 $\tau$ 调节: $$p_k(\tau) = \frac{e^{z_k/\tau}}{\sum_j e^{z_j/\tau}}$$ 温度对熵的影响: $$H(P_\tau) = -\sum_k p_k(\tau) \log p_k(\tau)$$ $\tau \to 0$:分布退化为 one-hot(只有最高 logit 概率为 1),熵 = 0 $\tau = 1$:标准 Softmax,熵为正常值 $\tau \to \infty$:分布趋向均匀,熵趋向最大值 $\log K$ 温度采样的信息论含义:高温度相当于向输出分布"注入不确定性"(增加熵),增加生成的多样性;低温度相当于压缩不确定性(减少熵),增加生成的一致性。 Top-p 采样(Nucleus Sampling) Top-p 采样不按温度调节,而是动态截断: 每次只保留累积概率超过 $p$(比如 0.9)的最小词集合 $\mathcal{V}_p$,在这个集合里重新归一化采样: $$\mathcal{V}_p = \text{最小集合使得} \sum_{k \in \mathcal{V}_p} p_k \geq p$$ 信息论角度:Top-p 相当于保留分布的"前 $p$ 概率质量",丢弃尾部的低概率词。 好处:分布尖峰时(模型确定),$\mathcal{V}_p$ 很小(只考虑少数高概率词,行文准确);分布平坦时(模型不确定),$\mathcal{V}_p$ 较大(允许更多选择,增加多样性)。自动适应模型的局部确定性。 DeepSeek API 默认 top_p = 0.9,是工程实践中的平衡点。 第八部分:交叉熵的数值问题与 LogSumExp 技巧 数值下溢问题 直接计算 Softmax 再取对数: probs = exp(logits) / sum(exp(logits)) # 可能数值溢出 log_probs = log(probs) 当 logits 很大(比如 100),exp(100) 会溢出(超过 float 最大值)。 LogSumExp 技巧 $$\log \sum_j e^{z_j} = c + \log \sum_j e^{z_j - c}, \quad c = \max_j z_j$$ 减去最大值再指数,最大项变成 $e^0 = 1$,其余项 $\leq 1$,不会溢出。 交叉熵的数值稳定计算: $$-\log p_{y^*} = -z_{y^*} + \log \sum_j e^{z_j} = -z_{y^*} + c + \log \sum_j e^{z_j - c}$$ PyTorch 的 F.cross_entropy 内部就用了这个技巧,直接接受 logits,不需要先算 Softmax——既数值稳定,又计算高效。 第九部分:完整代码 import numpy as np import torch import torch.nn.functional as F np.random.seed(42) torch.manual_seed(42) # ===== 1. 自信息量与信息熵 ===== print("===== 自信息量与信息熵 =====\n") def entropy(probs): probs = np.array(probs) probs = probs[probs > 0] # 排除零概率 return -np.sum(probs * np.log(probs)) # 几种分布的熵 distributions = { "确定性(p=1)": [1.0], "公平硬币(p=0.5)": [0.5, 0.5], "偏心硬币(p=0.9)": [0.9, 0.1], "公平骰子(K=6)": [1/6]*6, "均匀128K词表": [1/128000]*128000, } print(f"{'分布':25s} | {'熵(nats)':>10} | {'PPL':>10} | {'解释'}") print("-" * 70) for name, probs in distributions.items(): h = entropy(probs) ppl = np.exp(h) print(f"{name:25s} | {h:>10.4f} | {ppl:>10.2f}") # 验证最大熵定理:K个类别的均匀分布熵 = log(K) for K in [2, 6, 10, 128000]: h_uniform = np.log(K) h_computed = entropy([1/K]*K) print(f"K={K:7d}: 理论 log(K)={h_uniform:.4f}, 计算值={h_computed:.4f}") # ===== 2. 交叉熵:用错分布的代价 ===== print("\n===== 交叉熵:用错分布的代价 =====\n") # 真实分布 P(标签),模型分布 Q(预测) P_true = np.array([0.0, 1.0, 0.0]) # one-hot,真实类别是1 Q_good = np.array([0.05, 0.9, 0.05]) # 好模型 Q_bad = np.array([0.4, 0.2, 0.4]) # 差模型 def cross_entropy(P, Q): Q = np.clip(Q, 1e-10, 1.0) return -np.sum(P * np.log(Q)) def kl_divergence(P, Q): mask = P > 0 Q = np.clip(Q, 1e-10, 1.0) return np.sum(P[mask] * np.log(P[mask] / Q[mask])) h_P = entropy(P_true[P_true > 0]) ce_good = cross_entropy(P_true, Q_good) ce_bad = cross_entropy(P_true, Q_bad) kl_good = kl_divergence(P_true, Q_good) kl_bad = kl_divergence(P_true, Q_bad) print(f"真实分布 P: {P_true} (one-hot)") print(f"好模型 Q_good: {Q_good}") print(f"差模型 Q_bad: {Q_bad}\n") print(f"H(P) 真实熵: {h_P:.4f} (one-hot熵=0)") print(f"H(P,Q_good) 交叉熵: {ce_good:.4f} = H(P)+KL = {h_P:.4f}+{kl_good:.4f}") print(f"H(P,Q_bad) 交叉熵: {ce_bad:.4f} = H(P)+KL = {h_P:.4f}+{kl_bad:.4f}") print(f"\n→ 交叉熵 = 真实熵 + KL散度,最小化交叉熵即最小化KL散度") # ===== 3. 温度对熵的影响 ===== print("\n===== 温度对熵的影响 =====\n") logits = torch.tensor([3.0, 1.5, 1.0, 0.5, 0.2, -0.5]) vocab = ["猫", "狗", "鱼", "鸟", "虫", "草"] print(f"{'温度τ':>6} | {'熵(nats)':>10} | {'PPL':>8} | {'最高概率词':>8} | {'最高概率':>8}") print("-" * 55) for tau in [0.1, 0.3, 0.5, 1.0, 1.5, 2.0, 5.0]: p = F.softmax(logits / tau, dim=0) h = -(p * torch.log(p + 1e-10)).sum().item() ppl = np.exp(h) top_k = p.argmax().item() print(f"τ={tau:>4.1f} | {h:>10.4f} | {ppl:>8.3f} | {vocab[top_k]:>8} | {p[top_k].item():>8.4f}") # ===== 4. 语言模型训练过程中的PPL变化 ===== print("\n===== 语言模型训练中的PPL变化(模拟) =====\n") # 模拟不同训练阶段的模型对同一个token的预测概率 vocab_size = 128000 true_token_idx = 42 # 真实token scenarios = [ ("随机初始化", 1.0 / vocab_size), # 均匀猜测 ("训练1k步", 0.005), # 开始集中 ("训练10k步", 0.05), ("训练100k步", 0.20), ("训练1M步", 0.50), ("训练10M步", 0.80), ] print(f"{'训练阶段':14} | {'真实token概率':>14} | {'NLL损失':>8} | {'等效PPL':>10}") print("-" * 55) for stage, true_prob in scenarios: nll = -np.log(true_prob) # 等效PPL = 如果所有token的条件概率都等于true_prob时的PPL ppl = np.exp(nll) print(f"{stage:14} | {true_prob:>14.6f} | {nll:>8.4f} | {ppl:>10.2f}") # ===== 5. 数值稳定的交叉熵:LogSumExp 技巧 ===== print("\n===== LogSumExp 技巧验证 =====\n") # 极大的logits(会导致数值溢出) logits_large = torch.tensor([100.0, 101.0, 99.0]) label = torch.tensor([1]) # 真实类别是1 # 方法一:先算Softmax(可能溢出) try: probs_naive = torch.exp(logits_large) / torch.exp(logits_large).sum() loss_naive = -torch.log(probs_naive[1]) print(f"朴素方法: probs={probs_naive}, loss={loss_naive.item():.4f}") except Exception as e: print(f"朴素方法失败: {e}") # 方法二:LogSumExp(数值稳定) c = logits_large.max() log_sum_exp = c + torch.log(torch.exp(logits_large - c).sum()) log_prob_stable = logits_large[1] - log_sum_exp loss_stable = -log_prob_stable print(f"LogSumExp方法: log_sum_exp={log_sum_exp.item():.4f}, loss={loss_stable.item():.4f}") # 方法三:PyTorch内置(自动使用数值稳定计算) loss_pytorch = F.cross_entropy(logits_large.unsqueeze(0), label) print(f"PyTorch内置: loss={loss_pytorch.item():.4f}") print(f"两种数值稳定方法结果相同: {torch.isclose(loss_stable, loss_pytorch)}") # ===== 6. 互信息演示 ===== print("\n===== 互信息:变量间的信息共享 =====\n") # 构造两个相关的离散变量 # X = 天气(晴/阴/雨),Y = 是否带伞 # 联合分布 joint_P = np.array([ [0.30, 0.02], # 晴天:不带伞0.30,带伞0.02 [0.10, 0.10], # 阴天:不带伞0.10,带伞0.10 [0.03, 0.45], # 雨天:不带伞0.03,带伞0.45 ]) # 行:天气(晴/阴/雨),列:带伞(否/是) P_X = joint_P.sum(axis=1) # 天气边缘分布 P_Y = joint_P.sum(axis=0) # 带伞边缘分布 H_X = entropy(P_X) H_Y = entropy(P_Y) H_XY = entropy(joint_P.flatten()) # 条件熵 H(Y|X) = H(X,Y) - H(X) H_Y_given_X = H_XY - H_X # 互信息 I(X;Y) = H(Y) - H(Y|X) MI = H_Y - H_Y_given_X print(f"天气分布 P_X: {dict(zip(['晴','阴','雨'], P_X.round(2)))}") print(f"带伞分布 P_Y: {dict(zip(['否','是'], P_Y.round(2)))}") print(f"\nH(天气): {H_X:.4f} nats") print(f"H(带伞): {H_Y:.4f} nats") print(f"H(天气,带伞): {H_XY:.4f} nats") print(f"H(带伞|天气): {H_Y_given_X:.4f} nats (知道天气后,带伞不确定性降低)") print(f"I(天气;带伞): {MI:.4f} nats (互信息:天气和带伞共享的信息量)") print(f"\n→ 知道天气后,带伞的不确定性从{H_Y:.3f}降到{H_Y_given_X:.3f}") print(f"→ 互信息{MI:.3f}就是这个不确定性的减少量") 总结 这篇文章从 Shannon 的信息论出发,把信息熵、交叉熵和语言模型训练损失的关系完整打通了。 第一,自信息量 $I(x) = -\log P(x)$ 衡量单个事件的信息量:概率越小,发生时信息量越大。独立事件信息量可加,这是选择对数函数的根本原因。 第二,信息熵 $H(P) = -\mathbb{E}[\log P(x)]$ 是信息量的期望,衡量分布的平均不确定性。均匀分布熵最大(最不确定),确定性分布熵为 0。语言模型训练从高熵(随机猜测 PPL≈128000)向低熵(精确预测 PPL≈几)前进。 第三,交叉熵 $H(P,Q) = -\mathbb{E}_{x \sim P}[\log Q(x)]$ 是"用模型分布 $Q$ 替代真实分布 $P$ 编码数据的平均代价",总是 $\geq H(P)$。最小化交叉熵等价于最小化 KL 散度,让模型分布尽量接近真实数据分布。 第四,语言模型的训练损失就是平均交叉熵(等价于平均 NLL),困惑度 PPL 是其指数,直觉上表示"模型平均在多少个候选里犹豫"。条件熵视角下,训练目标是最小化"给定前文条件下下一个 token 的不确定性"。 第五,温度参数 $\tau$ 控制输出分布的熵:低温降低熵(更确定、更保守),高温提高熵(更多样、更随机)。Top-p 采样动态调整候选集大小,自适应模型的局部确定性。 第六,数值稳定性是工程实践的关键:LogSumExp 技巧通过减去最大值防止指数溢出,PyTorch 的 F.cross_entropy 直接接受 logits 并内部使用这个技巧。 下一篇预告: 第16篇,我们讲 KL 散度:DeepSeek R1 强化学习中的约束项从何而来? 交叉熵告诉我们"用错分布的代价",但这个代价是不对称的——$D_{KL}(P \| Q) \neq D_{KL}(Q \| P)$。这个不对称性有深刻的含义,直接解释了为什么 GRPO 的 KL 约束要写成 $D_{KL}(\pi_\theta \| \pi_{ref})$ 而不是反过来,以及为什么 VAE 的训练目标选 $D_{KL}(q \| p)$。 信息论告诉我们:不确定性是可以量化的,信息是可以度量的。Shannon 熵不仅仅是数学工具,它是一种思维方式——把"模型对数据有多惊讶"变成了一个可以优化的数字。每次梯度下降,DeepSeek V3 都在用 14.8 万亿个 token 告诉自己:对这些数据,你还可以更少地感到惊讶。
这是模型对真实 token $x_t$ 的"惊讶程度"——模型越确定,$P_\theta(x_t \mid x_{ 整个序列的平均交叉熵: $$L = \frac{1}{T}\sum_{t=1}^T (-\log P_\theta(x_t \mid x_{ 这正是 DeepSeek V3 的预训练损失。每一步梯度更新,都在减少模型对训练数据的"平均惊讶程度"。 困惑度再理解 $$\text{PPL} = e^{L} = \exp\left(\frac{1}{T}\sum_t -\log P_\theta(x_t \mid x_{ PPL 是所有条件概率之积的几何平均的倒数——平均每步模型的"有效候选数量"。 信息论解释:PPL 等于模型平均在"多少个等可能选项里猜",即模型用这个文本的"等效均匀词表大小"。PPL=10 意味着模型平均只需从 10 个同等可能的词里选,远好于随机的 128000。 训练过程中的熵变化 训练阶段 模型状态 典型PPL 熵(nats) 随机初始化 均匀猜测 ~128000 ~11.76 预训练初期 开始学词频 ~1000 ~6.9 预训练中期 掌握语法结构 ~50 ~3.9 预训练后期 理解语义 ~10 ~2.3 SOTA模型 深度理解 ~3-8 ~1.1-2.1 第五部分:二元交叉熵——二分类的特殊情形 推导 二分类:标签 $y \in \{0, 1\}$,模型预测正例概率 $\hat{p} = \sigma(z) \in (0,1)$。 真实分布 $P$:$P(1) = y$,$P(0) = 1-y$。 模型分布 $Q$:$Q(1) = \hat{p}$,$Q(0) = 1-\hat{p}$。 交叉熵: $$H(P, Q) = -P(1)\log Q(1) - P(0)\log Q(0)$$ $$= -y \log \hat{p} - (1-y)\log(1-\hat{p})$$ 这就是二元交叉熵损失(Binary Cross-Entropy),也叫 BCE Loss。 两种极端情形 $y=1$,$\hat{p} \to 1$(正确且自信):损失 $= -\log 1 = 0$ ✓ $y=1$,$\hat{p} \to 0$(预测错误且自信):损失 $= -\log 0 = +\infty$ ← 对自信错误惩罚极大 $y=0$,$\hat{p} \to 0$(正确且自信):损失 $= -\log 1 = 0$ ✓ $y=0$,$\hat{p} \to 1$(预测错误且自信):损失 $= -\log 0 = +\infty$ ← 同样的极大惩罚 交叉熵损失有一个良好的性质:对"自信地犯错"的惩罚是无穷大,迫使模型在不确定时保持谦逊,不要给出极端的概率预测。 第六部分:熵的其他形态 条件熵 条件熵(Conditional Entropy) $H(Y \mid X)$:已知 $X$ 的条件下,$Y$ 的平均不确定性: $$H(Y \mid X) = -\mathbb{E}_{x,y}[\log P(y \mid x)] = \sum_x P(x) H(Y \mid X=x)$$ 链式法则: $$H(X, Y) = H(X) + H(Y \mid X)$$ 联合不确定性 = $X$ 的不确定性 + 知道 $X$ 后 $Y$ 的不确定性。 语言模型的训练损失就是条件熵的估计: $$L = H(X_t \mid X_{ 目标是最小化"给定前文条件下,下一个词的不确定性"。 联合熵与互信息 互信息(Mutual Information) $I(X; Y)$:$X$ 和 $Y$ 之间共享的信息量: $$I(X; Y) = H(X) - H(X \mid Y) = H(Y) - H(Y \mid X)$$ $$= H(X) + H(Y) - H(X,Y)$$ 互信息衡量两个变量的相关程度: $I(X;Y) = 0$:完全独立,知道 $Y$ 不帮助预测 $X$ $I(X;Y) = H(X)$:$X$ 完全由 $Y$ 决定,知道 $Y$ 就知道 $X$ 在深度学习中的应用: 互信息最大化(Mutual Information Maximization)是无监督表示学习的一种范式——让编码器的输出(表示)和输入数据之间的互信息最大化。直觉是:好的表示应该保留输入的最多信息。 对比学习(Contrastive Learning)可以理解为最大化正样本对之间的互信息,最小化负样本对之间的互信息。CLIP、SimCLR 等模型的训练目标,本质上都是互信息最大化。 第七部分:温度、熵与采样策略 温度控制熵 语言模型的输出分布,用温度 $\tau$ 调节: $$p_k(\tau) = \frac{e^{z_k/\tau}}{\sum_j e^{z_j/\tau}}$$ 温度对熵的影响: $$H(P_\tau) = -\sum_k p_k(\tau) \log p_k(\tau)$$ $\tau \to 0$:分布退化为 one-hot(只有最高 logit 概率为 1),熵 = 0 $\tau = 1$:标准 Softmax,熵为正常值 $\tau \to \infty$:分布趋向均匀,熵趋向最大值 $\log K$ 温度采样的信息论含义:高温度相当于向输出分布"注入不确定性"(增加熵),增加生成的多样性;低温度相当于压缩不确定性(减少熵),增加生成的一致性。 Top-p 采样(Nucleus Sampling) Top-p 采样不按温度调节,而是动态截断: 每次只保留累积概率超过 $p$(比如 0.9)的最小词集合 $\mathcal{V}_p$,在这个集合里重新归一化采样: $$\mathcal{V}_p = \text{最小集合使得} \sum_{k \in \mathcal{V}_p} p_k \geq p$$ 信息论角度:Top-p 相当于保留分布的"前 $p$ 概率质量",丢弃尾部的低概率词。 好处:分布尖峰时(模型确定),$\mathcal{V}_p$ 很小(只考虑少数高概率词,行文准确);分布平坦时(模型不确定),$\mathcal{V}_p$ 较大(允许更多选择,增加多样性)。自动适应模型的局部确定性。 DeepSeek API 默认 top_p = 0.9,是工程实践中的平衡点。 第八部分:交叉熵的数值问题与 LogSumExp 技巧 数值下溢问题 直接计算 Softmax 再取对数: probs = exp(logits) / sum(exp(logits)) # 可能数值溢出 log_probs = log(probs) 当 logits 很大(比如 100),exp(100) 会溢出(超过 float 最大值)。 LogSumExp 技巧 $$\log \sum_j e^{z_j} = c + \log \sum_j e^{z_j - c}, \quad c = \max_j z_j$$ 减去最大值再指数,最大项变成 $e^0 = 1$,其余项 $\leq 1$,不会溢出。 交叉熵的数值稳定计算: $$-\log p_{y^*} = -z_{y^*} + \log \sum_j e^{z_j} = -z_{y^*} + c + \log \sum_j e^{z_j - c}$$ PyTorch 的 F.cross_entropy 内部就用了这个技巧,直接接受 logits,不需要先算 Softmax——既数值稳定,又计算高效。 第九部分:完整代码 import numpy as np import torch import torch.nn.functional as F np.random.seed(42) torch.manual_seed(42) # ===== 1. 自信息量与信息熵 ===== print("===== 自信息量与信息熵 =====\n") def entropy(probs): probs = np.array(probs) probs = probs[probs > 0] # 排除零概率 return -np.sum(probs * np.log(probs)) # 几种分布的熵 distributions = { "确定性(p=1)": [1.0], "公平硬币(p=0.5)": [0.5, 0.5], "偏心硬币(p=0.9)": [0.9, 0.1], "公平骰子(K=6)": [1/6]*6, "均匀128K词表": [1/128000]*128000, } print(f"{'分布':25s} | {'熵(nats)':>10} | {'PPL':>10} | {'解释'}") print("-" * 70) for name, probs in distributions.items(): h = entropy(probs) ppl = np.exp(h) print(f"{name:25s} | {h:>10.4f} | {ppl:>10.2f}") # 验证最大熵定理:K个类别的均匀分布熵 = log(K) for K in [2, 6, 10, 128000]: h_uniform = np.log(K) h_computed = entropy([1/K]*K) print(f"K={K:7d}: 理论 log(K)={h_uniform:.4f}, 计算值={h_computed:.4f}") # ===== 2. 交叉熵:用错分布的代价 ===== print("\n===== 交叉熵:用错分布的代价 =====\n") # 真实分布 P(标签),模型分布 Q(预测) P_true = np.array([0.0, 1.0, 0.0]) # one-hot,真实类别是1 Q_good = np.array([0.05, 0.9, 0.05]) # 好模型 Q_bad = np.array([0.4, 0.2, 0.4]) # 差模型 def cross_entropy(P, Q): Q = np.clip(Q, 1e-10, 1.0) return -np.sum(P * np.log(Q)) def kl_divergence(P, Q): mask = P > 0 Q = np.clip(Q, 1e-10, 1.0) return np.sum(P[mask] * np.log(P[mask] / Q[mask])) h_P = entropy(P_true[P_true > 0]) ce_good = cross_entropy(P_true, Q_good) ce_bad = cross_entropy(P_true, Q_bad) kl_good = kl_divergence(P_true, Q_good) kl_bad = kl_divergence(P_true, Q_bad) print(f"真实分布 P: {P_true} (one-hot)") print(f"好模型 Q_good: {Q_good}") print(f"差模型 Q_bad: {Q_bad}\n") print(f"H(P) 真实熵: {h_P:.4f} (one-hot熵=0)") print(f"H(P,Q_good) 交叉熵: {ce_good:.4f} = H(P)+KL = {h_P:.4f}+{kl_good:.4f}") print(f"H(P,Q_bad) 交叉熵: {ce_bad:.4f} = H(P)+KL = {h_P:.4f}+{kl_bad:.4f}") print(f"\n→ 交叉熵 = 真实熵 + KL散度,最小化交叉熵即最小化KL散度") # ===== 3. 温度对熵的影响 ===== print("\n===== 温度对熵的影响 =====\n") logits = torch.tensor([3.0, 1.5, 1.0, 0.5, 0.2, -0.5]) vocab = ["猫", "狗", "鱼", "鸟", "虫", "草"] print(f"{'温度τ':>6} | {'熵(nats)':>10} | {'PPL':>8} | {'最高概率词':>8} | {'最高概率':>8}") print("-" * 55) for tau in [0.1, 0.3, 0.5, 1.0, 1.5, 2.0, 5.0]: p = F.softmax(logits / tau, dim=0) h = -(p * torch.log(p + 1e-10)).sum().item() ppl = np.exp(h) top_k = p.argmax().item() print(f"τ={tau:>4.1f} | {h:>10.4f} | {ppl:>8.3f} | {vocab[top_k]:>8} | {p[top_k].item():>8.4f}") # ===== 4. 语言模型训练过程中的PPL变化 ===== print("\n===== 语言模型训练中的PPL变化(模拟) =====\n") # 模拟不同训练阶段的模型对同一个token的预测概率 vocab_size = 128000 true_token_idx = 42 # 真实token scenarios = [ ("随机初始化", 1.0 / vocab_size), # 均匀猜测 ("训练1k步", 0.005), # 开始集中 ("训练10k步", 0.05), ("训练100k步", 0.20), ("训练1M步", 0.50), ("训练10M步", 0.80), ] print(f"{'训练阶段':14} | {'真实token概率':>14} | {'NLL损失':>8} | {'等效PPL':>10}") print("-" * 55) for stage, true_prob in scenarios: nll = -np.log(true_prob) # 等效PPL = 如果所有token的条件概率都等于true_prob时的PPL ppl = np.exp(nll) print(f"{stage:14} | {true_prob:>14.6f} | {nll:>8.4f} | {ppl:>10.2f}") # ===== 5. 数值稳定的交叉熵:LogSumExp 技巧 ===== print("\n===== LogSumExp 技巧验证 =====\n") # 极大的logits(会导致数值溢出) logits_large = torch.tensor([100.0, 101.0, 99.0]) label = torch.tensor([1]) # 真实类别是1 # 方法一:先算Softmax(可能溢出) try: probs_naive = torch.exp(logits_large) / torch.exp(logits_large).sum() loss_naive = -torch.log(probs_naive[1]) print(f"朴素方法: probs={probs_naive}, loss={loss_naive.item():.4f}") except Exception as e: print(f"朴素方法失败: {e}") # 方法二:LogSumExp(数值稳定) c = logits_large.max() log_sum_exp = c + torch.log(torch.exp(logits_large - c).sum()) log_prob_stable = logits_large[1] - log_sum_exp loss_stable = -log_prob_stable print(f"LogSumExp方法: log_sum_exp={log_sum_exp.item():.4f}, loss={loss_stable.item():.4f}") # 方法三:PyTorch内置(自动使用数值稳定计算) loss_pytorch = F.cross_entropy(logits_large.unsqueeze(0), label) print(f"PyTorch内置: loss={loss_pytorch.item():.4f}") print(f"两种数值稳定方法结果相同: {torch.isclose(loss_stable, loss_pytorch)}") # ===== 6. 互信息演示 ===== print("\n===== 互信息:变量间的信息共享 =====\n") # 构造两个相关的离散变量 # X = 天气(晴/阴/雨),Y = 是否带伞 # 联合分布 joint_P = np.array([ [0.30, 0.02], # 晴天:不带伞0.30,带伞0.02 [0.10, 0.10], # 阴天:不带伞0.10,带伞0.10 [0.03, 0.45], # 雨天:不带伞0.03,带伞0.45 ]) # 行:天气(晴/阴/雨),列:带伞(否/是) P_X = joint_P.sum(axis=1) # 天气边缘分布 P_Y = joint_P.sum(axis=0) # 带伞边缘分布 H_X = entropy(P_X) H_Y = entropy(P_Y) H_XY = entropy(joint_P.flatten()) # 条件熵 H(Y|X) = H(X,Y) - H(X) H_Y_given_X = H_XY - H_X # 互信息 I(X;Y) = H(Y) - H(Y|X) MI = H_Y - H_Y_given_X print(f"天气分布 P_X: {dict(zip(['晴','阴','雨'], P_X.round(2)))}") print(f"带伞分布 P_Y: {dict(zip(['否','是'], P_Y.round(2)))}") print(f"\nH(天气): {H_X:.4f} nats") print(f"H(带伞): {H_Y:.4f} nats") print(f"H(天气,带伞): {H_XY:.4f} nats") print(f"H(带伞|天气): {H_Y_given_X:.4f} nats (知道天气后,带伞不确定性降低)") print(f"I(天气;带伞): {MI:.4f} nats (互信息:天气和带伞共享的信息量)") print(f"\n→ 知道天气后,带伞的不确定性从{H_Y:.3f}降到{H_Y_given_X:.3f}") print(f"→ 互信息{MI:.3f}就是这个不确定性的减少量") 总结 这篇文章从 Shannon 的信息论出发,把信息熵、交叉熵和语言模型训练损失的关系完整打通了。 第一,自信息量 $I(x) = -\log P(x)$ 衡量单个事件的信息量:概率越小,发生时信息量越大。独立事件信息量可加,这是选择对数函数的根本原因。 第二,信息熵 $H(P) = -\mathbb{E}[\log P(x)]$ 是信息量的期望,衡量分布的平均不确定性。均匀分布熵最大(最不确定),确定性分布熵为 0。语言模型训练从高熵(随机猜测 PPL≈128000)向低熵(精确预测 PPL≈几)前进。 第三,交叉熵 $H(P,Q) = -\mathbb{E}_{x \sim P}[\log Q(x)]$ 是"用模型分布 $Q$ 替代真实分布 $P$ 编码数据的平均代价",总是 $\geq H(P)$。最小化交叉熵等价于最小化 KL 散度,让模型分布尽量接近真实数据分布。 第四,语言模型的训练损失就是平均交叉熵(等价于平均 NLL),困惑度 PPL 是其指数,直觉上表示"模型平均在多少个候选里犹豫"。条件熵视角下,训练目标是最小化"给定前文条件下下一个 token 的不确定性"。 第五,温度参数 $\tau$ 控制输出分布的熵:低温降低熵(更确定、更保守),高温提高熵(更多样、更随机)。Top-p 采样动态调整候选集大小,自适应模型的局部确定性。 第六,数值稳定性是工程实践的关键:LogSumExp 技巧通过减去最大值防止指数溢出,PyTorch 的 F.cross_entropy 直接接受 logits 并内部使用这个技巧。 下一篇预告: 第16篇,我们讲 KL 散度:DeepSeek R1 强化学习中的约束项从何而来? 交叉熵告诉我们"用错分布的代价",但这个代价是不对称的——$D_{KL}(P \| Q) \neq D_{KL}(Q \| P)$。这个不对称性有深刻的含义,直接解释了为什么 GRPO 的 KL 约束要写成 $D_{KL}(\pi_\theta \| \pi_{ref})$ 而不是反过来,以及为什么 VAE 的训练目标选 $D_{KL}(q \| p)$。 信息论告诉我们:不确定性是可以量化的,信息是可以度量的。Shannon 熵不仅仅是数学工具,它是一种思维方式——把"模型对数据有多惊讶"变成了一个可以优化的数字。每次梯度下降,DeepSeek V3 都在用 14.8 万亿个 token 告诉自己:对这些数据,你还可以更少地感到惊讶。
整个序列的平均交叉熵:
$$L = \frac{1}{T}\sum_{t=1}^T (-\log P_\theta(x_t \mid x_{ 这正是 DeepSeek V3 的预训练损失。每一步梯度更新,都在减少模型对训练数据的"平均惊讶程度"。 困惑度再理解 $$\text{PPL} = e^{L} = \exp\left(\frac{1}{T}\sum_t -\log P_\theta(x_t \mid x_{ PPL 是所有条件概率之积的几何平均的倒数——平均每步模型的"有效候选数量"。 信息论解释:PPL 等于模型平均在"多少个等可能选项里猜",即模型用这个文本的"等效均匀词表大小"。PPL=10 意味着模型平均只需从 10 个同等可能的词里选,远好于随机的 128000。 训练过程中的熵变化 训练阶段 模型状态 典型PPL 熵(nats) 随机初始化 均匀猜测 ~128000 ~11.76 预训练初期 开始学词频 ~1000 ~6.9 预训练中期 掌握语法结构 ~50 ~3.9 预训练后期 理解语义 ~10 ~2.3 SOTA模型 深度理解 ~3-8 ~1.1-2.1 第五部分:二元交叉熵——二分类的特殊情形 推导 二分类:标签 $y \in \{0, 1\}$,模型预测正例概率 $\hat{p} = \sigma(z) \in (0,1)$。 真实分布 $P$:$P(1) = y$,$P(0) = 1-y$。 模型分布 $Q$:$Q(1) = \hat{p}$,$Q(0) = 1-\hat{p}$。 交叉熵: $$H(P, Q) = -P(1)\log Q(1) - P(0)\log Q(0)$$ $$= -y \log \hat{p} - (1-y)\log(1-\hat{p})$$ 这就是二元交叉熵损失(Binary Cross-Entropy),也叫 BCE Loss。 两种极端情形 $y=1$,$\hat{p} \to 1$(正确且自信):损失 $= -\log 1 = 0$ ✓ $y=1$,$\hat{p} \to 0$(预测错误且自信):损失 $= -\log 0 = +\infty$ ← 对自信错误惩罚极大 $y=0$,$\hat{p} \to 0$(正确且自信):损失 $= -\log 1 = 0$ ✓ $y=0$,$\hat{p} \to 1$(预测错误且自信):损失 $= -\log 0 = +\infty$ ← 同样的极大惩罚 交叉熵损失有一个良好的性质:对"自信地犯错"的惩罚是无穷大,迫使模型在不确定时保持谦逊,不要给出极端的概率预测。 第六部分:熵的其他形态 条件熵 条件熵(Conditional Entropy) $H(Y \mid X)$:已知 $X$ 的条件下,$Y$ 的平均不确定性: $$H(Y \mid X) = -\mathbb{E}_{x,y}[\log P(y \mid x)] = \sum_x P(x) H(Y \mid X=x)$$ 链式法则: $$H(X, Y) = H(X) + H(Y \mid X)$$ 联合不确定性 = $X$ 的不确定性 + 知道 $X$ 后 $Y$ 的不确定性。 语言模型的训练损失就是条件熵的估计: $$L = H(X_t \mid X_{ 目标是最小化"给定前文条件下,下一个词的不确定性"。 联合熵与互信息 互信息(Mutual Information) $I(X; Y)$:$X$ 和 $Y$ 之间共享的信息量: $$I(X; Y) = H(X) - H(X \mid Y) = H(Y) - H(Y \mid X)$$ $$= H(X) + H(Y) - H(X,Y)$$ 互信息衡量两个变量的相关程度: $I(X;Y) = 0$:完全独立,知道 $Y$ 不帮助预测 $X$ $I(X;Y) = H(X)$:$X$ 完全由 $Y$ 决定,知道 $Y$ 就知道 $X$ 在深度学习中的应用: 互信息最大化(Mutual Information Maximization)是无监督表示学习的一种范式——让编码器的输出(表示)和输入数据之间的互信息最大化。直觉是:好的表示应该保留输入的最多信息。 对比学习(Contrastive Learning)可以理解为最大化正样本对之间的互信息,最小化负样本对之间的互信息。CLIP、SimCLR 等模型的训练目标,本质上都是互信息最大化。 第七部分:温度、熵与采样策略 温度控制熵 语言模型的输出分布,用温度 $\tau$ 调节: $$p_k(\tau) = \frac{e^{z_k/\tau}}{\sum_j e^{z_j/\tau}}$$ 温度对熵的影响: $$H(P_\tau) = -\sum_k p_k(\tau) \log p_k(\tau)$$ $\tau \to 0$:分布退化为 one-hot(只有最高 logit 概率为 1),熵 = 0 $\tau = 1$:标准 Softmax,熵为正常值 $\tau \to \infty$:分布趋向均匀,熵趋向最大值 $\log K$ 温度采样的信息论含义:高温度相当于向输出分布"注入不确定性"(增加熵),增加生成的多样性;低温度相当于压缩不确定性(减少熵),增加生成的一致性。 Top-p 采样(Nucleus Sampling) Top-p 采样不按温度调节,而是动态截断: 每次只保留累积概率超过 $p$(比如 0.9)的最小词集合 $\mathcal{V}_p$,在这个集合里重新归一化采样: $$\mathcal{V}_p = \text{最小集合使得} \sum_{k \in \mathcal{V}_p} p_k \geq p$$ 信息论角度:Top-p 相当于保留分布的"前 $p$ 概率质量",丢弃尾部的低概率词。 好处:分布尖峰时(模型确定),$\mathcal{V}_p$ 很小(只考虑少数高概率词,行文准确);分布平坦时(模型不确定),$\mathcal{V}_p$ 较大(允许更多选择,增加多样性)。自动适应模型的局部确定性。 DeepSeek API 默认 top_p = 0.9,是工程实践中的平衡点。 第八部分:交叉熵的数值问题与 LogSumExp 技巧 数值下溢问题 直接计算 Softmax 再取对数: probs = exp(logits) / sum(exp(logits)) # 可能数值溢出 log_probs = log(probs) 当 logits 很大(比如 100),exp(100) 会溢出(超过 float 最大值)。 LogSumExp 技巧 $$\log \sum_j e^{z_j} = c + \log \sum_j e^{z_j - c}, \quad c = \max_j z_j$$ 减去最大值再指数,最大项变成 $e^0 = 1$,其余项 $\leq 1$,不会溢出。 交叉熵的数值稳定计算: $$-\log p_{y^*} = -z_{y^*} + \log \sum_j e^{z_j} = -z_{y^*} + c + \log \sum_j e^{z_j - c}$$ PyTorch 的 F.cross_entropy 内部就用了这个技巧,直接接受 logits,不需要先算 Softmax——既数值稳定,又计算高效。 第九部分:完整代码 import numpy as np import torch import torch.nn.functional as F np.random.seed(42) torch.manual_seed(42) # ===== 1. 自信息量与信息熵 ===== print("===== 自信息量与信息熵 =====\n") def entropy(probs): probs = np.array(probs) probs = probs[probs > 0] # 排除零概率 return -np.sum(probs * np.log(probs)) # 几种分布的熵 distributions = { "确定性(p=1)": [1.0], "公平硬币(p=0.5)": [0.5, 0.5], "偏心硬币(p=0.9)": [0.9, 0.1], "公平骰子(K=6)": [1/6]*6, "均匀128K词表": [1/128000]*128000, } print(f"{'分布':25s} | {'熵(nats)':>10} | {'PPL':>10} | {'解释'}") print("-" * 70) for name, probs in distributions.items(): h = entropy(probs) ppl = np.exp(h) print(f"{name:25s} | {h:>10.4f} | {ppl:>10.2f}") # 验证最大熵定理:K个类别的均匀分布熵 = log(K) for K in [2, 6, 10, 128000]: h_uniform = np.log(K) h_computed = entropy([1/K]*K) print(f"K={K:7d}: 理论 log(K)={h_uniform:.4f}, 计算值={h_computed:.4f}") # ===== 2. 交叉熵:用错分布的代价 ===== print("\n===== 交叉熵:用错分布的代价 =====\n") # 真实分布 P(标签),模型分布 Q(预测) P_true = np.array([0.0, 1.0, 0.0]) # one-hot,真实类别是1 Q_good = np.array([0.05, 0.9, 0.05]) # 好模型 Q_bad = np.array([0.4, 0.2, 0.4]) # 差模型 def cross_entropy(P, Q): Q = np.clip(Q, 1e-10, 1.0) return -np.sum(P * np.log(Q)) def kl_divergence(P, Q): mask = P > 0 Q = np.clip(Q, 1e-10, 1.0) return np.sum(P[mask] * np.log(P[mask] / Q[mask])) h_P = entropy(P_true[P_true > 0]) ce_good = cross_entropy(P_true, Q_good) ce_bad = cross_entropy(P_true, Q_bad) kl_good = kl_divergence(P_true, Q_good) kl_bad = kl_divergence(P_true, Q_bad) print(f"真实分布 P: {P_true} (one-hot)") print(f"好模型 Q_good: {Q_good}") print(f"差模型 Q_bad: {Q_bad}\n") print(f"H(P) 真实熵: {h_P:.4f} (one-hot熵=0)") print(f"H(P,Q_good) 交叉熵: {ce_good:.4f} = H(P)+KL = {h_P:.4f}+{kl_good:.4f}") print(f"H(P,Q_bad) 交叉熵: {ce_bad:.4f} = H(P)+KL = {h_P:.4f}+{kl_bad:.4f}") print(f"\n→ 交叉熵 = 真实熵 + KL散度,最小化交叉熵即最小化KL散度") # ===== 3. 温度对熵的影响 ===== print("\n===== 温度对熵的影响 =====\n") logits = torch.tensor([3.0, 1.5, 1.0, 0.5, 0.2, -0.5]) vocab = ["猫", "狗", "鱼", "鸟", "虫", "草"] print(f"{'温度τ':>6} | {'熵(nats)':>10} | {'PPL':>8} | {'最高概率词':>8} | {'最高概率':>8}") print("-" * 55) for tau in [0.1, 0.3, 0.5, 1.0, 1.5, 2.0, 5.0]: p = F.softmax(logits / tau, dim=0) h = -(p * torch.log(p + 1e-10)).sum().item() ppl = np.exp(h) top_k = p.argmax().item() print(f"τ={tau:>4.1f} | {h:>10.4f} | {ppl:>8.3f} | {vocab[top_k]:>8} | {p[top_k].item():>8.4f}") # ===== 4. 语言模型训练过程中的PPL变化 ===== print("\n===== 语言模型训练中的PPL变化(模拟) =====\n") # 模拟不同训练阶段的模型对同一个token的预测概率 vocab_size = 128000 true_token_idx = 42 # 真实token scenarios = [ ("随机初始化", 1.0 / vocab_size), # 均匀猜测 ("训练1k步", 0.005), # 开始集中 ("训练10k步", 0.05), ("训练100k步", 0.20), ("训练1M步", 0.50), ("训练10M步", 0.80), ] print(f"{'训练阶段':14} | {'真实token概率':>14} | {'NLL损失':>8} | {'等效PPL':>10}") print("-" * 55) for stage, true_prob in scenarios: nll = -np.log(true_prob) # 等效PPL = 如果所有token的条件概率都等于true_prob时的PPL ppl = np.exp(nll) print(f"{stage:14} | {true_prob:>14.6f} | {nll:>8.4f} | {ppl:>10.2f}") # ===== 5. 数值稳定的交叉熵:LogSumExp 技巧 ===== print("\n===== LogSumExp 技巧验证 =====\n") # 极大的logits(会导致数值溢出) logits_large = torch.tensor([100.0, 101.0, 99.0]) label = torch.tensor([1]) # 真实类别是1 # 方法一:先算Softmax(可能溢出) try: probs_naive = torch.exp(logits_large) / torch.exp(logits_large).sum() loss_naive = -torch.log(probs_naive[1]) print(f"朴素方法: probs={probs_naive}, loss={loss_naive.item():.4f}") except Exception as e: print(f"朴素方法失败: {e}") # 方法二:LogSumExp(数值稳定) c = logits_large.max() log_sum_exp = c + torch.log(torch.exp(logits_large - c).sum()) log_prob_stable = logits_large[1] - log_sum_exp loss_stable = -log_prob_stable print(f"LogSumExp方法: log_sum_exp={log_sum_exp.item():.4f}, loss={loss_stable.item():.4f}") # 方法三:PyTorch内置(自动使用数值稳定计算) loss_pytorch = F.cross_entropy(logits_large.unsqueeze(0), label) print(f"PyTorch内置: loss={loss_pytorch.item():.4f}") print(f"两种数值稳定方法结果相同: {torch.isclose(loss_stable, loss_pytorch)}") # ===== 6. 互信息演示 ===== print("\n===== 互信息:变量间的信息共享 =====\n") # 构造两个相关的离散变量 # X = 天气(晴/阴/雨),Y = 是否带伞 # 联合分布 joint_P = np.array([ [0.30, 0.02], # 晴天:不带伞0.30,带伞0.02 [0.10, 0.10], # 阴天:不带伞0.10,带伞0.10 [0.03, 0.45], # 雨天:不带伞0.03,带伞0.45 ]) # 行:天气(晴/阴/雨),列:带伞(否/是) P_X = joint_P.sum(axis=1) # 天气边缘分布 P_Y = joint_P.sum(axis=0) # 带伞边缘分布 H_X = entropy(P_X) H_Y = entropy(P_Y) H_XY = entropy(joint_P.flatten()) # 条件熵 H(Y|X) = H(X,Y) - H(X) H_Y_given_X = H_XY - H_X # 互信息 I(X;Y) = H(Y) - H(Y|X) MI = H_Y - H_Y_given_X print(f"天气分布 P_X: {dict(zip(['晴','阴','雨'], P_X.round(2)))}") print(f"带伞分布 P_Y: {dict(zip(['否','是'], P_Y.round(2)))}") print(f"\nH(天气): {H_X:.4f} nats") print(f"H(带伞): {H_Y:.4f} nats") print(f"H(天气,带伞): {H_XY:.4f} nats") print(f"H(带伞|天气): {H_Y_given_X:.4f} nats (知道天气后,带伞不确定性降低)") print(f"I(天气;带伞): {MI:.4f} nats (互信息:天气和带伞共享的信息量)") print(f"\n→ 知道天气后,带伞的不确定性从{H_Y:.3f}降到{H_Y_given_X:.3f}") print(f"→ 互信息{MI:.3f}就是这个不确定性的减少量") 总结 这篇文章从 Shannon 的信息论出发,把信息熵、交叉熵和语言模型训练损失的关系完整打通了。 第一,自信息量 $I(x) = -\log P(x)$ 衡量单个事件的信息量:概率越小,发生时信息量越大。独立事件信息量可加,这是选择对数函数的根本原因。 第二,信息熵 $H(P) = -\mathbb{E}[\log P(x)]$ 是信息量的期望,衡量分布的平均不确定性。均匀分布熵最大(最不确定),确定性分布熵为 0。语言模型训练从高熵(随机猜测 PPL≈128000)向低熵(精确预测 PPL≈几)前进。 第三,交叉熵 $H(P,Q) = -\mathbb{E}_{x \sim P}[\log Q(x)]$ 是"用模型分布 $Q$ 替代真实分布 $P$ 编码数据的平均代价",总是 $\geq H(P)$。最小化交叉熵等价于最小化 KL 散度,让模型分布尽量接近真实数据分布。 第四,语言模型的训练损失就是平均交叉熵(等价于平均 NLL),困惑度 PPL 是其指数,直觉上表示"模型平均在多少个候选里犹豫"。条件熵视角下,训练目标是最小化"给定前文条件下下一个 token 的不确定性"。 第五,温度参数 $\tau$ 控制输出分布的熵:低温降低熵(更确定、更保守),高温提高熵(更多样、更随机)。Top-p 采样动态调整候选集大小,自适应模型的局部确定性。 第六,数值稳定性是工程实践的关键:LogSumExp 技巧通过减去最大值防止指数溢出,PyTorch 的 F.cross_entropy 直接接受 logits 并内部使用这个技巧。 下一篇预告: 第16篇,我们讲 KL 散度:DeepSeek R1 强化学习中的约束项从何而来? 交叉熵告诉我们"用错分布的代价",但这个代价是不对称的——$D_{KL}(P \| Q) \neq D_{KL}(Q \| P)$。这个不对称性有深刻的含义,直接解释了为什么 GRPO 的 KL 约束要写成 $D_{KL}(\pi_\theta \| \pi_{ref})$ 而不是反过来,以及为什么 VAE 的训练目标选 $D_{KL}(q \| p)$。 信息论告诉我们:不确定性是可以量化的,信息是可以度量的。Shannon 熵不仅仅是数学工具,它是一种思维方式——把"模型对数据有多惊讶"变成了一个可以优化的数字。每次梯度下降,DeepSeek V3 都在用 14.8 万亿个 token 告诉自己:对这些数据,你还可以更少地感到惊讶。
这正是 DeepSeek V3 的预训练损失。每一步梯度更新,都在减少模型对训练数据的"平均惊讶程度"。
$$\text{PPL} = e^{L} = \exp\left(\frac{1}{T}\sum_t -\log P_\theta(x_t \mid x_{ PPL 是所有条件概率之积的几何平均的倒数——平均每步模型的"有效候选数量"。 信息论解释:PPL 等于模型平均在"多少个等可能选项里猜",即模型用这个文本的"等效均匀词表大小"。PPL=10 意味着模型平均只需从 10 个同等可能的词里选,远好于随机的 128000。 训练过程中的熵变化 训练阶段 模型状态 典型PPL 熵(nats) 随机初始化 均匀猜测 ~128000 ~11.76 预训练初期 开始学词频 ~1000 ~6.9 预训练中期 掌握语法结构 ~50 ~3.9 预训练后期 理解语义 ~10 ~2.3 SOTA模型 深度理解 ~3-8 ~1.1-2.1 第五部分:二元交叉熵——二分类的特殊情形 推导 二分类:标签 $y \in \{0, 1\}$,模型预测正例概率 $\hat{p} = \sigma(z) \in (0,1)$。 真实分布 $P$:$P(1) = y$,$P(0) = 1-y$。 模型分布 $Q$:$Q(1) = \hat{p}$,$Q(0) = 1-\hat{p}$。 交叉熵: $$H(P, Q) = -P(1)\log Q(1) - P(0)\log Q(0)$$ $$= -y \log \hat{p} - (1-y)\log(1-\hat{p})$$ 这就是二元交叉熵损失(Binary Cross-Entropy),也叫 BCE Loss。 两种极端情形 $y=1$,$\hat{p} \to 1$(正确且自信):损失 $= -\log 1 = 0$ ✓ $y=1$,$\hat{p} \to 0$(预测错误且自信):损失 $= -\log 0 = +\infty$ ← 对自信错误惩罚极大 $y=0$,$\hat{p} \to 0$(正确且自信):损失 $= -\log 1 = 0$ ✓ $y=0$,$\hat{p} \to 1$(预测错误且自信):损失 $= -\log 0 = +\infty$ ← 同样的极大惩罚 交叉熵损失有一个良好的性质:对"自信地犯错"的惩罚是无穷大,迫使模型在不确定时保持谦逊,不要给出极端的概率预测。 第六部分:熵的其他形态 条件熵 条件熵(Conditional Entropy) $H(Y \mid X)$:已知 $X$ 的条件下,$Y$ 的平均不确定性: $$H(Y \mid X) = -\mathbb{E}_{x,y}[\log P(y \mid x)] = \sum_x P(x) H(Y \mid X=x)$$ 链式法则: $$H(X, Y) = H(X) + H(Y \mid X)$$ 联合不确定性 = $X$ 的不确定性 + 知道 $X$ 后 $Y$ 的不确定性。 语言模型的训练损失就是条件熵的估计: $$L = H(X_t \mid X_{ 目标是最小化"给定前文条件下,下一个词的不确定性"。 联合熵与互信息 互信息(Mutual Information) $I(X; Y)$:$X$ 和 $Y$ 之间共享的信息量: $$I(X; Y) = H(X) - H(X \mid Y) = H(Y) - H(Y \mid X)$$ $$= H(X) + H(Y) - H(X,Y)$$ 互信息衡量两个变量的相关程度: $I(X;Y) = 0$:完全独立,知道 $Y$ 不帮助预测 $X$ $I(X;Y) = H(X)$:$X$ 完全由 $Y$ 决定,知道 $Y$ 就知道 $X$ 在深度学习中的应用: 互信息最大化(Mutual Information Maximization)是无监督表示学习的一种范式——让编码器的输出(表示)和输入数据之间的互信息最大化。直觉是:好的表示应该保留输入的最多信息。 对比学习(Contrastive Learning)可以理解为最大化正样本对之间的互信息,最小化负样本对之间的互信息。CLIP、SimCLR 等模型的训练目标,本质上都是互信息最大化。 第七部分:温度、熵与采样策略 温度控制熵 语言模型的输出分布,用温度 $\tau$ 调节: $$p_k(\tau) = \frac{e^{z_k/\tau}}{\sum_j e^{z_j/\tau}}$$ 温度对熵的影响: $$H(P_\tau) = -\sum_k p_k(\tau) \log p_k(\tau)$$ $\tau \to 0$:分布退化为 one-hot(只有最高 logit 概率为 1),熵 = 0 $\tau = 1$:标准 Softmax,熵为正常值 $\tau \to \infty$:分布趋向均匀,熵趋向最大值 $\log K$ 温度采样的信息论含义:高温度相当于向输出分布"注入不确定性"(增加熵),增加生成的多样性;低温度相当于压缩不确定性(减少熵),增加生成的一致性。 Top-p 采样(Nucleus Sampling) Top-p 采样不按温度调节,而是动态截断: 每次只保留累积概率超过 $p$(比如 0.9)的最小词集合 $\mathcal{V}_p$,在这个集合里重新归一化采样: $$\mathcal{V}_p = \text{最小集合使得} \sum_{k \in \mathcal{V}_p} p_k \geq p$$ 信息论角度:Top-p 相当于保留分布的"前 $p$ 概率质量",丢弃尾部的低概率词。 好处:分布尖峰时(模型确定),$\mathcal{V}_p$ 很小(只考虑少数高概率词,行文准确);分布平坦时(模型不确定),$\mathcal{V}_p$ 较大(允许更多选择,增加多样性)。自动适应模型的局部确定性。 DeepSeek API 默认 top_p = 0.9,是工程实践中的平衡点。 第八部分:交叉熵的数值问题与 LogSumExp 技巧 数值下溢问题 直接计算 Softmax 再取对数: probs = exp(logits) / sum(exp(logits)) # 可能数值溢出 log_probs = log(probs) 当 logits 很大(比如 100),exp(100) 会溢出(超过 float 最大值)。 LogSumExp 技巧 $$\log \sum_j e^{z_j} = c + \log \sum_j e^{z_j - c}, \quad c = \max_j z_j$$ 减去最大值再指数,最大项变成 $e^0 = 1$,其余项 $\leq 1$,不会溢出。 交叉熵的数值稳定计算: $$-\log p_{y^*} = -z_{y^*} + \log \sum_j e^{z_j} = -z_{y^*} + c + \log \sum_j e^{z_j - c}$$ PyTorch 的 F.cross_entropy 内部就用了这个技巧,直接接受 logits,不需要先算 Softmax——既数值稳定,又计算高效。 第九部分:完整代码 import numpy as np import torch import torch.nn.functional as F np.random.seed(42) torch.manual_seed(42) # ===== 1. 自信息量与信息熵 ===== print("===== 自信息量与信息熵 =====\n") def entropy(probs): probs = np.array(probs) probs = probs[probs > 0] # 排除零概率 return -np.sum(probs * np.log(probs)) # 几种分布的熵 distributions = { "确定性(p=1)": [1.0], "公平硬币(p=0.5)": [0.5, 0.5], "偏心硬币(p=0.9)": [0.9, 0.1], "公平骰子(K=6)": [1/6]*6, "均匀128K词表": [1/128000]*128000, } print(f"{'分布':25s} | {'熵(nats)':>10} | {'PPL':>10} | {'解释'}") print("-" * 70) for name, probs in distributions.items(): h = entropy(probs) ppl = np.exp(h) print(f"{name:25s} | {h:>10.4f} | {ppl:>10.2f}") # 验证最大熵定理:K个类别的均匀分布熵 = log(K) for K in [2, 6, 10, 128000]: h_uniform = np.log(K) h_computed = entropy([1/K]*K) print(f"K={K:7d}: 理论 log(K)={h_uniform:.4f}, 计算值={h_computed:.4f}") # ===== 2. 交叉熵:用错分布的代价 ===== print("\n===== 交叉熵:用错分布的代价 =====\n") # 真实分布 P(标签),模型分布 Q(预测) P_true = np.array([0.0, 1.0, 0.0]) # one-hot,真实类别是1 Q_good = np.array([0.05, 0.9, 0.05]) # 好模型 Q_bad = np.array([0.4, 0.2, 0.4]) # 差模型 def cross_entropy(P, Q): Q = np.clip(Q, 1e-10, 1.0) return -np.sum(P * np.log(Q)) def kl_divergence(P, Q): mask = P > 0 Q = np.clip(Q, 1e-10, 1.0) return np.sum(P[mask] * np.log(P[mask] / Q[mask])) h_P = entropy(P_true[P_true > 0]) ce_good = cross_entropy(P_true, Q_good) ce_bad = cross_entropy(P_true, Q_bad) kl_good = kl_divergence(P_true, Q_good) kl_bad = kl_divergence(P_true, Q_bad) print(f"真实分布 P: {P_true} (one-hot)") print(f"好模型 Q_good: {Q_good}") print(f"差模型 Q_bad: {Q_bad}\n") print(f"H(P) 真实熵: {h_P:.4f} (one-hot熵=0)") print(f"H(P,Q_good) 交叉熵: {ce_good:.4f} = H(P)+KL = {h_P:.4f}+{kl_good:.4f}") print(f"H(P,Q_bad) 交叉熵: {ce_bad:.4f} = H(P)+KL = {h_P:.4f}+{kl_bad:.4f}") print(f"\n→ 交叉熵 = 真实熵 + KL散度,最小化交叉熵即最小化KL散度") # ===== 3. 温度对熵的影响 ===== print("\n===== 温度对熵的影响 =====\n") logits = torch.tensor([3.0, 1.5, 1.0, 0.5, 0.2, -0.5]) vocab = ["猫", "狗", "鱼", "鸟", "虫", "草"] print(f"{'温度τ':>6} | {'熵(nats)':>10} | {'PPL':>8} | {'最高概率词':>8} | {'最高概率':>8}") print("-" * 55) for tau in [0.1, 0.3, 0.5, 1.0, 1.5, 2.0, 5.0]: p = F.softmax(logits / tau, dim=0) h = -(p * torch.log(p + 1e-10)).sum().item() ppl = np.exp(h) top_k = p.argmax().item() print(f"τ={tau:>4.1f} | {h:>10.4f} | {ppl:>8.3f} | {vocab[top_k]:>8} | {p[top_k].item():>8.4f}") # ===== 4. 语言模型训练过程中的PPL变化 ===== print("\n===== 语言模型训练中的PPL变化(模拟) =====\n") # 模拟不同训练阶段的模型对同一个token的预测概率 vocab_size = 128000 true_token_idx = 42 # 真实token scenarios = [ ("随机初始化", 1.0 / vocab_size), # 均匀猜测 ("训练1k步", 0.005), # 开始集中 ("训练10k步", 0.05), ("训练100k步", 0.20), ("训练1M步", 0.50), ("训练10M步", 0.80), ] print(f"{'训练阶段':14} | {'真实token概率':>14} | {'NLL损失':>8} | {'等效PPL':>10}") print("-" * 55) for stage, true_prob in scenarios: nll = -np.log(true_prob) # 等效PPL = 如果所有token的条件概率都等于true_prob时的PPL ppl = np.exp(nll) print(f"{stage:14} | {true_prob:>14.6f} | {nll:>8.4f} | {ppl:>10.2f}") # ===== 5. 数值稳定的交叉熵:LogSumExp 技巧 ===== print("\n===== LogSumExp 技巧验证 =====\n") # 极大的logits(会导致数值溢出) logits_large = torch.tensor([100.0, 101.0, 99.0]) label = torch.tensor([1]) # 真实类别是1 # 方法一:先算Softmax(可能溢出) try: probs_naive = torch.exp(logits_large) / torch.exp(logits_large).sum() loss_naive = -torch.log(probs_naive[1]) print(f"朴素方法: probs={probs_naive}, loss={loss_naive.item():.4f}") except Exception as e: print(f"朴素方法失败: {e}") # 方法二:LogSumExp(数值稳定) c = logits_large.max() log_sum_exp = c + torch.log(torch.exp(logits_large - c).sum()) log_prob_stable = logits_large[1] - log_sum_exp loss_stable = -log_prob_stable print(f"LogSumExp方法: log_sum_exp={log_sum_exp.item():.4f}, loss={loss_stable.item():.4f}") # 方法三:PyTorch内置(自动使用数值稳定计算) loss_pytorch = F.cross_entropy(logits_large.unsqueeze(0), label) print(f"PyTorch内置: loss={loss_pytorch.item():.4f}") print(f"两种数值稳定方法结果相同: {torch.isclose(loss_stable, loss_pytorch)}") # ===== 6. 互信息演示 ===== print("\n===== 互信息:变量间的信息共享 =====\n") # 构造两个相关的离散变量 # X = 天气(晴/阴/雨),Y = 是否带伞 # 联合分布 joint_P = np.array([ [0.30, 0.02], # 晴天:不带伞0.30,带伞0.02 [0.10, 0.10], # 阴天:不带伞0.10,带伞0.10 [0.03, 0.45], # 雨天:不带伞0.03,带伞0.45 ]) # 行:天气(晴/阴/雨),列:带伞(否/是) P_X = joint_P.sum(axis=1) # 天气边缘分布 P_Y = joint_P.sum(axis=0) # 带伞边缘分布 H_X = entropy(P_X) H_Y = entropy(P_Y) H_XY = entropy(joint_P.flatten()) # 条件熵 H(Y|X) = H(X,Y) - H(X) H_Y_given_X = H_XY - H_X # 互信息 I(X;Y) = H(Y) - H(Y|X) MI = H_Y - H_Y_given_X print(f"天气分布 P_X: {dict(zip(['晴','阴','雨'], P_X.round(2)))}") print(f"带伞分布 P_Y: {dict(zip(['否','是'], P_Y.round(2)))}") print(f"\nH(天气): {H_X:.4f} nats") print(f"H(带伞): {H_Y:.4f} nats") print(f"H(天气,带伞): {H_XY:.4f} nats") print(f"H(带伞|天气): {H_Y_given_X:.4f} nats (知道天气后,带伞不确定性降低)") print(f"I(天气;带伞): {MI:.4f} nats (互信息:天气和带伞共享的信息量)") print(f"\n→ 知道天气后,带伞的不确定性从{H_Y:.3f}降到{H_Y_given_X:.3f}") print(f"→ 互信息{MI:.3f}就是这个不确定性的减少量") 总结 这篇文章从 Shannon 的信息论出发,把信息熵、交叉熵和语言模型训练损失的关系完整打通了。 第一,自信息量 $I(x) = -\log P(x)$ 衡量单个事件的信息量:概率越小,发生时信息量越大。独立事件信息量可加,这是选择对数函数的根本原因。 第二,信息熵 $H(P) = -\mathbb{E}[\log P(x)]$ 是信息量的期望,衡量分布的平均不确定性。均匀分布熵最大(最不确定),确定性分布熵为 0。语言模型训练从高熵(随机猜测 PPL≈128000)向低熵(精确预测 PPL≈几)前进。 第三,交叉熵 $H(P,Q) = -\mathbb{E}_{x \sim P}[\log Q(x)]$ 是"用模型分布 $Q$ 替代真实分布 $P$ 编码数据的平均代价",总是 $\geq H(P)$。最小化交叉熵等价于最小化 KL 散度,让模型分布尽量接近真实数据分布。 第四,语言模型的训练损失就是平均交叉熵(等价于平均 NLL),困惑度 PPL 是其指数,直觉上表示"模型平均在多少个候选里犹豫"。条件熵视角下,训练目标是最小化"给定前文条件下下一个 token 的不确定性"。 第五,温度参数 $\tau$ 控制输出分布的熵:低温降低熵(更确定、更保守),高温提高熵(更多样、更随机)。Top-p 采样动态调整候选集大小,自适应模型的局部确定性。 第六,数值稳定性是工程实践的关键:LogSumExp 技巧通过减去最大值防止指数溢出,PyTorch 的 F.cross_entropy 直接接受 logits 并内部使用这个技巧。 下一篇预告: 第16篇,我们讲 KL 散度:DeepSeek R1 强化学习中的约束项从何而来? 交叉熵告诉我们"用错分布的代价",但这个代价是不对称的——$D_{KL}(P \| Q) \neq D_{KL}(Q \| P)$。这个不对称性有深刻的含义,直接解释了为什么 GRPO 的 KL 约束要写成 $D_{KL}(\pi_\theta \| \pi_{ref})$ 而不是反过来,以及为什么 VAE 的训练目标选 $D_{KL}(q \| p)$。 信息论告诉我们:不确定性是可以量化的,信息是可以度量的。Shannon 熵不仅仅是数学工具,它是一种思维方式——把"模型对数据有多惊讶"变成了一个可以优化的数字。每次梯度下降,DeepSeek V3 都在用 14.8 万亿个 token 告诉自己:对这些数据,你还可以更少地感到惊讶。
PPL 是所有条件概率之积的几何平均的倒数——平均每步模型的"有效候选数量"。
信息论解释:PPL 等于模型平均在"多少个等可能选项里猜",即模型用这个文本的"等效均匀词表大小"。PPL=10 意味着模型平均只需从 10 个同等可能的词里选,远好于随机的 128000。
二分类:标签 $y \in \{0, 1\}$,模型预测正例概率 $\hat{p} = \sigma(z) \in (0,1)$。
真实分布 $P$:$P(1) = y$,$P(0) = 1-y$。
模型分布 $Q$:$Q(1) = \hat{p}$,$Q(0) = 1-\hat{p}$。
交叉熵:
$$H(P, Q) = -P(1)\log Q(1) - P(0)\log Q(0)$$ $$= -y \log \hat{p} - (1-y)\log(1-\hat{p})$$
这就是二元交叉熵损失(Binary Cross-Entropy),也叫 BCE Loss。
$y=1$,$\hat{p} \to 1$(正确且自信):损失 $= -\log 1 = 0$ ✓
$y=1$,$\hat{p} \to 0$(预测错误且自信):损失 $= -\log 0 = +\infty$ ← 对自信错误惩罚极大
$y=0$,$\hat{p} \to 0$(正确且自信):损失 $= -\log 1 = 0$ ✓
$y=0$,$\hat{p} \to 1$(预测错误且自信):损失 $= -\log 0 = +\infty$ ← 同样的极大惩罚
交叉熵损失有一个良好的性质:对"自信地犯错"的惩罚是无穷大,迫使模型在不确定时保持谦逊,不要给出极端的概率预测。
条件熵(Conditional Entropy) $H(Y \mid X)$:已知 $X$ 的条件下,$Y$ 的平均不确定性:
$$H(Y \mid X) = -\mathbb{E}_{x,y}[\log P(y \mid x)] = \sum_x P(x) H(Y \mid X=x)$$
链式法则:
$$H(X, Y) = H(X) + H(Y \mid X)$$
联合不确定性 = $X$ 的不确定性 + 知道 $X$ 后 $Y$ 的不确定性。
语言模型的训练损失就是条件熵的估计:
$$L = H(X_t \mid X_{ 目标是最小化"给定前文条件下,下一个词的不确定性"。 联合熵与互信息 互信息(Mutual Information) $I(X; Y)$:$X$ 和 $Y$ 之间共享的信息量: $$I(X; Y) = H(X) - H(X \mid Y) = H(Y) - H(Y \mid X)$$ $$= H(X) + H(Y) - H(X,Y)$$ 互信息衡量两个变量的相关程度: $I(X;Y) = 0$:完全独立,知道 $Y$ 不帮助预测 $X$ $I(X;Y) = H(X)$:$X$ 完全由 $Y$ 决定,知道 $Y$ 就知道 $X$ 在深度学习中的应用: 互信息最大化(Mutual Information Maximization)是无监督表示学习的一种范式——让编码器的输出(表示)和输入数据之间的互信息最大化。直觉是:好的表示应该保留输入的最多信息。 对比学习(Contrastive Learning)可以理解为最大化正样本对之间的互信息,最小化负样本对之间的互信息。CLIP、SimCLR 等模型的训练目标,本质上都是互信息最大化。 第七部分:温度、熵与采样策略 温度控制熵 语言模型的输出分布,用温度 $\tau$ 调节: $$p_k(\tau) = \frac{e^{z_k/\tau}}{\sum_j e^{z_j/\tau}}$$ 温度对熵的影响: $$H(P_\tau) = -\sum_k p_k(\tau) \log p_k(\tau)$$ $\tau \to 0$:分布退化为 one-hot(只有最高 logit 概率为 1),熵 = 0 $\tau = 1$:标准 Softmax,熵为正常值 $\tau \to \infty$:分布趋向均匀,熵趋向最大值 $\log K$ 温度采样的信息论含义:高温度相当于向输出分布"注入不确定性"(增加熵),增加生成的多样性;低温度相当于压缩不确定性(减少熵),增加生成的一致性。 Top-p 采样(Nucleus Sampling) Top-p 采样不按温度调节,而是动态截断: 每次只保留累积概率超过 $p$(比如 0.9)的最小词集合 $\mathcal{V}_p$,在这个集合里重新归一化采样: $$\mathcal{V}_p = \text{最小集合使得} \sum_{k \in \mathcal{V}_p} p_k \geq p$$ 信息论角度:Top-p 相当于保留分布的"前 $p$ 概率质量",丢弃尾部的低概率词。 好处:分布尖峰时(模型确定),$\mathcal{V}_p$ 很小(只考虑少数高概率词,行文准确);分布平坦时(模型不确定),$\mathcal{V}_p$ 较大(允许更多选择,增加多样性)。自动适应模型的局部确定性。 DeepSeek API 默认 top_p = 0.9,是工程实践中的平衡点。 第八部分:交叉熵的数值问题与 LogSumExp 技巧 数值下溢问题 直接计算 Softmax 再取对数: probs = exp(logits) / sum(exp(logits)) # 可能数值溢出 log_probs = log(probs) 当 logits 很大(比如 100),exp(100) 会溢出(超过 float 最大值)。 LogSumExp 技巧 $$\log \sum_j e^{z_j} = c + \log \sum_j e^{z_j - c}, \quad c = \max_j z_j$$ 减去最大值再指数,最大项变成 $e^0 = 1$,其余项 $\leq 1$,不会溢出。 交叉熵的数值稳定计算: $$-\log p_{y^*} = -z_{y^*} + \log \sum_j e^{z_j} = -z_{y^*} + c + \log \sum_j e^{z_j - c}$$ PyTorch 的 F.cross_entropy 内部就用了这个技巧,直接接受 logits,不需要先算 Softmax——既数值稳定,又计算高效。 第九部分:完整代码 import numpy as np import torch import torch.nn.functional as F np.random.seed(42) torch.manual_seed(42) # ===== 1. 自信息量与信息熵 ===== print("===== 自信息量与信息熵 =====\n") def entropy(probs): probs = np.array(probs) probs = probs[probs > 0] # 排除零概率 return -np.sum(probs * np.log(probs)) # 几种分布的熵 distributions = { "确定性(p=1)": [1.0], "公平硬币(p=0.5)": [0.5, 0.5], "偏心硬币(p=0.9)": [0.9, 0.1], "公平骰子(K=6)": [1/6]*6, "均匀128K词表": [1/128000]*128000, } print(f"{'分布':25s} | {'熵(nats)':>10} | {'PPL':>10} | {'解释'}") print("-" * 70) for name, probs in distributions.items(): h = entropy(probs) ppl = np.exp(h) print(f"{name:25s} | {h:>10.4f} | {ppl:>10.2f}") # 验证最大熵定理:K个类别的均匀分布熵 = log(K) for K in [2, 6, 10, 128000]: h_uniform = np.log(K) h_computed = entropy([1/K]*K) print(f"K={K:7d}: 理论 log(K)={h_uniform:.4f}, 计算值={h_computed:.4f}") # ===== 2. 交叉熵:用错分布的代价 ===== print("\n===== 交叉熵:用错分布的代价 =====\n") # 真实分布 P(标签),模型分布 Q(预测) P_true = np.array([0.0, 1.0, 0.0]) # one-hot,真实类别是1 Q_good = np.array([0.05, 0.9, 0.05]) # 好模型 Q_bad = np.array([0.4, 0.2, 0.4]) # 差模型 def cross_entropy(P, Q): Q = np.clip(Q, 1e-10, 1.0) return -np.sum(P * np.log(Q)) def kl_divergence(P, Q): mask = P > 0 Q = np.clip(Q, 1e-10, 1.0) return np.sum(P[mask] * np.log(P[mask] / Q[mask])) h_P = entropy(P_true[P_true > 0]) ce_good = cross_entropy(P_true, Q_good) ce_bad = cross_entropy(P_true, Q_bad) kl_good = kl_divergence(P_true, Q_good) kl_bad = kl_divergence(P_true, Q_bad) print(f"真实分布 P: {P_true} (one-hot)") print(f"好模型 Q_good: {Q_good}") print(f"差模型 Q_bad: {Q_bad}\n") print(f"H(P) 真实熵: {h_P:.4f} (one-hot熵=0)") print(f"H(P,Q_good) 交叉熵: {ce_good:.4f} = H(P)+KL = {h_P:.4f}+{kl_good:.4f}") print(f"H(P,Q_bad) 交叉熵: {ce_bad:.4f} = H(P)+KL = {h_P:.4f}+{kl_bad:.4f}") print(f"\n→ 交叉熵 = 真实熵 + KL散度,最小化交叉熵即最小化KL散度") # ===== 3. 温度对熵的影响 ===== print("\n===== 温度对熵的影响 =====\n") logits = torch.tensor([3.0, 1.5, 1.0, 0.5, 0.2, -0.5]) vocab = ["猫", "狗", "鱼", "鸟", "虫", "草"] print(f"{'温度τ':>6} | {'熵(nats)':>10} | {'PPL':>8} | {'最高概率词':>8} | {'最高概率':>8}") print("-" * 55) for tau in [0.1, 0.3, 0.5, 1.0, 1.5, 2.0, 5.0]: p = F.softmax(logits / tau, dim=0) h = -(p * torch.log(p + 1e-10)).sum().item() ppl = np.exp(h) top_k = p.argmax().item() print(f"τ={tau:>4.1f} | {h:>10.4f} | {ppl:>8.3f} | {vocab[top_k]:>8} | {p[top_k].item():>8.4f}") # ===== 4. 语言模型训练过程中的PPL变化 ===== print("\n===== 语言模型训练中的PPL变化(模拟) =====\n") # 模拟不同训练阶段的模型对同一个token的预测概率 vocab_size = 128000 true_token_idx = 42 # 真实token scenarios = [ ("随机初始化", 1.0 / vocab_size), # 均匀猜测 ("训练1k步", 0.005), # 开始集中 ("训练10k步", 0.05), ("训练100k步", 0.20), ("训练1M步", 0.50), ("训练10M步", 0.80), ] print(f"{'训练阶段':14} | {'真实token概率':>14} | {'NLL损失':>8} | {'等效PPL':>10}") print("-" * 55) for stage, true_prob in scenarios: nll = -np.log(true_prob) # 等效PPL = 如果所有token的条件概率都等于true_prob时的PPL ppl = np.exp(nll) print(f"{stage:14} | {true_prob:>14.6f} | {nll:>8.4f} | {ppl:>10.2f}") # ===== 5. 数值稳定的交叉熵:LogSumExp 技巧 ===== print("\n===== LogSumExp 技巧验证 =====\n") # 极大的logits(会导致数值溢出) logits_large = torch.tensor([100.0, 101.0, 99.0]) label = torch.tensor([1]) # 真实类别是1 # 方法一:先算Softmax(可能溢出) try: probs_naive = torch.exp(logits_large) / torch.exp(logits_large).sum() loss_naive = -torch.log(probs_naive[1]) print(f"朴素方法: probs={probs_naive}, loss={loss_naive.item():.4f}") except Exception as e: print(f"朴素方法失败: {e}") # 方法二:LogSumExp(数值稳定) c = logits_large.max() log_sum_exp = c + torch.log(torch.exp(logits_large - c).sum()) log_prob_stable = logits_large[1] - log_sum_exp loss_stable = -log_prob_stable print(f"LogSumExp方法: log_sum_exp={log_sum_exp.item():.4f}, loss={loss_stable.item():.4f}") # 方法三:PyTorch内置(自动使用数值稳定计算) loss_pytorch = F.cross_entropy(logits_large.unsqueeze(0), label) print(f"PyTorch内置: loss={loss_pytorch.item():.4f}") print(f"两种数值稳定方法结果相同: {torch.isclose(loss_stable, loss_pytorch)}") # ===== 6. 互信息演示 ===== print("\n===== 互信息:变量间的信息共享 =====\n") # 构造两个相关的离散变量 # X = 天气(晴/阴/雨),Y = 是否带伞 # 联合分布 joint_P = np.array([ [0.30, 0.02], # 晴天:不带伞0.30,带伞0.02 [0.10, 0.10], # 阴天:不带伞0.10,带伞0.10 [0.03, 0.45], # 雨天:不带伞0.03,带伞0.45 ]) # 行:天气(晴/阴/雨),列:带伞(否/是) P_X = joint_P.sum(axis=1) # 天气边缘分布 P_Y = joint_P.sum(axis=0) # 带伞边缘分布 H_X = entropy(P_X) H_Y = entropy(P_Y) H_XY = entropy(joint_P.flatten()) # 条件熵 H(Y|X) = H(X,Y) - H(X) H_Y_given_X = H_XY - H_X # 互信息 I(X;Y) = H(Y) - H(Y|X) MI = H_Y - H_Y_given_X print(f"天气分布 P_X: {dict(zip(['晴','阴','雨'], P_X.round(2)))}") print(f"带伞分布 P_Y: {dict(zip(['否','是'], P_Y.round(2)))}") print(f"\nH(天气): {H_X:.4f} nats") print(f"H(带伞): {H_Y:.4f} nats") print(f"H(天气,带伞): {H_XY:.4f} nats") print(f"H(带伞|天气): {H_Y_given_X:.4f} nats (知道天气后,带伞不确定性降低)") print(f"I(天气;带伞): {MI:.4f} nats (互信息:天气和带伞共享的信息量)") print(f"\n→ 知道天气后,带伞的不确定性从{H_Y:.3f}降到{H_Y_given_X:.3f}") print(f"→ 互信息{MI:.3f}就是这个不确定性的减少量") 总结 这篇文章从 Shannon 的信息论出发,把信息熵、交叉熵和语言模型训练损失的关系完整打通了。 第一,自信息量 $I(x) = -\log P(x)$ 衡量单个事件的信息量:概率越小,发生时信息量越大。独立事件信息量可加,这是选择对数函数的根本原因。 第二,信息熵 $H(P) = -\mathbb{E}[\log P(x)]$ 是信息量的期望,衡量分布的平均不确定性。均匀分布熵最大(最不确定),确定性分布熵为 0。语言模型训练从高熵(随机猜测 PPL≈128000)向低熵(精确预测 PPL≈几)前进。 第三,交叉熵 $H(P,Q) = -\mathbb{E}_{x \sim P}[\log Q(x)]$ 是"用模型分布 $Q$ 替代真实分布 $P$ 编码数据的平均代价",总是 $\geq H(P)$。最小化交叉熵等价于最小化 KL 散度,让模型分布尽量接近真实数据分布。 第四,语言模型的训练损失就是平均交叉熵(等价于平均 NLL),困惑度 PPL 是其指数,直觉上表示"模型平均在多少个候选里犹豫"。条件熵视角下,训练目标是最小化"给定前文条件下下一个 token 的不确定性"。 第五,温度参数 $\tau$ 控制输出分布的熵:低温降低熵(更确定、更保守),高温提高熵(更多样、更随机)。Top-p 采样动态调整候选集大小,自适应模型的局部确定性。 第六,数值稳定性是工程实践的关键:LogSumExp 技巧通过减去最大值防止指数溢出,PyTorch 的 F.cross_entropy 直接接受 logits 并内部使用这个技巧。 下一篇预告: 第16篇,我们讲 KL 散度:DeepSeek R1 强化学习中的约束项从何而来? 交叉熵告诉我们"用错分布的代价",但这个代价是不对称的——$D_{KL}(P \| Q) \neq D_{KL}(Q \| P)$。这个不对称性有深刻的含义,直接解释了为什么 GRPO 的 KL 约束要写成 $D_{KL}(\pi_\theta \| \pi_{ref})$ 而不是反过来,以及为什么 VAE 的训练目标选 $D_{KL}(q \| p)$。 信息论告诉我们:不确定性是可以量化的,信息是可以度量的。Shannon 熵不仅仅是数学工具,它是一种思维方式——把"模型对数据有多惊讶"变成了一个可以优化的数字。每次梯度下降,DeepSeek V3 都在用 14.8 万亿个 token 告诉自己:对这些数据,你还可以更少地感到惊讶。
目标是最小化"给定前文条件下,下一个词的不确定性"。
互信息(Mutual Information) $I(X; Y)$:$X$ 和 $Y$ 之间共享的信息量:
$$I(X; Y) = H(X) - H(X \mid Y) = H(Y) - H(Y \mid X)$$
$$= H(X) + H(Y) - H(X,Y)$$
互信息衡量两个变量的相关程度:
在深度学习中的应用:
互信息最大化(Mutual Information Maximization)是无监督表示学习的一种范式——让编码器的输出(表示)和输入数据之间的互信息最大化。直觉是:好的表示应该保留输入的最多信息。
对比学习(Contrastive Learning)可以理解为最大化正样本对之间的互信息,最小化负样本对之间的互信息。CLIP、SimCLR 等模型的训练目标,本质上都是互信息最大化。
语言模型的输出分布,用温度 $\tau$ 调节:
$$p_k(\tau) = \frac{e^{z_k/\tau}}{\sum_j e^{z_j/\tau}}$$
温度对熵的影响:
$$H(P_\tau) = -\sum_k p_k(\tau) \log p_k(\tau)$$
温度采样的信息论含义:高温度相当于向输出分布"注入不确定性"(增加熵),增加生成的多样性;低温度相当于压缩不确定性(减少熵),增加生成的一致性。
Top-p 采样不按温度调节,而是动态截断:
每次只保留累积概率超过 $p$(比如 0.9)的最小词集合 $\mathcal{V}_p$,在这个集合里重新归一化采样:
$$\mathcal{V}_p = \text{最小集合使得} \sum_{k \in \mathcal{V}_p} p_k \geq p$$
信息论角度:Top-p 相当于保留分布的"前 $p$ 概率质量",丢弃尾部的低概率词。
好处:分布尖峰时(模型确定),$\mathcal{V}_p$ 很小(只考虑少数高概率词,行文准确);分布平坦时(模型不确定),$\mathcal{V}_p$ 较大(允许更多选择,增加多样性)。自动适应模型的局部确定性。
DeepSeek API 默认 top_p = 0.9,是工程实践中的平衡点。
top_p = 0.9
直接计算 Softmax 再取对数:
probs = exp(logits) / sum(exp(logits)) # 可能数值溢出 log_probs = log(probs)
当 logits 很大(比如 100),exp(100) 会溢出(超过 float 最大值)。
exp(100)
$$\log \sum_j e^{z_j} = c + \log \sum_j e^{z_j - c}, \quad c = \max_j z_j$$
减去最大值再指数,最大项变成 $e^0 = 1$,其余项 $\leq 1$,不会溢出。
交叉熵的数值稳定计算:
$$-\log p_{y^*} = -z_{y^*} + \log \sum_j e^{z_j} = -z_{y^*} + c + \log \sum_j e^{z_j - c}$$
PyTorch 的 F.cross_entropy 内部就用了这个技巧,直接接受 logits,不需要先算 Softmax——既数值稳定,又计算高效。
F.cross_entropy
import numpy as np import torch import torch.nn.functional as F np.random.seed(42) torch.manual_seed(42) # ===== 1. 自信息量与信息熵 ===== print("===== 自信息量与信息熵 =====\n") def entropy(probs): probs = np.array(probs) probs = probs[probs > 0] # 排除零概率 return -np.sum(probs * np.log(probs)) # 几种分布的熵 distributions = { "确定性(p=1)": [1.0], "公平硬币(p=0.5)": [0.5, 0.5], "偏心硬币(p=0.9)": [0.9, 0.1], "公平骰子(K=6)": [1/6]*6, "均匀128K词表": [1/128000]*128000, } print(f"{'分布':25s} | {'熵(nats)':>10} | {'PPL':>10} | {'解释'}") print("-" * 70) for name, probs in distributions.items(): h = entropy(probs) ppl = np.exp(h) print(f"{name:25s} | {h:>10.4f} | {ppl:>10.2f}") # 验证最大熵定理:K个类别的均匀分布熵 = log(K) for K in [2, 6, 10, 128000]: h_uniform = np.log(K) h_computed = entropy([1/K]*K) print(f"K={K:7d}: 理论 log(K)={h_uniform:.4f}, 计算值={h_computed:.4f}") # ===== 2. 交叉熵:用错分布的代价 ===== print("\n===== 交叉熵:用错分布的代价 =====\n") # 真实分布 P(标签),模型分布 Q(预测) P_true = np.array([0.0, 1.0, 0.0]) # one-hot,真实类别是1 Q_good = np.array([0.05, 0.9, 0.05]) # 好模型 Q_bad = np.array([0.4, 0.2, 0.4]) # 差模型 def cross_entropy(P, Q): Q = np.clip(Q, 1e-10, 1.0) return -np.sum(P * np.log(Q)) def kl_divergence(P, Q): mask = P > 0 Q = np.clip(Q, 1e-10, 1.0) return np.sum(P[mask] * np.log(P[mask] / Q[mask])) h_P = entropy(P_true[P_true > 0]) ce_good = cross_entropy(P_true, Q_good) ce_bad = cross_entropy(P_true, Q_bad) kl_good = kl_divergence(P_true, Q_good) kl_bad = kl_divergence(P_true, Q_bad) print(f"真实分布 P: {P_true} (one-hot)") print(f"好模型 Q_good: {Q_good}") print(f"差模型 Q_bad: {Q_bad}\n") print(f"H(P) 真实熵: {h_P:.4f} (one-hot熵=0)") print(f"H(P,Q_good) 交叉熵: {ce_good:.4f} = H(P)+KL = {h_P:.4f}+{kl_good:.4f}") print(f"H(P,Q_bad) 交叉熵: {ce_bad:.4f} = H(P)+KL = {h_P:.4f}+{kl_bad:.4f}") print(f"\n→ 交叉熵 = 真实熵 + KL散度,最小化交叉熵即最小化KL散度") # ===== 3. 温度对熵的影响 ===== print("\n===== 温度对熵的影响 =====\n") logits = torch.tensor([3.0, 1.5, 1.0, 0.5, 0.2, -0.5]) vocab = ["猫", "狗", "鱼", "鸟", "虫", "草"] print(f"{'温度τ':>6} | {'熵(nats)':>10} | {'PPL':>8} | {'最高概率词':>8} | {'最高概率':>8}") print("-" * 55) for tau in [0.1, 0.3, 0.5, 1.0, 1.5, 2.0, 5.0]: p = F.softmax(logits / tau, dim=0) h = -(p * torch.log(p + 1e-10)).sum().item() ppl = np.exp(h) top_k = p.argmax().item() print(f"τ={tau:>4.1f} | {h:>10.4f} | {ppl:>8.3f} | {vocab[top_k]:>8} | {p[top_k].item():>8.4f}") # ===== 4. 语言模型训练过程中的PPL变化 ===== print("\n===== 语言模型训练中的PPL变化(模拟) =====\n") # 模拟不同训练阶段的模型对同一个token的预测概率 vocab_size = 128000 true_token_idx = 42 # 真实token scenarios = [ ("随机初始化", 1.0 / vocab_size), # 均匀猜测 ("训练1k步", 0.005), # 开始集中 ("训练10k步", 0.05), ("训练100k步", 0.20), ("训练1M步", 0.50), ("训练10M步", 0.80), ] print(f"{'训练阶段':14} | {'真实token概率':>14} | {'NLL损失':>8} | {'等效PPL':>10}") print("-" * 55) for stage, true_prob in scenarios: nll = -np.log(true_prob) # 等效PPL = 如果所有token的条件概率都等于true_prob时的PPL ppl = np.exp(nll) print(f"{stage:14} | {true_prob:>14.6f} | {nll:>8.4f} | {ppl:>10.2f}") # ===== 5. 数值稳定的交叉熵:LogSumExp 技巧 ===== print("\n===== LogSumExp 技巧验证 =====\n") # 极大的logits(会导致数值溢出) logits_large = torch.tensor([100.0, 101.0, 99.0]) label = torch.tensor([1]) # 真实类别是1 # 方法一:先算Softmax(可能溢出) try: probs_naive = torch.exp(logits_large) / torch.exp(logits_large).sum() loss_naive = -torch.log(probs_naive[1]) print(f"朴素方法: probs={probs_naive}, loss={loss_naive.item():.4f}") except Exception as e: print(f"朴素方法失败: {e}") # 方法二:LogSumExp(数值稳定) c = logits_large.max() log_sum_exp = c + torch.log(torch.exp(logits_large - c).sum()) log_prob_stable = logits_large[1] - log_sum_exp loss_stable = -log_prob_stable print(f"LogSumExp方法: log_sum_exp={log_sum_exp.item():.4f}, loss={loss_stable.item():.4f}") # 方法三:PyTorch内置(自动使用数值稳定计算) loss_pytorch = F.cross_entropy(logits_large.unsqueeze(0), label) print(f"PyTorch内置: loss={loss_pytorch.item():.4f}") print(f"两种数值稳定方法结果相同: {torch.isclose(loss_stable, loss_pytorch)}") # ===== 6. 互信息演示 ===== print("\n===== 互信息:变量间的信息共享 =====\n") # 构造两个相关的离散变量 # X = 天气(晴/阴/雨),Y = 是否带伞 # 联合分布 joint_P = np.array([ [0.30, 0.02], # 晴天:不带伞0.30,带伞0.02 [0.10, 0.10], # 阴天:不带伞0.10,带伞0.10 [0.03, 0.45], # 雨天:不带伞0.03,带伞0.45 ]) # 行:天气(晴/阴/雨),列:带伞(否/是) P_X = joint_P.sum(axis=1) # 天气边缘分布 P_Y = joint_P.sum(axis=0) # 带伞边缘分布 H_X = entropy(P_X) H_Y = entropy(P_Y) H_XY = entropy(joint_P.flatten()) # 条件熵 H(Y|X) = H(X,Y) - H(X) H_Y_given_X = H_XY - H_X # 互信息 I(X;Y) = H(Y) - H(Y|X) MI = H_Y - H_Y_given_X print(f"天气分布 P_X: {dict(zip(['晴','阴','雨'], P_X.round(2)))}") print(f"带伞分布 P_Y: {dict(zip(['否','是'], P_Y.round(2)))}") print(f"\nH(天气): {H_X:.4f} nats") print(f"H(带伞): {H_Y:.4f} nats") print(f"H(天气,带伞): {H_XY:.4f} nats") print(f"H(带伞|天气): {H_Y_given_X:.4f} nats (知道天气后,带伞不确定性降低)") print(f"I(天气;带伞): {MI:.4f} nats (互信息:天气和带伞共享的信息量)") print(f"\n→ 知道天气后,带伞的不确定性从{H_Y:.3f}降到{H_Y_given_X:.3f}") print(f"→ 互信息{MI:.3f}就是这个不确定性的减少量")
这篇文章从 Shannon 的信息论出发,把信息熵、交叉熵和语言模型训练损失的关系完整打通了。
第一,自信息量 $I(x) = -\log P(x)$ 衡量单个事件的信息量:概率越小,发生时信息量越大。独立事件信息量可加,这是选择对数函数的根本原因。
第二,信息熵 $H(P) = -\mathbb{E}[\log P(x)]$ 是信息量的期望,衡量分布的平均不确定性。均匀分布熵最大(最不确定),确定性分布熵为 0。语言模型训练从高熵(随机猜测 PPL≈128000)向低熵(精确预测 PPL≈几)前进。
第三,交叉熵 $H(P,Q) = -\mathbb{E}_{x \sim P}[\log Q(x)]$ 是"用模型分布 $Q$ 替代真实分布 $P$ 编码数据的平均代价",总是 $\geq H(P)$。最小化交叉熵等价于最小化 KL 散度,让模型分布尽量接近真实数据分布。
第四,语言模型的训练损失就是平均交叉熵(等价于平均 NLL),困惑度 PPL 是其指数,直觉上表示"模型平均在多少个候选里犹豫"。条件熵视角下,训练目标是最小化"给定前文条件下下一个 token 的不确定性"。
第五,温度参数 $\tau$ 控制输出分布的熵:低温降低熵(更确定、更保守),高温提高熵(更多样、更随机)。Top-p 采样动态调整候选集大小,自适应模型的局部确定性。
第六,数值稳定性是工程实践的关键:LogSumExp 技巧通过减去最大值防止指数溢出,PyTorch 的 F.cross_entropy 直接接受 logits 并内部使用这个技巧。
下一篇预告:
第16篇,我们讲 KL 散度:DeepSeek R1 强化学习中的约束项从何而来?
交叉熵告诉我们"用错分布的代价",但这个代价是不对称的——$D_{KL}(P \| Q) \neq D_{KL}(Q \| P)$。这个不对称性有深刻的含义,直接解释了为什么 GRPO 的 KL 约束要写成 $D_{KL}(\pi_\theta \| \pi_{ref})$ 而不是反过来,以及为什么 VAE 的训练目标选 $D_{KL}(q \| p)$。
信息论告诉我们:不确定性是可以量化的,信息是可以度量的。Shannon 熵不仅仅是数学工具,它是一种思维方式——把"模型对数据有多惊讶"变成了一个可以优化的数字。每次梯度下降,DeepSeek V3 都在用 14.8 万亿个 token 告诉自己:对这些数据,你还可以更少地感到惊讶。
还没有评论,来抢沙发吧!
博客管理员
40 篇文章
还没有评论,来抢沙发吧!