观察到 100 次抛硬币有 70 次正面,为什么答案就是 0.70? 因为 0.70 是让「看到这份数据」的概率最大的那个值——这叫最大似然。 把似然取对数、取负号、除以样本数,得到的就是交叉熵损失。 它不是设计出来的,是推出来的。 平方误差也有同样的来路:它对应「噪声是高斯分布」这个假设。
上一节看到交叉熵比平方误差好用,但没说它凭什么。 这一节补上这个推导。 它不长,但它是理解「为什么这个损失配这个输出层」的唯一途径。 推导的起点是一个很朴素的问题:给定数据,参数应该是多少? 最大似然给的回答是:让这份数据出现概率最大的那个参数。
一、把数据当已知,把参数当未知
平时我们想的是「参数定了,数据会是什么样」。 似然函数把这个方向反过来:数据已经看到了,问「参数取什么值时,看到这份数据的可能性最大」。 抛 100 次、70 次正面,正面概率记作 p。 那么「恰好看到这份数据」的概率是:
L(p) = p 的 70 次方 * (1 - p) 的 30 次方
这个式子的自变量是 p,数据(70 和 30)是固定的常数。 这就是似然和概率的区别:概率是数据的函数,似然是参数的函数。
二、算一遍,看最大值在哪
取几个候选 p 代进去:
| 候选 p | 似然 | 对数似然 |
|---|---|---|
| 0.50 | 7.888609e-31 | -69.3147 |
| 0.60 | 3.407119e-28 | -63.2465 |
| 0.65 | 1.682582e-27 | -61.6495 |
| 0.68 | 2.692299e-27 | -61.1794 |
| 0.70 | 2.954612e-27 | -61.0864 |
| 0.72 | 2.679265e-27 | -61.1843 |
| 0.75 | 1.557717e-27 | -61.7266 |
| 0.80 | 1.766847e-28 | -63.9032 |
| 0.90 | 6.265787e-34 | -76.4528 |
| 似然那一列小得没法比(10 的负 31 次方),所以实际都看对数似然。 | ||
| 对数把连乘变成连加,也把极小的数变成几十的负数,好算也好比。 | ||
| 关键是:对数函数是单调递增的,所以「让对数最大」和「让原式最大」是同一件事。 | ||
| 最大值落在 0.70。 | ||
| 在 0.001 的网格上扫一遍,结果也是 0.700。 | ||
| 这和「正面次数除以总次数」完全一致。 | ||
| 所以那个教科书答案不是拍脑袋,它就是最大似然解。 |
三、从似然推到交叉熵
把每个样本看成一次伯努利试验。 n 个样本的似然是它们概率的连乘:
L = 连乘 [ a_i 的 t_i 次方 * (1 - a_i) 的 (1 - t_i) 次方 ]
a_i 是网络对这个样本的输出(预测概率),
t_i 是标签(0 或 1)。
连乘容易下溢——100 个 0.7 相乘已经到 10 的负 16 次方。 所以取对数,连乘变连加:
ln L = 求和 [ t_i * ln(a_i) + (1 - t_i) * ln(1 - a_i) ]
我们要最大化这个式子。习惯上把「最大化」改成「最小化负号」,再除以样本数变成平均值,就得到:
L = -1/n * 求和 [ t * ln(a) + (1 - t) * ln(1 - a) ]
这就是二分类的交叉熵损失。 注意整个过程没有任何「设计」的动作:只有「假设样本独立」和「用最大似然定参数」两个前提。
四、平方误差也有它的来路
如果假设噪声是高斯分布,推出来的就是平方误差。 高斯分布的概率密度里有个 exp(负的误差平方除以 2 倍方差),取对数之后,那个平方项就留下来了。 所以两种损失不是「谁更对」,是「假设不同」:
| 损失 | 背后的假设 | 适合的场景 |
|---|---|---|
| 平方误差 | 误差服从高斯分布 | 回归,输出是实数 |
| 交叉熵 | 标签服从伯努利分布 | 分类,输出是概率 |
| 分类问题的标签只有 0 和 1,说它是伯努利更贴;回归问题的标签是任意实数,说它是高斯更贴。 | ||
| 这就是「交叉熵配分类、平方误差配回归」这条惯例的来源。 |
五、两种损失惩罚错误的陡度
同一次「猜错」,两种损失各罚多少:
| 候选 p | 负对数似然 | 平方误差 |
|---|---|---|
| 0.90 | 0.7645 | 0.2500 |
| 0.95 | 0.9346 | 0.2725 |
| 0.99 | 1.3886 | 0.2941 |
| 真实比例是 0.70,候选从 0.90 一路挪到 0.99。 | ||
| 交叉熵从 0.76 涨到 1.39,几乎翻倍;平方误差从 0.25 只涨到 0.29。 | ||
| 因为平方误差有上界:标签是 0 或 1,最大也就是 1。 | ||
| 交叉熵没有上界,p 趋近于 0 时它趋近于无穷。 | ||
| 所以交叉熵对「几乎不可能还发生」这件事罚得极重,平方误差罚得很轻。 | ||
| 这也解释了上一节那张梯度表:交叉熵在饱和时梯度反而最大,因为它对离谱的错误毫不手软。 |
六、动手 Demo:交叉熵不是设计出来的,是推出来的
这段代码用一个 100 次、70 次正面的样本,把似然函数、对数似然、最大似然解都算一遍; 最后比两种损失在「猜错」时的惩罚有多陡。 重点看三处。第二段似然从 1e-31 涨到 1e-27 再掉回 1e-34;第三段网格扫描落在 0.700; 第六段 0.99 时交叉熵 1.3886 对平方误差 0.2941。
# -*- coding: utf-8 -*-
import unicodedata
import numpy as np
def wd(s):
return sum(2 if unicodedata.east_asian_width(c) in ('W', 'F') else 1 for c in s)
def pad(s, n):
return s + ' ' * max(0, n - wd(s))
rg = np.random.default_rng(3)
n = 100
y = (rg.random(n) < 0.7).astype(float)
k = int(y.sum())
print('[1] 一个最朴素的问题:观察到 %d 次正面、%d 次反面,硬币正面的概率是多少' % (k, n - k))
print(' 教科书答案是 %d/%d = %.4f,但那个数是从哪来的' % (k, n, k / n))
print('')
print('[2] 似然函数:把「数据已经固定」,反过来当成「概率的函数」')
print(' 似然 L(p) = p 的 k 次方 * (1-p) 的 (n-k) 次方')
print(' 它回答的是:「如果正面概率是 p,那么恰好看到这份数据的可能性有多大」')
print('')
print(' 候选 p 似然 对数似然 负对数似然(总和)')
best_p, best_ll = 0.0, -1e18
for p in (0.50, 0.60, 0.65, 0.68, 0.70, 0.72, 0.75, 0.80, 0.90):
L = p ** k * (1.0 - p) ** (n - k)
ll = k * np.log(p) + (n - k) * np.log(1.0 - p)
if ll > best_ll:
best_ll, best_p = ll, p
print(' ' + pad('%.2f' % p, 11) + pad('%.6e' % L, 21)
+ pad('%.4f' % ll, 14) + '%.4f' % (-ll))
print('')
print('[3] 在网格上找最大值,看它落在哪')
grid = np.linspace(0.01, 0.99, 981)
lls = k * np.log(grid) + (n - k) * np.log(1.0 - grid)
i = int(np.argmax(lls))
print(' 网格步长 0.001,最大值在 p = %.3f' % grid[i])
print(' 和「正面次数除以总次数」的 %.4f 完全一致' % (k / n))
print(' 所以「频率估计」不是拍脑袋,它就是最大似然解')
print('')
print('[4] 最大似然怎么变成交叉熵的')
print(' 把每个样本看成一次伯努利试验,似然 = 每个样本概率的连乘')
print(' 连乘容易下溢(100 个 0.7 相乘已经很小),所以取对数变连加')
print(' 再取负号、除以样本数,就是平均负对数似然:')
print(' -1/n * sum[ t*ln(a) + (1-t)*ln(1-a) ]')
print(' 这正是二分类的交叉熵损失。它不是「设计」出来的,是推出来的')
print('')
print('[5] 那平方误差为什么也算「合理」')
print(' 平方误差对应的是「噪声是高斯分布」这个假设下的最大似然')
print(' 两个假设不一样,导出的损失就不一样;哪个更贴数据,哪个就更好用')
print(' 分类问题的标签是 0 和 1,用伯努利假设更贴;回归问题的标签是实数,用高斯假设更贴')
print('')
print('[6] 数值比一下:两种损失在「猜错」时的惩罚有多陡')
print(' 真实比例 %.2f;候选 p 从 0.70 挪到 0.90,偏离 0.20' % (k / n))
for p in (0.90, 0.95, 0.99):
nll = -(k * np.log(p) + (n - k) * np.log(1.0 - p)) / n
mse = np.mean((y - p) ** 2)
print(' ' + pad('p = %.2f' % p, 11) + pad('负对数似然 %.4f' % nll, 20)
+ '平方误差 %.4f' % mse)
print(' 交叉熵涨得快得多:它把「几乎不可能还发生」这件事罚得很重')
print(' 平方误差有上界(最大就是 1),所以它对离谱的错误不够敏感')
实跑输出:
--------------------------------------------------------------------
似然函数:交叉熵不是设计出来的,是推出来的
--------------------------------------------------------------------
[1] 一个最朴素的问题:观察到 70 次正面、30 次反面,硬币正面的概率是多少
教科书答案是 70/100 = 0.7000,但那个数是从哪来的
[2] 似然函数:把「数据已经固定」,反过来当成「概率的函数」
似然 L(p) = p 的 k 次方 * (1-p) 的 (n-k) 次方
它回答的是:「如果正面概率是 p,那么恰好看到这份数据的可能性有多大」
候选 p 似然 对数似然 负对数似然(总和)
0.50 7.888609e-31 -69.3147 69.3147
0.60 3.407119e-28 -63.2465 63.2465
0.65 1.682582e-27 -61.6495 61.6495
0.68 2.692299e-27 -61.1794 61.1794
0.70 2.954612e-27 -61.0864 61.0864
0.72 2.679265e-27 -61.1843 61.1843
0.75 1.557717e-27 -61.7266 61.7266
0.80 1.766847e-28 -63.9032 63.9032
0.90 6.265787e-34 -76.4528 76.4528
[3] 在网格上找最大值,看它落在哪
网格步长 0.001,最大值在 p = 0.700
和「正面次数除以总次数」的 0.7000 完全一致
所以「频率估计」不是拍脑袋,它就是最大似然解
[4] 最大似然怎么变成交叉熵的
把每个样本看成一次伯努利试验,似然 = 每个样本概率的连乘
连乘容易下溢(100 个 0.7 相乘已经很小),所以取对数变连加
再取负号、除以样本数,就是平均负对数似然:
-1/n * sum[ t*ln(a) + (1-t)*ln(1-a) ]
这正是二分类的交叉熵损失。它不是「设计」出来的,是推出来的
[5] 那平方误差为什么也算「合理」
平方误差对应的是「噪声是高斯分布」这个假设下的最大似然
两个假设不一样,导出的损失就不一样;哪个更贴数据,哪个就更好用
分类问题的标签是 0 和 1,用伯努利假设更贴;回归问题的标签是实数,用高斯假设更贴
[6] 数值比一下:两种损失在「猜错」时的惩罚有多陡
真实比例 0.70;候选 p 从 0.70 挪到 0.90,偏离 0.20
p = 0.90 负对数似然 0.7645 平方误差 0.2500
p = 0.95 负对数似然 0.9346 平方误差 0.2725
p = 0.99 负对数似然 1.3886 平方误差 0.2941
交叉熵涨得快得多:它把「几乎不可能还发生」这件事罚得很重
平方误差有上界(最大就是 1),所以它对离谱的错误不够敏感
第二段那个「似然」列几乎没法看,全是 10 的负 30 次方上下。
这不是数值不稳定,是连乘的真实结果——100 个概率相乘本来就这么小。
所以实际计算一律用对数,从第一步就开始取对数,不等连乘完再取。
第三段用 981 个点的网格扫,最大值落在 0.700。
这里没有用任何求导、任何优化器——只是把候选值代进去比大小。
一维问题可以这么粗暴地解,这也是「最大似然」这个名字最朴素的体现。
第四段的推导里,唯一需要留意的假设是「样本独立」。
独立才能把概率写成连乘;不独立的话似然函数根本写不出来。
真实数据里样本之间常常有相关性(比如同一段视频的相邻帧),那时这个推导就只是近似。
第六段那个「平方误差有上界」值得记住:标签是 0 或 1,所以 (a 减 t) 的平方最大就是 1。
交叉熵没有上界,p 越接近 0 越大。
一个损失函数有没有上界,决定了它对「灾难性错误」的敏感程度。
代码里 k 是正面的次数,n - k 是反面,都从随机数据里数出来。
所以「70 次」不是写死的,是这一份数据恰好 70 次——换个种子会变成 68 或 72,但结论一样。
30 秒实验
- 把
n从 100 改成 10,重跑。 似然的最大值点会从 0.700 移到 0.7 附近的整数比上(比如 7/10)。 样本越少,最大似然解越不稳定——这就是「小样本估计不可靠」的数学表述。 - 把
y的生成概率从 0.7 改成 0.3,重跑。最大似然解跟着变成 0.3 附近。 似然函数只认数据,不认任何先验。 - 把第六段的候选 p 加到 1.0,重跑。负对数似然会变成无穷大(log(0))。
代码里用
np.log会给出警告,所以实际实现都要加一个极小数做保护。
常见误区
- 「似然就是概率」。 概率是「参数已知,数据会怎样」;似然是「数据已知,参数该是多少」。 两者表达式一样,但谁是变量、谁是常数正好相反。
- 「交叉熵是人为设计出来让梯度变好看的」。 它是从「样本独立 + 最大似然」两个前提推出来的,没有调参的余地。 梯度好看是推出来的副产品,不是设计目标。
- 「平方误差不如交叉熵,应该全部换掉」。 回归问题上平方误差就是高斯的最大似然解,完全正确。 该换的是「分类问题用平方误差」这种搭配。
- 「最大似然总能给出正确答案」。 它给的是「最符合观测数据的参数」,样本少或数据有偏时它会错得很自信。 加先验(最大后验)或加正则,就是在修这个问题。
自测三问(点开对答案)
1. 似然函数和概率函数的区别是什么? —— 表达式可以一样,但角色相反。概率函数里参数是常数、数据是变量; 似然函数里数据是常数、参数是变量。似然回答的是「参数取什么值,看到这份数据的可能性最大」。 2. 交叉熵损失是怎么从似然推出来的? —— 假设样本独立,n 个样本的似然是各自概率的连乘。 取对数把连乘变连加,再取负号、除以样本数变成平均值,就得到平均负对数似然。 它的形式正好是交叉熵,整个推导只有「独立」一个假设。 3. 为什么分类用交叉熵、回归用平方误差? —— 因为两种损失对应两种不同的分布假设。交叉熵对应伯努利分布,适合标签是 0 或 1 的分类; 平方误差对应高斯分布,适合标签是实数的回归。选哪个取决于「你认为数据是怎么生成的」。损失定了、激活定了,剩下的问题是怎么把梯度用起来。 每步用多少样本算梯度?全批量准但慢,纯随机快但抖。 下一节把三种做法放在一起,比步数、比耗时、比曲线。
- ☐ (本节完,下一节 2.8 随机梯度下降法)