从概率到学习目标
前几章已经准备好了三件工具:用函数描述预测,用导数调整参数,用概率描述不确定性。现在把它们接起来。
训练前,我们必须告诉程序什么叫“更好”。预测耗时时,导读用了平方误差;预测一件事会不会发生时,机器学习代码却经常出现对数和交叉熵。
这些目标是随意挑的吗?这一章从一个很小的概率模型出发,看看它们怎样自然地出现。最后,再用一条直线做一次完整的数学篇实践。
1. 哪个概率更能解释已经看到的数据
假设我们记录了十次“任务是否在约定时间内完成”:7 次完成,记作 1;3 次没完成,记作 0。暂时不考虑文档之间的差别,假设每次独立,按时完成的概率都是 。
候选模型说 、 或 ,哪一个更符合这十条记录?
对于一串已经观察到的标签,在这个独立伯努利模型下,它的概率为:
叫似然。这里把数据固定下来,让候选参数 变化,比较哪种参数让这份数据出现得更合理。
注意,我们关心的是这串具体的标签。如果只问“十次中恰好有七次成功,不管顺序”,还要乘组合数 。不过这个因子与 无关,不会改变哪一个候选 最好。
这也不是在求“参数 本身的概率”。似然描述数据在给定参数下的概率或密度;若要求参数的后验分布,还需要像第三章那样明确先验等条件。
2. 为什么大家喜欢取对数
样本一多,许多小概率连乘,结果会小得难以表示。假如 1000 个因子都为 0.1,乘积就是 ,常见的浮点数已经无法正常保存它。
对数能把乘法变成加法:
本章的 都指自然对数,也写作 ,底数为 。它在正数范围内严格递增,因此一个似然比另一个大,取对数后大小顺序仍然不变。
于是:
我们习惯把训练写成“最小化损失”,再加一个负号,就得到负对数似然,英文缩写 NLL:
最大化似然与最小化负对数似然,会选出相同参数。把它再除以样本数 10,也不改变本次优化的最优位置,只是让不同规模的数据集更容易比较平均损失。
图 1:候选概率过低或过高,都不能很好解释这份同时含有 0 和 1 的数据。
如果想用上一章学到的导数检查,利用 ,可以得到:
令它等于 0,整理后就是 ,所以 。在 内,这里的二阶导数为正,因此这个驻点确实是最低点。也可以直接看曲线或用网格搜索核对,不必现在就熟悉二阶导数。
这种让已观测数据的似然最大的参数估计,叫最大似然估计。得到 0.7,并不保证未来任务的真实概率恰好是 0.7:样本很少,模型的独立与同概率假设也可能不适合实际情况。
3. 从一个共同概率,到每条样本自己的预测
刚才所有任务共用一个 。更实用的模型会根据输入 ,给第 条任务预测一个概率 。
令标签 为 0 或 1,单条样本的概率可以统一写为:
如果 ,式子留下 ;如果 ,式子留下 。对它取负对数,就得到:
这叫二元交叉熵损失。平均所有样本:
当真实标签是 1,模型给出 0.9,损失约为 ;若只给 0.01,损失约为 4.605。模型越自信地把真实结果说成“不可能”,代价就越大。
严格的 0 概率会让真实发生事件的损失变为无穷。实现时需要留意数值稳定性,不能先把极小的概率算成 0,再指望取对数恢复它。第七章会从模型的原始分数出发,使用稳定的计算方式。
多分类也是同一个想法:模型给每个类别一个概率,观察到哪个类别,就用那个类别的负对数概率作为损失。预测下一个 Token 的语言模型,也会用到这样的目标,只是类别变成了词表中的 Token,并且概率以已有上下文为条件。
4. 熵:这份分布本身有多难猜
先不讨论训练,把注意力放在一个已知的离散分布 上。它的各类概率记作 。
一次结果越少见,看到它时越“意外”。可以用 给这种意外程度赋一个数值。把所有可能结果按真实概率加权平均,就得到熵:
比如两个类别的概率为 ,熵约为 0.562。若两个类别各占一半,则熵为 ,平均更难猜;若结果完全确定,熵就是 0。
这里使用自然对数,单位常称为 nat;若改用以 2 为底的对数,单位就是 bit。换底只会整体缩放数值,不改变哪种预测更好。
这不是说某个具体对象“有多少知识”,也不是说熵越低就越有价值。它衡量的是一个给定概率分布中的不确定性,必须先说清楚分布描述什么。
5. 交叉熵:真实分布和预测分布不一样时
现在真实分布仍是 ,但我们按另一个分布 预测。结果按 发生,意外程度却按模型的 计算,于是得到交叉熵:
如果 ,而模型只给出 ,交叉熵为 0.693。若模型恰好使用 ,交叉熵就回到熵 0.562。
两者的差可以写成:
这叫 KL 散度。对于合法的离散分布,它非负;两个分布相同则为 0。如果某个类别在 中概率大于 0,却被 赋成 0,散度会是无穷。
图 2:固定真实分布 时, 不变,最小化交叉熵也就等价于最小化 。
KL 散度能衡量分布差异,但不是普通的几何距离。尤其是,交换顺序通常会改变结果:
本例两个方向分别约为 0.131 和 0.144。不要因为符号中间看起来像一条分隔线,就认为左右对称。
单条标签怎么变成一个分布
如果一条样本的真实类别是第 2 类,可以用 表示它。这个只有真实类别位置为 1、其余为 0 的向量叫 one-hot 向量。
把它代入交叉熵,零项都不贡献,剩下:
这就接回了前面的分类损失。单条标签的 one-hot 表示,不代表真实世界对这个输入完全没有随机性;它只是这次实际观测结果的编码方式。
对于 的项,我们采用连续极限约定,让它贡献 0。代码会明确跳过这些项,避免直接计算 。
6. 平方误差也能从概率出发
再回到打印耗时。假设模型预测的是条件均值 ,真实耗时还叠加了一份噪声:
表示所有待训练参数。例如,直线模型中就是斜率和固定项。假设噪声彼此独立,都服从均值为 0、方差为固定 的正态分布。把预测值简记为 ,单条样本的条件密度为:
这里是概率密度,不是连续变量恰好等于某个实数的概率。和第三章一样,不要把密度的高度当作事件概率。
把各样本密度相乘、取负对数,可以整理为:
不随模型参数变化。在 固定时,最小化这个式子,就是最小化平方误差之和;再除以 ,就是 MSE。
原来,平方误差与正态噪声模型可以联系起来。
这不表示使用 MSE 前必须证明所有噪声都正态。即使不采用这个概率解释,MSE 仍可以作为一个明确的优化目标;只是不能顺手把正态模型下的全部结论都带过去。极端误差会被平方放大,如果任务更在意典型绝对偏差,也可能选择其他目标。
7. 手算、画图,再用代码核对
下面的脚本使用 Python 3.9+ 标准库,比较不同候选概率,并计算熵、交叉熵和两个方向的 KL 散度:
python3 src/examples/learning_objectives.py
也可以下载脚本,执行 python3 learning_objectives.py。
"""第 5 章:伯努利似然、交叉熵与 KL 散度;自然对数,单位 nat。"""
from math import exp, inf, isclose, log
LABELS = [1] * 7 + [0] * 3
def bernoulli_nll(labels, probability):
if not 0 < probability < 1:
raise ValueError("本例的候选概率必须严格位于 0 和 1 之间")
return -sum(log(probability) if y == 1 else log(1 - probability) for y in labels)
def cross_entropy(p, q):
if len(p) != len(q) or not p:
raise ValueError("两个分布必须非空且类别一一对应")
for distribution in (p, q):
if any(value < 0 for value in distribution) or not isclose(sum(distribution), 1):
raise ValueError("概率必须非负且总和为 1")
result = 0.0
for actual, predicted in zip(p, q):
if actual == 0:
continue # 采用 0 log 0 = 0 的连续极限约定
if predicted == 0:
return inf
result -= actual * log(predicted)
return result
def main():
for probability in [0.2, 0.5, 0.7, 0.9]:
value = bernoulli_nll(LABELS, probability)
likelihood = exp(-value)
print(f"p={probability:.1f},似然={likelihood:.6f},平均负对数似然={value / len(LABELS):.6f}")
candidates = [i / 100 for i in range(1, 100)]
best = min(candidates, key=lambda p: bernoulli_nll(LABELS, p))
print(f"网格搜索选择的概率:{best:.2f}")
p, q = [0.75, 0.25], [0.5, 0.5]
entropy = cross_entropy(p, p)
cross = cross_entropy(p, q)
reverse_kl = cross_entropy(q, p) - cross_entropy(q, q)
print(f"H(p)={entropy:.6f},H(p,q)={cross:.6f}")
print(f"KL(p||q)={cross - entropy:.6f},KL(q||p)={reverse_kl:.6f}")
if __name__ == "__main__":
main()
结果为:
p=0.2,似然=0.000007,平均负对数似然=1.193550
p=0.5,似然=0.000977,平均负对数似然=0.693147
p=0.7,似然=0.002224,平均负对数似然=0.610864
p=0.9,似然=0.000478,平均负对数似然=0.764528
网格搜索选择的概率:0.70
H(p)=0.562335,H(p,q)=0.693147
KL(p||q)=0.130812,KL(q||p)=0.143841
这段代码没有使用分类模型,只是在一组候选概率中比较目标函数。先把“为什么这个损失更小”看清楚,再用梯度去训练复杂模型,会容易很多。
本例 0.01 间隔的网格恰好包含 0.7。换成别的数据,最优概率未必恰好落在这些候选值上。和导读的枚举一样,搜索结果受候选范围与精度限制。
8. 第一篇实践:让一条直线把五章串起来
现在可以做一次完整的小实验了。目标很具体:拟合一条直线,看看学习率、数据噪声和样本数量分别会造成什么变化。
我们自己生成数据:
是在 5~25 页之间均匀抽取的整数, 分别设为 2 秒或 10 秒。真实关系是我们规定的,所以能知道模型到底偏离了这条线多少。这是模拟实验,不是对真实打印机的性能报告。
先把计算尺度整理一下
直接用页数训练,斜率梯度会乘上较大的 。为了更容易选择学习率,我们把已知输入范围换算为:
于是 落在 内,训练的模型是 。这里的 15 和 10 来自实验预先规定的范围,不是拿测试结果挑出的数字。
要换回原始页数的表达,只需展开:
因此,归一化后的 ,就对应原来的斜率 3、固定项 20。变换坐标并没有改变我们能表达的这组直线。
实验一:只改变学习率
固定 80 条、噪声标准差为 2 秒的训练数据,从相同初值开始,分别用 0.02、0.2、1.1 的学习率更新:
图 3:纵轴采用对数刻度,每跨一个相同的刻度距离,表示相同的倍数变化。发散曲线达到演示阈值后提前停止。
这次不比较哪个模型泛化更好,只看同一个目标怎样被优化。小步子可能只是还没走够;发散则需要重新考虑步长和尺度。
实验二:改变噪声和样本量
然后固定学习率和优化方式,比较四种组合:8 条或 80 条样本,噪声标准差为 2 秒或 10 秒。每种设置用 30 个种子重新生成数据并训练。
对每个模型,在 5~25 页的整数网格上,计算它与无噪声真函数 的平均绝对差,下面称为“参考 MAE”。
这不是新任务上的实际预测误差:真实新任务还有噪声。我们只是借助模拟中已知的真函数,单独观察拟合本身偏到了哪里。真实项目不知道这条真线,需要独立的验证与测试数据;第六章会采用那样的流程。
图 4:每个点代表一次重新生成数据并训练的实验,橙色短线是 30 次结果的平均值。散点的分散程度不是均值的置信区间。
把实验跑起来
实践脚本同样只使用 Python 标准库。运行:
python3 src/examples/foundations_practice.py
下面保留完整实现,方便你改动生成数据和训练部分:
"""第一篇实践:已知生成规律的模拟实验;没有真实设备数据。"""
from math import isfinite
from random import Random
from statistics import mean, stdev
def make_data(count, noise, seed):
rng = Random(seed)
data = []
for _ in range(count):
pages = rng.randint(5, 25)
seconds = 3 * pages + 20 + rng.gauss(0, noise)
data.append((pages, seconds))
return data
def predict(pages, a, b):
# 让输入落在 [-1, 1],避免原始页数尺度使步长过大。
return a * ((pages - 15) / 10) + b
def fit(data, rate=0.2, steps=1000):
a, b = 0.0, 0.0
history = [mean(y*y for _, y in data)]
for _ in range(steps):
errors = [predict(x, a, b) - y for x, y in data]
da = 2 * mean(error * ((x-15)/10) for error, (x, _) in zip(errors, data))
db = 2 * mean(errors)
a, b = a - rate * da, b - rate * db
loss = mean((predict(x, a, b) - y)**2 for x, y in data)
history.append(loss)
if not isfinite(loss) or loss > 1e12:
break # 停止明显发散的演示,避免继续放大数值。
if max(abs(da), abs(db)) < 1e-10:
break
return a, b, history
def reference_mae(a, b):
# 仅在模拟中知道无噪声真函数,用固定网格衡量拟合的偏离。
pages = range(5, 26)
return mean(abs(predict(x, a, b) - (3*x+20)) for x in pages)
def repeated_errors(count, noise):
errors = []
for seed in range(1000, 1030):
a, b, _ = fit(make_data(count, noise, seed))
errors.append(reference_mae(a, b))
return errors
def main():
data = make_data(80, noise=2, seed=2026)
for rate in [0.02, 0.2, 1.1]:
a, b, history = fit(data, rate, steps=120)
if not isfinite(history[-1]) or history[-1] > 1e12:
print(f"学习率 {rate:g}:损失过大,在第 {len(history)-1} 步停止")
else:
print(f"学习率 {rate:g}:120 步后训练 MSE={history[-1]:.3f}")
a, b, _ = fit(data)
print(f"换回页数单位的直线:y ≈ {a/10:.3f} x + {b-1.5*a:.3f}")
for noise in [2, 10]:
for count in [8, 80]:
errors = repeated_errors(count, noise)
print(f"噪声标准差={noise},样本数={count}:参考 MAE 均值={mean(errors):.3f},实验间标准差={stdev(errors):.3f}")
if __name__ == "__main__":
main()
本次运行得到:
学习率 0.02:120 步后训练 MSE=7.236
学习率 0.2:120 步后训练 MSE=4.722
学习率 1.1:损失过大,在第 42 步停止
换回页数单位的直线:y ≈ 2.999 x + 19.478
噪声标准差=2,样本数=8:参考 MAE 均值=0.732,实验间标准差=0.365
噪声标准差=2,样本数=80:参考 MAE 均值=0.280,实验间标准差=0.146
噪声标准差=10,样本数=8:参考 MAE 均值=3.661,实验间标准差=1.825
噪声标准差=10,样本数=80:参考 MAE 均值=1.398,实验间标准差=0.728
数据越多,平均拟合偏差在这组模拟中越小;噪声更大,拟合也更容易摇摆。但别把四个平均数读成“每增加十倍样本,所有任务都会改善同样多”。这里的结论依赖于我们设定的线性真函数、独立正态噪声和输入范围。
如果目标关系本来就不是直线,再增加样本,也不能让一条直线完整表示所有弯曲。数据数量、模型表达能力和优化过程,是三个不同的问题。
最后留下几行自己的实验记录:用了哪些设置、原先猜会怎样、实际发生了什么,以及这个模拟还没有验证什么。它们会在后续章节的模型比较中继续派上用场。
9. 自测与小实验
- 最大化似然为什么可以改写成最小化负对数似然?再除以固定样本数,会改变最优参数吗?
- 十次独立观测中有八个 1、两个 0,使用共同伯努利概率时,最大似然估计是多少?
- 标签为 1 时,模型给出 0.9 和 0.1,哪一个交叉熵损失更大?
- 对 ,为什么 在 时达到最小值?这是否表示熵也一定等于 0?
- 若真实可能发生的类别被预测为概率 0,交叉熵会怎样?
- 什么时候本章的正态噪声负对数似然与 MSE 有相同的最优模型参数?
- 实践中若得到 ,换回原始页数后,直线是什么?增加样本量能保证训练损失每次都下降吗?
展开参考答案
- 对数严格递增,保留大小顺序;负号把最大化变成最小化。除以同一个正数也不改变最优位置。
- 为 。它是这份数据下的估计,不是真实未来概率的保证。
- 0.1 的损失更大,约为 2.303;0.9 的损失约为 0.105。
- 因为 ,KL 散度非负,两个分布相同时为 0。此时交叉熵等于熵,但这个 的熵仍大于 0。
- 会成为无穷。实现中需要注意端点与数值稳定性,不能把数值下溢误当作真实概率为 0。
- 在本章给定的独立、同方差正态噪声模型下,且 固定时,与模型参数有关的项只剩正倍数的平方误差之和。
- 为 。增加数据不保证训练损失每次下降:新样本可能更难、噪声也不同;更多数据带来的价值主要在于更可靠地估计目标关系,而不是让旧的训练分数越来越漂亮。
进阶选做:给真函数加上 ,仍然只拟合直线。重做噪声与样本量实验,观察模型表达能力不够时,增加数据能解决哪些问题,又留下哪些问题。
第一篇到这里告一段落。接下来进入线性回归:第一个完整模型,把数据划分、训练、正则化和评估放到同一个实际流程中。
想进一步核对概率解释,可以选读 Stanford CS229 讲义中线性回归的概率解释部分;关于熵、交叉熵和 KL 散度,可参阅《深度学习》的概率与信息论章节。先围绕本章例子查阅即可,不必一口气读完整章。