# Lecture 9 · 函数逼近与策略梯度

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

---

## 从表格法到函数逼近

**上一讲（[[Lecture 8 · 强化学习]]）**：

- 环境是未知的 MDP，转移概率与奖励都摸不到，只能通过「行动 → 观察反馈」的交互来学习。
- 学了四大算法：Model-Based Monte Carlo、Model-Free Monte Carlo、SARSA、Q-Learning。
- 它们有一个共同前提：为**每一个** $(s, a)$ 对单独存一个 Q 值——这叫**表格法（tabular method）**。

**本讲（MDPs III）**：

- 表格法的死穴：当状态是图像、句子这类高维对象时，状态空间大到无法枚举，「一格一值」既存不下也学不完。
- **函数逼近（function approximation）**：用带参数 $\theta$ 的函数 $Q_\theta(s, a)$ 代替查找表，让相似状态共享参数、互相泛化。
- 退一步看全景，学习最优行为有三条路：**model-based / value-based / policy-based** 三大范式。
- **策略梯度（policy gradient）/ REINFORCE**：跳过 Q 值这个中间量，**直接**参数化并优化策略 $\pi_\theta(a \mid s)$。

**一句话主线**：把强化学习装进「假设类 / 损失函数 / 优化算法」这个机器学习框架（[[Lecture 2 · 线性回归]] 以来反复出现的三件套），最后干脆绕过所有中间量，直接对策略做梯度上升。

---

## 1. 回顾：表格法的边界

### 1.1 强化学习的统一形式

上一讲的三个 model-free 算法（Monte Carlo、SARSA、Q-Learning）表面上各有一套流程，其实共享同一套骨架：

- 维护当前估计 $\hat{Q}(s, a)$；
- 每看到一条经验，构造一个**目标（target）**——对「从这里出发未来能拿多少」的一次估计；
- 朝目标方向挪一小步：

$$\hat{Q}(s, a) \leftarrow \hat{Q}(s, a) + \alpha \left[\text{target} - \hat{Q}(s, a)\right]$$

其中 $\alpha$ 是学习率（步长），$\text{target} - \hat{Q}(s,a)$ 是「预测误差」：目标比当前估计高就上调，低就下调。三个算法的区别只在**目标怎么算**，以及**用哪个策略产生数据**：

| 算法 | on/off-policy | target（估计效用的方式） |
| :--- | :---: | :--- |
| **Model-Free Monte Carlo** | on-policy，估计 $Q_\pi$ | $u_t$：完整 rollout 从 $t$ 时刻起的折扣奖励和（不自举） |
| **SARSA** | on-policy，估计 $Q_\pi$ | $r + \gamma \hat{Q}(s', a')$，其中 $a'$ 是**实际执行**的下一动作（自举） |
| **Q-Learning** | off-policy，估计 $Q^*$ | $r + \gamma \max_{a'} \hat{Q}(s', a')$（自举） |

> [!warning] 易错点
> SARSA 目标中的 $a'$ 不是「贪婪地取 $\pi(s')$」，而是智能体在 $s'$ **真的采取**的那个动作——它由带探索的行为策略采出，可能是探索出来的「笨」动作。正因为目标里包含探索行为本身，SARSA 学到的是「边探索边行动的这个策略」的价值（on-policy）；而 Q-Learning 的目标取 $\max_{a'}$，无论探索时干了什么，学的都是最优策略的价值（off-policy）。

> [!tip] 直觉
> on-policy 与 off-policy 的判据只有一句话：产生数据的**探索策略**和你想估价值的**估计策略**是不是同一个。相同 → on-policy；不同 → off-policy。Q-Learning 用 ε-贪婪到处乱撞，却始终朝 $Q^*$ 逼近，这种「用别人的经验学最优」正是 off-policy 的威力——也是它能复用旧数据（经验回放）的根源。

### 1.2 一次交互长什么样

源码里先用上一讲的 `QLearning` 在不可靠电车 MDP（`FlakyTramMDP`，见 [[Lecture 7 · 马尔可夫决策过程]]）上跑一段，让我们看清「智能体眼中的世界」——它看不到转移概率，每一步只拿到五元组 $(s, a, r, s', \text{is\_end})$：

```python
from mdp import FlakyTramMDP, value_iteration
from reinforcement_learning import QLearning, walk_tram_policy, simulate

mdp = FlakyTramMDP(num_locs=6, failure_prob=0.1)
policy = partial(walk_tram_policy, mdp.num_locs)
rl = QLearning(exploration_policy=policy, epsilon=0.4, discount=1, learning_rate=0.1)

# 一次交互（智能体只看到 s, a, r, s', is_end）
action = rl.get_action(state=1)
rl.incorporate_feedback(state=1, action="walk", reward=-1, next_state=2, is_end=False)
rl.incorporate_feedback(state=2, action="walk", reward=-1, next_state=3, is_end=False)
rl.incorporate_feedback(state=3, action="tram", reward=-2, next_state=6, is_end=True)

# 多次 rollout，价值就是平均效用
value = simulate(mdp, rl, num_trials=100)  # @inspect value
```

每条 rollout $\tau$ 产生一个**效用（utility）**——折扣奖励之和 $u(\tau) = r_1 + \gamma r_2 + \gamma^2 r_3 + \cdots$；跑很多条 rollout 取平均，就是这套「探索 + 学习」流程实际拿到的价值。

### 1.3 状态空间爆炸

表格法要求「每个 $(s, a)$ 一个格子」。电车 MDP 只有几个位置，当然没问题；可现实任务里的状态往往是：

- **一张图像**：学习机器人操作策略时，状态就是摄像头此刻的画面；
- **一句话**：做定理证明时，状态是当前推导到的数学命题文本。

> [!warning] 易错点
> 所有可能的图像 / 句子有**天文数字**那么多（作为参照，围棋约有 $10^{170}$ 种合法局面，而可见宇宙的原子数才约 $10^{80}$）。为每个 $(s,a)$ 存一个 Q 值既存不下、也永远访问不全——绝大多数状态一辈子只会出现一次，表格法对没见过的格子完全无话可说。问题不只是内存，更是**泛化**：表格法在格子之间不传递任何信息。

**出路**：不再逐格记忆，而是**学一个函数**——把「这个状态见过没有」换成「这个状态和见过的状态像不像」，让相似的状态共享参数、互相泛化。

```mermaid
graph LR
    T["表格法 Tabular<br/>每个 (s,a) 一个格子"] -->|状态是图像/句子| X["存不下<br/>访问不全"]
    T -->|泛化| F["函数逼近<br/>Q_θ(s,a)"]
    F --> G["相似状态<br/>共享参数、互相泛化"]

    style T fill:#ffcdd2,stroke:#c62828
    style X fill:#ffcdd2,stroke:#c62828
    style F fill:#c8e6c9,stroke:#2e7d32
    style G fill:#e1f5fe,stroke:#0277bd
```

---

## 2. 函数逼近（Function Approximation）

### 2.1 三大设计问题（呼应机器学习）

把 Q 值从「查找表」换成「带参数 $\theta$ 的函数」$Q_\theta(s, a)$，立刻回到机器学习那三个老问题：

```mermaid
graph TD
    Q["Q_θ(s, a)"] --> H["1. 假设类<br/>Q_θ 能是哪些函数?"]
    Q --> L["2. 损失函数<br/>怎么判断 Q_θ 好不好?"]
    Q --> O["3. 优化算法<br/>怎么降低损失?"]

    style Q fill:#ffe0b2,stroke:#e65100
    style H fill:#e1f5fe,stroke:#0277bd
    style L fill:#f3e5f5,stroke:#7b1fa2
    style O fill:#c8e6c9,stroke:#2e7d32
```

> [!tip] 直觉
> 这套「假设类（hypothesis class）/ 损失函数（loss function）/ 优化算法（optimization algorithm）」正是 [[Lecture 2 · 线性回归]] 与 [[Lecture 4 · 深度学习]] 的框架。函数逼近的本质，就是**把强化学习问题翻译成一个监督学习问题**：只是「标签」不再来自人工标注，而是算法自己用奖励和自举构造出来的目标。一旦翻译完成，机器学习的全部武器库（特征工程、神经网络、SGD、正则化）都可以直接搬过来。

### 2.2 假设类：特征 + 线性 / MLP

**第一步**：把状态和动作映射成一个**特征向量（feature vector）** $\phi(s, a) \in \mathbb{R}^d$。
**第二步**：在特征上套一个函数：

- **线性函数**：$Q_\theta(s, a) = \phi(s, a) \cdot \theta$，参数 $\theta \in \mathbb{R}^d$ 就是权重向量；
- **多层感知机（MLP）**：$Q_\theta(s, a) = \text{MLP}_\theta(\phi(s, a))$，表达力更强，可以刻画特征间的非线性交互。

对电车 MDP，源码用最简单的 **one-hot 特征**（等价于退化回表格，但走的是「函数逼近」的机器）：把 $(s, a)$ 编号成一个整数，再变成长度 $|\mathcal{S}| \cdot |\mathcal{A}|$ 的 one-hot 向量。

```python
def phi(self, state: int, action: str) -> torch.Tensor:
    # (state, action) → 整数索引 0 ... |states|*|actions|-1
    index = (state - 1) * len(self.actions) + self.actions.index(action)
    # 长度 num_features 的 one-hot 向量
    vector = one_hot(index, self.num_features)
    return vector
```

> [!note] 为什么刻意用 one-hot
> one-hot 特征让每个 $(s,a)$ 拥有独立参数、互不干扰，从而**精确模拟表格法**，方便和上一讲对照验证代码正确性。真正的威力在于替换自由：只要把 $\phi$ 换成图像的 CNN 特征或句子的 embedding，同一套代码就能处理巨大状态空间——泛化能力全部来自特征（或网络）让「相似状态的特征相近」。

### 2.3 损失与优化：带梯度的 Q-Learning

把 Q 值算出来，再和目标比：

- **目标**（自举，同 Q-Learning）：

$$\text{target} = r + \gamma \max_{a'} Q_\theta(s', a') \qquad (\text{终止时 } \text{target} = r)$$

- **损失**（标准平方损失）：

$$\mathcal{L}(\theta) = \left(Q_\theta(s, a) - \text{target}\right)^2$$

- **优化**：对 $\theta$ 求梯度，走一步 SGD。

这就是把上一讲 Q-Learning 的「更新一行」推广成了「对参数求梯度」：表格法里 $\hat Q(s,a) \leftarrow \hat Q(s,a) + \alpha(\text{target} - \hat Q(s,a))$ 恰好是这个平方损失在 one-hot 特征下的 SGD 步——两者严格等价。完整实现（忠于源码 `ParameterizedQLearning`）：

```python
class ParameterizedQLearning(RLAlgorithm):
    def __init__(self, num_locs, actions, exploration_policy,
                 epsilon, discount, learning_rate):
        self.actions = actions
        self.num_features = num_locs * len(actions)
        self.exploration_policy = exploration_policy
        self.epsilon = epsilon
        self.discount = discount
        # 线性假设类：φ(s,a) → 一个标量 Q 值
        self.model = nn.Linear(self.num_features, 1)
        self.optimizer = torch.optim.SGD(self.model.parameters(), lr=learning_rate)

    def Q(self, state, action):
        """Q_θ(s, a) = model(φ(s, a))"""
        phi = self.phi(state, action)
        return self.model(phi)

    def pi(self, state):
        """π(s) = argmax_a Q_θ(s, a)"""
        q_values = {a: self.Q(state, a) for a in self.actions}
        return max(q_values.keys(), key=lambda k: q_values[k].item())

    def get_action(self, state):
        # ε-贪婪
        if torch.rand(1).item() < self.epsilon:
            return self.exploration_policy(state)   # 探索
        else:
            return self.pi(state)                   # 利用

    def incorporate_feedback(self, state, action, reward, next_state, is_end):
        # 1) 构造目标（自举）
        if is_end:
            target = reward
        else:
            next_action = self.pi(next_state)
            target = reward + self.discount * self.Q(next_state, next_action)

        # 2) 预测值 + 平方损失
        value = self.Q(state, action)
        loss = (value - target) ** 2

        # 3) 梯度步更新参数
        self.optimizer.zero_grad()
        loss.backward()          # 反向传播算梯度
        self.optimizer.step()    # 更新 θ
```

> [!warning] 易错点：目标里的梯度（半梯度方法）
> 注意 $\text{target}$ 本身也含 $Q_\theta$，直接对整个损失反传时，梯度会同时流过预测项和目标项。标准做法（Sutton & Barto 称为**半梯度方法（semi-gradient）**，DQN 的 target network 也是同一思想）是把目标**视为常数**（PyTorch 里 `target.detach()`），只对预测项求梯度——因为目标扮演的是「临时标签」的角色。讲义代码为求简洁没有 detach，在这个小 MDP 上照样收敛，但在复杂问题上两者行为可能不同。
>
> 更进一步：**函数逼近 + 自举 + off-policy** 三者叠加（正是这里的配方）在理论上可能**发散**，Sutton & Barto 称之为「致命三要素（deadly triad）」。实践中 DQN 用经验回放 + 目标网络等技巧压住了它，但要知道收敛没有免费保证。

### 2.4 跑起来 & 与真值对照

用参数化 Q-Learning 跑多条 rollout，再和「直接解 MDP」（[[Lecture 7 · 马尔可夫决策过程]] 的价值迭代）算出的真值比：

```python
rl = ParameterizedQLearning(num_locs=mdp.num_locs, actions=["walk", "tram"],
        exploration_policy=policy, epsilon=0.4, discount=1, learning_rate=0.1)

value = simulate(mdp, rl, num_trials=100)          # 跑 100 条 rollout

# 抽取当前最优策略与对应价值
states = range(1, mdp.num_locs + 1)
pi = {s: rl.pi(s) for s in states}                 # π_θ(s)
V  = {s: rl.Q(s, pi[s]) for s in states}           # V_θ(s) = Q_θ(s, π_θ(s))

# 与真值对照
result = value_iteration(mdp)                       # result.values, result.pi
# 结论：数量级对得上，被访问越多的状态越准
```

> [!tip] 直觉
> 函数逼近下，$V_\theta$ 对**访问频繁**的状态更准——因为那里拿到的梯度信号多；冷门状态只能靠泛化「蹭」到一点更新。这正是泛化的代价与好处一体两面：数据在状态间被复用，但精度按访问分布加权。深挖一层，这意味着学到的函数是在「探索策略的状态分布」下拟合的——换一个探索策略，同一个假设类会给出不同的近似。

**小结（函数逼近）**：

- 用 $\theta$ 参数化 $Q_\theta(s, a)$，代替「一格一值」的查找表；
- 把状态、动作映射成特征 $\phi(s, a)$，让相似的 $(s,a)$ 特征相近；
- 用线性函数或 MLP 完成 $\phi(s, a) \to Q_\theta(s, a)$；
- 定义 $Q_\theta(s, a)$ 与自举目标 $r + \gamma \max_{a'} Q_\theta(s', a')$ 之间的平方损失；
- 每条经验做一步 SGD——表格 Q-Learning 是它在 one-hot 特征下的特例。

---

## 3. 三大范式：To Model or Not to Model？

到目前为止，我们要么估计 MDP（model-based），要么估计 Q 值（value-based）。退一步看，学习最优行为其实有**三条路**：

```mermaid
flowchart LR
    subgraph MB["Model-Based"]
        M1["估计 MDP<br/>T̂, R̂"] --> M2["价值迭代"] --> M3["策略 π(s)"]
    end
    subgraph VB["Value-Based"]
        V1["估计 Q 值<br/>Q(s,a)"] --> V2["argmax_a"] --> V3["策略 π(s)"]
    end
    subgraph PB["Policy-Based"]
        P1["直接估计策略<br/>π_θ(a∣s)"] --> P3["策略 π(s)"]
    end

    style M1 fill:#ffe0b2,stroke:#e65100
    style V1 fill:#e1f5fe,stroke:#0277bd
    style P1 fill:#c8e6c9,stroke:#2e7d32
    style M3 fill:#f3e5f5,stroke:#7b1fa2
    style V3 fill:#f3e5f5,stroke:#7b1fa2
    style P3 fill:#f3e5f5,stroke:#7b1fa2
```

| 范式 | 估计什么 | 得到策略的方式 | 代表算法 | 直觉 |
| :--- | :--- | :--- | :--- | :--- |
| **Model-based** | MDP 的 $T$、$R$ | 对估计的 MDP 跑价值迭代 | Model-Based VI | 先把世界摸清楚，再规划 |
| **Value-based** | Q 值 $Q(s,a)$ | $\pi(s) = \arg\max_a Q(s,a)$ | SARSA、Q-Learning | 不建模世界，只学「每步值多少」 |
| **Policy-based** | 策略 $\pi_\theta(a \mid s)$ | 直接就是策略 | REINFORCE | 跳过中间量，直接学「怎么做」 |

> [!tip] 直觉
> 越往下越「直接」：model-based 绕两道弯（世界模型 → 规划 → 策略），value-based 绕一道弯（Q 值 → argmax），policy-based 一步到位。中间量越少，越不容易被「学得很准但用不上」的部分拖累——比如 model-based 可能花大量容量去精确预测和决策无关的环境细节。反过来，中间量也不是白学的：模型可以做规划和想象，Q 值给出「差多少」的定量信息。三条路各有取舍，现代算法（如 actor-critic、AlphaZero）往往是混合体。

**为什么要直接估计策略？** 把策略看成一个**分类器**：输入状态 $s$，输出动作 $a$——这不就是 [[Lecture 3 · 线性分类]] 吗？但硬分类器的 $\arg\max$ 不可导，没法做梯度优化，所以我们优化一个**概率分类器** $\pi_\theta(a \mid s)$：给每个动作一个概率，既可导、又自带探索。这正是策略梯度的起点。

---

## 4. 从模仿学习到策略梯度

### 4.1 模仿学习（Imitation Learning）：有示范就好办

假如有人给我们**示范**了好策略——一条专家 rollout $\tau$：

```python
rollout = Rollout(steps=[
    Step(action="walk", prob=1, reward=-1, state=2),
    Step(action="walk", prob=1, reward=-1, state=3),
    Step(action="tram", prob=1, reward=-2, state=6),
], discount=1)
```

那就把它拆成监督学习样本「状态 → 动作」，当分类问题训练：

```python
examples = [
    Example(input=1, output="walk"),
    Example(input=2, output="walk"),
    Example(input=3, output="tram"),
]
# 目标：J(θ) = Σ_t log π_θ(a_t | s_{t-1})   （最大化示范动作的对数似然）
```

目标函数即示范动作的对数似然：

$$J(\theta) = \sum_{t} \log \pi_\theta(a_t \mid s_{t-1})$$

最大化它就是让策略在专家到过的每个状态里，尽量复现专家的选择。这就是**模仿学习（imitation learning）**，也叫**行为克隆（behavior cloning）**。现实例子：机器人**遥操作（teleoperation）**示范、数学题的**人类书写解答**——大语言模型的 SFT（监督微调）本质上就是对人类示范的行为克隆（见 [[Lecture 15 · 对齐 I：SFT 与 RLHF]]）。

> [!warning] 易错点：行为克隆的分布偏移
> 行为克隆只在**专家到过的状态**上训练。一旦学到的策略犯一个小错、走进专家从没去过的状态，它就没了参照，往往越错越远——误差随步数**复合放大**（compounding errors）。这是模仿学习的经典缺陷（DAgger 等方法通过让专家在线纠错来缓解）。
>
> 而在真正的强化学习里问题更根本：我们**压根没有示范**——只有一个奖励函数。没人告诉我们「这一步该走 walk 还是 tram」。那怎么办？

### 4.2 策略梯度定理

思路：既然没示范，就**用自己的策略采样 rollout，按效用给它们加权**——好的 rollout 多学一点、差的少学一点（甚至反着学）。

**一条 rollout 的概率**。设 $\tau = (s_0, a_1, r_1, s_1, a_2, r_2, s_2, \ldots)$，它被采出来的概率是一连串因子的乘积：

$$p_\theta(\tau) = p(s_0) \cdot \pi_\theta(a_1 \mid s_0) \cdot T(s_0, a_1, s_1) \cdot \pi_\theta(a_2 \mid s_1) \cdot T(s_1, a_2, s_2) \cdots$$

- $p(s_0)$：初始状态分布，环境给定；
- $\pi_\theta(a_t \mid s_{t-1})$：**策略**——整条链里**唯一含 $\theta$** 的部分；
- $T(s_{t-1}, a_t, s_t)$：转移分布，**不含 $\theta$**，环境说了算。

**目标**：最大化期望效用（$u(\tau)$ 表示 rollout 的折扣奖励和）：

$$V(\theta) = \mathbb{E}_{\tau \sim p_\theta}[u(\tau)] = \sum_\tau p_\theta(\tau)\, u(\tau)$$

直接对 $V(\theta)$ 求梯度。关键一步是**对数导数技巧（log-derivative trick）**：由 $\nabla \log p = \nabla p / p$ 得 $\nabla p = p\, \nabla \log p$，把「概率的梯度」改写成「概率 × 对数概率的梯度」：

$$
\begin{aligned}
\nabla_\theta V(\theta)
&= \nabla_\theta \sum_\tau p_\theta(\tau)\, u(\tau) \\
&= \sum_\tau \nabla_\theta\, p_\theta(\tau)\, u(\tau) \\
&= \sum_\tau p_\theta(\tau)\, \nabla_\theta \log p_\theta(\tau)\, u(\tau) \\
&= \mathbb{E}_{\tau \sim p_\theta}\!\left[\nabla_\theta \log p_\theta(\tau) \cdot u(\tau)\right]
\end{aligned}
$$

这就是**策略梯度定理（policy gradient theorem）**。逐符号读：$u(\tau)$ 给整条轨迹打分，$\nabla_\theta \log p_\theta(\tau)$ 指向「让这条轨迹更可能出现」的参数方向，两者相乘再取期望——**让高分轨迹变得更可能，低分轨迹变得更不可能**。

> [!tip] 直觉
> 为什么非要绕道 $\log$？因为原始形式 $\sum_\tau \nabla p_\theta(\tau)\, u(\tau)$ 不是一个期望——$\nabla p_\theta(\tau)$ 不是概率分布，没法用采样近似。改写成 $\mathbb{E}_\theta[\cdots]$ 之后，**只要看到期望，就可以用采样无偏替换**：采一条 $\tau \sim p_\theta$，算 $\nabla_\theta \log p_\theta(\tau)\, u(\tau)$，它的期望恰好就是真梯度。于是一个「要遍历所有轨迹」的不可能问题，变成了「采几条轨迹」的蒙特卡洛问题。这个估计器在统计学里叫**得分函数估计器（score function estimator）**，也叫似然比（likelihood ratio）方法。

### 4.3 REINFORCE：把 T 消掉

对 $\log p_\theta(\tau)$ 做分解（乘积取对数变求和）：

$$\log p_\theta(\tau) = \log p(s_0) + \sum_{t \ge 1} \log \pi_\theta(a_t \mid s_{t-1}) + \sum_{t \ge 1} \log T(s_{t-1}, a_t, s_t)$$

对 $\theta$ 求梯度时，$p(s_0)$ 和 $T(\cdot)$ 都**不含 $\theta$，梯度为 0**，整段直接消失！于是单条 rollout 的梯度估计是：

$$\nabla_\theta J(\theta; \tau) = u(\tau) \cdot \sum_{t \ge 1} \nabla_\theta \log \pi_\theta(a_t \mid s_{t-1})$$

这就是 **REINFORCE 算法**（Williams, 1992）——**完全不需要知道转移概率 $T$**，也不用建模它：环境的随机性在「采样 rollout」这一步已经被自然地体现了。

```mermaid
graph LR
    R["采样 rollout 𝜏<br/>~ 自己的策略 π_θ"] --> U["算 utility(𝜏)"]
    U --> W["按 utility 加权<br/>对 (s,a) 做模仿学习"]
    W --> G["∇_θ J = utility · Σ ∇log π_θ(a∣s)"]
    G --> S["梯度上升更新 θ"]
    S -.下一条 rollout.-> R

    style R fill:#e1f5fe,stroke:#0277bd
    style U fill:#ffe0b2,stroke:#e65100
    style W fill:#f3e5f5,stroke:#7b1fa2
    style G fill:#fce4ec,stroke:#c2185b
    style S fill:#c8e6c9,stroke:#2e7d32
```

> [!tip] 直觉
> **REINFORCE = 对自己策略的示范做模仿学习，但按效用加权**。对比 4.1：模仿学习最大化 $\sum_t \log \pi_\theta(a_t \mid s_{t-1})$，REINFORCE 最大化 $u(\tau) \sum_t \log \pi_\theta(a_t \mid s_{t-1})$——形式上只多了一个权重。极端情形下若 $u(\tau) \in \{0, 1\}$（成功 / 失败），它就退化成「只模仿自己成功的那些 rollout」——非常符合直觉。这恰好也是当代大模型 RLVR（可验证奖励强化学习）的骨架：答案验证通过记 1、否则记 0，只强化做对的推理轨迹（见 [[Lecture 16 · 对齐 II：RLVR]]）。

> [!warning] 易错点
> REINFORCE 是严格 **on-policy** 的：策略梯度定理里的期望是对**当前** $\pi_\theta$ 的分布取的，所以每次参数更新后，旧 rollout 就「过期」了，不能直接复用（除非引入重要性采样加权——这正是 PPO 等现代算法的出发点）。这也是策略梯度法样本效率低的主要原因之一。

**小结（策略梯度）**：

- 目标：直接优化策略参数，最大化期望效用 $V(\theta) = \mathbb{E}_\theta[u(\tau)]$；
- 靠对数导数技巧，把梯度写成期望，从而可以用采样得到**无偏**梯度估计；
- 转移概率 $T$ 在求梯度时消失——不需要环境模型；
- 算法：采一条 rollout（on-policy），用其中每个 $(s, a)$ 做「按 $u(\tau)$ 加权的模仿学习」。

---

## 5. REINFORCE 实现

### 5.1 策略参数化：softmax 概率策略

策略 $\pi_\theta(a \mid s)$ 用一个线性层 + softmax 实现——和 [[Lecture 3 · 线性分类]] 的多类逻辑回归一模一样：

$$\pi_\theta(a \mid s) = \operatorname{softmax}\big(W \phi(s)\big)_a = \frac{\exp(w_a \cdot \phi(s))}{\sum_{a'} \exp(w_{a'} \cdot \phi(s))}$$

其中 $\phi(s)$ 是状态特征（这里是 one-hot），$W$ 的每一行 $w_a$ 是动作 $a$ 的打分权重，softmax 把打分（logits）归一化成合法概率分布（`Reinforce` 类，忠于源码）：

```python
class Reinforce(RLAlgorithm):
    def __init__(self, num_locs, actions, discount, learning_rate):
        self.actions = actions
        self.discount = discount
        # φ(s) 是长度 num_locs 的 one-hot；输出每个动作的 logit
        self.model = nn.Linear(num_locs, len(actions))
        self.optimizer = torch.optim.SGD(self.model.parameters(), lr=learning_rate)
        # 记录当前 rollout（像 Monte Carlo 那样攒完一整条再更新）
        self.start_state = None
        self.rollout = []
        self.utility = 0

    def phi(self, state):
        return one_hot(state - 1, self.num_locs)

    def pi(self, state):
        """返回动作上的分布 π_θ(·|s)"""
        logits = self.model(self.phi(state))
        probs = torch.softmax(logits, dim=0)
        return dict(zip(self.actions, probs.tolist()))
```

### 5.2 get_action：从策略里采样

注意——**不需要显式的探索策略**，因为随机策略本身就带来探索：

```python
    def get_action(self, state):
        """从 π_θ(a|s) 采样一个动作"""
        logits = self.model(self.phi(state))
        probs = torch.softmax(logits, dim=0)
        index = torch.multinomial(probs, num_samples=1).item()  # 按概率采样
        return self.actions[index]
```

> [!tip] 直觉
> value-based 方法的策略是 $\arg\max$，天生确定性，必须外挂 ε-贪婪才有探索；policy-based 的策略本身就是分布，按概率采样天然带探索——训练初期分布接近均匀（随机探索），随着某些动作的概率被推高，探索自动收窄。这是 REINFORCE 相较 Q-Learning 在结构上更简洁的一点。代价是：如果概率过早坍缩到单一动作，探索也会过早熄火（实践中常加熵正则来防止）。

### 5.3 incorporate_feedback：攒完一条 rollout 再更新

和 Model-Free Monte Carlo 一样，要等回合**结束**才更新——因为权重 $u(\tau)$ 需要完整轨迹才能算出来。损失用**交叉熵**实现「按效用加权的模仿学习」：

```python
    def incorporate_feedback(self, state, action, reward, next_state, is_end):
        # 记录 rollout，累计效用
        if self.start_state is None:
            self.start_state = state
        self.utility += reward * self.discount ** len(self.rollout)
        self.rollout.append(Step(action=action, prob=1, reward=reward, state=next_state))

        if is_end:
            loss = 0
            for i, step in enumerate(self.rollout):
                state = self.start_state if i == 0 else self.rollout[i - 1].state
                action = step.action

                # 对 state -> action 算交叉熵损失
                logits = self.model(self.phi(state))
                cross_entropy = nn.CrossEntropyLoss()
                target = one_hot(self.actions.index(action), len(self.actions))
                # 关键：整条 rollout 的效用作为权重
                loss += self.utility * cross_entropy(logits, target)

            # 一步梯度更新
            self.optimizer.zero_grad()
            loss.backward()
            self.optimizer.step()

            # 重置 rollout
            self.start_state = None
            self.rollout = []
            self.utility = 0
```

> [!note] 看清对应关系
> `cross_entropy(logits, target)` 对 one-hot 目标恰好等于 $-\log \pi_\theta(a \mid s)$；乘上 `self.utility` 就是 $-u(\tau) \log \pi_\theta(a \mid s)$。对这个损失做梯度**下降**，等价于对 $u(\tau) \sum_t \log \pi_\theta(a_t \mid s_{t-1})$ 做梯度**上升**——正是 4.3 的 REINFORCE 公式。另注意每一步都用**整条** rollout 的效用 $u(\tau)$ 作权重（而非各步自己的未来回报），这正是第 6 节 returns-to-go 要改进的点。

> [!example] 效用为负会怎样？
> 电车 MDP 的奖励全是负数（走一步 $-1$、坐电车 $-2$），所以每条 rollout 的 $u(\tau) < 0$，「加权交叉熵」的权重是负的——梯度更新会**压低**所有被采到动作的概率。这听起来荒谬，但 softmax 是归一化的：坏得少的轨迹被压得轻、坏得多的被压得重，**相对**概率仍朝正确方向移动，只是又慢又抖。这正是「效用的绝对水平不重要、相对差异才重要」的一个鲜活例子——引出第 6 节的基线。

**小结（实现）**：

- `get_action`：从 $\pi_\theta(a \mid s)$ 采样，探索内置于随机策略；
- `incorporate_feedback`：攒完整条 rollout，用其中的 $(s, a)$ 对做按 $u(\tau)$ 加权的交叉熵更新。

---

## 6. 增强：降低方差

REINFORCE 给出的是 $\nabla_\theta V(\theta)$ 的**无偏**估计，但**方差很大**：单条 rollout 的效用受随机策略和随机转移双重扰动，抖动剧烈，导致梯度方向忽东忽西、学习又慢又不稳。能不能估得更好？

### 6.1 偏差-方差：先看均值估计小实验

源码用一个「估计均值 $\mu = \mathbb{E}[f(i)] = \sum_i p(i) f(i)$」的小玩具，讲清评价估计器的**三要素：偏差（bias）/ 方差（variance）/ 成本（cost）**：

| 估计器 | 做法 | 偏差 | 方差 | 成本 |
| :--- | :--- | :---: | :---: | :---: |
| **estimator1** | 采 1 个点返回 $f(i)$ | 无偏 | 高 | 低 |
| **estimator2** | 采 2 个点取平均 | 无偏 | 更低 | 更高（2 次采样） |
| **estimator3** | 采 1 个点再**加噪声** | 无偏 | 更高（更糟） | 低 |
| **estimator4** | 减去一个均值为 0 的 $\text{offset}(i)$ | 无偏 | **更低** | 低 |

```python
def estimator4():
    offsets = torch.tensor([-6., -6., 6., 6.])   # 一个「魔法」偏移，均值为 0
    index = torch.multinomial(probs, num_samples=1)
    estimate = points[index] - offsets[index]     # E[f - offset] = E[f]
    return estimate
```

> [!tip] 直觉
> 核心洞见：**减去一个期望为 0 的量，不改变均值（仍无偏），却可能大幅降低方差**——前提是这个 offset 与 $f(i)$ **正相关**：$f$ 偏高时 offset 也偏高，相减后波动互相抵消。统计学里这叫**控制变量法（control variates）**。类比 rollout：estimator2 是「多采几条 rollout 求平均」（有效但贵），estimator4 是「找个聪明的 offset 白赚方差」（便宜）。策略梯度的所有增强，本质都在找这样的 offset。

### 6.2 基线（Baseline）：那个「offset」怎么找？

**关键恒等式**：设 $b(s)$ 是**只依赖状态 $s$、不依赖动作 $a$** 的任意函数，则对每个状态 $s$：

$$\mathbb{E}_{a \sim \pi_\theta(\cdot \mid s)}\left[\nabla_\theta \log \pi_\theta(a \mid s) \cdot b(s)\right] = 0$$

证明（把策略梯度定理的推导反着走一遍，核心是「概率归一化为常数 1」）：

$$
\begin{aligned}
\sum_a \pi_\theta(a \mid s) &= 1 \quad (\text{常数}) \\
\Rightarrow\ \nabla_\theta \sum_a \pi_\theta(a \mid s) \, b(s) &= 0 \\
\Rightarrow\ \sum_a \nabla_\theta\, \pi_\theta(a \mid s) \, b(s) &= 0 \\
\Rightarrow\ \sum_a \pi_\theta(a \mid s)\, \nabla_\theta \log \pi_\theta(a \mid s)\, b(s) &= 0 \\
\Rightarrow\ \mathbb{E}_{a \sim \pi_\theta}\left[\nabla_\theta \log \pi_\theta(a \mid s) \cdot b(s)\right] &= 0
\end{aligned}
$$

第三行到第四行再次用了对数导数技巧 $\nabla \pi = \pi \nabla \log \pi$。注意 $b(s)$ 能提出来全靠它**不依赖 $a$**——若 $b$ 依赖动作，就不能从对 $a$ 的求和中拿出，恒等式立刻失效。

> [!tip] 直觉
> **类比 [[Lecture 6 · UCS 与 A* 搜索]] 里的启发式函数**：$b(s)$ 就像 A\* 的 $h(s)$——注入领域知识（对状态好坏的先验估计）来改进算法效率，却（在满足条件时）不破坏正确性。这里「正确性」是无偏性：减掉 $b(s)$ 后梯度期望纹丝不动，方差却降下来了。减去基线后的权重 $u(\tau) - b(s)$ 读作「这条轨迹**比在 $s$ 处的平均预期好多少**」——学习信号从「绝对分数」变成「相对惊喜」，恰好治好了 5.3 例子里「全负效用」的病。

### 6.3 三种降方差增强

| 增强 | 目标函数中第 $t$ 步的权重 | 偏差 | 方差 |
| :--- | :--- | :---: | :---: |
| **1. 基线（baseline）** | $u(\tau) - b(s_{t-1})$ | 无偏 | ↓ |
| **2. 回报到底（returns-to-go）** | $\left(r_t + \gamma r_{t+1} + \gamma^2 r_{t+2} + \cdots\right) - b(s_{t-1})$ | 无偏 | ↓↓ |
| **3. 自举（bootstrapping）** | $Q(s_{t-1}, a_t) - b(s_{t-1})$ | **有偏** | ↓↓↓ |

- **基线**：从效用里减去 $b(s)$——最常用的选择是状态价值估计 $b(s) \approx V(s)$，只保留「比平均好多少」的信号；
- **回报到底**：第 $t$ 步的动作只能影响**它之后**的奖励，之前的奖励与它无关（因果性）——那部分在期望下同样是零，却白白贡献方差。用「从 $t$ 起的未来回报」替换全局效用 $u(\tau)$，等于把每一步与它无关的噪声砍掉，仍然无偏；
- **自举**：干脆用（学出来的、有偏的）价值函数 $Q(s, a)$ 替代采样回报——不再依赖整条轨迹的随机性，方差最小，但价值函数估不准的误差会**系统性**地带进梯度（有偏）。这一步把 policy-based 和 value-based 缝在一起：策略是 **actor**（演员），价值函数是 **critic**（评论家）——这就是 **actor-critic** 方法的雏形。

> [!note] 延伸：优势函数与现代算法
> 取 $b(s) = V^\pi(s)$、并把回报换成 $Q^\pi(s,a)$ 后，权重变成**优势函数（advantage function）** $A^\pi(s, a) = Q^\pi(s, a) - V^\pi(s)$：「在 $s$ 处选 $a$ 比按策略平均行事好多少」。如何在「无偏高方差的采样回报」与「有偏低方差的价值估计」之间连续调节，正是 GAE（Generalized Advantage Estimation）做的事；再加上限制每次更新幅度的 clipping，就得到 PPO——大模型 RLHF 的主力算法（见 [[Lecture 15 · 对齐 I：SFT 与 RLHF]]）。而 GRPO 则用「同一道题采多条 rollout、组内平均分当基线」来免去训练 critic——正是本节思想在大模型上的直接应用。

**小结（增强）**：

- 这场游戏的目标：为 $\mathbb{E}_\theta[\nabla_\theta \log p_\theta(\tau) \cdot u(\tau)]$ 找一个**低偏差、低方差、低成本**的估计；
- **基线**：减去只依赖状态的 $b(s)$，通用的降方差手段，仍然无偏；
- **回报到底**：利用因果性砍掉与当前动作无关的过去奖励，仍然无偏；
- **自举**：用价值函数替代采样回报，方差最低但引入偏差——通往 actor-critic。

---

## 7. 算法的通用形式

回到最高层，本讲所有方法（连同上一讲）都能塞进同一个三步模板：

```mermaid
flowchart TD
    A["1. 用探索策略<br/>产生 rollout"] --> B["2. 构造损失函数"]
    B --> C["3. 梯度步更新参数"]
    C -.循环.-> A

    B --> B1["MDP 参数<br/>→ model-based"]
    B --> B2["Q 值<br/>→ value-based"]
    B --> B3["策略<br/>→ policy-based"]

    style A fill:#e1f5fe,stroke:#0277bd
    style B fill:#ffe0b2,stroke:#e65100
    style C fill:#c8e6c9,stroke:#2e7d32
    style B1 fill:#f3e5f5,stroke:#7b1fa2
    style B2 fill:#f3e5f5,stroke:#7b1fa2
    style B3 fill:#f3e5f5,stroke:#7b1fa2
```

1. **用探索策略产生 rollout**——数据从交互中来（ε-贪婪外挂探索，或随机策略内置探索）；
2. **构造某个损失函数**——损失针对谁，就落入哪个范式：MDP 参数（model-based）/ Q 值（value-based）/ 策略（policy-based）；
3. **用梯度步更新参数**——SGD 一小步，回到第 1 步继续交互。

> [!tip] 直觉
> 一句话收束：在 MDP 与 RL 里，我们始终在**最大化期望效用**；三大范式只是把「拿什么当可学习对象」这个问题回答成了模型、Q 值或策略。而「交互产生数据 → 构造损失 → 梯度更新」的循环，让强化学习最终与监督学习共享同一台优化机器——差别只在数据是自己采的、标签是自己造的。

---

## 8. 总结

```mermaid
mindmap
  root((函数逼近与策略梯度))
    表格法的局限
      每个 s,a 对存一个 Q 值
      状态是图像或句子时失效
      存不下、也访问不全
    函数逼近
      带参数 θ 的 Q 函数
      特征 φ 加 线性或MLP
      平方损失 加 SGD
      泛化：状态间共享参数
    三大范式
      model-based 估计 MDP
      value-based 估计 Q 值
      policy-based 估计策略
    策略梯度 REINFORCE
      直接参数化随机策略
      策略梯度定理 无偏
      utility 加权的模仿学习
      转移 T 被消掉
    降方差
      偏差 方差 成本
      基线 b 仍无偏
      returns-to-go 因果性
      自举 有偏但方差小
      通往 actor-critic
```

**关键要点**：

- **表格法**为每个 $(s,a)$ 存一个 Q 值，状态空间巨大（图像、句子）时既存不下也无法泛化，彻底失效。
- **函数逼近**：用 $Q_\theta(s,a) = \phi(s,a) \cdot \theta$（或 MLP）代替查找表，落回「假设类 / 损失 / 优化」框架——把 RL 翻译成监督学习，标签由算法自己构造。
- **带梯度的 Q-Learning**：目标 $r + \gamma \max_{a'} Q_\theta(s',a')$，平方损失，对 $\theta$ 做 SGD；表格 Q-Learning 是其 one-hot 特例。注意半梯度与「致命三要素」的收敛陷阱。
- **三大范式**：model-based（估计 MDP）、value-based（估计 Q 值）、policy-based（估计策略）——越往后中间量越少、越直接。
- **REINFORCE**：直接参数化 $\pi_\theta(a \mid s)$（softmax 分类器），靠策略梯度定理得到无偏梯度 $u(\tau) \sum_t \nabla_\theta \log \pi_\theta(a_t \mid s_{t-1})$；本质是「对自己策略、按效用加权的模仿学习」，且**消去了转移 $T$**；随机策略自带探索，但严格 on-policy。
- **降方差**：基线 $b(s)$（控制变量，无偏）、returns-to-go（因果性，无偏）、自举（有偏但方差最小，通往 actor-critic）——都在给梯度估计找一个「期望为 0 的 offset」。
- **通用形式**：① 产生 rollout ② 构造损失（MDP / Q / 策略）③ 梯度步；核心永远是**最大化期望效用**。

**下一讲预告**：到目前为止环境都是「中立」的 MDP——它按固定的 $T$ 随机反应，不跟你作对。可如果环境里有**对手**、有人主动跟你唱反调呢？期望效用最大化就得换成「最坏情况下的最优」→ [[Lecture 10 · 博弈 I：Minimax 与 α-β 剪枝]]：两人零和博弈、minimax 与 α-β 剪枝。

---

## 复习自测

> [!question]- Q1：one-hot 特征下的参数化 Q-Learning 为什么「等价于」表格法？换什么特征才能真正发挥函数逼近的威力？
> one-hot 特征让每个 $(s,a)$ 对应参数向量里一个独立分量，更新 $Q_\theta(s,a)$ 只动那一个分量、不影响其他格子——行为与「一格一值」的表格完全相同（平方损失的 SGD 步恰好还原表格更新式）。要发挥威力，需要让**相似状态的特征相近**的表示：图像用 CNN 特征、文本用 embedding，或人工设计的低维特征。此时一次更新会同时改变所有相似状态的 Q 值，数据被复用，没见过的状态也能靠泛化给出估计。

> [!question]- Q2：推导中转移概率 $T$ 为什么消失了？这带来什么实际好处？
> 因为 $\log p_\theta(\tau)$ 分解为 $\log p(s_0) + \sum_t \log \pi_\theta(a_t \mid s_{t-1}) + \sum_t \log T(\cdot)$，而 $p(s_0)$ 与 $T$ 都不含 $\theta$，对 $\theta$ 求梯度时为零。好处：REINFORCE 完全不需要知道或估计环境模型——环境的随机性通过「采样 rollout」自然进入估计，这使策略梯度可直接用于动力学未知、甚至无法建模的真实环境（机器人、对话）。

> [!question]- Q3：REINFORCE 为什么不需要 ε-贪婪？它的探索从哪来？有什么隐患？
> 因为 $\pi_\theta(a \mid s)$ 本身是概率分布，`get_action` 按概率采样，非最优动作也有机会被选中——探索内置于随机策略。而 value-based 的策略是确定性的 $\arg\max$，必须外挂 ε-贪婪。隐患：如果某动作概率被过早推到接近 1（分布坍缩），探索会自行熄火且很难恢复——实践中常加熵正则鼓励分布保持「散开」。

> [!question]- Q4：算一算——所有 rollout 的效用都在 $100$ 附近、只差 $\pm 1$，vanilla REINFORCE 会怎样？基线如何救场？
> 每条轨迹的权重都是约 $+100$，梯度几乎把**所有**采到的动作概率都往上推，好坏轨迹的差别只占信号的约 $1\%$——有用的「相对差异」被巨大的「共同水平」淹没，梯度方差大、学习极慢。减去基线 $b(s) \approx 100$ 后，权重变成 $\pm 1$ 量级的「相对惊喜」：比平均好的强化、比平均差的抑制，信号纯度大增而期望不变（基线恒等式保证无偏）。

> [!question]- Q5：基线 $b(s)$ 为什么不引入偏差？如果让 $b$ 依赖动作 $a$ 还成立吗？
> 核心是恒等式 $\mathbb{E}_{a \sim \pi_\theta}[\nabla_\theta \log \pi_\theta(a \mid s)\, b(s)] = 0$：由 $\sum_a \pi_\theta(a \mid s) = 1$ 是常数，对 $\theta$ 求梯度为零，而 $b(s)$ 不依赖 $a$ 可整体提出求和号外。若 $b$ 依赖 $a$，它无法提出对 $a$ 的求和，$\sum_a \nabla \pi_\theta(a \mid s)\, b(s,a)$ 一般不为零——减去它会改变梯度期望，引入偏差。这正是「自举增强用 $Q(s,a)$ 替换回报是有偏的」的同一根源。

> [!question]- Q6：三种增强（基线 / returns-to-go / 自举）各自靠什么降方差？按「偏差-方差」怎么排序？
> 基线靠控制变量：减去与回报正相关、期望贡献为零的 $b(s)$，抵消共同波动；returns-to-go 靠因果性：第 $t$ 步动作影响不了 $t$ 之前的奖励，把那部分纯噪声砍掉；自举靠「以偏换稳」：用学到的 $Q(s,a)$ 替代整条采样回报，摆脱轨迹随机性。方差从高到低：vanilla > 基线 > returns-to-go > 自举；偏差上前三者无偏，只有自举有偏——它同时是 actor-critic 的起点。

---

## 参考资料

- 💻 [policy_gradient.py](https://github.com/stanford-cs221/autumn2025-lectures/blob/main/policy_gradient.py) — 本讲源码
- 📖 [Sutton & Barto: Reinforcement Learning (2nd ed.)](http://incompleteideas.net/book/the-book-2nd.html) — 第 9 章（on-policy 函数逼近）、第 11 章（off-policy 与致命三要素）、第 13 章（策略梯度方法）
- 📄 [Simple Statistical Gradient-Following Algorithms for Connectionist Reinforcement Learning (Williams, 1992)](https://link.springer.com/article/10.1007/BF00992696) — REINFORCE 原始论文
- 📄 [Policy Gradient Methods for RL with Function Approximation (Sutton et al., 2000)](https://proceedings.neurips.cc/paper/1999/file/464d828b85b0bed98e80ade0a5c43b0f-Paper.pdf) — 策略梯度定理与 actor-critic
- 📄 [High-Dimensional Continuous Control Using Generalized Advantage Estimation (Schulman et al., 2015)](https://arxiv.org/abs/1506.02438) — GAE：在偏差与方差之间连续调节
- 🎥 [David Silver's RL Course — Lecture 6 (Value Function Approximation) & Lecture 7 (Policy Gradient)](https://www.davidsilver.uk/teaching/) — DeepMind RL 课程
- 🌐 [CS221 课程主页](https://stanford-cs221.github.io/) — 讲义与作业
