工作台课程

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

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

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

CS221: Artificial Intelligence — Principles and Techniques · Stanford · Autumn 2025
📅 Oct 20 · 💻 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')$(自举)

注意

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

提示

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})$:

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 只有几个位置,当然没问题;可现实任务里的状态往往是:

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

注意

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

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

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)$,立刻回到机器学习那三个老问题:

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

提示

这套「假设类(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 向量。

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

说明

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):

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()    # 更新 θ

注意

注意 $\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 · 马尔可夫决策过程 的价值迭代)算出的真值比:

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
# 结论:数量级对得上,被访问越多的状态越准

提示

函数逼近下,$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)。退一步看,学习最优行为其实有三条路

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 跳过中间量,直接学「怎么做」

提示

越往下越「直接」: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$:

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)

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

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(未找到对应页面))。

注意

行为克隆只在专家到过的状态上训练。一旦学到的策略犯一个小错、走进专家从没去过的状态,它就没了参照,往往越错越远——误差随步数复合放大(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)$ 指向「让这条轨迹更可能出现」的参数方向,两者相乘再取期望——让高分轨迹变得更可能,低分轨迹变得更不可能

提示

为什么非要绕道 $\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」这一步已经被自然地体现了。

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

提示

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(未找到对应页面))。

注意

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 类,忠于源码):

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:从策略里采样

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

    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]

提示

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

5.3 incorporate_feedback:攒完一条 rollout 再更新

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

    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

说明

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 要改进的点。

例子

电车 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)$ 无偏 更低
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

提示

核心洞见:减去一个期望为 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$ 的求和中拿出,恒等式立刻失效。

提示

类比 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 方法的雏形。

说明

取 $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. 算法的通用形式

回到最高层,本讲所有方法(连同上一讲)都能塞进同一个三步模板:

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 步继续交互。

提示

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


8. 总结

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 与 α-β 剪枝。


复习自测

题目

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

题目

因为 $\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」自然进入估计,这使策略梯度可直接用于动力学未知、甚至无法建模的真实环境(机器人、对话)。

题目

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

题目

每条轨迹的权重都是约 $+100$,梯度几乎把所有采到的动作概率都往上推,好坏轨迹的差别只占信号的约 $1\%$——有用的「相对差异」被巨大的「共同水平」淹没,梯度方差大、学习极慢。减去基线 $b(s) \approx 100$ 后,权重变成 $\pm 1$ 量级的「相对惊喜」:比平均好的强化、比平均差的抑制,信号纯度大增而期望不变(基线恒等式保证无偏)。

题目

核心是恒等式 $\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)$ 替换回报是有偏的」的同一根源。

题目

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


参考资料