# Lecture 14 · 贝叶斯网络 III：参数学习

> **CS221: Artificial Intelligence — Principles and Techniques** · Stanford · Autumn 2025
> 📅 Nov 5 · 💻 [`bayes_learning.py`](https://github.com/stanford-cs221/autumn2025-lectures/blob/main/bayes_learning.py)

---

## 承上启下

**前两讲（[[Lecture 12 · 贝叶斯网络 I：建模与推断]] / [[Lecture 13 · 贝叶斯网络 II：Gibbs 采样与条件独立]]）**：

- 我们已经有了完整的贝叶斯网络——有向无环图（DAG）+ 每个节点的**局部条件分布** $p(x_i \mid x_{\mathrm{Parents}(i)})$；
- 一直**假设这些概率已知**，任务是做**推断（inference）**：精确推断、拒绝采样、Gibbs 采样；
- 还搞清了图结构如何编码**条件独立**（d-分离、马尔可夫毯）。

**本讲（贝叶斯网络 III）**：

- 追问一个一直被跳过的问题——**这些概率从哪来？**
- 任务反过来：给定**训练数据**，**学习（learn）**每个局部条件分布的参数 $\theta$；
- 核心答案朴素得惊人：**计数 + 归一化（count + normalize）**。

> [!tip] 直觉：推断与学习是同一枚硬币的两面
> 推断是「参数已知，问变量」；学习是「变量已知（数据），问参数」。贝叶斯网络的因子分解让两边都变简单：推断可以局部传播，学习可以局部计数。本讲的三层递进——完全可观测（闭式解）→ 数据稀疏（平滑）→ 部分可观测（EM 迭代）——正对应「数据的信息量逐级递减，算法的代价逐级递增」。

本讲的逻辑线（对齐源码 `main()`）：

1. **完全可观测**设定（所有变量都被观测到）→ 计数与归一化 → 用最大似然证明它；
2. **拉普拉斯平滑**（Laplace smoothing）→ 防止零概率、对抗过拟合；
3. **EM 算法**（Expectation Maximization）→ 处理**部分可观测**（有隐变量）的数据。

---

## 1. 回顾：贝叶斯网络三步走

源码开头 `review_bayesian_networks()` 用经典的 **盗窃 $B$（Burglary）/ 地震 $E$（Earthquake）/ 警报 $A$（Alarm）** 例子快速回顾定义。

**联合分布**就像一个「描述世界如何运作的数据库」，构造步骤：定义变量 → 连边（DAG）→ 写局部条件分布 → 相乘：

$$P(B = b, E = e, A = a) = p(b)\, p(e)\, p(a \mid b, e)$$

```python
from bayes import ProbTable
from einops import einsum

# 1. 随机变量 X = (B, E, A)   2. DAG（B → A ← E）   3. 局部条件分布
p_b = ProbTable("B", [0.95, 0.05])                    # p(b)
p_e = ProbTable("E", [0.95, 0.05])                    # p(e)
p_a_given_be = ProbTable("A | B E",
    lambda b, e, a: a == (b or e), shape=(2, 2, 2))    # p(a | b, e)：有任一发生则报警

# 4. 联合分布 = 所有局部条件分布相乘：P(B,E,A) = p(b)·p(e)·p(a|b,e)
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"))
```

**推断**（像对联合分布做 SQL 查询）三种方法回顾：

| 方法 | 思路 | 精确/近似 |
| :--- | :--- | :---: |
| 精确推断 | 选中 $A{=}1$ 的切片，边缘化掉 $E$，除以 $P(A{=}1)$ | 精确 |
| 拒绝采样 | 前向采样 $(B, E, A)$，不满足 $A{=}1$ 就丢弃 | 近似 |
| Gibbs 采样 | 固定 $A{=}1$，交替按 $P(B \mid E, A)$、$P(E \mid B, A)$ 重采样 | 近似（MCMC） |

**条件独立**一句话判据：给定 $C$，$A$ 与 $B$ 条件独立 ⟺ 从 $A$ 到 $B$ 的每条路径都被 $C$ **阻塞**。三种模式——链 $X \to C \to Y$、分叉 $X \leftarrow C \to Y$、碰撞 $X \to Z \leftarrow Y$（要求 $C$ 不含 $Z$ 及其后代）——在 [[Lecture 13 · 贝叶斯网络 II：Gibbs 采样与条件独立]] 已展开。

> [!note] 一对经典对照，务必记牢
> - $A \leftarrow C \to B$（共同原因）：**给定 $C$ 独立**，但边缘不独立；
> - $A \to C \leftarrow B$（共同结果 / v-结构）：**边缘独立**，但给定 $C$ 不独立（explaining away，解释消去）。

**本讲要回答的问题**：上面这些数字 `p_b = [0.95, 0.05]` 到底是怎么来的？

---

## 2. 完全可观测设定：计数 + 归一化

源码 `introduce_fully_observable_setting()` 定义了本讲的**监督学习**任务：

- **输入**：一组样本，每个样本是**对所有变量的完整赋值**（fully observable）；
- **输出**：所有局部条件分布的参数。

策略是先用一串越来越复杂的例子建立直觉，最后统一成一个通用算法，再从最大似然原理证明它。

### 2.1 单变量：最简单的计数

变量 $R \in \{1, 2, 3, 4, 5\}$：一部电影的评分。参数就是整张概率表 $\theta = (p_R(1), \dots, p_R(5))$。

```python
from collections import defaultdict
from util import normalize_dict

# 训练数据：10 个评分
training_data = [{"R": r} for r in [1, 3, 4, 4, 4, 4, 4, 5, 5, 5]]

# 1. 计数：每个评分出现了几次
counts_r = defaultdict(float)
for x in training_data:
    counts_r[x["R"]] += 1
# counts_r = {1: 1, 3: 1, 4: 5, 5: 3}

# 2. 归一化：除以总数，得到概率
p_r = normalize_dict(counts_r)
# p_r = {1: 0.1, 3: 0.1, 4: 0.5, 5: 0.3}
```

写成公式，估计值就是**经验频率**：

$$\hat{p}_R(r) = \frac{\mathrm{count}(r)}{n}$$

其中 $\mathrm{count}(r)$ 是取值 $r$ 在数据中出现的次数，$n$ 是样本总数。就这么简单——**count + normalize**。

```mermaid
flowchart LR
    D["训练数据<br/>{R=1,3,4,4,4,4,4,5,5,5}"] --> C["① 计数 Count<br/>counts_r"]
    C --> N["② 归一化 Normalize<br/>÷ 总数"]
    N --> P["p_R<br/>{1:.1, 3:.1, 4:.5, 5:.3}"]

    style D fill:#e1f5fe,stroke:#0277bd
    style C fill:#ffe0b2,stroke:#e65100
    style P fill:#c8e6c9,stroke:#2e7d32
```

### 2.2 两变量：局部计数，忽略无关变量

变量加一个 $G \in \{\text{drama}, \text{comedy}\}$（类型）。结构 $G \to R$，联合分布：

$$P(G = g, R = r) = p_G(g) \cdot p_R(r \mid g)$$

```python
training_data = [
    {"G": "drama",  "R": 4},
    {"G": "drama",  "R": 4},
    {"G": "drama",  "R": 5},
    {"G": "comedy", "R": 1},
    {"G": "comedy", "R": 5},
]

# 1. 计数：p_G 直接数 G；p_R(·|g) 按 g 分桶数 R
counts_g  = defaultdict(float)
counts_gr = defaultdict(lambda: defaultdict(float))
for x in training_data:
    counts_g[x["G"]] += 1
    counts_gr[x["G"]][x["R"]] += 1

# 2. 归一化
p_g = normalize_dict(counts_g)                 # {drama: 0.6, comedy: 0.4}
p_r_given_g = {}
for g in counts_g:
    p_r_given_g[g] = normalize_dict(counts_gr[g])
# p_r_given_g[drama]  = {4: 2/3, 5: 1/3}
# p_r_given_g[comedy] = {1: 0.5, 5: 0.5}
```

条件分布的估计公式——**按父取值分桶后再数频率**：

$$\hat{p}_R(r \mid g) = \frac{\mathrm{count}(g, r)}{\sum_{r'} \mathrm{count}(g, r')}$$

> [!tip] 直觉：为什么可以「忽略其他变量」
> 估计某个局部条件分布时，**其他所有变量都可以无视**：估 $p_R(r \mid g)$ 只需在「$G = g$ 的那些样本」里数 $R$。这不是偷懒的近似，而是精确正确的（§3 会证明）——因为似然按局部条件分布因子分解，每个因子的最优解只依赖「自己和自己的父」这几列数据。这正是贝叶斯网络「局部性」在学习端的体现，与推断端「马尔可夫毯之外皆可忽略」（[[Lecture 13 · 贝叶斯网络 II：Gibbs 采样与条件独立]]）遥相呼应。

### 2.3 v-结构与倒 v-结构：无非是多几个桶

源码用 `v_structure()` 和 `inverted_v_structure()` 强调：**结构再花哨，算法不变**。

**v-结构** $G \to R \leftarrow A$（类型与获奖 $A$ 两个父节点汇聚到评分 $R$）：

$$P(g, a, r) = p_G(g)\, p_A(a)\, p_R(r \mid g, a)$$

唯一要点是——**同时以所有父节点为条件**，桶的键是父取值元组 $(g, a)$：

```python
counts_gar = defaultdict(lambda: defaultdict(float))
for x in training_data:
    counts_gar[(x["G"], x["A"])][x["R"]] += 1   # 键 = 全部父节点的联合取值
```

**倒 v-结构** $R_1 \leftarrow G \to R_2$（一个类型驱动两个用户的评分）：

$$P(g, r_1, r_2) = p_G(g)\, p_{R_1}(r_1 \mid g)\, p_{R_2}(r_2 \mid g)$$

$R_1$、$R_2$ 各有**自己独立的一套参数**（`counts_gr1`、`counts_gr2` 两套），分别在 $G$ 的桶里数。

> [!warning] 易错点：推断难 ≠ 学习难
> v-结构与倒 v-结构在**推断**时行为迥异（explaining away vs. 条件独立），但在（完全可观测的）**学习**时都只是「按父节点分桶计数」——别被结构吓到。学习的难度不取决于图形状，而取决于数据是否完整（§5）。

### 2.4 参数共享：多个节点共用同一套参数

`parameter_sharing()` 是本节最重要的概念。还是 $R_1 \leftarrow G \to R_2$，但现在写成：

$$P(G = g, R_1 = r_1, R_2 = r_2) = p_G(g) \cdot p_R(r_1 \mid g) \cdot p_R(r_2 \mid g) \qquad \leftarrow \text{注意：两处都是同一个 } p_R!$$

参数只有两套：$\theta = (p_G, p_R)$——**不是** $p_{R_1}$ 和 $p_{R_2}$。这就是**参数共享（parameter sharing / tying）**：不同节点被**同一个**局部条件分布「驱动」。

```mermaid
graph TD
    G["G 类型"] --> R1["R1 用户1评分"]
    G --> R2["R2 用户2评分"]
    P["共享参数 p_R(· | g)"] -."驱动".-> R1
    P -."驱动".-> R2

    style G fill:#e1f5fe,stroke:#0277bd
    style P fill:#f3e5f5,stroke:#7b1fa2
    style R1 fill:#c8e6c9,stroke:#2e7d32
    style R2 fill:#c8e6c9,stroke:#2e7d32
```

代码上的唯一改动：$R_1$ 和 $R_2$ **累加到同一个 `counts_gr`**：

```python
counts_gr = defaultdict(lambda: defaultdict(float))
for x in training_data:
    counts_g[x["G"]] += 1
    # 关键：R1 和 R2 都增量到同一个 counts_gr！
    counts_gr[x["G"]][x["R1"]] += 1
    counts_gr[x["G"]][x["R2"]] += 1
```

> [!tip] 直觉：为什么推断「看不见」共享、学习却生死攸关
> - 推断只是**读取**局部条件分布——两张内容相同的表和一张被引用两次的表，读出来毫无区别；
> - 学习是**写入**局部条件分布——是否共用同一个「计数桶」直接决定每套参数分到多少数据。共享一套参数，等于把两个节点的数据**合并**起来估计，样本效率翻倍。
> 这与深度学习中的权重共享（CNN 卷积核、Transformer 各位置共用同一套权重）是同一个思想：[[Lecture 12 · 贝叶斯网络 I：建模与推断]] 提过，自回归语言模型的「图」毫无稀疏性可言，它的紧凑性全靠所有位置共享同一个 Transformer。

**何时用参数共享？** 这是一个**建模决策（modeling decision）**：

| 维度 | 参数**更少**（共享） | 参数**更多**（不共享） |
| :--- | :--- | :--- |
| 需要的样本量 | 少（更省数据） | 多 |
| 灵活性 | 低 | 高（各节点可不同） |
| 适用假设 | 两个用户「相似」 | 两个用户「不同」 |

### 2.5 隐马尔可夫模型（HMM）：转移与发射的共享

HMM 是参数共享的**教科书级应用**（[[Lecture 12 · 贝叶斯网络 I：建模与推断]] 里用它做过物体追踪）：

- $H_t \in \{0, 1\}$：$t$ 时刻物体的位置（隐状态）；
- $E_t \in \{0, 1\}$：$t$ 时刻的传感器读数（观测）。

$$P(H, E) = p_{\text{start}}(h_1) \prod_{t=2}^{T} p_{\text{trans}}(h_t \mid h_{t-1}) \prod_{t=1}^{T} p_{\text{emit}}(e_t \mid h_t)$$

参数只有三套：$\theta = (p_{\text{start}}, p_{\text{trans}}, p_{\text{emit}})$——**起始分布、转移分布、发射分布**，被所有时刻共享。链条无论多长，参数量不变，这正是 HMM 能处理任意长度序列的原因。

```mermaid
graph LR
    H1["H1"] -->|p_trans| H2["H2"] -->|p_trans| H3["H3"]
    H1 -->|p_emit| E1["E1"]
    H2 -->|p_emit| E2["E2"]
    H3 -->|p_emit| E3["E3"]
    S(["p_start"]) -.-> H1

    style H1 fill:#e1f5fe,stroke:#0277bd
    style H2 fill:#e1f5fe,stroke:#0277bd
    style H3 fill:#e1f5fe,stroke:#0277bd
    style E1 fill:#fff9c4,stroke:#f9a825
    style E2 fill:#fff9c4,stroke:#f9a825
    style E3 fill:#fff9c4,stroke:#f9a825
```

在**完全可观测**（$H$ 也被观测到）的设定下，学习依旧是 count + normalize，只是把每条轨迹上**所有转移对** $(h_t, h_{t+1})$ 累加进同一套 `counts_trans`、**所有发射对** $(h_t, e_t)$ 累加进同一套 `counts_emit`：

```python
training_data = [
    {"H1": 0, "E1": 0, "H2": 1, "E2": 1, "H3": 0, "E3": 0},
    {"H1": 0, "E1": 1, "H2": 0, "E2": 1, "H3": 0, "E3": 1},
]
counts_start = defaultdict(float)
counts_trans = defaultdict(lambda: defaultdict(float))   # 所有时刻共享
counts_emit  = defaultdict(lambda: defaultdict(float))   # 所有时刻共享
for x in training_data:
    counts_start[x["H1"]] += 1
    counts_emit[x["H1"]][x["E1"]] += 1;  counts_trans[x["H1"]][x["H2"]] += 1
    counts_emit[x["H2"]][x["E2"]] += 1;  counts_trans[x["H2"]][x["H3"]] += 1
    counts_emit[x["H3"]][x["E3"]] += 1
# 归一化：p_start 直接归一；p_trans / p_emit 按前一隐状态 h 分桶归一
p_start = normalize_dict(counts_start)
p_trans = {h: normalize_dict(counts_trans[h]) for h in counts_trans}
p_emit  = {h: normalize_dict(counts_emit[h])  for h in counts_emit}
```

### 2.6 通用算法：一个函数搞定所有结构

`general_bayesian_networks()` 把前面所有特例抽象成一个统一框架：

- 设 $D$ 为局部条件分布**类型**的集合。HMM 里 $D = \{\text{start}, \text{trans}, \text{emit}\}$；
- 参数 $\theta = \{p_d : d \in D\}$；
- 联合分布：
$$P(x_1, \dots, x_n) = \prod_{i=1}^{n} p_{d_i}\big(x_i \mid x_{\mathrm{Parents}(i)}\big)$$
  其中 $d_i$ 是节点 $X_i$ 使用的参数类型——多个 $X_i$ 可以指向同一个 $d_i$（即参数共享）。

用一个**网络结构字典**描述整张图（变量名 → (参数名, 父变量名列表)）：

```python
network_structure = {
    # 变量名 -> (参数名, 父变量名列表)
    "H1": ("start", []),
    "H2": ("trans", ["H1"]),
    "H3": ("trans", ["H2"]),   # H2、H3 共享 "trans"
    "E1": ("emit",  ["H1"]),
    "E2": ("emit",  ["H2"]),
    "E3": ("emit",  ["H3"]),   # 三个 E 共享 "emit"
}
theta = fully_observable_learning(network_structure, training_data)
```

通用学习器就是把「计数 + 归一化」写成一个循环——**这是本讲代码的核心**：

```python
def fully_observable_learning(network_structure, training_data, pseudocounts=None):
    """对贝叶斯网络做监督学习（完全可观测）。
    network_structure: 变量名 -> (参数名, 父变量名列表)
    training_data:     字典列表，每个字典是对所有变量的完整赋值
    pseudocounts:      预置的伪计数（用于拉普拉斯平滑，见 §4）
    返回 theta：参数名 -> 父取值 -> 取值 -> 概率
    """
    # 初始化计数（若给了伪计数，就从伪计数出发）
    # counts[参数名][父取值元组][取值] = 出现次数
    if pseudocounts is None:
        counts = defaultdict(lambda: defaultdict(lambda: defaultdict(float)))
    else:
        counts = deepcopy(pseudocounts)

    # ① 计数：遍历每个样本的每个变量
    for x in training_data:
        for var, value in x.items():
            parameter_name, parent_vars = network_structure[var]
            parents_value = tuple(x[pv] for pv in parent_vars)  # 父节点的联合取值
            counts[parameter_name][parents_value][value] += 1   # 参数共享在此自动发生

    # ② 归一化：对每个 (参数, 父取值) 桶归一化
    theta = defaultdict(lambda: defaultdict(lambda: defaultdict(float)))
    for parameter_name in counts:
        for parents_value in counts[parameter_name]:
            theta[parameter_name][parents_value] = \
                normalize_dict(counts[parameter_name][parents_value])
    return theta
```

> [!note] 一个函数覆盖所有例子
> 前面 2.1–2.5 所有例子，其实都是这一个函数的特例。参数共享「免费」发生——因为共享同一个 `parameter_name` 的变量，自然累加进同一个计数桶。写成公式，通用估计就是：
> $$\hat{p}_d(v \mid \pi) = \frac{\mathrm{count}_d(\pi, v)}{\sum_{v'} \mathrm{count}_d(\pi, v')}$$
> 其中 $d$ 是参数类型，$\pi$ 是父节点联合取值，$v$ 是本节点取值。

---

## 3. 最大似然估计：为什么「计数 + 归一化」是对的

`maximum_likelihood()` 回答了一个自然的疑问：count + normalize 只是「看起来合理」，还是有理论依据？答案是——它**恰好**就是**最大似然估计（Maximum Likelihood Estimation, MLE）**的闭式解。

### 3.1 最大似然原理

**最大似然原理**：找到使观测数据出现概率最大的参数：

$$\hat{\theta} = \arg\max_{\theta} \prod_{x \in \mathcal{D}} P(X = x; \theta) = \arg\max_{\theta} \sum_{x \in \mathcal{D}} \log P(X = x; \theta)$$

其中 $\mathcal{D}$ 是训练集；取对数把连乘变连加（不改变最优点，因为 $\log$ 单调），既避免数值下溢又便于求导。

> [!note]
> 结论先行：对（完全可观测的）贝叶斯网络，这个优化问题有**闭式解（closed form）**——就是 count + normalize，**无需任何梯度下降或迭代优化**。这与 [[Lecture 2 · 线性回归]] 里最小二乘有正规方程闭式解异曲同工：目标函数结构足够好时，优化问题一步到位。

### 3.2 单变量的证明（拉格朗日乘子）

以数据 $\{R{=}1, R{=}5, R{=}5\}$ 为例，记 $c_r = \mathrm{count}(r)$，目标是：

$$\max_{\theta} \; \sum_{r} c_r \log p_R(r) \qquad \text{s.t.} \quad \sum_{r} p_R(r) = 1$$

为等式约束引入**拉格朗日乘子（Lagrange multiplier）** $\mu$：

$$\mathcal{L}(\theta, \mu) = \sum_{r} c_r \log p_R(r) + \mu \Big(1 - \sum_{r} p_R(r)\Big)$$

令对每个 $p_R(r)$ 的偏导为零：

$$\frac{\partial \mathcal{L}}{\partial p_R(r)} = \frac{c_r}{p_R(r)} - \mu = 0 \quad \Longrightarrow \quad p_R(r) = \frac{c_r}{\mu}$$

代回约束 $\sum_r p_R(r) = 1$ 得 $\mu = \sum_r c_r = n$，于是：

$$\hat{p}_R(r) = \frac{c_r}{n}$$

——**正是频率**！本例中 $\hat{p}_R(1) = 1/3$、$\hat{p}_R(5) = 2/3$。

### 3.3 两变量：似然自动分解

数据 $\{(\text{drama}, 4), (\text{drama}, 5), (\text{comedy}, 5)\}$，似然是：

$$\max_{\theta}\; p_G(\text{dr})\, p_R(4 \mid \text{dr}) \cdot p_G(\text{dr})\, p_R(5 \mid \text{dr}) \cdot p_G(\text{co})\, p_R(5 \mid \text{co})$$

**关键一步**——把因子按「涉及哪套参数」重新分组：

$$\max_{\theta}\; \underbrace{\big[p_G(\text{dr})\, p_G(\text{dr})\, p_G(\text{co})\big]}_{\text{只关于 } p_G} \cdot \underbrace{\big[p_R(4 \mid \text{dr})\, p_R(5 \mid \text{dr})\big]}_{\text{只关于 } p_R(\cdot \mid \text{dr})} \cdot \underbrace{\big[p_R(5 \mid \text{co})\big]}_{\text{只关于 } p_R(\cdot \mid \text{co})}$$

三组因子**没有共享参数**，整体最大化 = 各组分别最大化。每个子问题都长得和 §3.2 的单变量情形一模一样，各自 count + normalize 即可解。

```mermaid
flowchart TD
    L["整体对数似然<br/>Σ_x log P(x; θ)"] --> S1["子问题: p_G"]
    L --> S2["子问题: p_R(·|drama)"]
    L --> S3["子问题: p_R(·|comedy)"]
    S1 --> A["计数 + 归一化"]
    S2 --> A
    S3 --> A

    style L fill:#ffe0b2,stroke:#e65100
    style A fill:#c8e6c9,stroke:#2e7d32
```

> [!tip] 直觉：因子分解 ⇒ 优化可分解
> 这就是为什么「忽略其他变量、只做局部计数」是正确的：对数似然天然按（参数类型, 父取值）拆成一堆互不干扰的项，全局最优 = 各局部子问题最优的拼接。同一个原理也支撑着现代语言模型：预训练目标 $\sum_t \log p(x_t \mid x_{1:t-1})$ 正是自回归贝叶斯网络的最大似然（见 [[Lecture 17 · 语言模型]]；CS336 的 [[Lecture 15 · 对齐 I：SFT 与 RLHF]] 里 SFT 也是同一个目标，只是数据换成了示范）。区别在于 Transformer 的参数不是查表而是神经网络，闭式解不再存在，只能用梯度下降逼近。

---

## 4. 拉普拉斯平滑：别让概率变成 0

`introduce_laplace_smoothing()` 指出纯 MLE 的一个尴尬。看这个例子：

```python
network_structure = {"R": ("R", [])}
training_data = [{"R": 1}, {"R": 4}]
theta = fully_observable_learning(network_structure, training_data)
# p_R = {1: 0.5, 4: 0.5}，而 p_R(2) = p_R(3) = p_R(5) = 0
```

> [!warning] 易错点：零概率是过拟合
> 我们真的相信 $p_R(2) = 0$ 吗？只是**没在这两个样本里见到** 2，不代表评分 2 **不可能**出现。零概率的杀伤力极大：任何包含该取值的联合概率、似然、推断结果都会被**乘成 0**，一票否决。这是**数据稀疏**下的过拟合——MLE 对没见过的事件过度自信。

**解法：拉普拉斯平滑（Laplace / add-λ smoothing）**——给每个取值的计数都预加一个**伪计数（pseudocount）** $\lambda$：

$$\hat{p}_{\lambda}(v) = \frac{\mathrm{count}(v) + \lambda}{n + \lambda \lvert \mathcal{V} \rvert}$$

其中 $n$ 是（该桶内的）样本总数，$\lvert\mathcal{V}\rvert$ 是取值域大小；分母加 $\lambda \lvert\mathcal{V}\rvert$ 保证归一。$\lambda = 1$ 即经典的 add-one smoothing。

```python
def get_pseudocounts(smoothing: float):
    # 给 R 的每个可能取值都预置 smoothing 个伪计数
    return {"R": {(): {1: smoothing, 2: smoothing, 3: smoothing,
                       4: smoothing, 5: smoothing}}}

# λ = 1：每个取值先「白送」1 次
pseudocounts = get_pseudocounts(smoothing=1)
theta = fully_observable_learning(network_structure, training_data, pseudocounts)
# 计数变为 {1: 2, 2: 1, 3: 1, 4: 2, 5: 1}，总数 7
# p_R = {1: 2/7, 2: 1/7, 3: 1/7, 4: 2/7, 5: 1/7}  ← 不再有 0！
```

注意实现之巧：伪计数直接作为 `fully_observable_learning` 的**初始 `counts`**（`deepcopy(pseudocounts)`），真实数据在它之上继续累加——平滑与学习共用同一段代码。

**$\lambda$ 构成一条从「完全信数据」到「完全不信数据」的连续谱**：$\lambda \to 0$ 退回原始 MLE（可能有 0），$\lambda$ 适中是数据与先验的折中，$\lambda \to \infty$ 趋于均匀分布：

```mermaid
graph LR
    Z["λ → 0<br/>最大似然 MLE<br/>（可能出现 0）"] --- M["λ 适中<br/>平滑估计<br/>（数据+先验）"] --- I["λ → ∞<br/>均匀分布<br/>（无视数据）"]

    style Z fill:#ffcdd2,stroke:#c62828
    style M fill:#c8e6c9,stroke:#2e7d32
    style I fill:#e1f5fe,stroke:#0277bd
```

> [!tip] 直觉：伪计数就是先验，会被数据冲淡
> 伪计数相当于「在看数据之前就假装每个取值见过 $\lambda$ 次」——贝叶斯视角下，这恰是给参数加了 Dirichlet 先验后取后验均值/众数（MAP）的效果。源码验证：无论 $\lambda$ 设多大，喂进 `[{R:4}] * 1000` 后，$\hat p_R(4)$ 都会压倒性地趋近 1——分子分母里真实计数按 $n$ 增长而伪计数不动，**样本量越大先验影响越小**。这与贝叶斯直觉完全吻合：数据最终说了算。

---

## 5. EM 算法：数据不完整时怎么办

前面所有例子都假设**每个样本对所有变量都有观测**。`introduce_expectation_maximization()` 打破这个假设：**如果某些变量从没被观测到呢？**

### 5.1 部分可观测设定

还是 $R_1 \leftarrow G \to R_2$，但训练数据里**没有 $G$**（类型是隐藏的）：

```python
# 完全可观测（对照）：G 也在
training_data = [{"G": "drama", "R1": 1, "R2": 1},
                 {"G": "comedy", "R1": 2, "R2": 2}]

# 部分可观测（本节）：没有 G！
training_data = [{"R1": 1, "R2": 1},
                 {"R1": 2, "R2": 2}]
```

此时最大似然要最大化**观测数据**的似然，隐变量 $G$ 被**求和边缘化**掉：

$$\max_{\theta} \sum_{(r_1, r_2) \in \mathcal{D}} \log \underbrace{\sum_{g} P(G = g, R_1 = r_1, R_2 = r_2; \theta)}_{\text{对隐变量求和}}$$

> [!warning] 易错点：log 里的求和毁掉了闭式解
> 完全可观测时是 $\log \prod = \sum \log$，每个 $\log p$ 各自独立、可分组求解；现在**求和号钻进了 $\log$ 内部**（$\log \sum_g \cdots$），对数拆不开乘积，似然不再按参数因子分解——**没有闭式解了**，目标还变成非凸的。这正是隐变量带来的根本困难。

**三个玩家**：观测变量 $(R_1, R_2)$、未观测变量 $G$、参数 $\theta$。难点在于 **$G$ 和 $\theta$ 都不知道**。

### 5.2 先有鸡还是先有蛋

这是一个典型的**鸡生蛋（chicken-and-egg）**问题：

- 若**知道参数 $\theta$** → 可以算隐变量的后验 $P(G = g \mid R_1, R_2; \theta)$（一次贝叶斯网络推断！）；
- 若**知道隐变量 $G$** → 数据变成完全可观测，像 §2 那样 count + normalize 即得 $\theta$。

**EM 算法（Expectation Maximization，[Dempster et al. 1977]）**的破局办法：**随机初始化 $\theta$，然后反复迭代**两步——

```mermaid
flowchart LR
    Init["随机初始化 θ<br/>（打破对称！）"] --> E
    subgraph Loop["迭代直到收敛"]
        E["E 步<br/>用当前 θ 算 P(G|R1,R2)<br/>→ 生成加权的完整数据"] --> M["M 步<br/>在加权数据上 count+normalize<br/>→ 更新 θ"]
        M -->|新的 θ| E
    end
    M --> Out["局部最优 θ"]

    style Init fill:#e1f5fe,stroke:#0277bd
    style E fill:#f3e5f5,stroke:#7b1fa2
    style M fill:#ffe0b2,stroke:#e65100
    style Out fill:#c8e6c9,stroke:#2e7d32
```

- **E 步（Expectation）**：用当前 $\theta$ 对每个样本计算隐变量的后验，作为**软标签**权重：
  $$q_x(g) = P(G = g \mid R_1, R_2; \theta) = \frac{p_G(g)\, p_R(r_1 \mid g)\, p_R(r_2 \mid g)}{\sum_{g'} p_G(g')\, p_R(r_1 \mid g')\, p_R(r_2 \mid g')}$$
  据此把每个样本「幻想」成若干条**加权的完整赋值**；
- **M 步（Maximization）**：在这些**加权**赋值上做 count + normalize（把 §2 里的「+1」换成「+ 权重 $q_x(g)$」），得到最大化期望对数似然的新 $\theta$。

> [!tip] 直觉：EM = 带软标签的 count + normalize
> E 步是**推断**（用现有模型猜缺失的列），M 步是**完全可观测学习**（把猜出来的列当真数据用，只是带权重）。EM 没有发明新原语，只是把本单元前两讲的推断和本讲的计数**交替使用**。可以证明每轮迭代观测似然单调不减，所以它是一个在非凸地形上稳步向上的爬山过程。

### 5.3 源码：EM 的完整实现

```python
def expectation_maximization(training_data, num_iterations: int):
    # 随机初始化 θ = (p_g, p_r_given_g)——注意故意设成非均匀以打破对称
    p_g = {"drama": 0.5, "comedy": 0.5}
    p_r_given_g = {
        "comedy": {1: 0.4, 2: 0.6},   # 例：p(r=1 | g=comedy) = 0.4
        "drama":  {1: 0.6, 2: 0.4},
    }

    for iteration in range(num_iterations):
        # ===== E 步：猜隐变量，据此给训练数据加权 =====
        weighted_training_data = []   # 「幻想」出来的加权完整数据
        for x in training_data:
            # 先算未归一化的 P(G=g, R1, R2; θ)
            q = {}
            for g in p_g:
                q[g] = p_g[g] * p_r_given_g[g][x["R1"]] * p_r_given_g[g][x["R2"]]
            # 归一化得到后验 q(g) = P(G=g | R1, R2; θ)
            q = normalize_dict(q)
            # 每个样本按后验权重「分裂」成多条带权完整赋值
            for g in q:
                weighted_training_data.append((x | {"G": g}, q[g]))

        # ===== M 步：在加权数据上 count + normalize =====
        counts_g  = defaultdict(float)
        counts_gr = defaultdict(lambda: defaultdict(float))
        for x, weight in weighted_training_data:
            counts_g[x["G"]] += weight              # 关键：+= 权重，而非 +1
            counts_gr[x["G"]][x["R1"]] += weight
            counts_gr[x["G"]][x["R2"]] += weight

        p_g = normalize_dict(counts_g)
        p_r_given_g = {g: normalize_dict(counts_gr[g]) for g in counts_gr}

    return {"p_g": p_g, "p_r_given_g": p_r_given_g}
```

调用与效果：

```python
training_data = [{"R1": 1, "R2": 1}, {"R1": 2, "R2": 2}]
theta = expectation_maximization(training_data, num_iterations=5)
# 迭代后：模型自发把 (1,1) 归为一类、(2,2) 归为另一类
```

> [!note] EM 的本质是聚类
> 模型自己「发明」隐变量的取值来**解释数据**。上例中，$(R_1, R_2) = (1,1)$ 与 $(2,2)$ 会被逐渐归到不同的 $G$ 上——这正是**混合模型 / 软聚类**（k-means 可视为 EM 的「硬标签」极限：E 步只保留后验最大的那个类）。而且 **EM 会放大初始偏好（amplifies initial preferences）**：初始 $\theta$ 稍微偏向哪边，E 步就朝那边多分权重，M 步再强化——正反馈直到收敛。对更「混杂」的数据（含 $(1,2)$、$(2,1)$ 这类跨类样本），学到的概率会更平滑，以兼顾异质赋值。

### 5.4 EM 的性质与坑

| 性质 | 说明 |
| :--- | :--- |
| ✅ 单调不减 | **每次迭代保证不降低**观测数据似然，收敛到驻点/局部最大 |
| ⚠️ 非全局最优 | 不保证收敛到**全局**最大——结果依赖初始化，常用多次随机重启取最优 |
| ⚠️ 需打破对称 | 若各类初始参数完全相同，E 步给出的后验也完全相同，M 步更新后依旧对称——永远分不开各类 |
| ⚠️ 标签可置换 | 隐变量只能恢复到**标签置换**意义下（drama / comedy 可整体互换，似然不变）|

> [!note] 延伸：EM 的著名特例
> - **HMM + EM = Baum-Welch 算法**：若 §2.5 的 HMM 里 $H$ 不可观测（现实中传感器只给 $E$），就用 EM 学 $p_{\text{start}}, p_{\text{trans}}, p_{\text{emit}}$，其中 E 步用前向-后向（forward-backward）算法算隐状态后验；
> - **高斯混合模型（GMM）**：连续版软聚类，E/M 两步结构完全相同；
> - 一句话总结 EM：**E 步补全隐变量（软标签），M 步计数更新参数**，交替上升似然的爬山过程——带隐变量版的 count + normalize。

---

## 6. 总结

```mermaid
mindmap
  root((贝叶斯网络<br/>参数学习))
    完全可观测
      count + normalize
      忽略无关变量, 按父节点分桶
      通用学习器 fully_observable_learning
    参数共享
      多节点共用一套分布
      HMM: start/trans/emit
      建模决策: 数据量 vs 灵活性
    最大似然 MLE
      count+normalize 是闭式解
      似然按局部分布因子分解
      拉格朗日乘子证明
    拉普拉斯平滑
      counts 全部 +λ, 防零概率
      λ→0 退回MLE, λ→∞ 均匀
      伪计数 = 先验, 被数据冲淡
    EM 算法
      处理隐变量/部分观测
      E步后验加权, M步加权计数
      聚类/软标签, 局部最优
```

**关键要点**：

- **学习任务**：给定训练数据，估计贝叶斯网络每个**局部条件分布**的参数 $\theta$。
- **完全可观测**下，算法就是 **count + normalize**：$\hat p_d(v \mid \pi) = \mathrm{count}_d(\pi, v) / \sum_{v'} \mathrm{count}_d(\pi, v')$——估计每个局部分布时**忽略其他变量**、按父节点联合取值分桶；一个 `fully_observable_learning` 函数覆盖所有结构。
- **参数共享**：多个节点共用同一套局部条件分布（如 HMM 的转移/发射）——推断时无所谓（只读），**学习时决定计数桶**（写入）；用不用是**建模决策**，本质是样本效率与灵活性的权衡。
- **最大似然**：count + normalize 不是拍脑袋，而是 MLE 的**闭式解**——拉格朗日乘子法给出 $\hat p(r) = c_r / n$，且似然按局部条件分布因子分解，全局最优 = 局部子问题拼接。
- **拉普拉斯平滑**：所有计数加伪计数 $\lambda$，$\hat p_\lambda(v) = (\mathrm{count}(v) + \lambda)/(n + \lambda\lvert\mathcal V\rvert)$，**防止零概率**；$\lambda \to 0$ 退回 MLE、$\lambda \to \infty$ 趋均匀，数据一多平滑被冲淡（伪计数 = 先验）。
- **EM 算法**：数据**不完整**（有隐变量）时，似然含 $\log \sum$、无闭式解——**E 步**用当前参数算隐变量后验 $q_x(g)$、生成加权完整数据；**M 步**在加权数据上 count + normalize；反复迭代、似然单调不减、收敛到**局部**最优（本质是软聚类，HMM 版即 Baum-Welch）。

**下一讲预告**：到此为止，变量型模型单元（建模 → 推断 → 学习）完整收官。下一讲 [[Lecture 15 · 逻辑 I：命题逻辑]] 画风突变，转向**逻辑（Logic）**——用语法、语义、推理规则做**更高阶的确定性推理**（如「每个学生都有一个 TA」这类断言），从命题逻辑、模型、蕴含（entailment）讲到归结（resolution）。

---

## 复习自测

> [!question]- Q1：用拉格朗日乘子推导：数据中取值 $r$ 出现 $c_r$ 次，为什么 MLE 恰好是 $\hat p(r) = c_r / n$？
> 目标 $\max \sum_r c_r \log p(r)$ 受约束 $\sum_r p(r) = 1$。拉格朗日函数对 $p(r)$ 求偏导：$c_r / p(r) - \mu = 0$，得 $p(r) = c_r / \mu$；代回约束得 $\mu = \sum_r c_r = n$。即每个概率与其计数成正比、按总数归一——count + normalize 正是驻点（且由 $\log$ 的凹性是最大值）。

> [!question]- Q2：为什么参数共享在推断时「看不见」、在学习时却至关重要？HMM 里共享了什么？
> 推断只**读取**局部条件分布的数值：两张内容相同的独立表与一张被引用多次的共享表给出完全一样的联合分布。学习要**写入**参数：共享意味着多个节点的数据累加进同一个计数桶，等价于假设「这些节点由同一机制生成」，样本效率成倍提高（代价是牺牲各节点差异化的灵活性）。HMM 把所有时刻的转移都绑到一套 $p_{\text{trans}}$、所有发射绑到一套 $p_{\text{emit}}$，因此参数量与序列长度无关。

> [!question]- Q3：算一算：数据 $\{R{=}1, R{=}4\}$、取值域 $\{1,\dots,5\}$、$\lambda = 2$，平滑后 $\hat p_\lambda(1)$ 和 $\hat p_\lambda(2)$ 各是多少？
> 每个取值预加 2：计数变为 $\{1{:}3, 2{:}2, 3{:}2, 4{:}3, 5{:}2\}$，总数 $2 + 2 \times 5 = 12$。故 $\hat p_\lambda(1) = 3/12 = 0.25$，$\hat p_\lambda(2) = 2/12 \approx 0.167$。对照 $\lambda = 1$ 时的 $2/7 \approx 0.286$ 与 $1/7 \approx 0.143$：$\lambda$ 越大，分布越被拉向均匀的 $0.2$。

> [!question]- Q4：隐变量为什么让 MLE 失去闭式解？EM 的两步分别在做本单元哪一讲的事？
> 观测似然是 $\sum_x \log \sum_g P(g, x; \theta)$——求和钻进了 $\log$ 内部，对数拆不开乘积，似然不再按参数分组因子分解，还变成非凸。EM 的 E 步是**推断**：用当前参数算隐变量后验（Lecture 12/13 的技术）；M 步是**完全可观测学习**：在软标签加权的数据上 count + normalize（本讲 §2）。交替进行可保证观测似然单调不减。

> [!question]- Q5：如果把 EM 的初始参数设成完全对称（两类的 $p_{r \mid g}$ 相同），会发生什么？为什么？
> 死锁在对称点：E 步对每个样本给出两类相同的后验（各 0.5），M 步据此算出的两类参数仍然相同，下一轮依旧——算法永远无法把数据分成不同的类。因为 EM 只会**放大**初始偏好，初始零偏好就无偏好可放大。所以实现里故意用非均匀初始化（0.6/0.4）打破对称；实践中还会多次随机重启以逃离糟糕的局部最优。

---

## 参考资料

- 💻 [bayes_learning.py](https://github.com/stanford-cs221/autumn2025-lectures/blob/main/bayes_learning.py) — 本讲源码
- 📖 [CS221 Bayesian Networks: Supervised Learning](https://stanford-cs221.github.io/autumn2023/modules/module.html#include=bayesian-networks%2Fsupervised-learning.js&level=0&mode=print6pp) — 监督学习模块
- 📖 [CS221 Bayesian Networks: Smoothing](https://stanford-cs221.github.io/autumn2023/modules/module.html#include=bayesian-networks%2Fsmoothing.js&mode=print6pp) — 拉普拉斯平滑模块
- 📖 [CS221 Bayesian Networks: EM Algorithm](https://stanford-cs221.github.io/autumn2023/modules/module.html#include=bayesian-networks%2Fem-algorithm.js&mode=print6pp) — EM 算法模块
- 📄 [Maximum Likelihood from Incomplete Data via the EM Algorithm (Dempster, Laird & Rubin, 1977)](https://www.jstor.org/stable/2984875) — EM 算法奠基论文
- 📖 [Koller & Friedman, *Probabilistic Graphical Models* (2009)](https://mitpress.mit.edu/9780262013192/probabilistic-graphical-models/) — 第 17–19 章：参数估计与 EM
- 🌐 [CS221 课程主页](https://stanford-cs221.github.io/) — 全部讲义与模块
