工作台课程

CS221 · 人工智能:原理与技术

Lecture 12 · 贝叶斯网络 I:建模与推断

Lecture 12 · 贝叶斯网络 I:建模与推断

CS221: Artificial Intelligence — Principles and Techniques · Stanford · Autumn 2025
📅 Oct 29 · 💻 bayes.py


承上启下:从状态型模型到变量型模型

前几周(状态型模型 state-based models):我们一直用「状态 + 动作」对世界建模,核心活动是推理与搜索。回顾这条线:

本讲开始(变量型模型 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)$$

分子是「筛选出的那一片联合概率」,分母是证据概率(保证条件分布归一)。分三步执行:

  1. 筛选(select):只留 $R = 1$ 的行 → $P(S = 0, R = 1) = 0.08$,$P(S = 1, R = 1) = 0.02$
  2. 算证据概率:$P(R = 1) = 0.08 + 0.02 = 0.10$
  3. 除以证据概率:$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 步

  1. 定义变量:$B$(盗窃)、$E$(地震)、$A$(报警)
  2. 用有向边连接变量,边表示「直接影响」:$B \to A$、$E \to A$
  3. 为每个变量写下局部条件分布 $p(x \mid \mathrm{parents}(x))$
  4. 联合分布 = 各局部条件分布之积

图结构(有向无环图 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()。把两个例子抽象成通用框架。

说明

  1. 定义一组随机变量 $X = (X_1, \dots, X_n)$;
  2. 在变量上定义任意一个有向无环图(DAG, directed acyclic graph)
  3. 对每个节点 $X_i$,定义局部条件分布 $p(x_i \mid x_{\mathrm{Parents}(i)})$;
  4. 联合分布 = 各局部条件分布之积:
    $$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)就是一个贝叶斯网络:

  1. 随机变量:词元(token)$X_1, \dots, X_T$;
  2. :从之前所有词元指向当前词元($X_1, \dots, X_{t-1} \to X_t$);
  3. 局部条件分布(就是 Transformer):$p(x_t \mid x_1, \dots, x_{t-1})$;
  4. 联合分布
    $$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)

把分布写成程序,就有了一个天然的近似推断方法:

说明

  1. 前向采样一大堆样本;
  2. 丢弃与证据不符的样本;
  3. 在留下的样本里统计查询变量的频率
    直觉上这就是用「模拟世界很多次,只看与观测一致的那些平行世界」来近似条件概率。

源码把「证据」和「查询」都抽象成函数,让算法适用于任意网络:

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 函数,换个 programqueryevidence 就能跑报警、医疗、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 最可能生成这段回复」。两者共用同一个联合分布,只是条件化的方向不同。


参考资料