# Lecture 8 · 强化学习

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

---

## 承上启下：从已知模型到未知环境

上一讲 [[Lecture 7 · 马尔可夫决策过程]] 中，我们**完全知道** MDP 的转移概率 $T(s,a,s')$ 和奖励函数 $\text{Reward}(s,a,s')$，于是可以坐在桌前用贝尔曼方程做纯计算：策略评估算出给定策略的价值 $V_\pi$，价值迭代算出最优策略 $\pi^*$。

本讲把「已知模型」这个假设也去掉：**转移概率和奖励函数都不知道**，环境是一个黑盒。智能体只能实际执行动作、观察环境返回的奖励和新状态，**从交互经验中学习**最优策略——这就是**强化学习（Reinforcement Learning, RL）**。掷骰子之前，你连骰子有几个面都不知道，只能靠掷。

本讲的四个算法（Model-Based、Monte Carlo、SARSA、Q-Learning）都假设状态可以逐个列举、Q 值存成一张表。当状态空间大到表存不下（如围棋）时怎么办？这是下一讲 [[Lecture 9 · 函数逼近与策略梯度]] 的主题。

---

## 1. 强化学习框架

### 1.1 核心循环

```mermaid
sequenceDiagram
    participant Agent as 智能体 (RL 算法)
    participant Env as 环境 (MDP)
    
    loop 每个时间步
        Agent->>Env: 动作 a_t
        Env->>Agent: 奖励 r_t, 观测 s_{t+1}
        Note over Agent: 学习并更新策略
    end
```

每个时间步重复三件事：

1. **智能体**根据当前状态产生一个动作；
2. **环境**（内部其实是一个 MDP，但智能体看不到它的参数）返回奖励和新状态；
3. **智能体**把这条反馈 $(s, a, r, s')$ 吸收进自己的内部状态，改进未来的决策。

> [!tip] 直觉
> 与监督学习（[[Lecture 1 · 张量、梯度与监督学习]]）对比能看清 RL 的难点：监督学习每个样本都带正确答案 $y$，而 RL 只有一个标量奖励——它告诉你「这样做得了几分」，却不告诉你「正确动作是什么」；奖励还可能**延迟**到很多步之后才出现；更微妙的是，训练数据的分布由智能体自己的行为决定——走什么路，就只见过什么路。这三点（评价式反馈、延迟奖励、数据分布自决）是 RL 一切算法设计的根源。

### 1.2 RL 算法接口

```python
class RLAlgorithm:
    def get_action(self, state: Any) -> Any:
        """根据当前状态选择动作"""
        raise NotImplementedError
    
    def incorporate_feedback(self, state: Any, action: Any, reward: float, 
                            next_state: Any, is_end: bool) -> None:
        """从反馈中学习，更新内部策略"""
        raise NotImplementedError
```

所有 RL 算法都实现这两个方法：`get_action` 决定「怎么行动」，`incorporate_feedback` 决定「怎么从反馈中学」。本讲四个算法的差异全部体现在这两个方法内部。

### 1.3 模拟框架

```python
def simulate(mdp: MDP, rl: RLAlgorithm, num_trials: int) -> float:
    """让 RL 算法在 MDP 中运行 num_trials 轮，返回平均效用"""
    utilities = []
    
    for trial in range(num_trials):
        state = mdp.start_state()
        steps = []
        
        while not mdp.is_end(state):
            # 智能体选择动作
            action = rl.get_action(state)
            
            # 环境产生结果（根据 MDP 采样）
            successors = [s for s in mdp.successors(state) if s.action == action]
            probs = [s.prob for s in successors]
            step = np.random.choice(successors, p=probs)
            
            # 智能体从反馈中学习
            rl.incorporate_feedback(state, action, step.reward, step.state, 
                                   mdp.is_end(step.state))
            
            steps.append(step)
            state = step.state
        
        utilities.append(compute_utility(steps, mdp.discount()))
    
    return np.mean(utilities)
```

注意信息隔离：`simulate` 里的 `mdp` 只被用来**采样**，RL 算法拿到的仅仅是 $(s, a, r, s', \text{is\_end})$ 五元组——它永远看不到 `mdp.successors` 里的概率。官方讲义的实验环境是 `FlakyTramMDP(num_locs=10, failure_prob=0.4)`（[[Lecture 7 · 马尔可夫决策过程]] 的不可靠电车）。

### 1.4 策略 vs 智能体

| 维度 | 策略（Policy） | 智能体（RL Algorithm） |
|:---|:---|:---|
| 定义 | 状态 → 动作的映射 | 有学习能力的策略 |
| 是否改变 | **静态**（固定） | **动态**（随经验改进） |
| 例子 | `π(s) = "walk"` | Q-Learning、SARSA |

**核心思想**：RL 算法可以看作「会自我改写的策略」——它内部维护一个当前策略，并利用每条反馈逐步改进它。

---

## 2. 基于模型的强化学习（Model-Based RL）

### 2.1 核心思想

既然不知道 MDP，最朴素的想法：先从数据中**估计**出 MDP，再对估计出的 MDP 用上一讲的价值迭代求解。

**三阶段流程**：

1. **探索阶段**：用探索策略（如均匀随机选动作）与环境交互，累积转移与奖励数据；
2. **计算阶段**：对估计出的 $\hat{T}, \hat{R}$ 运行价值迭代，得到（估计意义下的）最优策略；
3. **利用阶段**：执行这个策略收割回报。

```mermaid
flowchart LR
    A["探索<br/>随机动作"] --> B["估计 MDP<br/>T̂, R̂"]
    B --> C["价值迭代<br/>计算 π*"]
    C --> D["利用<br/>执行 π*"]
    
    style A fill:#e1f5fe,stroke:#0277bd
    style C fill:#ffe0b2,stroke:#e65100
    style D fill:#c8e6c9,stroke:#2e7d32
```

### 2.2 估计 MDP：数数即可

转移概率用**频率**估计（即最大似然估计）：

$$\hat{T}(s, a, s') = \frac{\#(s, a, s')}{\#(s, a)}$$

其中 $\#(s,a,s')$ 是「在 $s$ 执行 $a$ 落到 $s'$」被观测到的次数，$\#(s,a)$ 是「在 $s$ 执行 $a$」的总次数。奖励是确定值，记录一次即可。

```python
class EstimatedMDP(MDP):
    """从反馈中学习的 MDP"""
    def __init__(self, discount: float):
        self.rewards = defaultdict(float)  # (s, a, s') → reward
        self.transitions = defaultdict(lambda: defaultdict(lambda: defaultdict(int)))
        # state → action → next_state → count
        self.end_states = set()
        self.discount_ = discount
    
    def incorporate_feedback(self, state, action, reward, next_state, is_end):
        """从一次交互中更新估计"""
        self.rewards[(state, action, next_state)] = reward
        self.transitions[state][action][next_state] += 1  # 计数
        if is_end:
            self.end_states.add(next_state)
    
    def successors(self, state):
        """基于计数估计转移概率"""
        successors = []
        for action, next_state_counts in self.transitions[state].items():
            total_count = sum(next_state_counts.values())
            for next_state, count in next_state_counts.items():
                prob = count / total_count  # 频率估计概率
                reward = self.rewards[(state, action, next_state)]
                successors.append(Step(action, prob, reward, next_state))
        return successors
```

> [!example] 频率估计
> 观测到 $(s{=}1, a{=}\text{tram})$ 共 10 次，其中 6 次到达 $s'{=}2$、4 次留在 $s'{=}1$，则
> $$\hat{T}(1, \text{tram}, 2) = \tfrac{6}{10} = 0.6, \qquad \hat{T}(1, \text{tram}, 1) = \tfrac{4}{10} = 0.4$$
> 恰好接近真实的失败概率 0.4。样本越多，估计越准（大数定律）。

### 2.3 Model-Based 价值迭代算法

```python
class ModelBasedValueIteration(RLAlgorithm):
    def __init__(self, exploration_policy: Policy, discount: float):
        self.exploration_policy = exploration_policy
        self.exploitation_policy = None
        self.mdp = EstimatedMDP(discount=discount)
    
    def run_value_iteration(self):
        """对估计的 MDP 运行价值迭代"""
        result = value_iteration(self.mdp)
        self.exploitation_policy = result.pi
    
    def get_action(self, state):
        if self.exploitation_policy is None:
            # 探索阶段：随机动作
            return self.exploration_policy(state)
        else:
            # 利用阶段：执行最优策略
            return self.exploitation_policy[state]
    
    def incorporate_feedback(self, state, action, reward, next_state, is_end):
        # 更新估计的 MDP
        self.mdp.incorporate_feedback(state, action, reward, next_state, is_end)
```

> [!warning] 易错点
> 探索策略必须**覆盖所有的 $(s,a)$ 对**。频率估计只对见过的转移有数——如果某个动作从未被尝试，估计的 MDP 里它就不存在，价值迭代自然永远不会选它，哪怕它其实是最优动作。这就是为什么探索阶段要用随机策略而不是「看起来好」的策略：省下的探索会变成永久的盲区。

### 2.4 优缺点

**优点**：

- 概念清晰：把「学习」和「规划」干净地分成两个阶段，每个阶段都用已有工具（计数估计 + 价值迭代）；
- **样本效率高**：每条经验都沉淀进 $\hat{T}, \hat{R}$，之后的价值迭代可以反复利用全部数据。

**缺点**：

- 需要显式存储和估计整个 MDP：参数量是 $O(|S|^2 |A|)$ 级别，状态空间大时既存不下也估不准；
- 探索与利用需要手动分阶段切换，「探索多久才够」没有原则性答案。

**改进方向**：注意到我们最终要的只是最优策略（即每个状态的 $\arg\max_a Q^*$）——能否**跳过建模 MDP，直接估计 Q 值**？

---

## 3. 无模型蒙特卡洛（Model-Free Monte Carlo）

### 3.1 核心思想

回顾最优 Q 值的定义（[[Lecture 7 · 马尔可夫决策过程]]）：

$$Q^*(s, a) = \sum_{s'} T(s, a, s')\left[\text{Reward}(s, a, s') + \gamma V^*(s')\right]$$

它是一个期望——而**期望可以不通过模型、直接用样本平均来估计**。这就是蒙特卡洛（Monte Carlo）思路：从 $(s,a)$ 出发跑完整回合，把实际获得的折扣效用记下来，多次平均：

$$\hat{Q}(s, a) = \frac{\text{从 } (s,a) \text{ 出发观测到的效用之和}}{\text{访问次数}}$$

这样 $T$ 和 $\text{Reward}$ 从头到尾都不需要显式出现——它们的影响已经「烘焙」在采样出的轨迹里了。

### 3.2 算法实现

```python
class ModelFreeMonteCarlo(RLAlgorithm):
    def __init__(self, exploration_policy: Policy, epsilon: float, discount: float):
        self.exploration_policy = exploration_policy
        self.epsilon = epsilon  # ε-贪婪探索
        self.discount = discount
        
        # 维护每个 (s, a) 的累计效用和访问次数
        self.sum_utilities = defaultdict(lambda: defaultdict(float))  # s → a → 总效用
        self.counts = defaultdict(lambda: defaultdict(int))           # s → a → 计数
        
        # 当前 rollout 的历史
        self.rollout = []
        self.start_state = None
    
    def Q(self, state, action):
        """估计的 Q 值 = 平均效用"""
        if self.counts[state][action] == 0:
            return 0
        return self.sum_utilities[state][action] / self.counts[state][action]
    
    def pi(self, state):
        """当前策略：选择 Q 值最大的动作"""
        actions = list(self.counts[state].keys())
        q_values = [self.Q(state, a) for a in actions]
        return actions[np.argmax(q_values)]
    
    def get_action(self, state):
        """ε-贪婪探索"""
        if len(self.counts[state]) == 0 or np.random.random() < self.epsilon:
            return self.exploration_policy(state)  # 探索
        else:
            return self.pi(state)  # 利用
    
    def incorporate_feedback(self, state, action, reward, next_state, is_end):
        # 记录这一步
        self.rollout.append(Step(action, 1, reward, next_state))
        if self.start_state is None:
            self.start_state = state
        
        # 回合结束时，回溯计算每步的效用并更新统计量
        if is_end:
            utilities = [0] * (len(self.rollout) + 1)
            
            # 从后往前计算效用
            for i in reversed(range(len(self.rollout))):
                step = self.rollout[i]
                state = self.start_state if i == 0 else self.rollout[i-1].state
                
                # u_i = r_i + γ u_{i+1}
                utilities[i] = step.reward + self.discount * utilities[i+1]
                
                # 更新累计统计
                self.sum_utilities[state][step.action] += utilities[i]
                self.counts[state][step.action] += 1
            
            # 重置历史
            self.start_state = None
            self.rollout = []
```

实现要点：回合结束后**从后往前**回溯，利用递推 $u_i = r_i + \gamma\, u_{i+1}$ 一趟算出每一步位置的「后续效用」，再把它计入对应 $(s,a)$ 的平均。这个平均也可以写成等价的增量形式——

$$\hat{Q}(s,a) \leftarrow \hat{Q}(s,a) + \eta \left[u - \hat{Q}(s,a)\right], \qquad \eta = \frac{1}{\#(s,a)}$$

即「朝着新样本 $u$ 挪一小步」，为下一节的 TD 更新埋下伏笔。

### 3.3 ε-贪婪探索

**探索-利用困境（Exploration-Exploitation Dilemma）**：只利用（总选当前最优）可能一辈子错过真正的好动作；只探索（总随机选）则永远不收割学到的知识。任何 RL 算法都必须在两者之间取舍。

**ε-贪婪（ε-greedy）策略**是最简单的折中：

$$\pi_{\text{act}}(s) = \begin{cases} \text{随机动作} & \text{以概率 } \epsilon \\ \arg\max_a \hat{Q}(s, a) & \text{以概率 } 1 - \epsilon \end{cases}$$

```python
if np.random.random() < epsilon:
    action = random_action()  # 探索
else:
    action = argmax_a Q(s, a)  # 利用
```

> [!tip] 直觉
> ε-贪婪解决了 Model-Based 方法「手动分阶段」的尴尬：探索和利用不再是先后两个阶段，而是**每一步都以概率混合**，学习与收割同时进行。官方实验取 $\epsilon = 0.4$——探索占比相当高，因为电车问题的最优路线需要连续「冒险」坐电车才能被发现。

### 3.4 优缺点

**优点**：

- 完全无模型：不存 $\hat T$、$\hat R$，只存一张 Q 表，空间 $O(|S||A|)$；
- 估计**无偏（unbiased）**：效用样本是真实回报，平均值收敛到真实期望。

**缺点**：

- 必须等到回合**结束**才能计算效用、执行更新——反馈严重延迟，长回合下学习极慢，无限回合（持续型任务）下干脆不可用；
- **方差高**：一条轨迹的效用累积了后续每一步的随机性，样本噪声大，需要很多回合才能平均掉。

**改进方向**：能否不等回合结束、**每走一步就更新**？

---

## 4. SARSA（同策略 TD 学习）

> [!warning] 易错点：on-policy ≠ 在线，off-policy ≠ 离线
> SARSA 是 **on-policy（同策略）**、Q-Learning 是 **off-policy（异策略）**，指的是「学习目标对应的策略」与「实际行动的策略」是否为同一个。这与 online/offline learning（在线/离线学习，指数据是边交互边到达还是事先给定的静态数据集）是**两组完全不同的概念**——SARSA 和 Q-Learning 都是在线算法。中文资料常把 off-policy 误译作「离线」，复习时注意区分。

### 4.1 核心思想：自举（Bootstrapping）

蒙特卡洛用**真实效用**做更新目标，代价是要等完整轨迹：

$$u = r_0 + \gamma r_1 + \gamma^2 r_2 + \cdots + \gamma^n r_n \quad (\text{需要整个回合})$$

**SARSA** 走一步就更新：真实效用中「未来」的部分用**当前的 Q 值估计**顶替——

$$u = r_0 + \gamma\, \hat{Q}_\pi(s_1, a_1) \quad (\text{只需一步观测})$$

这种「用自己的估计值构造自己的更新目标」正是上一讲价值迭代里见过的**自举（bootstrapping）**，用在从采样数据中学习时称为**时序差分（Temporal Difference, TD）学习**。

```mermaid
graph LR
    S0["s_0"] -->|"a_0, r_0"| S1["s_1"]
    S1 -->|"a_1"| S2["s_2"]
    S2 -->|"a_2"| S3["..."]
    
    MC["蒙特卡洛<br/>等到结束"] -.需要完整轨迹.-> S3
    SARSA["SARSA<br/>立即更新"] -.用 Q 估计截断.-> S1
    
    style MC fill:#ffcdd2,stroke:#c62828
    style SARSA fill:#c8e6c9,stroke:#2e7d32
```

> [!tip] 直觉
> 蒙特卡洛与 TD 是一对**偏差-方差权衡**：MC 的目标无偏但方差大（累积整条轨迹的随机性）；TD 的目标只含一步随机性、方差小，但 $\hat{Q}$ 本身不准时目标有偏。实践中 TD 通常学得快得多——尤其在长回合问题里，它把「一整局的成败」拆成「每一步的落差」来学。TD 思想后面还会反复出现：[[Lecture 11 · 博弈 II：TD 学习与同时博弈]] 用它来学习棋局估值函数。

### 4.2 SARSA 算法

**名称来源**：每次更新恰好用到五元组 State–Action–Reward–State–Action：

$$s_t \xrightarrow{a_t} r_t,\ s_{t+1} \xrightarrow{a_{t+1}} \cdots$$

更新规则（$\alpha$ 为学习率）：

$$\hat{Q}_\pi(s, a) \leftarrow \hat{Q}_\pi(s, a) + \alpha \big[\underbrace{r + \gamma\, \hat{Q}_\pi(s', a')}_{\text{TD 目标}} - \hat{Q}_\pi(s, a)\big]$$

方括号内是 **TD 误差（TD error）**：新证据（走了一步后的估计）与旧估计的落差。Q 值朝着消除这个落差的方向挪 $\alpha$ 步。

```python
class SARSA(RLAlgorithm):
    def __init__(self, exploration_policy, epsilon, discount, learning_rate):
        self.exploration_policy = exploration_policy
        self.epsilon = epsilon
        self.discount = discount
        self.learning_rate = learning_rate  # 学习率 α
        self.Q = defaultdict(lambda: defaultdict(float))  # s → a → Q值
    
    def get_action(self, state):
        """ε-贪婪"""
        if len(self.Q[state]) == 0 or np.random.random() < self.epsilon:
            return self.exploration_policy(state)
        else:
            return self.pi(state)
    
    def pi(self, state):
        """当前策略"""
        actions = list(self.Q[state].keys())
        q_values = [self.Q[state][a] for a in actions]
        return actions[np.argmax(q_values)]
    
    def incorporate_feedback(self, state, action, reward, next_state, is_end):
        # 关键：用 get_action（含 ε 探索的当前行为策略）选择 next_action（on-policy）
        next_action = self.get_action(next_state)
        
        # 自举估计效用
        u = reward + self.discount * self.Q[next_state].get(next_action, 0)
        
        # 梯度更新：Q(s,a) ← Q(s,a) + α[u - Q(s,a)]
        self.Q[state][action] += self.learning_rate * (u - self.Q[state][action])
```

### 4.3 为什么 SARSA 是同策略（on-policy）

SARSA 估计的是**当前行为策略**（含 ε 探索）的 Q 值 $Q_\pi(s,a)$，满足的递归是固定策略版的贝尔曼方程：

$$Q_\pi(s, a) = \sum_{s'} T(s, a, s') \left[ \text{Reward}(s, a, s') + \gamma\, Q_\pi(s', \pi(s')) \right]$$

代码中的对应：TD 目标里的 `next_action` 由 `self.get_action(next_state)` 产生——这正是智能体**实际会执行**的动作（可能是贪婪的，也可能是探索的随机动作）。「评估谁就按谁采样」，学习目标与行为一致，故称同策略。代价是：它学到的是「带着探索噪声行动的自己」的价值，而不是最优策略的价值。

---

## 5. Q-Learning（异策略 TD 学习）

### 5.1 核心区别

- **SARSA**：估计当前行为策略的 $Q_\pi$（on-policy）；
- **Q-Learning**：直接估计**最优策略**的 $Q^*$（off-policy）。

问题：最优策略还不知道，怎么估计它的 Q 值？答案：构造 TD 目标时，下一步动作不用实际执行的那个，而是**假设下一步按当前 Q 值贪婪地选最优**——用贪婪策略充当最优策略的代理。

### 5.2 Q-Learning 算法

更新规则：

$$\hat{Q}(s, a) \leftarrow \hat{Q}(s, a) + \alpha \big[\, r + \gamma \max_{a'} \hat{Q}(s', a') - \hat{Q}(s, a) \big]$$

与 SARSA 唯一的区别：TD 目标里的 $\hat{Q}(s',a')$ 换成了 $\max_{a'} \hat{Q}(s', a')$。

```python
class QLearning(SARSA):  # 继承 SARSA，只改一行
    def incorporate_feedback(self, state, action, reward, next_state, is_end):
        # 关键：用 pi（贪婪策略）选择 next_action（off-policy）
        next_action = self.pi(next_state)  # 而非 self.get_action
        
        # 自举估计效用（用贪婪动作）
        u = reward + self.discount * self.Q[next_state].get(next_action, 0)
        
        # 梯度更新
        self.Q[state][action] += self.learning_rate * (u - self.Q[state][action])
```

**一行之差**：

- SARSA：`next_action = self.get_action(next_state)`——下一步**实际会怎么走**（含探索）；
- Q-Learning：`next_action = self.pi(next_state)`——下一步**最优应该怎么走**（纯贪婪）。

### 5.3 Off-Policy 的威力

**行为策略（behavior policy）≠ 目标策略（target policy）**：

- 行为策略：实际与环境交互、负责探索的策略（ε-贪婪）；
- 目标策略：学习目标所对应的策略（贪婪 ≈ 最优）。

Q-Learning 把两者解耦：**一边用探索性的行为收集数据，一边学习最优策略的价值**。探索造成的「绕路」不会污染学习目标——更新式里的 $\max$ 假定未来总是走最优。

```mermaid
graph TD
    A["ε-贪婪<br/>行为策略"] -->|执行动作| B["环境"]
    B -->|反馈| C["Q-Learning"]
    C -->|更新| D["Q* 值<br/>最优策略"]
    D -.目标策略.-> C
    
    style A fill:#e1f5fe,stroke:#0277bd
    style D fill:#c8e6c9,stroke:#2e7d32
```

> [!note] SARSA 与 Q-Learning 学出的策略可以真的不同：悬崖行走
> Sutton & Barto 的经典例子：智能体沿悬崖边从起点走到终点，掉崖大惩罚。Q-Learning 学到贴崖最短路（最优策略确实如此——它假设未来不犯错）；SARSA 学到离崖一格的绕行路——因为它评估的是「带着 ε 概率随机乱走的自己」，贴崖行走对这个策略来说期望代价很高。训练期间（仍在探索时）SARSA 的实际得分反而更高。这说明 on/off-policy 不是实现细节，而是「为哪个策略负责」的语义差异。

### 5.4 贝尔曼方程视角

Q-Learning 更新式不是拍脑袋设计的——它正是**贝尔曼最优方程**（[[Lecture 7 · 马尔可夫决策过程]]）的采样版：

$$Q^*(s, a) = \sum_{s'} T(s, a, s') \left[ \text{Reward}(s, a, s') + \gamma \max_{a'} Q^*(s', a') \right]$$

已知模型时，价值迭代对 $s'$ 求期望；不知模型时，Q-Learning 用环境采出的单个 $s'$ 代替期望，再以小步长 $\alpha$ 平均掉采样噪声——相当于对损失 $\big(\hat{Q}(s,a) - \text{target}\big)^2$ 做随机梯度下降。价值迭代 : Q-Learning ≈ 全量梯度 : SGD。

---

## 6. 算法对比

### 6.1 总览

| 算法 | 需要模型 | 估计对象 | 同/异策略 | 更新时机 | 收敛目标 |
|:---|:---:|:---:|:---:|:---:|:---|
| **Model-Based VI** | 估计 $\hat T,\hat R$ | 间接（经 VI） | — | 探索期结束后 | $\pi^*$（若探索充分） |
| **Monte Carlo** | 不需要 | Q 值 | 同策略 | 回合结束 | $Q_\pi$ |
| **SARSA** | 不需要 | Q 值 | 同策略 | 每步 | $Q_\pi$ |
| **Q-Learning** | 不需要 | Q 值 | 异策略 | 每步 | $Q^*$ |

> [!note] 官方讲义的横向实验
> 六个选手在 `FlakyTramMDP(10, 0.4)` 上同台（$\epsilon=0.4$、$\alpha=0.1$、$\gamma=1$、各测 20 局）：固定基线 `tram_if_possible_policy`、Model-Based 的探索期与利用期、Monte Carlo、SARSA、Q-Learning。要点结论：Model-Based 探索期得分很差（随机走）而利用期接近最优，体现「先亏后赚」；三个无模型算法边学边得分，Q-Learning 最终逼近最优策略的水平。

### 6.2 可视化对比

```mermaid
mindmap
  root((强化学习算法))
    基于模型
      估计 T 和 R
      运行价值迭代
      样本效率高
      需存整个 MDP
    无模型
      蒙特卡洛
        完整回合更新
        无偏估计
        高方差
      SARSA
        每步更新
        自举 TD
        同策略学 Q_π
      Q-Learning
        每步更新
        自举 TD
        异策略学 Q*
```

### 6.3 探索策略一览

所有算法都要回答「什么时候试新动作」。除 ε-贪婪外的常见方案：

- **ε-贪婪（ε-greedy）**：以固定概率 ε 随机探索。简单、够用，是本讲所有无模型算法的默认选择；
- **ε 衰减**：训练初期 ε 大（多探索），随经验增长逐渐减小（多利用），缓解固定 ε「学好了还在乱走」的浪费；
- **玻尔兹曼探索（Boltzmann / softmax）**：按 $\pi(a \mid s) \propto \exp\!\big(\hat{Q}(s,a)/\tau\big)$ 采样——Q 值高的动作被选中的概率大，但不是独占；温度 $\tau$ 控制随机程度，$\tau \to 0$ 退化为贪婪；
- **UCB（Upper Confidence Bound）**：给每个动作加上与访问次数成反比的「不确定性加成」，优先尝试访问少的动作——「乐观面对不确定性」，比盲目随机更有方向感。

```python
# ε 衰减示例
epsilon = max(0.01, 1.0 - episode / 1000)  # 从 1.0 衰减到 0.01
```

---

## 7. 学习率与收敛

### 7.1 学习率的作用

更新公式可以改写成凸组合形式：

$$\hat{Q}(s,a) \leftarrow (1-\alpha)\, \hat{Q}(s,a) + \alpha\, u$$

即新估计是「旧估计」与「新目标 $u$」的加权平均，$\alpha$ 决定信新的还是信旧的：

- $\alpha = 0$：完全不学，Q 值冻结；
- $\alpha = 1$：完全相信最新一个样本，之前的经验全部丢弃——噪声环境下会剧烈震荡；
- $\alpha \in (0,1)$：对历史样本做指数加权平均，越新的样本权重越大。

### 7.2 收敛条件（Robbins–Monro）

理论上 Q-Learning 收敛到 $Q^*$（Watkins & Dayan, 1992）需要两个条件：

1. **充分探索**：每个 $(s,a)$ 对被访问无限多次——没见过的动作谈不上估准；
2. **学习率适当衰减**（Robbins–Monro 条件）：

$$\sum_{t=1}^{\infty} \alpha_t = \infty \qquad \text{且} \qquad \sum_{t=1}^{\infty} \alpha_t^2 < \infty$$

   第一条保证步长总量无限——无论初始估计错得多离谱都能被修正过来；第二条保证步长平方和有限——采样噪声的累积影响最终趋于零，估计能稳定下来。例如 $\alpha_t = 1/t$ 同时满足两条（此时更新恰好等价于算术平均）。

**实践**：常用固定小学习率（官方实验 $\alpha = 0.1$）。理论上不保证收敛（会在 $Q^*$ 附近小幅波动），但对非平稳环境反而更合适——旧经验会自然被遗忘。

---

## 8. 总结

### 8.1 核心对比

```mermaid
flowchart TD
    A["强化学习"] --> B{"知道 MDP?"}
    B -->|是| C["价值迭代<br/>（上一讲）"]
    B -->|否| D{"估计模型?"}
    D -->|是| E["Model-Based<br/>估计 T, R"]
    D -->|否| F{"何时更新?"}
    F -->|回合结束| G["Monte Carlo<br/>完整轨迹"]
    F -->|每步| H{"同/异策略?"}
    H -->|同策略| I["SARSA<br/>学 Q_π"]
    H -->|异策略| J["Q-Learning<br/>学 Q*"]
    
    style C fill:#e3f2fd,stroke:#1976d2
    style E fill:#fff3e0,stroke:#ef6c00
    style G fill:#fce4ec,stroke:#c2185b
    style I fill:#f3e5f5,stroke:#7b1fa2
    style J fill:#e8f5e9,stroke:#2e7d32
```

### 8.2 关键要点

**强化学习的三大核心挑战**：

1. **探索 vs 利用**：只利用会错过更优动作，只探索则浪费已学知识；ε-贪婪等机制在两者间按概率折中；
2. **延迟奖励**：关键决策的回报可能许多步之后才显现（坐电车「亏」2 分钟，收益要到终点才能确认），算法必须能把远期结果归因到早期动作；
3. **信用分配（credit assignment）**：一个回合的总分由许多动作共同造成，到底该表扬/责怪哪一步？MC 把整段回报记到途经的每个 $(s,a)$ 头上，TD 则用逐步的落差把功过一层层向前传。

**四大算法一句话总结**：

- **Model-Based**：先数数估计出 MDP，再用价值迭代规划（样本效率高，但要存整个模型、探索需手动分阶段）；
- **Monte Carlo**：用完整回合的真实效用平均出 Q 值（无偏但高方差，且必须等回合结束）;
- **SARSA**：每步自举更新，学**当前行为策略**的 $Q_\pi$（同策略——为「会探索的自己」负责）；
- **Q-Learning**：每步自举更新，TD 目标取 $\max$，学**最优策略**的 $Q^*$（异策略——边随意探索边学最优，本质是贝尔曼最优方程的 SGD 版）。

> [!note] 延伸：这套框架的现代回响
> 表格型 Q-Learning 是深度强化学习的直系祖先：DQN 用神经网络替代 Q 表打 Atari，AlphaGo 在此之上加搜索。大语言模型的对齐同样是 RL 框架的实例——把人类偏好或可验证的正确性当作奖励信号来优化策略，见 CS336 的 [[Lecture 15 · 对齐 I：SFT 与 RLHF]] 与 [[Lecture 16 · 对齐 II：RLVR]]。

**下一讲预告**：本讲的 Q 表要求逐个状态存数值。状态空间巨大（围棋约 $10^{170}$ 种状态）时表存不下，也无法泛化到没见过的状态 → 用函数逼近（神经网络）表示 Q 与策略，见 [[Lecture 9 · 函数逼近与策略梯度]]。

---

## 复习自测

> [!question]- Q1：SARSA 与 Q-Learning 的更新式只差在哪里？为什么这一行代码决定了同策略/异策略？
> 差别只在 TD 目标里下一步动作的来源：SARSA 用 `get_action(s')`（实际要执行的 ε-贪婪动作），Q-Learning 用 `pi(s')`（纯贪婪的 $\max_{a'}$）。TD 目标里「下一步按谁走」定义了「在评估哪个策略」：按实际行为走 → 学行为策略的 $Q_\pi$（同策略）；按贪婪走 → 学最优策略的 $Q^*$（异策略），哪怕实际行为仍在探索。

> [!question]- Q2：蒙特卡洛为什么必须等回合结束？它和 TD 的偏差-方差权衡是什么？
> MC 的更新目标是真实效用 $u = \sum_t \gamma^t r_t$，只有回合结束才能算出来。它无偏（样本就是真实回报）但方差高（整条轨迹的随机性都累积在一个数里）。TD 用 $r + \gamma \hat{Q}(s',a')$ 截断未来：只含一步随机性、方差小、每步都能更新，但 $\hat{Q}$ 未收敛时目标有偏。长回合问题中 TD 通常显著更快。

> [!question]- Q3：算一算——设 $\alpha=0.5$、$\gamma=1$，当前 $\hat{Q}(s,a)=0$，观测到 $(s, a, r{=}5, s')$，且 $\hat{Q}(s',a_1)=2$（贪婪动作）、$\hat{Q}(s',a_2)=-1$。若 ε-贪婪在 $s'$ 实际选了探索动作 $a_2$，SARSA 与 Q-Learning 各把 $\hat{Q}(s,a)$ 更新成多少？
> SARSA 用实际动作 $a_2$：目标 $= 5 + (-1) = 4$，更新 $\hat{Q}(s,a) = 0 + 0.5(4-0) = 2$。
> Q-Learning 取 $\max$（即 $a_1$）：目标 $= 5 + 2 = 7$，更新 $\hat{Q}(s,a) = 0 + 0.5(7-0) = 3.5$。
> 同一条经验，两个算法给出不同更新——因为它们在为不同的策略估值。

> [!question]- Q4：把 ε 设为 0（纯贪婪）会发生什么？
> 初期 Q 值几乎全为默认值，贪婪策略会锁死在最先碰巧得到正反馈的动作上；未尝试过的动作 Q 值永远不更新，即使其中藏着最优动作也永远发现不了。这违反收敛条件之一「每个 $(s,a)$ 被访问无限次」。这就是探索-利用困境的极端形式：零探索 → 可能永久次优。

> [!question]- Q5：Model-Based 方法样本效率高的原因是什么？代价是什么？
> 每条经验都永久沉淀进 $\hat T, \hat R$ 的计数里，价值迭代在整个估计模型上反复传播这些信息——一条数据能影响所有状态的价值；而无模型 TD 每条数据只更新一个 $(s,a)$ 条目就丢弃。代价：要存 $O(|S|^2|A|)$ 规模的模型，状态空间大时估不准也存不下；且规划质量受模型误差上限约束（没见过的转移就是盲区）。

---

## 参考资料

- 💻 [reinforcement_learning.py](https://github.com/stanford-cs221/autumn2025-lectures/blob/main/reinforcement_learning.py)
- 📖 [Sutton & Barto: Reinforcement Learning (2nd ed.)](http://incompleteideas.net/book/the-book-2nd.html) — RL 圣经；第 5 章 MC、第 6 章 TD（含悬崖行走例子）
- 📄 [Q-Learning (Watkins, 1989)](https://link.springer.com/content/pdf/10.1007/BF00992698.pdf) — Q-Learning 原始论文
- 📄 [Q-Learning 收敛性证明 (Watkins & Dayan, 1992)](https://link.springer.com/article/10.1007/BF00992698)
- 📄 [SARSA (Rummery & Niranjan, 1994)](https://www.cs.toronto.edu/~cebly/Classes/CSC2542_2_2011/Readings/sarsa-icml96.pdf)
- 🎥 [David Silver's RL Course](https://www.davidsilver.uk/teaching/) — DeepMind RL 课程
