某种病的患病率是 1%,检测的准确率是 95%。你测出阳性,真正患病的概率是多少?大多数人(包括不少医生)脱口而出”95%“,正确答案不到 20%。概率论的价值就在这里:直觉在不确定性面前很容易出错,而一套简单的计算规则能把它纠正过来。

前置:会用 Python 写循环和函数即可。

从数数开始

最朴素的概率定义是:所有结果等可能时,

于是概率问题的第一步往往是计数。经典的生日问题:一个房间里有 个人,至少两人同一天生日的概率是多少?

直接数”至少两人相同”很麻烦,数反面容易得多:所有人生日都不同。第一个人有 365 种选择,第二个人 364 种,……,第 个人 种。

import numpy as np
def p_shared(k):
    p_distinct = 1.0
    for i in range(k):
        p_distinct *= (365 - i) / 365
    return 1 - p_distinct
 
for k in [10, 23, 50, 70]:
    print(k, round(p_shared(k), 4))
 
# 用模拟交叉验证:公式和实验应当吻合
rng = np.random.default_rng(0)
trials = rng.integers(0, 365, size=(100_000, 23))
hit = np.mean([len(set(row)) < 23 for row in trials])
print("simulated k=23:", hit)

输出:

10 0.1169
23 0.5073
50 0.9704
70 0.9992
simulated k=23: 0.50699

23 个人就超过一半。直觉错在哪?我们下意识地在想”有人和我同一天生日”,但实际要比较的是所有人对:23 个人有 对,每对撞上的概率是 ,机会比想象的多得多。这和哈希冲突是同一个现象:往 个桶里扔大约 个元素就很可能撞上(见 数据结构 里的哈希表)。

两个技巧值得留下:一是算补集,“至少一个”的问题几乎总该从”一个也没有”算起;二是用模拟验证,几行 numpy 就能检查一个公式是否算错,这在后面会反复使用。计数本身的系统方法(排列、组合、容斥)见 离散数学。

朴素定义的前提”等可能”经常被悄悄违反。掷两颗骰子,点数和为 7 有 6 种结果、和为 12 只有 1 种,如果把”和”的 11 个取值当成等可能,就会错。判断标准是回到最细的结果(有序的两颗骰子点数对,共 36 个),确认它们对称,再去数。

停一下,检查理解单选

一个哈希表有 10000 个桶,键均匀随机地落入桶中。插入大约多少个键时,出现至少一次冲突的概率就超过一半?

条件概率与贝叶斯

现在回到开头的检测问题。我们需要的工具是条件概率:已知 发生, 发生的概率

直觉是:既然知道 发生了,样本空间就缩小成 ,在里面看 占多少。

把定义改写一下,,再代回去,就得到贝叶斯公式:

分母 通常用全概率公式展开:把世界按 和非 切成两块,。

代入数字。设 = 患病, = 阳性。,,。

不用公式也能想明白:想象 10000 个人,其中 100 人患病,95 人测出阳性;9900 个健康人里有 5% 也就是 495 人假阳性。阳性一共 590 人,真病人只有 95 个。先验太小时,即便检测很准,假阳性的人数也会压倒真阳性。

import numpy as np
rng = np.random.default_rng(1)
n = 1_000_000
sick = rng.random(n) < 0.01
# 病人以 0.95 测阳,健康人以 0.05 测阳
positive = np.where(sick, rng.random(n) < 0.95, rng.random(n) < 0.05)
print("P(sick | positive) ≈", sick[positive].mean().round(4))
# 再测一次也阳性:上一次的后验成为这一次的先验
both = positive & np.where(sick, rng.random(n) < 0.95, rng.random(n) < 0.05)
print("P(sick | 两次阳性) ≈", sick[both].mean().round(4))

输出:

P(sick | positive) ≈ 0.1617
P(sick | 两次阳性) ≈ 0.7849

第二次检测把概率从 16% 推到 78% 左右。这体现了贝叶斯思维的核心:后验 ∝ 似然 × 先验,新证据不断更新旧信念。这个前提是两次检测在给定是否患病的条件下相互独立。

独立的定义是 ,等价于 :知道 不改变你对 的判断。独立和互斥是两码事,互斥的两个事件(概率都大于 0)一定不独立,因为知道一个发生,另一个就不可能发生了。

上面两次检测用到的是更细的概念:条件独立。给定是否患病,两次检测结果互不影响;但不给定这个条件时,两次结果是强相关的,第一次阳性会让第二次阳性的概率明显上升,因为它提高了”患病”的可能。条件独立和独立互不蕴含。朴素贝叶斯分类器的”朴素”,就是假设给定类别后各个特征条件独立,这个假设通常不成立,但分类效果往往依然不错。

条件概率还有一个经典陷阱:Monty Hall。三扇门后一辆车、两只羊,你选 1 号门,知道答案的主持人打开另一扇有羊的门,问你换不换。换门赢的概率是 2/3。关键在于主持人的行为不是随机的,他必须开一扇羊门,这个约束本身就携带了信息。条件概率的计算永远要问清楚:信息是怎么产生的。

停一下,检查理解单选

某垃圾邮件过滤器:垃圾邮件占 20%,它对垃圾邮件报警的概率是 90%,对正常邮件误报的概率是 10%。一封邮件被报警,它是垃圾邮件的概率最接近多少?

随机变量与期望

事件只能回答”是/否”,很多问题关心的是一个数:掷 10 次硬币几次正面?一个请求的延迟多少毫秒?随机变量 就是把每个结果映射到一个数的函数,它的分布描述了每个取值的概率。

最常用的汇总量是期望,即按概率加权的平均:。期望有一条几乎被低估的性质,线性性:

它不需要 和 独立。这让很多看起来很难的问题变得一行可解,诀窍是指示变量:把复杂的计数拆成一堆 0/1 变量之和。

例子: 个人把帽子扔进一堆,随机各取一顶,平均有几个人拿回自己的帽子?设 表示第 个人拿回自己的帽子,那么 ,所以

无论 是 3 还是 100 万,答案都是 1。各 之间显然不独立(前 个人都拿对了,最后一个人一定拿对),但线性性不在乎。

import numpy as np
rng = np.random.default_rng(2)
for n in [3, 10, 1000]:
    matches = [(rng.permutation(n) == np.arange(n)).sum() for _ in range(20000)]
    print(n, np.mean(matches).round(3), "P(0 matches) =", np.mean(np.array(matches) == 0).round(3))

输出:

3 1.001 P(0 matches) = 0.333
10 1.001 P(0 matches) = 0.367
1000 1.001 P(0 matches) = 0.367

顺便看到一个漂亮的数: 大时一个都没拿对的概率趋向 。期望告诉我们中心在哪,方差告诉我们离中心有多散:

方差不是线性的:;只有 独立(更准确地说不相关)时才有 。一般情况要加上 ,协方差 衡量两者一起变动的倾向,归一化后就是相关系数 。注意相关系数为 0 只说明没有线性关系: 在 上均匀分布, 完全由 决定,但两者相关系数为 0。

还有一个计算期望的利器:全期望公式 ,即先在每种情况下求条件期望,再按情况的概率加权平均。它的用法是”对第一步取条件”。

例子:反复掷硬币直到出现正面,平均要掷几次?设答案为 。第一次掷出正面(概率 ),总共 1 次;掷出反面(概率 ),已经用掉 1 次,而且局面和开始时一模一样,还要平均再掷 次。于是

这正是几何分布的期望。同样的套路可以求”连续两次正面”要等多久:设三个状态(刚开始、上一次是正面、已完成),对每个状态列一个方程,解一个小线性方程组,答案是 6 次。```python import numpy as np rng = np.random.default_rng(6) def flips_until_HH(): count, prev_head = 0, False while True: count += 1 head = rng.random() < 0.5 if head and prev_head: return count prev_head = head print(“E[等到 HH 的次数] ≈”, np.mean([flips_until_HH() for _ in range(100_000)]).round(3))


输出:

```text
E[等到 HH 的次数] ≈ 5.994

这种”按下一步的情况拆开,得到关于期望的方程”的思路,在强化学习里就是 Bellman 方程。

停一下,检查理解多选

关于期望和方差,哪些说法正确?

几个反复出现的分布

具体问题里的随机变量,大多数属于少数几个”家族”。记住它们各自描述什么故事,比记住公式更有用。

分布故事期望方差
Bernoulli()一次试验成功与否
Binomial() 次独立试验的成功次数
Geometric()直到第一次成功所需的试验次数
Poisson()单位时间内稀有事件发生次数
Uniform()区间上等可能
Exponential()稀有事件之间的等待时间
Normal()许多小扰动之和

它们之间有联系。Binomial 是 个 Bernoulli 之和,所以期望 可以直接由线性性得到。 很大、 很小、 固定时,Binomial 近似 Poisson:一个网站每秒有一百万用户、每人以百万分之三的概率点击,每秒点击数大约是 Poisson(3)。Poisson 过程里两次事件之间的间隔服从指数分布。

连续分布用概率密度 描述,。密度本身不是概率,可以大于 1,只有积分才是。

指数分布有一个反直觉的性质:无记忆性。。已经等了 10 分钟的公交(如果到达间隔真的是指数分布),剩余等待时间的分布和刚到站时完全一样。几何分布是它的离散版本。

import numpy as np
rng = np.random.default_rng(3)
x = rng.exponential(scale=5.0, size=1_000_000)  # 平均等待 5 分钟
print("E[X]             ≈", x.mean().round(3))
print("E[X - 10 | X>10] ≈", (x[x > 10] - 10).mean().round(3))  # 已经等了 10 分钟
# Binomial(n=1000, p=0.003) 与 Poisson(3) 的前几项概率
from math import comb, exp, factorial
for k in range(5):
    b = comb(1000, k) * 0.003**k * 0.997**(1000 - k)
    p = exp(-3) * 3**k / factorial(k)
    print(k, round(b, 4), round(p, 4))

输出:

E[X]             ≈ 5.004
E[X - 10 | X>10] ≈ 4.99
0 0.0496 0.0498
1 0.1491 0.1494
2 0.2242 0.224
3 0.2244 0.224
4 0.1683 0.168

正态分布在表里看似只是一员,但它的地位特殊,原因在下一节。

停一下,检查理解单选

服务器平均每分钟收到 2 个故障报警,报警按 Poisson 过程到达。刚刚收到一个报警,到下一个报警的平均等待时间是多少?

大数定律与中心极限定理

前面一直在用模拟验证答案:扔 10 万次,取平均,和公式对上。为什么这么做是对的?

设 独立同分布,均值 ,方差 。样本均值 的期望是 ,方差是 (独立时方差相加,再除以 )。方差随 趋于 0,所以:

大数定律:。模拟有效、蒙特卡洛估计有效、小批量梯度是全量梯度的无偏估计,都是这条定律在背书。

方差 还告诉我们误差缩得多快:标准差是 。想把误差减半,样本要变成 4 倍;想多一位有效数字,样本要变成 100 倍。

大数定律只说均值收敛到哪,没说它在收敛途中长什么样。中心极限定理回答了这个:不管 本身是什么分布(只要方差有限),

这就是正态分布无处不在的原因:许多独立小扰动的叠加,结果都像正态。

import numpy as np
rng = np.random.default_rng(4)
# 指数分布严重右偏,看看它的样本均值会怎样
for n in [1, 5, 30, 200]:
    means = rng.exponential(1.0, size=(100_000, n)).mean(axis=1)
    z = (means - 1.0) / (1.0 / np.sqrt(n))
    skew = np.mean(z**3)
    inside = np.mean(np.abs(z) < 1.96)
    print(f"n={n:3d}  std(mean)={means.std():.4f}  skew={skew:.3f}  P(|z|<1.96)={inside:.3f}")

输出:

n=  1  std(mean)=0.9956  skew=1.981  P(|z|<1.96)=0.949
n=  5  std(mean)=0.4471  skew=0.895  P(|z|<1.96)=0.956
n= 30  std(mean)=0.1818  skew=0.347  P(|z|<1.96)=0.953
n=200  std(mean)=0.0707  skew=0.145  P(|z|<1.96)=0.951

指数分布的偏度是 2,取 30 个平均后偏度降到 0.35 左右,200 个时只剩 0.15,大致按 衰减,分布越来越对称。样本均值的标准差也精确地按 缩小。最后一列要谨慎解读: 时落在 内的比例也恰好接近 95%,这只是巧合,单看一个区间概率并不能说明分布已经是正态的,偏度这种形状指标更能说明问题。

这两个定理有两个直接的工程含义。第一,误差棒:报告一个从 次实验估计出的指标时, 给出约 95% 的置信区间。比较两个模型在 benchmark 上的分数时,如果题目只有几百道,1–2 个百分点的差距可能完全在噪声里(见 评估)。第二,batch size:梯度是许多样本梯度的平均,batch 增大到 4 倍,梯度噪声的标准差减半。

定理的前提也要记住。独立和方差有限任何一个不满足,结论都可能失效。网络延迟、文件大小这类重尾分布,少数极端值主导平均,均值收敛得非常慢;这时看中位数和 p99 比看均值更有意义。

停一下,检查理解单选

用 1 万次模拟估计一个概率,标准误差约为 0.005。想把标准误差降到 0.001,大约需要多少次模拟?

从概率到估计:最大似然

到这里,我们一直在”已知分布,算概率”。机器学习面对的是反方向:已知数据,猜分布的参数。

扔一枚硬币 10 次,7 次正面。 该估计为多少?最大似然估计(MLE)的想法是:选择让观测数据最可能出现的参数。似然函数

取对数(乘积变求和,单调性不变),求导令其为 0:,得 。和直觉一致,就是频率。

import numpy as np
p = np.linspace(0.01, 0.99, 99)
loglik = 7 * np.log(p) + 3 * np.log(1 - p)
print("argmax p =", p[np.argmax(loglik)].round(2))
# 正态数据的 MLE:均值就是样本均值,方差是除以 n 的样本方差
rng = np.random.default_rng(5)
x = rng.normal(loc=3.0, scale=2.0, size=10_000)
print("mu_hat =", x.mean().round(3), " sigma2_hat =", x.var().round(3))

输出:

argmax p = 0.7
mu_hat = 3.043  sigma2_hat = 4.047

这件事和机器学习的关系比看上去紧密得多。对独立样本,最大化 等价于最小化负对数似然。若假设 服从以模型输出为中心的正态分布,负对数似然就是均方误差;若假设 是类别分布、模型输出 softmax 概率,负对数似然就是交叉熵。语言模型预训练最小化的正是下一个 token 的负对数似然。这条推导在 从线性回归到神经网络 里展开。

MLE 只看数据。数据很少时它会给出极端估计:扔 3 次全是正面,MLE 说 ,即下次一定正面,这显然过于自信。贝叶斯的做法是给参数一个先验,再用数据更新。选后验最大的点,叫 MAP 估计。对硬币,如果先验相当于”事先已经看过 1 次正面 1 次反面”,3 次正面后的 MAP 估计是 而不是 1。对线性模型,参数的正态先验在 MAP 里变成了 L2 正则项,也就是 weight decay。正则化可以理解成先验。

停一下,检查理解多选

关于最大似然估计,哪些说法正确?

记住这三件事

  1. 条件概率要先问”信息是怎么来的”;先验很小时,即使证据很准,后验也可能很低。
  2. 期望是线性的,不需要独立;把复杂计数拆成指示变量,很多问题一行可解。
  3. 样本均值的误差按 缩小且近似正态;最大似然把”拟合数据”变成”最小化负对数似然”,交叉熵和 MSE 都由此而来。

延伸阅读

  • Harvard Stat 110(Joe Blitzstein)课程主页,讲课视频在 YouTube
  • Blitzstein & Hwang,《Introduction to Probability》(免费 PDF)
  • Seeing Theory:布朗大学的可视化概率入门
  • MIT 6.041 / 6.431《Probabilistic Systems Analysis》(Bertsekas & Tsitsiklis 教材)
  • Christopher Bishop,《Pattern Recognition and Machine Learning》第 1–2 章(MLE、MAP 与 ML 的联系)