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

> **CS221: Artificial Intelligence — Principles and Techniques** · Stanford · Autumn 2025
> 📅 Oct 29 · 💻 [`bayes.py`](https://github.com/stanford-cs221/autumn2025-lectures/blob/main/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)$。

> [!tip] 直觉：为什么要切换视角
> 前半学期的模型都嵌在「感知—推理—行动」循环的**行动**子系统里，输出是一个动作或策略。但很多问题根本不涉及行动——医生想知道「病人得了什么病」，追踪系统想知道「物体现在在哪」。这些问题的输出是**对世界状态的信念（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 |

> [!note] 联合分布是「真相之源」
> 任何关于这些变量的问题——边缘概率、条件概率、独立性——原则上都能从联合分布算出来。后面所有推断算法，本质上都是在「读」这张表（或它的紧凑表示）。

源码用一个通用的 `ProbTable` 类来承载概率表——它既能表示局部条件分布，也能表示边缘/条件分布：

```python
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
```

用它建出上面的联合分布表：

```python
# 行是 S，列是 R
P_SR = ProbTable("S R", [[0.20, 0.08],
                         [0.70, 0.02]])
```

> [!tip] 直觉：概率表就是张量
> 一个 $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` 求和：

```python
# 对 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` 表达：

```python
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]
```

> [!tip] 直觉：贝叶斯定理就藏在这三步里
> 条件化的定义是 $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)$。

**问题**：已知堵车且是秋天，下雨的概率是多少？

> [!note] 概率推断 = 在联合分布上执行查询
> **类比：在数据库上执行 SQL 查询。** 联合分布是那张大表，推断就是查询：`WHERE` 子句对应证据（条件化），`SELECT` 的列对应查询变量，没被提到的列被聚合掉（边缘化）。

一个问题（query）由两部分组成：

- **证据（evidence）**：已观测到的条件，如 $T = 1, A = 1$（堵车且秋天）；
- **查询（query）**：关心的变量，如 $R$（是否下雨）。

写成概率记号就是 $P(R \mid T = 1, A = 1)$。

> [!warning] 易错点：第三类变量去哪了
> 既不在证据、也不在查询里的变量（这里是 $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）：

```mermaid
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` + 函数表达）：

```python
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))
```

> [!note] 确定性也可以是条件分布
> 这里 $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 末尾）。

```python
# 张量外积：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"))
```

> [!tip] 直觉：贝叶斯网络的本质一句话
> 把一个指数级大的联合分布，**分解**成每个节点一张「只依赖自己父节点」的小表，联合分布 = 所有小表相乘。之所以能这样省参数，是因为图结构隐含了条件独立假设——每个变量给定父节点后，与其他「非后代」变量无关（这一点在 [[Lecture 13 · 贝叶斯网络 II：Gibbs 采样与条件独立]] 会被正式化为 d-分离）。

### 3.3 在报警网络上做推断

有了联合分布 $P(B, E, A)$，就能回答任何问题。全部用 §1.3 的「筛选 → 算证据 → 相除」三步。

**① $P(B = 1)$：没有任何信息时的盗窃概率**

```python
P_B = ProbTable("B", einsum(P_BEA.p, "b e a -> b"))   # 边缘化掉 E、A
```
结果与先验 $p(b)$ 一致——盗窃概率很低（0.05）。

**② $P(B = 1 \mid A = 1)$：只知道报警响**

```python
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)$：报警响 + 有地震**

```python
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)
```
结果：**盗窃概率又回落到很低**！

> [!example] 数值演算：三个概率到底是多少
> 因为报警是确定性 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）

为什么加上「有地震」后，盗窃概率反而降了？

> [!note] 解释消去（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)$；
> - **即使两个原因本身相互独立！**

> [!tip] 直觉
> 报警为什么响？地震已经把它「解释掉」了，所以不必再拿盗窃来解释。注意这个定性推理模式**没有被写进任何一张概率表**——它是从「联合分布 = 局部分布之积」的数学里**自然涌现**的。这正是贝叶斯网络的魅力：写下生成结构，合理的推理行为自动跟着来。

> [!note] 记号约定（源码专门强调）
> - 小写 $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）：

```mermaid
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
```

> [!tip] 直觉：结构就是医学常识
> 咳嗽 $H$ 有两个原因（感冒 $C$、过敏 $A$）；眼睛痒 $I$ 只由过敏 $A$ 引起。$I$ 不是咳嗽的原因——但它会成为通往 $A$ 的一条「侧信道」：看到眼睛痒 → 更相信过敏 → 过敏足以解释咳嗽 → 感冒嫌疑下降。建图的时候只写「谁直接影响谁」，这条推理链是推断时自动走出来的。

**第 3 步**，局部条件分布：

```python
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))
```

> [!note] 硬 OR vs 软 OR
> 报警例子里 $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)$$

```python
# 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)$：咳嗽时，感冒的概率？**

```python
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)$：咳嗽 + 眼睛痒时，感冒的概率？**

```python
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]
```

**结果**：**加上「眼睛痒」后，感冒的概率反而降低了！**

> [!example] 数值演算：感冒概率从 0.28 掉到 0.13
> 手算验证（把 $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 更隐蔽的解释消去

为什么？

> [!note] 更微妙的解释消去
> - 结果 $H$（咳嗽）有两个原因：$C$（感冒）、$A$（过敏）；
> - 眼睛痒 $I$ **不是** $H$ 的原因……
> - 但观测到 $I = 1$ 会**提高过敏 $A$ 的概率**，而 $A$ 是咳嗽的一个原因；
> - 于是过敏「接管」了对咳嗽的解释，感冒的概率被挤下去。

> [!tip] 直觉：证据在网络中传播
> 观测证据会沿着网络**传播**，提高或降低其他节点的概率。信息不是只在直接相邻的节点间流动，而是穿过整张图：$I \to A \to H \to C$，两跳之外的证据照样改变信念。「哪些路径通、哪些路径被堵住」正是下一讲条件独立与 d-分离要回答的问题。

---

## 5. 贝叶斯网络的一般定义

对应源码 `introduce_bayesian_networks()`。把两个例子抽象成通用框架。

> [!note] 贝叶斯网络（Bayesian Network）四要素
> 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)}$ 是这些父节点的取值。

```mermaid
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
```

> [!warning] 三条容易搞错的提醒
> - **每个节点一张局部条件分布**（不是每条边一张！）；
> - 局部条件分布**一次性依赖全部父节点**（$p(a \mid b, e)$ 是一张联合考虑 $b, e$ 的表，不是两张单独的表相乘）；
> - 分清 $p$（定义给出）和 $P$（法则推导）。

**联合分布 vs 贝叶斯网络对比**：

| 维度 | 直接写联合分布表 | 贝叶斯网络 |
| :--- | :--- | :--- |
| 参数量 | $2^n - 1$（指数爆炸） | $\sum_i 2^{\lvert \mathrm{Parents}(X_i) \rvert}$（随父节点数增长） |
| 可读性 | 一堆数字，看不出结构 | 图直接显示「谁影响谁」 |
| 建模方式 | 硬填 | 4 步：变量 → 边 → 局部分布 → 相乘 |
| 表达的独立性 | 隐藏 | 由图结构显式编码 |

> [!example] 参数量算一算（二值变量）
> - 报警网络：联合表需 $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})$$

```mermaid
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](https://arxiv.org/abs/2502.01236)）**——给定一段特定回复（例如「Sure, here's how you make a bomb」），反推**哪些 prompt 最可能生成它**。

> [!tip] 直觉：生成与推断是同一分布的两个方向
> 「生成」和「推断」共用同一个联合分布：前向生成是按拓扑序采样，反向追因是条件化 + 边缘化。另注意一个反差：这个 DAG 是**全连接**的（每个词元依赖全部历史），完全没有独立性假设可省参数——它的紧凑性来自所有位置**共享同一个 Transformer**（参数共享，见 [[Lecture 14 · 贝叶斯网络 III：参数学习]]）。语言模型本身在 [[Lecture 17 · 语言模型]] 展开，Transformer 架构细节见 CS336 的 [[Lecture 3 · 架构与超参数]]。

---

## 7. 概率编程与拒绝采样

对应源码 `introduce_rejection_sampling()`——**本讲的核心近似推断算法**。

### 7.1 从「写分布」到「写程序」

至此，我们都是**写下局部条件分布**来定义贝叶斯网络。现在换一种方式：用**概率程序（probabilistic program）**来定义。

先定义一个抛硬币的原语：

```python
def Bernoulli(prob: float) -> int:
    """以概率 prob 返回 1，以 1 - prob 返回 0。"""
    return np.random.choice([0, 1], p=[1 - prob, prob])
```

于是报警网络就是一段**会跑的程序**——每次调用产生一个样本：

```python
def alarm():
    B = Bernoulli(0.05)   # 盗窃
    E = Bernoulli(0.05)   # 地震
    A = B or E            # 报警 = 盗窃 OR 地震
    return {"B": B, "E": E, "A": A}
```

医疗诊断也一样，程序结构与网络的拓扑顺序**一一对应**（先采父节点，再采子节点）：

```python
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}
```

> [!tip] 直觉：概率程序 = 联合分布的另一种写法
> 程序按拓扑序前向采样，采出的样本恰好服从联合分布 $P(X)$——因为每一行 `Bernoulli(...)` 就是一个局部条件分布 $p(x_i \mid \mathrm{parents})$，逐行采样正对应逐因子相乘。写程序比写表更灵活：条件分布可以是任意代码（`if`、循环、外部函数），这也是「概率编程语言」这个研究方向的出发点。

### 7.2 拒绝采样（Rejection Sampling）

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

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

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

```python
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)$**（报警网络）：

```python
query    = lambda sample: sample["B"]        # 关心盗窃 B
evidence = lambda sample: sample["A"] == 1   # 保留报警响的样本
result = rejection_sampling(alarm, query, evidence, num_samples=1000)
```

**采样流程图**：

```mermaid
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
```

> [!warning] 致命缺点：怕罕见证据
> 采 $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$：各时刻的传感器读数。

```mermaid
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
```

```python
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 说的反向推断：

```python
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)
```

> [!note] 一套算法，任意网络
> 同一套 `rejection_sampling` 函数，换个 `program`、`query`、`evidence` 就能跑报警、医疗、HMM——这就是「用程序表示联合分布」的威力。HMM 会在 [[Lecture 14 · 贝叶斯网络 III：参数学习]] 作为参数共享的教科书例子再次出现。

---

## 8. 讨论：贝叶斯网络的思维方式

对应源码 `discussion()`。用贝叶斯网络思考，需要一次**思维方式的转变**。

| 范式 | 数据流向 | 含义 |
| :--- | :--- | :--- |
| **传统机器学习** | 输入 → 输出 | 学一个从 $x$ 到 $y$ 的映射（判别式 discriminative） |
| **贝叶斯网络** | 病因/隐变量 → 症状/观测 | 先建生成过程（generative），再**反向**从症状推病因 |

以「咳嗽 → 感冒？」为例：分类器直接学「咳嗽 → 感冒」的映射；贝叶斯网络则先建「感冒/过敏 → 咳嗽/眼睛痒」的生成故事，再反过来做推断。

**贝叶斯网络的优势**（源码列了四条）：

- 🧩 **处理异质缺失信息**：训练和测试时都能应对「东缺一块、西缺一块」的数据——缺哪个变量，就把它边缘化掉，模型不需要为每种缺失模式单独训练；
- 📚 **融入先验知识**：孟德尔遗传定律、物理定律这类领域知识可以直接写进图结构与局部分布，不必等数据「重新发现」它们；
- 🔍 **可解释**：所有中间变量都有明确语义（过敏、位置……），推断结果能沿着图逐步解释，不是黑箱；
- 🎯 **通向因果模型**：贝叶斯网络是因果推断（干预 intervention、反事实 counterfactual）的前身——Pearl 的因果革命正是从这里出发的。

---

## 9. 总结

```mermaid
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）**来刻画网络中「谁与谁无关」。

---

## 复习自测

> [!question]- Q1：同样表示 $n$ 个二值变量的分布，贝叶斯网络凭什么比联合分布表省参数？省下的代价是什么？
> 联合表需要 $2^n - 1$ 个数；贝叶斯网络只需 $\sum_i 2^{\lvert\mathrm{Parents}(X_i)\rvert}$ 个，稀疏图时是指数级节省。省参数的本质是**做了条件独立假设**：每个变量给定父节点后与非后代无关。代价是表达能力受限——如果真实世界的依赖关系不符合所画的图，模型就有系统性偏差。建模就是在「紧凑」与「忠实」之间做取舍。

> [!question]- Q2：算一算：报警网络中 $\varepsilon = 0.05$，求 $P(B = 1 \mid A = 1)$。
> $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%。

> [!question]- Q3：医疗诊断例子里，「眼睛痒」既不是咳嗽的原因也不是结果，为什么观测它会改变「感冒」的概率？
> 因为信息沿图传播：$I = 1$ 提高了过敏 $A$ 的后验（$A$ 是 $I$ 的唯一原因），而 $A$ 与 $C$ 同为咳嗽 $H$ 的原因；在已观测 $H = 1$ 的前提下，$A$ 概率升高就把「对咳嗽的解释权」抢走，把 $C$ 的概率压低——这是经由中间节点传导的解释消去。数值上感冒概率从约 0.28 降到约 0.13。

> [!question]- Q4：拒绝采样为什么「灵活但怕罕见证据」？定量说明其成本。
> 灵活：它只需要能前向运行的程序（任意结构、任意条件分布），不做任何解析计算。怕罕见证据：每个样本独立生成、事后才检查证据，通过率恰是 $P(E = e)$；要拿到 $M$ 个有效样本，期望要采 $M / P(E=e)$ 个。当 $P(E=e) = 10^{-4}$ 时，99.99% 的计算被丢弃。

> [!question]- Q5：把自回归语言模型看成贝叶斯网络，「生成回复」和「越狱」分别对应什么概率操作？
> 生成回复是**前向采样**：按拓扑序从 $p(x_t \mid x_{1:t-1})$ 逐词元采样，得到服从联合分布的样本。越狱是**反向推断**：把回复当证据、prompt 当查询，求 $P(X_{1:k} \mid X_{k+1:T} = \text{给定回复})$——即「哪些 prompt 最可能生成这段回复」。两者共用同一个联合分布，只是条件化的方向不同。

---

## 参考资料

- 💻 [bayes.py](https://github.com/stanford-cs221/autumn2025-lectures/blob/main/bayes.py) — 本讲源码（报警 / 医疗 / HMM / 拒绝采样）
- 📖 [CS221 讲义模块：Bayesian Networks — Definitions](https://stanford-cs221.github.io/autumn2023/modules/module.html#include=bayesian-networks%2Fdefinitions.js&mode=print6pp)
- 📄 [Pearl, *Probabilistic Reasoning in Intelligent Systems* (1988)](https://dl.acm.org/doi/book/10.5555/534975) — 贝叶斯网络的奠基之作
- 📖 [Koller & Friedman, *Probabilistic Graphical Models: Principles and Techniques* (2009)](https://mitpress.mit.edu/9780262013192/probabilistic-graphical-models/) — 概率图模型权威教材
- 📄 [Emerging Vulnerabilities in Frontier Models: Jailbreaking as Inference (2025)](https://arxiv.org/abs/2502.01236) — 把越狱视为反向概率推断
- 🔗 [einops / einsum 文档](https://einops.rocks/) — 用张量记号表达概率运算
- 🌐 [CS221 课程主页（Autumn 2025）](https://stanford-cs221.github.io/)
