CS221 · 人工智能:原理与技术
Lecture 12 · 贝叶斯网络 I:建模与推断
源文件:lecture-12.md
Lecture 12 · 贝叶斯网络 I:建模与推断
CS221: Artificial Intelligence — Principles and Techniques · Stanford · Autumn 2025
📅 Oct 29 · 💻bayes.py
承上启下:从状态型模型到变量型模型
前几周(状态型模型 state-based models):我们一直用「状态 + 动作」对世界建模,核心活动是推理与搜索。回顾这条线:
- 搜索问题中动作结果是确定性的,用 Lecture 5 · 搜索算法 I 与 Lecture 6 · UCS 与 A* 搜索 的算法找最优动作序列;
- MDP 中动作结果是随机的,用 Lecture 7 · 马尔可夫决策过程 的价值迭代求最优策略;
- 博弈中存在对手,用 Lecture 10 · 博弈 I:Minimax 与 α-β 剪枝 的 minimax / expectimax 应对;
- 这些模型虽然形态各异,但共同点是:核心问题始终是「该采取什么动作」。
本讲开始(变量型模型 variable-based models):换一种建模视角——用一组随机变量描述世界,做概率推断:
- 不再问「怎么行动」,而是问「世界是什么样子」;
- 把世界的状态表示成一组变量 $X = (X_1, \dots, X_n)$;
- 用联合概率分布 $P(X_1, \dots, X_n)$ 作为「真相之源(source of truth)」;
- 核心问题变成:给定证据,某个变量的概率是多少,即求 $P(Q \mid E = e)$。
提示
前半学期的模型都嵌在「感知—推理—行动」循环的行动子系统里,输出是一个动作或策略。但很多问题根本不涉及行动——医生想知道「病人得了什么病」,追踪系统想知道「物体现在在哪」。这些问题的输出是对世界状态的信念(belief),需要一种纯粹「用概率描述世界」的语言。这是本课程从「决策」走向「知识表示与推断」的分水岭。
为什么要建模世界? 回顾方法论的两个阵营(对应源码 model_based_motivation()):
| 阵营 | 含义 | 例子 |
|---|---|---|
| 无模型(model-free) | 只学「该做什么动作、能拿多少效用」,不解释世界 | 分类、回归、SARSA、Q-learning、TD 学习(Lecture 8 · 强化学习、Lecture 11 · 博弈 II:TD 学习与同时博弈) |
| 有模型(model-based) | 显式刻画世界如何运作 | 搜索、价值迭代(MDP)、minimax(博弈) |
无模型方法更直接、更便宜;有模型方法更灵活——比如可以在不改变转移模型的情况下更换奖励函数、回答训练时没预设的问题。贝叶斯网络把「有模型」推向极致:显式写出世界的概率结构。
本讲与后两讲构成一个完整单元:本讲讲建模(如何紧凑表示联合分布)与推断(精确推断 + 拒绝采样);Lecture 13 · 贝叶斯网络 II:Gibbs 采样与条件独立 讲更高效的近似推断与图结构编码的独立性;Lecture 14 · 贝叶斯网络 III:参数学习 回答「这些概率从哪来」。
1. 概率复习:联合、边缘、条件
在讲网络之前,先把三个基本操作理清。这一节对应源码的 review_probability()。
1.1 随机变量与联合分布
用一组随机变量(random variables)表示世界的属性:
- 阳光 $S \in \{0, 1\}$(sunshine)
- 下雨 $R \in \{0, 1\}$(rain)
对全部变量的一次赋值(assignment)就是世界的一个可能状态;两个二值变量共有 4 种状态。联合分布(joint distribution)给出每种赋值的概率:
| S | R | P(S, R) |
|---|---|---|
| 0 | 0 | 0.20 |
| 0 | 1 | 0.08 |
| 1 | 0 | 0.70 |
| 1 | 1 | 0.02 |
说明
任何关于这些变量的问题——边缘概率、条件概率、独立性——原则上都能从联合分布算出来。后面所有推断算法,本质上都是在「读」这张表(或它的紧凑表示)。
源码用一个通用的 ProbTable 类来承载概率表——它既能表示局部条件分布,也能表示边缘/条件分布:
class ProbTable:
"""任意概率表:局部条件分布 / 边缘分布 / 条件分布都可以。
描述串形如 "S R | C D=1":竖线左边是「生成变量」,右边是「条件变量」。
数据可以是概率张量,也可以是「赋值 → 概率」的函数。
"""
def __init__(self, description: str, data, shape=None):
if callable(data):
# 用函数逐格填充张量
self.probs = np.empty(shape)
def recurse(assignment):
if len(assignment) == len(shape):
self.probs[tuple(assignment)] = data(*assignment)
else:
for i in range(shape[len(assignment)]):
recurse(assignment + [i])
recurse([])
else:
self.probs = np.array(data)
# …解析 description,分出 gen_vars / cond_vars…
@property
def p(self):
return self.probs
用它建出上面的联合分布表:
# 行是 S,列是 R
P_SR = ProbTable("S R", [[0.20, 0.08],
[0.70, 0.02]])
提示
一个 $n$ 变量的联合分布,就是一个 $n$ 维张量(tensor),每个维度对应一个变量的取值域。既然是张量,边缘化、条件化等所有概率运算就都能用 einsum(einops 记号)统一表达——「对某变量求和」就是「把该下标从输出里删掉」。这是本讲代码的统一语言,也和 Lecture 1 · 张量、梯度与监督学习 里的张量视角一脉相承。
1.2 边缘化(Marginalization)
边缘化掉一个变量 = 把只在该变量上取值不同的赋值「压扁」相加。以求 $P(S)$ 为例:
$$P(S = s) = \sum_{r} P(S = s, R = r)$$
其中 $s, r$ 是具体取值,求和遍历被边缘化变量 $R$ 的整个取值域。代入数值:
$$P(S = 0) = 0.20 + 0.08 = 0.28, \qquad P(S = 1) = 0.70 + 0.02 = 0.72$$
用 einsum 一行搞定——把 r 从输出里去掉,就等于对 r 求和:
# 对 r 求和:s r -> s
P_S = ProbTable("S", einsum(P_SR.p, "s r -> s")) # [0.28, 0.72]
1.3 条件化(Conditioning)与贝叶斯定理
条件化在 $R = 1$ 上 = 只保留满足条件的赋值,再重新归一化。写成公式:
$$P(S = s \mid R = 1) = \frac{P(S = s, R = 1)}{P(R = 1)}, \qquad P(R = 1) = \sum_{s} P(S = s, R = 1)$$
分子是「筛选出的那一片联合概率」,分母是证据概率(保证条件分布归一)。分三步执行:
- 筛选(select):只留 $R = 1$ 的行 → $P(S = 0, R = 1) = 0.08$,$P(S = 1, R = 1) = 0.02$
- 算证据概率:$P(R = 1) = 0.08 + 0.02 = 0.10$
- 除以证据概率:$P(S = 0 \mid R = 1) = 0.08 / 0.10 = 0.8$,$P(S = 1 \mid R = 1) = 0.02 / 0.10 = 0.2$
这三步(筛选 → 算证据 → 相除)是本讲所有精确推断的骨架。用 einsum 表达:
R1 = np.array([0, 1]) # 证据向量 [R = 1]
# 第 1 步:用 R1 把张量筛到 r = 1 → s r, r -> s
P_SR1 = ProbTable("S R=1", einsum(P_SR.p, R1, "s r, r -> s"))
# 第 2 步:对 s 求和,得到 P(R = 1) → s ->
P_R1 = ProbTable("R=1", einsum(P_SR1.p, "s ->"))
# 第 3 步:相除得到条件分布
P_S_given_R1 = ProbTable("S | R=1", P_SR1.p / P_R1.p) # [0.8, 0.2]
提示
条件化的定义是 $P(S \mid R) = P(S, R) / P(R)$;而联合又能反着分解为 $P(S, R) = P(R \mid S)\,P(S)$,代入就得到贝叶斯定理(Bayes’ theorem):
$$P(S \mid R) = \frac{P(R \mid S)\, P(S)}{P(R)}$$
其中 $P(S)$ 是先验(prior),$P(R \mid S)$ 是似然(likelihood),$P(R)$ 是证据(evidence),起归一化作用。「先验 × 似然,再归一化」正是筛选—算证据—相除的另一种说法。
本节小结:
| 操作 | 直觉 | einsum 动作 |
|---|---|---|
| 联合分布 | 真相之源 | —— |
| 边缘化 | 压扁掉不关心的变量 | 从输出下标里删掉它(求和) |
| 条件化 | 按证据筛选 + 归一化 | 乘证据向量 → 求和 → 相除 |
2. 概率推断(Probabilistic Inference)
对应源码 introduce_probabilistic_inference()。
设想 4 个变量:$S$(阳光)、$R$(下雨)、$T$(堵车 traffic)、$A$(秋天 autumn),联合分布是 $P(S, R, T, A)$。
问题:已知堵车且是秋天,下雨的概率是多少?
说明
类比:在数据库上执行 SQL 查询。 联合分布是那张大表,推断就是查询:WHERE 子句对应证据(条件化),SELECT 的列对应查询变量,没被提到的列被聚合掉(边缘化)。
一个问题(query)由两部分组成:
- 证据(evidence):已观测到的条件,如 $T = 1, A = 1$(堵车且秋天);
- 查询(query):关心的变量,如 $R$(是否下雨)。
写成概率记号就是 $P(R \mid T = 1, A = 1)$。
注意
既不在证据、也不在查询里的变量(这里是 $S$),全部被边缘化掉。它们不是我们关心的,但它们的存在会影响答案——正确做法是「求和积掉」,而不是无视或随便固定一个值。
一般形式(对应 introduce_bayesian_networks()):
$$\text{给定联合分布 } P(X_1, \dots, X_n),\ \text{证据 } E = e\ (E \subseteq X),\ \text{查询 } Q \subseteq X,\ \text{输出 } P(Q \mid E = e)$$
例如 $E = (X_3, X_7)$、$e = (0, 1)$、$Q = (X_2, X_4)$。
问题来了:变量一多,联合分布表就有 $2^n$ 项(二值变量情形),根本写不下、填不满。贝叶斯网络就是用来紧凑地表示联合分布的工具。
3. 贝叶斯网络:报警器例子
对应源码 introduce_alarm()。这是本讲的第一个核心例子。
3.1 问题
🚨 地震、入室盗窃与报警器
- 地震和盗窃是独立事件,各自发生概率 $\varepsilon = 0.05$
- 两者任一发生都会触发报警器
- 现在你听到了报警声
- 「得知发生了地震」会如何改变「发生盗窃」的概率?
比较两个量,哪个大?
- $P(B = 1 \mid A = 1)$:只知道报警响;
- $P(B = 1 \mid A = 1, E = 1)$:既知道报警响、又知道有地震。
要解决它,需要两步:构造联合分布 + 执行概率推断。
3.2 四步构造联合分布
不直接写庞大的联合表,而是用更符合直觉的方式。贝叶斯网络的建模只有 4 步:
- 定义变量:$B$(盗窃)、$E$(地震)、$A$(报警)
- 用有向边连接变量,边表示「直接影响」:$B \to A$、$E \to A$
- 为每个变量写下局部条件分布 $p(x \mid \mathrm{parents}(x))$
- 联合分布 = 各局部条件分布之积
图结构(有向无环图 DAG):
graph TD
B["B 盗窃"] --> A["A 报警"]
E["E 地震"] --> A
style B fill:#e1f5fe,stroke:#0277bd
style E fill:#e1f5fe,stroke:#0277bd
style A fill:#ffe0b2,stroke:#e65100
第 3 步的局部条件分布(源码用 ProbTable + 函数表达):
epsilon = 0.05 # 罕见事件的概率
p_b = ProbTable("B", [1 - epsilon, epsilon]) # p(b):盗窃先验
p_e = ProbTable("E", [1 - epsilon, epsilon]) # p(e):地震先验
# p(a | b, e):报警 = 盗窃 OR 地震(确定性 OR)
p_a_given_be = ProbTable("A | B E",
lambda b, e, a: a == (b or e),
shape=(2, 2, 2))
说明
这里 $p(a \mid b, e) = \mathbf{1}[a = (b \lor e)]$ 是一个确定性的 OR——$\mathbf{1}[\cdot]$ 是指示函数,条件成立取 1、否则取 0。只要 $b$ 或 $e$ 有一个为 1,报警必响。它依然是合法的「条件分布」,只是概率非 0 即 1。
第 4 步,联合分布是三个局部分布的乘积——对所有取值组合 $b, e, a$:
$$P(B = b, E = e, A = a) = p(b)\, p(e)\, p(a \mid b, e)$$
其中小写 $p$ 是按定义给出的局部条件分布,大写 $P$ 是由乘积规则推导出的联合分布(记号约定见 §3.4 末尾)。
# 张量外积:b, e, (b e a) -> b e a
P_BEA = ProbTable("B E A",
einsum(p_b.p, p_e.p, p_a_given_be.p, "b, e, b e a -> b e a"))
提示
把一个指数级大的联合分布,分解成每个节点一张「只依赖自己父节点」的小表,联合分布 = 所有小表相乘。之所以能这样省参数,是因为图结构隐含了条件独立假设——每个变量给定父节点后,与其他「非后代」变量无关(这一点在 Lecture 13 · 贝叶斯网络 II:Gibbs 采样与条件独立 会被正式化为 d-分离)。
3.3 在报警网络上做推断
有了联合分布 $P(B, E, A)$,就能回答任何问题。全部用 §1.3 的「筛选 → 算证据 → 相除」三步。
① $P(B = 1)$:没有任何信息时的盗窃概率
P_B = ProbTable("B", einsum(P_BEA.p, "b e a -> b")) # 边缘化掉 E、A
结果与先验 $p(b)$ 一致——盗窃概率很低(0.05)。
② $P(B = 1 \mid A = 1)$:只知道报警响
a1 = np.array([0, 1]) # 证据 [A = 1]
P_BA1 = ProbTable("B A=1", einsum(P_BEA.p, a1, "b e a, a -> b")) # 筛选 A=1,积掉 E
P_A1 = ProbTable("A=1", einsum(P_BA1.p, "b ->")) # 证据概率 P(A=1)
P_B_given_A1 = ProbTable("B | A=1", P_BA1.p / P_A1.p) # 相除
结果:盗窃概率大幅上升!听到报警,自然更怀疑进贼了。
③ $P(B = 1 \mid A = 1, E = 1)$:报警响 + 有地震
a1 = np.array([0, 1]); e1 = np.array([0, 1]) # 双证据
P_BA1E1 = ProbTable("B A=1 E=1", einsum(P_BEA.p, a1, e1, "b e a, a, e -> b"))
P_A1E1 = ProbTable("A=1 E=1", einsum(P_BA1E1.p, "b ->"))
P_B_given_A1E1 = ProbTable("B | A=1 E=1", P_BA1E1.p / P_A1E1.p)
结果:盗窃概率又回落到很低!
例子
因为报警是确定性 OR,可以手算验证:
- $P(A = 1) = 1 - P(B = 0)P(E = 0) = 1 - 0.95^2 = 0.0975$(至少一个原因发生);
-
$B = 1$ 必然触发 $A = 1$,故 $P(B = 1, A = 1) = P(B = 1) = 0.05$,于是
$$P(B = 1 \mid A = 1) = \frac{0.05}{0.0975} \approx 0.51$$
——从 5% 跳到约 51%,放大十倍; -
再得知 $E = 1$:地震已经足以解释报警($E = 1 \Rightarrow A = 1$),证据 $A = 1$ 对 $B$ 不再提供任何额外信息,于是
$$P(B = 1 \mid A = 1, E = 1) = P(B = 1 \mid E = 1) = P(B = 1) = 0.05$$
——完全回落到先验。
3.4 关键现象:解释消去(Explaining Away)
为什么加上「有地震」后,盗窃概率反而降了?
说明
- 两个原因($B$、$E$)共同正向影响一个结果($A$);
- 一旦观测到结果($A = 1$),
- 再观测到其中一个原因($E = 1$),会降低另一个原因($B = 1$)的概率;
- 符号:$P(B = 1 \mid A = 1, E = 1) < P(B = 1 \mid A = 1)$;
- 即使两个原因本身相互独立!
提示
报警为什么响?地震已经把它「解释掉」了,所以不必再拿盗窃来解释。注意这个定性推理模式没有被写进任何一张概率表——它是从「联合分布 = 局部分布之积」的数学里自然涌现的。这正是贝叶斯网络的魅力:写下生成结构,合理的推理行为自动跟着来。
说明
- 小写 $e$:具体取值;大写 $E$:随机变量;
- 小写 $p(e)$:局部条件分布(按定义给出,是建模者写下的);
- 大写 $P(E)$:从联合分布按概率法则推导出的边缘/条件分布;
- 大写 $P(E = e)$:从某个分布里取出的一个具体概率(一个数)。
4. 医疗诊断例子
对应源码 introduce_medical_diagnosis()。同样的配方,但网络更复杂,解释消去以更微妙的方式出现。
4.1 建模
🩺 问题:你在咳嗽、而且眼睛发痒。你感冒了吗?
第 1 步,定义变量:
- 感冒 $C \in \{0, 1\}$(Cold)
- 过敏 $A \in \{0, 1\}$(Allergies)
- 咳嗽 $H \in \{0, 1\}$(cougH)
- 眼睛痒 $I \in \{0, 1\}$(Itchy eyes)
第 2 步,连边(DAG):
graph TD
C["C 感冒"] --> H["H 咳嗽"]
A["A 过敏"] --> H
A --> I["I 眼睛痒"]
style C fill:#e1f5fe,stroke:#0277bd
style A fill:#e1f5fe,stroke:#0277bd
style H fill:#ffe0b2,stroke:#e65100
style I fill:#ffe0b2,stroke:#e65100
提示
咳嗽 $H$ 有两个原因(感冒 $C$、过敏 $A$);眼睛痒 $I$ 只由过敏 $A$ 引起。$I$ 不是咳嗽的原因——但它会成为通往 $A$ 的一条「侧信道」:看到眼睛痒 → 更相信过敏 → 过敏足以解释咳嗽 → 感冒嫌疑下降。建图的时候只写「谁直接影响谁」,这条推理链是推断时自动走出来的。
第 3 步,局部条件分布:
p_c = ProbTable("C", [0.9, 0.1]) # p(c):感冒先验
p_a = ProbTable("A", [0.8, 0.2]) # p(a):过敏先验
# p(h | c, a):若 H 与 (C or A) 一致则 0.9,否则 0.1(带噪声的 OR)
p_h_given_ca = ProbTable("H | C A",
lambda c, a, h: 0.9 if h == (c or a) else 0.1,
shape=(2, 2, 2))
# p(i | a):若 I 与 A 一致则 0.9,否则 0.1
p_i_given_a = ProbTable("I | A",
lambda a, i: 0.9 if i == a else 0.1,
shape=(2, 2))
说明
报警例子里 $p(a \mid b, e)$ 是硬 OR(非 0 即 1);这里放宽成软 OR(noisy-OR 的简化版)——即使有病因,也只有 0.9 的概率出现症状;没有病因,也有 0.1 的概率「假咳」。这更贴近现实:因果关系几乎总带噪声。
第 4 步,联合分布 = 四个局部分布相乘:
$$P(C = c, A = a, H = h, I = i) = p(c)\, p(a)\, p(h \mid c, a)\, p(i \mid a)$$
# c, a, (c a h), (a i) -> c a h i
P_CAHI = ProbTable("C A H I",
einsum(p_c.p, p_a.p, p_h_given_ca.p, p_i_given_a.p,
"c, a, c a h, a i -> c a h i"))
4.2 推断
① $P(C = 1 \mid H = 1)$:咳嗽时,感冒的概率?
h1 = np.array([0, 1]) # 证据 [H = 1]
# 筛选 H=1,同时把非查询/证据的 A、I 边缘化掉 → c a h i, h -> c
P_CH1 = ProbTable("C H=1", einsum(P_CAHI.p, h1, "c a h i, h -> c"))
P_H1 = ProbTable("H=1", einsum(P_CH1.p, "c ->"))
P_C_given_H1 = ProbTable("C | H=1", P_CH1.p / P_H1.p)
② $P(C = 1 \mid H = 1, I = 1)$:咳嗽 + 眼睛痒时,感冒的概率?
h1 = np.array([0, 1]); i1 = np.array([0, 1])
P_CH1I1 = ProbTable("C H=1 I=1", einsum(P_CAHI.p, h1, i1, "c a h i, h, i -> c"))
P_H1I1 = ProbTable("H=1 I=1", einsum(P_CH1I1.p, "c ->"))
P_C_given_H1I1 = ProbTable("C | H=1 I=1", P_CH1I1.p / P_H1I1.p)
# 源码断言:加上眼睛痒后,感冒概率反而更低
assert P_C_given_H1I1.p[1] < P_C_given_H1.p[1]
结果:加上「眼睛痒」后,感冒的概率反而降低了!
例子
手算验证(把 $A$ 边缘化掉):
-
$P(C = 1, H = 1) = 0.1 \times (0.8 \times 0.9 + 0.2 \times 0.9) = 0.09$;$P(C = 0, H = 1) = 0.9 \times (0.8 \times 0.1 + 0.2 \times 0.9) = 0.234$,
故 $P(C = 1 \mid H = 1) = 0.09 / 0.324 \approx 0.28$; -
加上 $I = 1$:$P(C = 1, H = 1, I = 1) = 0.1 \times (0.8 \times 0.9 \times 0.1 + 0.2 \times 0.9 \times 0.9) = 0.0234$;$P(C = 0, H = 1, I = 1) = 0.9 \times (0.8 \times 0.1 \times 0.1 + 0.2 \times 0.9 \times 0.9) = 0.153$,
故 $P(C = 1 \mid H = 1, I = 1) = 0.0234 / 0.1764 \approx 0.13$。
「眼睛痒」这条与咳嗽无直接因果的证据,把感冒概率砍掉了一半多。
4.3 更隐蔽的解释消去
为什么?
说明
- 结果 $H$(咳嗽)有两个原因:$C$(感冒)、$A$(过敏);
- 眼睛痒 $I$ 不是 $H$ 的原因……
- 但观测到 $I = 1$ 会提高过敏 $A$ 的概率,而 $A$ 是咳嗽的一个原因;
- 于是过敏「接管」了对咳嗽的解释,感冒的概率被挤下去。
提示
观测证据会沿着网络传播,提高或降低其他节点的概率。信息不是只在直接相邻的节点间流动,而是穿过整张图:$I \to A \to H \to C$,两跳之外的证据照样改变信念。「哪些路径通、哪些路径被堵住」正是下一讲条件独立与 d-分离要回答的问题。
5. 贝叶斯网络的一般定义
对应源码 introduce_bayesian_networks()。把两个例子抽象成通用框架。
说明
- 定义一组随机变量 $X = (X_1, \dots, X_n)$;
- 在变量上定义任意一个有向无环图(DAG, directed acyclic graph);
- 对每个节点 $X_i$,定义局部条件分布 $p(x_i \mid x_{\mathrm{Parents}(i)})$;
- 联合分布 = 各局部条件分布之积:
$$P(X_1 = x_1, \dots, X_n = x_n) = \prod_{i=1}^{n} p\big(x_i \mid x_{\mathrm{Parents}(i)}\big)$$
其中 $\mathrm{Parents}(i)$ 是节点 $X_i$ 在 DAG 中的父节点集合,$x_{\mathrm{Parents}(i)}$ 是这些父节点的取值。
graph LR
subgraph DAG["有向无环图 + 局部条件分布"]
X1["X₁<br/>p(x₁)"] --> X3["X₃<br/>p(x₃|x₁,x₂)"]
X2["X₂<br/>p(x₂)"] --> X3
X3 --> X4["X₄<br/>p(x₄|x₃)"]
end
DAG --> J["联合分布<br/>P(X) = Πᵢ p(xᵢ | parents(xᵢ))"]
style X1 fill:#e1f5fe,stroke:#0277bd
style X2 fill:#e1f5fe,stroke:#0277bd
style X3 fill:#f3e5f5,stroke:#7b1fa2
style X4 fill:#ffe0b2,stroke:#e65100
style J fill:#c8e6c9,stroke:#2e7d32
注意
- 每个节点一张局部条件分布(不是每条边一张!);
- 局部条件分布一次性依赖全部父节点($p(a \mid b, e)$ 是一张联合考虑 $b, e$ 的表,不是两张单独的表相乘);
- 分清 $p$(定义给出)和 $P$(法则推导)。
联合分布 vs 贝叶斯网络对比:
| 维度 | 直接写联合分布表 | 贝叶斯网络 |
|---|---|---|
| 参数量 | $2^n - 1$(指数爆炸) | $\sum_i 2^{\lvert \mathrm{Parents}(X_i) \rvert}$(随父节点数增长) |
| 可读性 | 一堆数字,看不出结构 | 图直接显示「谁影响谁」 |
| 建模方式 | 硬填 | 4 步:变量 → 边 → 局部分布 → 相乘 |
| 表达的独立性 | 隐藏 | 由图结构显式编码 |
例子
- 报警网络:联合表需 $2^3 - 1 = 7$ 个数;贝叶斯网络只需 $1 + 1 + 4 = 6$ 个($p(b)$、$p(e)$ 各 1 个自由参数,$p(a \mid b, e)$ 每种父取值组合 1 个,共 4 个)。
- 医疗网络:联合表需 $2^4 - 1 = 15$ 个数;网络只需 $1 + 1 + 4 + 2 = 8$ 个。
- 变量越多、图越稀疏,节省越夸张:100 个二值变量、每个至多 2 个父节点时,$2^{100} - 1$ 对比至多 $100 \times 4$。紧凑性的来源不是魔法,而是图结构编码的条件独立假设。
6. 万物皆可贝叶斯网络:自回归语言模型
对应源码 language_models()。一个漂亮的观察:很多东西暗地里就是贝叶斯网络。
自回归语言模型(autoregressive language model)就是一个贝叶斯网络:
- 随机变量:词元(token)$X_1, \dots, X_T$;
- 边:从之前所有词元指向当前词元($X_1, \dots, X_{t-1} \to X_t$);
- 局部条件分布(就是 Transformer):$p(x_t \mid x_1, \dots, x_{t-1})$;
- 联合分布:
$$P(X_1, \dots, X_T) = \prod_{t=1}^{T} p(x_t \mid x_1, \dots, x_{t-1})$$
graph LR
X1["X₁"] --> X2["X₂"]
X1 --> X3["X₃"]
X2 --> X3
X3 --> X4["X₄"]
X1 --> X4
X2 --> X4
style X1 fill:#e1f5fe,stroke:#0277bd
style X2 fill:#e1f5fe,stroke:#0277bd
style X3 fill:#e1f5fe,stroke:#0277bd
style X4 fill:#ffe0b2,stroke:#e65100
- 平常我们前向采样:给 prompt,生成 response,即 $X_1, X_2, X_3 \to X_4, X_5, X_6$;
- 那反向做概率推断意味着什么?→ 由 $X_4, X_5, X_6$ 反推 $X_1, X_2, X_3$;
- 🎯 应用:越狱语言模型(jailbreaking)——给定一段特定回复(例如「Sure, here’s how you make a bomb」),反推哪些 prompt 最可能生成它。
提示
「生成」和「推断」共用同一个联合分布:前向生成是按拓扑序采样,反向追因是条件化 + 边缘化。另注意一个反差:这个 DAG 是全连接的(每个词元依赖全部历史),完全没有独立性假设可省参数——它的紧凑性来自所有位置共享同一个 Transformer(参数共享,见 Lecture 14 · 贝叶斯网络 III:参数学习)。语言模型本身在 Lecture 17 · 语言模型 展开,Transformer 架构细节见 CS336 的 Lecture 3 · 架构与超参数(未找到对应页面)。
7. 概率编程与拒绝采样
对应源码 introduce_rejection_sampling()——本讲的核心近似推断算法。
7.1 从「写分布」到「写程序」
至此,我们都是写下局部条件分布来定义贝叶斯网络。现在换一种方式:用概率程序(probabilistic program)来定义。
先定义一个抛硬币的原语:
def Bernoulli(prob: float) -> int:
"""以概率 prob 返回 1,以 1 - prob 返回 0。"""
return np.random.choice([0, 1], p=[1 - prob, prob])
于是报警网络就是一段会跑的程序——每次调用产生一个样本:
def alarm():
B = Bernoulli(0.05) # 盗窃
E = Bernoulli(0.05) # 地震
A = B or E # 报警 = 盗窃 OR 地震
return {"B": B, "E": E, "A": A}
医疗诊断也一样,程序结构与网络的拓扑顺序一一对应(先采父节点,再采子节点):
def medical_diagnosis():
C = Bernoulli(0.1) # 感冒
A = Bernoulli(0.2) # 过敏
H = Bernoulli(0.9 if C or A else 0.1) # 咳嗽(软 OR)
I = Bernoulli(0.9 if A else 0.1) # 眼睛痒
return {"C": C, "A": A, "H": H, "I": I}
提示
程序按拓扑序前向采样,采出的样本恰好服从联合分布 $P(X)$——因为每一行 Bernoulli(...) 就是一个局部条件分布 $p(x_i \mid \mathrm{parents})$,逐行采样正对应逐因子相乘。写程序比写表更灵活:条件分布可以是任意代码(if、循环、外部函数),这也是「概率编程语言」这个研究方向的出发点。
7.2 拒绝采样(Rejection Sampling)
把分布写成程序,就有了一个天然的近似推断方法:
说明
- 前向采样一大堆样本;
- 丢弃与证据不符的样本;
- 在留下的样本里统计查询变量的频率。
直觉上这就是用「模拟世界很多次,只看与观测一致的那些平行世界」来近似条件概率。
源码把「证据」和「查询」都抽象成函数,让算法适用于任意网络:
def rejection_sampling(program, query, evidence, num_samples):
"""
program : 定义贝叶斯网络,返回一个样本
query : sample -> 关心的取值
evidence: sample -> 该样本是否保留(是否符合证据)
"""
counts = defaultdict(int) # 统计每个查询取值出现的次数
for _ in range(num_samples):
sample = program() # 前向采样一个样本
if evidence(sample): # 只保留符合证据的样本
counts[query(sample)] += 1 # 记录查询取值
# 归一化:次数 -> 概率
total_count = sum(counts.values())
probs = {q: counts[q] / total_count for q in counts}
return probs
用它算 $P(B \mid A = 1)$(报警网络):
query = lambda sample: sample["B"] # 关心盗窃 B
evidence = lambda sample: sample["A"] == 1 # 保留报警响的样本
result = rejection_sampling(alarm, query, evidence, num_samples=1000)
采样流程图:
flowchart TD
Start["前向采样<br/>program()"] --> Match{"符合证据?<br/>evidence(sample)"}
Match -->|否| Reject["丢弃 ❌"]
Match -->|是| Keep["保留<br/>counts[query]++ "]
Reject --> More{"还要采样?"}
Keep --> More
More -->|是| Start
More -->|否| Norm["归一化<br/>counts → 概率"]
style Start fill:#e1f5fe,stroke:#0277bd
style Match fill:#ffe0b2,stroke:#e65100
style Reject fill:#ffcdd2,stroke:#c62828
style Keep fill:#c8e6c9,stroke:#2e7d32
style Norm fill:#c8e6c9,stroke:#2e7d32
注意
采 $N$ 个样本,期望只有 $N \cdot P(E = e)$ 个能通过证据筛选。若证据概率是 $10^{-4}$,想要 1000 个有效样本就得采一千万个——绝大多数计算都被白白丢弃。
✅ 优点:极其灵活(对任意概率程序都适用,完全不关心网络结构),且当样本数 $\to \infty$ 时估计收敛到真实条件概率(一致性)。「怕罕见证据」这个短板正是 Lecture 13 · 贝叶斯网络 II:Gibbs 采样与条件独立 里 Gibbs 采样要解决的问题。
7.3 隐马尔可夫模型:物体追踪
拒绝采样对更复杂的网络也照样适用。源码用一个隐马尔可夫模型(Hidden Markov Model, HMM)做物体追踪:
- 隐变量 $H_1, \dots, H_T$:物体在各时刻的位置;
- 观测变量 $E_1, \dots, E_T$:各时刻的传感器读数。
graph LR
H1["H₁"] --> H2["H₂"] --> H3["H₃"] --> H4["H₄"] --> H5["H₅"]
H1 --> E1["E₁"]
H2 --> E2["E₂"]
H3 --> E3["E₃"]
H4 --> E4["E₄"]
H5 --> E5["E₅"]
style H1 fill:#e1f5fe,stroke:#0277bd
style H2 fill:#e1f5fe,stroke:#0277bd
style H3 fill:#f3e5f5,stroke:#7b1fa2
style H4 fill:#e1f5fe,stroke:#0277bd
style H5 fill:#e1f5fe,stroke:#0277bd
style E5 fill:#ffe0b2,stroke:#e65100
def hidden_markov_model():
"""隐变量 = 物体位置 H;观测 = 传感器读数 E。"""
num_steps = 5
H = [None] * num_steps # 位置
E = [None] * num_steps # 传感器读数
for t in range(num_steps):
# 位置:在上一位置基础上随机 +0/+1(随机游走)
H[t] = (H[t - 1] if t > 0 else 0) + Bernoulli(0.5)
# 观测:在真实位置基础上加噪声
E[t] = H[t] + Bernoulli(0.5)
return {"H": H, "E": E}
问题:已知第 5 步传感器读数为 2,物体在第 3 步的位置?即求 $P(H_3 \mid E_5 = 2)$——注意这是「用未来的观测推断过去的位置」,正是 §6 说的反向推断:
query = lambda sample: sample["H"][2] # H₃(下标从 0 起)
evidence = lambda sample: sample["E"][4] == 2 # E₅ = 2
result = rejection_sampling(hidden_markov_model, query, evidence, num_samples=200)
说明
同一套 rejection_sampling 函数,换个 program、query、evidence 就能跑报警、医疗、HMM——这就是「用程序表示联合分布」的威力。HMM 会在 Lecture 14 · 贝叶斯网络 III:参数学习 作为参数共享的教科书例子再次出现。
8. 讨论:贝叶斯网络的思维方式
对应源码 discussion()。用贝叶斯网络思考,需要一次思维方式的转变。
| 范式 | 数据流向 | 含义 |
|---|---|---|
| 传统机器学习 | 输入 → 输出 | 学一个从 $x$ 到 $y$ 的映射(判别式 discriminative) |
| 贝叶斯网络 | 病因/隐变量 → 症状/观测 | 先建生成过程(generative),再反向从症状推病因 |
以「咳嗽 → 感冒?」为例:分类器直接学「咳嗽 → 感冒」的映射;贝叶斯网络则先建「感冒/过敏 → 咳嗽/眼睛痒」的生成故事,再反过来做推断。
贝叶斯网络的优势(源码列了四条):
- 🧩 处理异质缺失信息:训练和测试时都能应对「东缺一块、西缺一块」的数据——缺哪个变量,就把它边缘化掉,模型不需要为每种缺失模式单独训练;
- 📚 融入先验知识:孟德尔遗传定律、物理定律这类领域知识可以直接写进图结构与局部分布,不必等数据「重新发现」它们;
- 🔍 可解释:所有中间变量都有明确语义(过敏、位置……),推断结果能沿着图逐步解释,不是黑箱;
- 🎯 通向因果模型:贝叶斯网络是因果推断(干预 intervention、反事实 counterfactual)的前身——Pearl 的因果革命正是从这里出发的。
9. 总结
mindmap
root((贝叶斯网络 I))
从状态到变量
状态型: 搜索/MDP/博弈
变量型: 用概率描述世界
核心问题: 给定证据求查询概率
概率三操作
联合分布 = 真相之源
边缘化 = 求和积掉
条件化 = 筛选+归一化
概率表就是张量 einsum
贝叶斯网络
DAG + 局部条件分布
联合分布 = 局部分布之积
4步建模
解释消去
经典例子
报警: B,E → A 硬OR
医疗: C,A → H,I 软OR
语言模型 / HMM
近似推断
概率程序 = 联合分布
拒绝采样: 采样→筛选→统计
灵活但怕罕见证据
关键要点:
- 概率三操作:联合分布(真相之源)、边缘化($P(S=s) = \sum_r P(S=s, R=r)$,求和积掉变量)、条件化(筛选证据 + 归一化,$P(S \mid R=1) = P(S, R=1)/P(R=1)$)。
- 概率表就是张量,可以用
einsum统一表达所有运算。 - 贝叶斯网络 = 有向无环图 + 每个节点一张局部条件分布;联合分布是各局部分布之积:$P(x_1, \dots, x_n) = \prod_i p(x_i \mid x_{\mathrm{Parents}(i)})$。
- 建模四步:定义变量 → 连有向边 → 写局部条件分布 → 相乘得联合分布。参数量从 $2^n - 1$ 降到 $\sum_i 2^{\lvert\mathrm{Parents}\rvert}$,紧凑性来自图编码的条件独立假设。
- 概率推断:给定证据 $E = e$ 求 $P(Q \mid E = e)$,非查询/证据变量全部边缘化掉(类比 SQL 查询)。
- 解释消去:观测到共同结果后,再观测到一个原因会降低另一个原因的概率($P(B{=}1 \mid A{=}1, E{=}1) < P(B{=}1 \mid A{=}1)$),即使两原因先验独立——从数学中自然涌现。
- 概率编程:把联合分布写成会跑的程序(按拓扑序前向采样),每行采样对应一个局部条件分布。
- 拒绝采样:采一堆样本 → 丢弃不符证据的 → 统计查询频率;很灵活但很慢——期望有效样本数只有 $N \cdot P(E=e)$,证据罕见时几乎全被拒;样本 $\to \infty$ 时收敛到真值。
下一讲预告:拒绝采样在证据罕见时太浪费。Lecture 13 · 贝叶斯网络 II:Gibbs 采样与条件独立 讲更快的近似推断——Gibbs 采样(MCMC),并引入条件独立与马尔可夫毯(Markov blanket)来刻画网络中「谁与谁无关」。
复习自测
题目
联合表需要 $2^n - 1$ 个数;贝叶斯网络只需 $\sum_i 2^{\lvert\mathrm{Parents}(X_i)\rvert}$ 个,稀疏图时是指数级节省。省参数的本质是做了条件独立假设:每个变量给定父节点后与非后代无关。代价是表达能力受限——如果真实世界的依赖关系不符合所画的图,模型就有系统性偏差。建模就是在「紧凑」与「忠实」之间做取舍。
题目
$A = 1$ 当且仅当 $B, E$ 至少一个为 1,故 $P(A=1) = 1 - 0.95^2 = 0.0975$。又 $B = 1$ 必然导致 $A = 1$,所以 $P(B=1, A=1) = P(B=1) = 0.05$。于是 $P(B=1 \mid A=1) = 0.05 / 0.0975 \approx 0.51$:一声警报把盗窃概率从 5% 抬到约 51%。
题目
因为信息沿图传播:$I = 1$ 提高了过敏 $A$ 的后验($A$ 是 $I$ 的唯一原因),而 $A$ 与 $C$ 同为咳嗽 $H$ 的原因;在已观测 $H = 1$ 的前提下,$A$ 概率升高就把「对咳嗽的解释权」抢走,把 $C$ 的概率压低——这是经由中间节点传导的解释消去。数值上感冒概率从约 0.28 降到约 0.13。
题目
灵活:它只需要能前向运行的程序(任意结构、任意条件分布),不做任何解析计算。怕罕见证据:每个样本独立生成、事后才检查证据,通过率恰是 $P(E = e)$;要拿到 $M$ 个有效样本,期望要采 $M / P(E=e)$ 个。当 $P(E=e) = 10^{-4}$ 时,99.99% 的计算被丢弃。
题目
生成回复是前向采样:按拓扑序从 $p(x_t \mid x_{1:t-1})$ 逐词元采样,得到服从联合分布的样本。越狱是反向推断:把回复当证据、prompt 当查询,求 $P(X_{1:k} \mid X_{k+1:T} = \text{给定回复})$——即「哪些 prompt 最可能生成这段回复」。两者共用同一个联合分布,只是条件化的方向不同。
参考资料
- 💻 bayes.py — 本讲源码(报警 / 医疗 / HMM / 拒绝采样)
- 📖 CS221 讲义模块:Bayesian Networks — Definitions
- 📄 Pearl, Probabilistic Reasoning in Intelligent Systems (1988) — 贝叶斯网络的奠基之作
- 📖 Koller & Friedman, Probabilistic Graphical Models: Principles and Techniques (2009) — 概率图模型权威教材
- 📄 Emerging Vulnerabilities in Frontier Models: Jailbreaking as Inference (2025) — 把越狱视为反向概率推断
- 🔗 einops / einsum 文档 — 用张量记号表达概率运算
- 🌐 CS221 课程主页(Autumn 2025)