【深度强化学习精通】第3讲 | 蒙特卡洛与时序差分:无模型学习
蒙特卡洛方法:基于完整回合的回报进行估计,无偏但方差大,仅适用于回合制任务。时序差分学习:结合自举思想,可以在线学习,方差小但有偏。SARSA:同策略TD控制,学习实际执行策略的价值,较为保守。Q-Learning:异策略TD控制,学习最优策略的价值,较为激进。n步TD与资格迹:在MC和TD(0)之间进行权衡,提供了更灵活的更新方式。
摘要:本讲深入探讨强化学习中的无模型学习方法。我们将系统讲解蒙特卡洛方法的两种变体(首次访问与每次访问)、时序差分学习的核心思想,以及SARSA和Q-Learning两种经典算法的原理与实现。通过完整的代码实战,你将学会在未知环境中从零开始学习最优策略,并掌握算法性能对比与超参数调优的方法。
环境声明
- Python版本:Python 3.10+
- 核心依赖:NumPy 1.24+, Matplotlib 3.7+, Gymnasium 0.29+
- 开发工具:PyCharm / VS Code / Jupyter Notebook
- 操作系统:Windows / macOS / Linux
pip install numpy matplotlib gymnasium
学习目标
完成本讲学习后,你将能够:
- 理解蒙特卡洛方法的核心思想与两种变体的区别
- 掌握时序差分学习的基本原理与优势
- 深入理解SARSA与Q-Learning算法的异同
- 能够独立实现MC、SARSA、Q-Learning三种算法
- 学会进行算法性能对比与超参数调优
- 理解n步TD与资格迹的基本概念
1. 无模型学习:在未知世界中探索
1.1 为什么需要无模型学习
在上一讲中,我们学习了马尔可夫决策过程(MDP)的形式化定义,并讨论了基于动态规划的求解方法。然而,动态规划方法有一个关键前提:必须知道环境的完整模型,即状态转移概率P(s’|s,a)和奖励函数R(s,a)。
在现实世界中,这个前提往往难以满足:
- 复杂系统:机器人控制、自动驾驶等系统的动力学方程过于复杂,难以精确建模
- 未知环境:游戏AI面对的新游戏、推荐系统面对的新用户群体
- 随机性:金融市场、用户行为等具有高度不确定性
**无模型学习(Model-Free Learning)**正是解决这类问题的利器。它不依赖环境的先验知识,而是通过与环境的直接交互来学习最优策略。
一句话总结:无模型学习就像一位探险家在未知的大陆上探索——没有地图,没有指南,只能通过亲身经历来绘制地图、积累经验。
1.2 无模型学习的分类
无模型学习方法主要分为两大类:
| 方法类别 | 代表算法 | 核心思想 | 适用场景 |
|---|---|---|---|
| 蒙特卡洛方法 | MC Prediction, MC Control | 基于完整回合的回报进行估计 | 回合制任务(如围棋、迷宫) |
| 时序差分方法 | TD(0), SARSA, Q-Learning | 基于单步转移进行自举更新 | 连续任务或回合制任务 |
这两类方法各有优劣,我们将在后续章节详细探讨。
2. 蒙特卡洛方法:从经验中学习
2.1 蒙特卡洛方法的核心思想
蒙特卡洛(Monte Carlo, MC)方法是一类基于随机采样的统计方法。在强化学习中,MC方法的核心思想是:通过采样多条完整的回合(episode),用经验平均回报来估计价值函数。
具体来说,MC方法遵循以下流程:
- 采样:让智能体按照某种策略与环境交互,生成完整的回合轨迹
- 计算回报:对于每个访问过的状态(或状态-动作对),计算从该时刻开始的累积折扣回报
- 更新估计:用回报的样本均值来更新价值函数的估计
关键特点:MC方法必须等待一个回合结束后才能进行更新,因为它需要知道完整的回报G_t。
2.2 首次访问与每次访问
在MC方法中,同一个状态可能在同一个回合中被多次访问。这就引出了两种变体:
首次访问MC(First-Visit MC):只考虑每个回合中第一次访问某个状态的回报。
每次访问MC(Every-Visit MC):考虑每个回合中所有访问某个状态的回报。
数学表达:
对于状态s,首次访问MC的价值估计为:
V(s)=1N(s)∑i=1N(s)Gi(first)(s) V(s) = \frac{1}{N(s)} \sum_{i=1}^{N(s)} G_i^{(first)}(s) V(s)=N(s)1i=1∑N(s)Gi(first)(s)
其中,N(s)N(s)N(s)是状态s被首次访问的回合数,Gi(first)(s)G_i^{(first)}(s)Gi(first)(s)是第i个回合中首次访问s后的回报。
每次访问MC的价值估计为:
V(s)=1M(s)∑j=1M(s)Gj(s) V(s) = \frac{1}{M(s)} \sum_{j=1}^{M(s)} G_j(s) V(s)=M(s)1j=1∑M(s)Gj(s)
其中,M(s)M(s)M(s)是状态s被访问的总次数(包括同一回合内的多次访问)。
两种方法的比较:
| 特性 | 首次访问MC | 每次访问MC |
|---|---|---|
| 收敛性 | 收敛到真实价值函数 | 收敛到真实价值函数(有偏但一致) |
| 样本效率 | 每个回合只用一个样本 | 每个回合可用多个样本 |
| 实现复杂度 | 需要记录是否已访问 | 无需记录访问状态 |
| 方差 | 较高 | 略低 |
在大多数实际应用中,两种方法的性能差异不大,但首次访问MC的理论分析更为简洁。
2.3 增量式蒙特卡洛更新
直接存储所有回报再取平均的方式内存开销较大。我们可以使用增量式更新:
V(St)←V(St)+α[Gt−V(St)] V(S_t) \leftarrow V(S_t) + \alpha [G_t - V(S_t)] V(St)←V(St)+α[Gt−V(St)]
其中,α\alphaα是学习率(或步长)。当α=1N(St)\alpha = \frac{1}{N(S_t)}α=N(St)1时,这等价于样本平均。
使用固定学习率的好处是可以跟踪非平稳环境(环境特性随时间变化),但会引入估计偏差。
2.4 蒙特卡洛控制:策略迭代
MC方法不仅可以用于策略评估(预测问题),还可以用于策略优化(控制问题)。
MC控制的基本框架(广义策略迭代):
- 策略评估:使用当前策略采样多个回合,估计动作价值函数Q(s,a)
- 策略改进:基于Q函数进行贪婪更新,得到新的策略
探索问题:如果总是执行贪婪动作,某些状态-动作对可能永远不会被访问,导致估计不准确。因此需要引入探索机制。
epsilon-贪婪策略:
π(a∣s)={1−ϵ+ϵ∣A(s)∣if a=argmaxaQ(s,a)ϵ∣A(s)∣otherwise \pi(a|s) = \begin{cases} 1 - \epsilon + \frac{\epsilon}{|A(s)|} & \text{if } a = \arg\max_a Q(s,a) \\ \frac{\epsilon}{|A(s)|} & \text{otherwise} \end{cases} π(a∣s)={1−ϵ+∣A(s)∣ϵ∣A(s)∣ϵif a=argmaxaQ(s,a)otherwise
其中,ϵ\epsilonϵ是探索概率。当ϵ\epsilonϵ逐渐衰减到0时,策略收敛到最优策略。
3. 时序差分学习:自举的力量
3.1 TD方法的核心思想
时序差分(Temporal Difference, TD)学习是强化学习中最核心的思想之一。它结合了蒙特卡洛方法和动态规划的优点:
- 像MC一样:直接从与环境的交互中学习,无需环境模型
- 像DP一样:利用自举(bootstrapping)进行更新,无需等待回合结束
TD(0)更新公式:
V(St)←V(St)+α[Rt+1+γV(St+1)−V(St)] V(S_t) \leftarrow V(S_t) + \alpha [R_{t+1} + \gamma V(S_{t+1}) - V(S_t)] V(St)←V(St)+α[Rt+1+γV(St+1)−V(St)]
其中,Rt+1+γV(St+1)R_{t+1} + \gamma V(S_{t+1})Rt+1+γV(St+1)称为TD目标,δt=Rt+1+γV(St+1)−V(St)\delta_t = R_{t+1} + \gamma V(S_{t+1}) - V(S_t)δt=Rt+1+γV(St+1)−V(St)称为TD误差。
直观理解:TD方法用当前的估计来更新当前的估计。它认为"从下一个状态获得的价值"加上"立即奖励"就是对当前价值的更好估计。
3.2 TD与MC的比较
| 特性 | 蒙特卡洛 | 时序差分TD(0) |
|---|---|---|
| 更新时机 | 回合结束后 | 每步之后 |
| 自举 | 否 | 是 |
| 偏差 | 无偏估计 | 有偏估计(因自举) |
| 方差 | 高(回报累积多步随机性) | 低(只考虑一步随机性) |
| 适用性 | 仅回合制任务 | 回合制和连续任务 |
| 收敛速度 | 较慢 | 通常较快 |
TD的优势:
- 在线学习:无需等待回合结束,可以实时更新
- 低方差:由于只考虑一步转移,方差比MC小得多
- 连续任务:可以应用于没有终止状态的连续任务
TD的劣势:
- 有偏估计:由于使用了自举,估计是有偏的(尤其在早期)
- 初始值敏感:初始价值估计会影响学习过程
3.3 SARSA算法:同策略TD控制
SARSA是一种同策略(on-policy)TD控制算法。"同策略"意味着用于生成数据的策略与正在学习的策略相同。
SARSA名称来源:更新公式使用了状态-动作-奖励-状态-动作五元组 (St,At,Rt+1,St+1,At+1)(S_t, A_t, R_{t+1}, S_{t+1}, A_{t+1})(St,At,Rt+1,St+1,At+1)。
SARSA更新公式:
Q(St,At)←Q(St,At)+α[Rt+1+γQ(St+1,At+1)−Q(St,At)] Q(S_t, A_t) \leftarrow Q(S_t, A_t) + \alpha [R_{t+1} + \gamma Q(S_{t+1}, A_{t+1}) - Q(S_t, A_t)] Q(St,At)←Q(St,At)+α[Rt+1+γQ(St+1,At+1)−Q(St,At)]
SARSA算法流程:
- 初始化Q表(如全零或随机值)
- 对于每个回合:
- 初始化状态S
- 使用当前策略(如epsilon-贪婪)选择动作A
- 对于回合的每一步:
- 执行动作A,观察奖励R和新状态S’
- 使用当前策略选择新动作A’
- 更新Q(S, A)
- S = S’, A = A’
- 如果S是终止状态,结束回合
SARSA的特点:
- 学习的是实际执行策略的价值
- 对探索比较保守(因为学习的是包含探索的策略)
- 收敛到的是epsilon-贪婪策略下的最优Q函数
3.4 Q-Learning算法:异策略TD控制
Q-Learning是强化学习领域最具影响力的算法之一,由Watkins在1989年提出。它是一种异策略(off-policy)算法。
异策略 vs 同策略:
- 同策略:学习的是正在执行的策略
- 异策略:学习的是另一个策略(通常是贪婪策略),而执行的是探索性策略
Q-Learning更新公式:
Q(St,At)←Q(St,At)+α[Rt+1+γmaxaQ(St+1,a)−Q(St,At)] Q(S_t, A_t) \leftarrow Q(S_t, A_t) + \alpha [R_{t+1} + \gamma \max_a Q(S_{t+1}, a) - Q(S_t, A_t)] Q(St,At)←Q(St,At)+α[Rt+1+γamaxQ(St+1,a)−Q(St,At)]
与SARSA的关键区别:Q-Learning使用max操作来选择下一个状态的最优动作,而不是实际执行的下一个动作。
Q-Learning算法流程:
- 初始化Q表
- 对于每个回合:
- 初始化状态S
- 对于回合的每一步:
- 使用epsilon-贪婪策略选择动作A
- 执行动作A,观察奖励R和新状态S’
- 更新Q(S, A)使用max操作
- S = S’
- 如果S是终止状态,结束回合
Q-Learning的优势:
- 直接学习最优策略的价值
- 对探索更加激进(因为学习的是最优动作价值,不管实际执行什么)
- 理论上可以收敛到最优Q函数
3.5 SARSA与Q-Learning的对比
| 特性 | SARSA | Q-Learning |
|---|---|---|
| 策略类型 | 同策略 | 异策略 |
| 学习目标 | 实际执行策略的价值 | 最优策略的价值 |
| 更新方式 | 使用实际执行的下一个动作 | 使用最大Q值的动作 |
| 探索性 | 较保守 | 较激进 |
| 风险 | 较低(避免危险动作) | 较高(可能尝试危险动作) |
| 收敛目标 | epsilon-贪婪最优 | 贪婪最优 |
选择建议:
- 如果环境安全,探索成本低,选择Q-Learning
- 如果环境有风险,需要避免危险动作,选择SARSA
4. n步TD与资格迹
4.1 n步TD:MC与TD的折中
MC方法使用完整回合的回报,方差大但无偏;TD(0)只使用一步,方差小但有偏。n步TD是两者的折中。
n步回报定义:
Gt:t+n=Rt+1+γRt+2+⋯+γn−1Rt+n+γnV(St+n) G_{t:t+n} = R_{t+1} + \gamma R_{t+2} + \cdots + \gamma^{n-1} R_{t+n} + \gamma^n V(S_{t+n}) Gt:t+n=Rt+1+γRt+2+⋯+γn−1Rt+n+γnV(St+n)
n步TD更新:
V(St)←V(St)+α[Gt:t+n−V(St)] V(S_t) \leftarrow V(S_t) + \alpha [G_{t:t+n} - V(S_t)] V(St)←V(St)+α[Gt:t+n−V(St)]
特殊情况:
- 当n=1时,退化为TD(0)
- 当n趋向无穷大(或回合结束)时,退化为MC
n步SARSA:类似地,可以定义n步SARSA用于控制问题。
4.2 资格迹:高效的多步学习
n步TD的一个问题是计算开销大,需要存储n步的转移。资格迹(Eligibility Traces)提供了一种高效的实现方式。
核心思想:为每个状态(或状态-动作对)维护一个"资格"值,表示该状态对当前TD误差的贡献程度。
TD(lambda)算法:
- 初始化资格迹E(s)=0
- 对于每个回合的每一步:
- 计算TD误差:δ=R+γV(S′)−V(S)\delta = R + \gamma V(S') - V(S)δ=R+γV(S′)−V(S)
- 更新资格迹:E(S)=E(S)+1E(S) = E(S) + 1E(S)=E(S)+1(累积迹)或E(S)=1E(S) = 1E(S)=1(替代迹)
- 对所有状态s:V(s)=V(s)+αδE(s)V(s) = V(s) + \alpha \delta E(s)V(s)=V(s)+αδE(s)
- 衰减资格迹:E(s)=γλE(s)E(s) = \gamma \lambda E(s)E(s)=γλE(s)
其中,λ∈[0,1]\lambda \in [0,1]λ∈[0,1]是迹衰减参数。当λ=0\lambda=0λ=0时,退化为TD(0);当λ=1\lambda=1λ=1时,近似MC方法。
资格迹的优势:
- 只需单步计算,但等效于多步更新
- 可以在线更新,无需等待多步
- 对近期访问的状态给予更高的更新权重
5. 算法实现
5.1 环境设置:悬崖行走
我们将使用Gymnasium中的"CliffWalking-v0"环境来测试三种算法。这是一个经典的网格世界问题:
- 4x12的网格
- 智能体从起点(3,0)出发,目标是(3,11)
- 底部一行是悬崖,掉下去奖励-100,回到起点
- 每步奖励-1
- 动作:上、下、左、右
import numpy as np
import matplotlib.pyplot as plt
from collections import defaultdict
import gymnasium as gym
from typing import Dict, Tuple, List, Callable
# 设置随机种子以保证可复现
np.random.seed(42)
# 创建环境
env = gym.make('CliffWalking-v0')
print(f"状态空间大小: {env.observation_space.n}")
print(f"动作空间大小: {env.action_space.n}")
5.2 蒙特卡洛控制实现
class MonteCarloAgent:
"""蒙特卡洛控制智能体(使用epsilon-贪婪策略)"""
def __init__(self, n_states: int, n_actions: int,
epsilon: float = 0.1, gamma: float = 1.0):
self.n_states = n_states
self.n_actions = n_actions
self.epsilon = epsilon
self.gamma = gamma
# 初始化Q表
self.Q = np.zeros((n_states, n_actions))
# 用于存储每个状态-动作对的访问次数
self.visit_count = defaultdict(int)
def get_action(self, state: int, training: bool = True) -> int:
"""epsilon-贪婪策略选择动作"""
if training and np.random.random() < self.epsilon:
return np.random.randint(self.n_actions)
else:
# 贪婪选择
return np.argmax(self.Q[state])
def update(self, episode: List[Tuple[int, int, float]]):
"""
使用一个回合的经验更新Q值
episode: [(state, action, reward), ...]
"""
G = 0
visited = set()
# 从后向前遍历,计算回报
for t in range(len(episode) - 1, -1, -1):
state, action, reward = episode[t]
G = self.gamma * G + reward
# 首次访问MC
if (state, action) not in visited:
visited.add((state, action))
# 增量式更新
self.visit_count[(state, action)] += 1
alpha = 1.0 / self.visit_count[(state, action)]
self.Q[state, action] += alpha * (G - self.Q[state, action])
def train_mc(env, n_episodes: int = 500, epsilon: float = 0.1,
gamma: float = 1.0) -> Tuple[MonteCarloAgent, List[float]]:
"""训练蒙特卡洛智能体"""
agent = MonteCarloAgent(
env.observation_space.n,
env.action_space.n,
epsilon=epsilon,
gamma=gamma
)
rewards_history = []
for episode in range(n_episodes):
state, _ = env.reset(seed=episode)
episode_data = []
total_reward = 0
done = False
# 生成一个回合
while not done:
action = agent.get_action(state, training=True)
next_state, reward, terminated, truncated, _ = env.step(action)
done = terminated or truncated
episode_data.append((state, action, reward))
total_reward += reward
state = next_state
# 更新Q值
agent.update(episode_data)
rewards_history.append(total_reward)
return agent, rewards_history
5.3 SARSA算法实现
class SARSAAgent:
"""SARSA智能体(同策略TD控制)"""
def __init__(self, n_states: int, n_actions: int,
alpha: float = 0.1, epsilon: float = 0.1,
gamma: float = 1.0):
self.n_states = n_states
self.n_actions = n_actions
self.alpha = alpha
self.epsilon = epsilon
self.gamma = gamma
# 初始化Q表
self.Q = np.zeros((n_states, n_actions))
def get_action(self, state: int, training: bool = True) -> int:
"""epsilon-贪婪策略选择动作"""
if training and np.random.random() < self.epsilon:
return np.random.randint(self.n_actions)
else:
return np.argmax(self.Q[state])
def update(self, state: int, action: int, reward: float,
next_state: int, next_action: int, done: bool):
"""SARSA更新"""
if done:
target = reward
else:
target = reward + self.gamma * self.Q[next_state, next_action]
td_error = target - self.Q[state, action]
self.Q[state, action] += self.alpha * td_error
def train_sarsa(env, n_episodes: int = 500, alpha: float = 0.1,
epsilon: float = 0.1, gamma: float = 1.0) -> Tuple[SARSAAgent, List[float]]:
"""训练SARSA智能体"""
agent = SARSAAgent(
env.observation_space.n,
env.action_space.n,
alpha=alpha,
epsilon=epsilon,
gamma=gamma
)
rewards_history = []
for episode in range(n_episodes):
state, _ = env.reset(seed=episode)
action = agent.get_action(state, training=True)
total_reward = 0
done = False
while not done:
next_state, reward, terminated, truncated, _ = env.step(action)
done = terminated or truncated
next_action = agent.get_action(next_state, training=True)
agent.update(state, action, reward, next_state, next_action, done)
total_reward += reward
state = next_state
action = next_action
rewards_history.append(total_reward)
return agent, rewards_history
5.4 Q-Learning算法实现
class QLearningAgent:
"""Q-Learning智能体(异策略TD控制)"""
def __init__(self, n_states: int, n_actions: int,
alpha: float = 0.1, epsilon: float = 0.1,
gamma: float = 1.0):
self.n_states = n_states
self.n_actions = n_actions
self.alpha = alpha
self.epsilon = epsilon
self.gamma = gamma
# 初始化Q表
self.Q = np.zeros((n_states, n_actions))
def get_action(self, state: int, training: bool = True) -> int:
"""epsilon-贪婪策略选择动作"""
if training and np.random.random() < self.epsilon:
return np.random.randint(self.n_actions)
else:
return np.argmax(self.Q[state])
def update(self, state: int, action: int, reward: float,
next_state: int, done: bool):
"""Q-Learning更新(使用max操作)"""
if done:
target = reward
else:
target = reward + self.gamma * np.max(self.Q[next_state])
td_error = target - self.Q[state, action]
self.Q[state, action] += self.alpha * td_error
def train_qlearning(env, n_episodes: int = 500, alpha: float = 0.1,
epsilon: float = 0.1, gamma: float = 1.0) -> Tuple[QLearningAgent, List[float]]:
"""训练Q-Learning智能体"""
agent = QLearningAgent(
env.observation_space.n,
env.action_space.n,
alpha=alpha,
epsilon=epsilon,
gamma=gamma
)
rewards_history = []
for episode in range(n_episodes):
state, _ = env.reset(seed=episode)
total_reward = 0
done = False
while not done:
action = agent.get_action(state, training=True)
next_state, reward, terminated, truncated, _ = env.step(action)
done = terminated or truncated
agent.update(state, action, reward, next_state, done)
total_reward += reward
state = next_state
rewards_history.append(total_reward)
return agent, rewards_history
5.5 性能对比与可视化
def moving_average(data: List[float], window: int = 100) -> np.ndarray:
"""计算移动平均"""
return np.convolve(data, np.ones(window)/window, mode='valid')
def evaluate_policy(env, agent, n_episodes: int = 100) -> float:
"""评估策略(不使用探索)"""
total_rewards = 0
for episode in range(n_episodes):
state, _ = env.reset(seed=episode + 10000)
done = False
while not done:
action = agent.get_action(state, training=False)
state, reward, terminated, truncated, _ = env.step(action)
done = terminated or truncated
total_rewards += reward
return total_rewards / n_episodes
def plot_learning_curves(results: Dict[str, List[float]], window: int = 100):
"""绘制学习曲线对比"""
plt.figure(figsize=(12, 5))
# 原始奖励
plt.subplot(1, 2, 1)
for name, rewards in results.items():
plt.plot(rewards, alpha=0.3, label=f'{name} (raw)')
if len(rewards) >= window:
smoothed = moving_average(rewards, window)
plt.plot(range(window-1, len(rewards)), smoothed,
linewidth=2, label=f'{name} (smoothed)')
plt.xlabel('Episode')
plt.ylabel('Total Reward')
plt.title('Learning Curves Comparison')
plt.legend()
plt.grid(True, alpha=0.3)
# 移动平均对比
plt.subplot(1, 2, 2)
for name, rewards in results.items():
if len(rewards) >= window:
smoothed = moving_average(rewards, window)
plt.plot(range(window-1, len(rewards)), smoothed,
linewidth=2, label=name)
plt.xlabel('Episode')
plt.ylabel('Average Reward')
plt.title(f'Moving Average (window={window})')
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('learning_curves_comparison.png', dpi=150, bbox_inches='tight')
plt.show()
def visualize_policy(env, agent, title: str):
"""可视化学习到的策略"""
# CliffWalking是4x12的网格
grid_height = 4
grid_width = 12
policy = np.zeros((grid_height, grid_width), dtype=int)
for state in range(env.observation_space.n):
row = state // grid_width
col = state % grid_width
action = agent.get_action(state, training=False)
policy[row, col] = action
# 动作映射: 0=上, 1=右, 2=下, 3=左
action_arrows = {0: '↑', 1: '→', 2: '↓', 3: '←'}
fig, ax = plt.subplots(figsize=(12, 4))
for row in range(grid_height):
for col in range(grid_width):
action = policy[row, col]
ax.text(col, grid_height - 1 - row, action_arrows[action],
ha='center', va='center', fontsize=16)
ax.set_xlim(-0.5, grid_width - 0.5)
ax.set_ylim(-0.5, grid_height - 0.5)
ax.set_xticks(range(grid_width))
ax.set_yticks(range(grid_height))
ax.set_yticklabels(range(grid_height - 1, -1, -1))
ax.grid(True, alpha=0.3)
ax.set_title(title)
plt.savefig(f'{title.replace(" ", "_")}.png', dpi=150, bbox_inches='tight')
plt.show()
# 主实验代码
if __name__ == "__main__":
# 训练参数
N_EPISODES = 500
ALPHA = 0.1
EPSILON = 0.1
GAMMA = 1.0
print("开始训练蒙特卡洛智能体...")
mc_agent, mc_rewards = train_mc(env, N_EPISODES, EPSILON, GAMMA)
print("开始训练SARSA智能体...")
sarsa_agent, sarsa_rewards = train_sarsa(env, N_EPISODES, ALPHA, EPSILON, GAMMA)
print("开始训练Q-Learning智能体...")
ql_agent, ql_rewards = train_qlearning(env, N_EPISODES, ALPHA, EPSILON, GAMMA)
# 绘制学习曲线
results = {
'Monte Carlo': mc_rewards,
'SARSA': sarsa_rewards,
'Q-Learning': ql_rewards
}
plot_learning_curves(results)
# 评估最终策略
print("\n最终策略评估(100回合平均奖励):")
print(f"Monte Carlo: {evaluate_policy(env, mc_agent):.2f}")
print(f"SARSA: {evaluate_policy(env, sarsa_agent):.2f}")
print(f"Q-Learning: {evaluate_policy(env, ql_agent):.2f}")
# 可视化策略
visualize_policy(env, mc_agent, "Monte Carlo Policy")
visualize_policy(env, sarsa_agent, "SARSA Policy")
visualize_policy(env, ql_agent, "Q-Learning Policy")
6. 超参数影响分析
6.1 学习率alpha的影响
学习率alpha控制每次更新的步长。在TD方法中,alpha的选择对性能有显著影响:
def analyze_alpha_impact(env, n_episodes=500, alphas=[0.01, 0.05, 0.1, 0.3, 0.5]):
"""分析学习率对Q-Learning的影响"""
plt.figure(figsize=(12, 5))
for alpha in alphas:
agent, rewards = train_qlearning(env, n_episodes, alpha=alpha)
smoothed = moving_average(rewards, window=50)
plt.plot(range(49, len(rewards)), smoothed,
linewidth=2, label=f'alpha={alpha}')
plt.xlabel('Episode')
plt.ylabel('Average Reward')
plt.title('Impact of Learning Rate (alpha) on Q-Learning')
plt.legend()
plt.grid(True, alpha=0.3)
plt.savefig('alpha_impact.png', dpi=150, bbox_inches='tight')
plt.show()
analyze_alpha_impact(env)
分析结论:
- alpha过小(如0.01):学习速度慢,收敛慢
- alpha适中(如0.1):平衡了学习速度和稳定性
- alpha过大(如0.5):震荡剧烈,可能无法收敛
6.2 探索率epsilon的影响
epsilon控制探索与利用的权衡:
def analyze_epsilon_impact(env, n_episodes=500, epsilons=[0.01, 0.1, 0.3, 0.5]):
"""分析探索率对SARSA的影响"""
plt.figure(figsize=(12, 5))
for epsilon in epsilons:
agent, rewards = train_sarsa(env, n_episodes, epsilon=epsilon)
smoothed = moving_average(rewards, window=50)
plt.plot(range(49, len(rewards)), smoothed,
linewidth=2, label=f'epsilon={epsilon}')
plt.xlabel('Episode')
plt.ylabel('Average Reward')
plt.title('Impact of Exploration Rate (epsilon) on SARSA')
plt.legend()
plt.grid(True, alpha=0.3)
plt.savefig('epsilon_impact.png', dpi=150, bbox_inches='tight')
plt.show()
analyze_epsilon_impact(env)
分析结论:
- epsilon过小(如0.01):探索不足,容易陷入局部最优
- epsilon适中(如0.1):平衡探索与利用
- epsilon过大(如0.5):过度探索,学习到的策略不是最优
6.3 epsilon衰减策略
固定epsilon的问题在于:早期需要更多探索,后期需要更多利用。可以使用衰减策略:
class DecayEpsilonAgent(QLearningAgent):
"""带epsilon衰减的Q-Learning智能体"""
def __init__(self, n_states, n_actions, alpha=0.1,
epsilon_start=1.0, epsilon_end=0.01,
epsilon_decay=0.995, gamma=1.0):
super().__init__(n_states, n_actions, alpha, epsilon_start, gamma)
self.epsilon_start = epsilon_start
self.epsilon_end = epsilon_end
self.epsilon_decay = epsilon_decay
self.episode_count = 0
def get_action(self, state, training=True):
if training:
self.epsilon = max(self.epsilon_end,
self.epsilon_start * (self.epsilon_decay ** self.episode_count))
return super().get_action(state, training)
def end_episode(self):
self.episode_count += 1
def compare_epsilon_strategies(env, n_episodes=500):
"""对比固定epsilon与衰减epsilon"""
# 固定epsilon
fixed_agent, fixed_rewards = train_qlearning(env, n_episodes, epsilon=0.1)
# 衰减epsilon
decay_agent = DecayEpsilonAgent(
env.observation_space.n,
env.action_space.n,
epsilon_start=1.0,
epsilon_end=0.01,
epsilon_decay=0.99
)
decay_rewards = []
for episode in range(n_episodes):
state, _ = env.reset(seed=episode)
total_reward = 0
done = False
while not done:
action = decay_agent.get_action(state, training=True)
next_state, reward, terminated, truncated, _ = env.step(action)
done = terminated or truncated
decay_agent.update(state, action, reward, next_state, done)
total_reward += reward
state = next_state
decay_agent.end_episode()
decay_rewards.append(total_reward)
# 绘制对比
plt.figure(figsize=(10, 5))
plt.plot(moving_average(fixed_rewards, 50), label='Fixed epsilon=0.1')
plt.plot(moving_average(decay_rewards, 50), label='Decaying epsilon')
plt.xlabel('Episode')
plt.ylabel('Average Reward')
plt.title('Fixed vs Decaying Epsilon Strategy')
plt.legend()
plt.grid(True, alpha=0.3)
plt.savefig('epsilon_decay_comparison.png', dpi=150, bbox_inches='tight')
plt.show()
compare_epsilon_strategies(env)
7. 避坑小贴士
在学习和实现无模型强化学习算法时,以下是新手容易犯的错误及解决方案:
7.1 Q表初始化问题
常见错误:将Q表初始化为全零,导致在epsilon-贪婪策略下,多个动作具有相同的Q值,选择动作时总是选择索引最小的动作。
解决方案:
- 使用小的随机值初始化Q表
- 或者使用乐观初始化(初始化为较大值),鼓励早期探索
# 乐观初始化示例
self.Q = np.ones((n_states, n_actions)) * 10.0
7.2 学习率设置不当
常见错误:学习率过大导致震荡,过小导致收敛极慢。
解决方案:
- 从0.1开始尝试
- 使用自适应学习率(如Adam优化器,但在简单Q学习中较少使用)
- 随着训练进行逐渐降低学习率
7.3 折扣因子gamma的误解
常见错误:在回合制任务中设置gamma=1,导致回报无界;或在需要长期规划的任务中设置gamma过小。
解决方案:
- 对于有限回合任务,gamma=1通常可行
- 对于无限回合或长期任务,gamma=0.9或0.99更合适
- 注意gamma接近1时,回报的方差会增大
7.4 探索与利用的失衡
常见错误:epsilon设置过大或过小,或者一直使用固定epsilon。
解决方案:
- 使用epsilon衰减策略
- 考虑使用更高级的策略如Softmax或UCB
- 监控训练过程中的探索率,确保有足够的探索
7.5 状态表示问题
常见错误:在部分可观测环境中假设完全可观测,导致状态表示不完整。
解决方案:
- 确认环境是否满足马尔可夫性质
- 对于部分可观测环境,考虑使用RNN或历史状态堆叠
- 使用函数逼近(如神经网络)处理连续或高维状态空间
8. 延伸阅读与总结
8.1 本讲要点总结
-
蒙特卡洛方法:基于完整回合的回报进行估计,无偏但方差大,仅适用于回合制任务。
-
时序差分学习:结合自举思想,可以在线学习,方差小但有偏。
-
SARSA:同策略TD控制,学习实际执行策略的价值,较为保守。
-
Q-Learning:异策略TD控制,学习最优策略的价值,较为激进。
-
n步TD与资格迹:在MC和TD(0)之间进行权衡,提供了更灵活的更新方式。
8.2 理论深入:收敛性与致命三元组
8.2.1 Q-Learning收敛性定理
定理(Watkins & Dayan, 1992):在满足以下条件时,Q-Learning以概率1收敛到最优Q函数Q*:
条件1:充分探索
每个状态-动作对(s,a)被无限次访问:
∑t=1∞1{St=s,At=a}=∞,∀s,a\sum_{t=1}^{\infty} \mathbf{1}\{S_t=s, A_t=a\} = \infty, \quad \forall s,at=1∑∞1{St=s,At=a}=∞,∀s,a
条件2:学习率条件
学习率αt(s,a)\alpha_t(s,a)αt(s,a)满足:
∑t=1∞αt(s,a)=∞,∑t=1∞αt2(s,a)<∞\sum_{t=1}^{\infty} \alpha_t(s,a) = \infty, \quad \sum_{t=1}^{\infty} \alpha_t^2(s,a) < \inftyt=1∑∞αt(s,a)=∞,t=1∑∞αt2(s,a)<∞
常用的学习率调度:
- 样本平均:αt(s,a)=1Nt(s,a)\alpha_t(s,a) = \frac{1}{N_t(s,a)}αt(s,a)=Nt(s,a)1(满足条件)
- 固定学习率:αt(s,a)=α\alpha_t(s,a) = \alphaαt(s,a)=α(不满足,但实践中常用)
- 多项式衰减:αt(s,a)=1tβ\alpha_t(s,a) = \frac{1}{t^\beta}αt(s,a)=tβ1,β∈(0.5,1]\beta \in (0.5, 1]β∈(0.5,1](满足条件)
条件3:有界奖励
∣R(s,a,s′)∣≤Rmax<∞|R(s,a,s')| \leq R_{max} < \infty∣R(s,a,s′)∣≤Rmax<∞
证明思路:
- 将Q-Learning视为随机逼近算法
- 证明更新算子是压缩映射
- 应用随机逼近收敛定理
8.2.2 SARSA收敛性
定理:在相同条件下,SARSA收敛到epsilon-贪婪策略下的最优Q函数。
注意:SARSA收敛的不是全局最优Q*,而是当前探索策略下的最优Q值。这就是为什么SARSA比Q-Learning更保守。
8.2.3 致命三元组(Deadly Triad)
定义:当以下三个条件同时满足时,强化学习算法可能发散:
- 函数逼近(Function Approximation):使用神经网络等参数化函数代替Q表
- 自举(Bootstrapping):使用当前估计更新当前估计(如TD方法)
- 异策略学习(Off-policy Learning):学习的目标策略与执行的行为策略不同
为什么危险?
考虑线性函数逼近下的Q-Learning:
- Q(s,a) ≈ w^T φ(s,a),其中φ(s,a)是特征向量
- 更新规则:w ← w + α[r + γ max_a’ w^T φ(s’,a’) - w^T φ(s,a)] φ(s,a)
发散示例(Tsitsiklis & Van Roy, 1997):
即使在一个简单的MDP中,使用线性函数逼近的TD(0)也可能发散。这是因为:
- 自举引入了循环依赖
- 函数逼近引入了近似误差
- 异策略学习改变了数据分布
解决方案:
| 方法 | 解决思路 | 代表算法 |
|---|---|---|
| 梯度TD | 使用真实的梯度下降 | GTD, GTD2, TDC |
| 经验回放 | 打破数据相关性 | DQN(配合目标网络) |
| 信任区域 | 限制策略更新幅度 | TRPO, PPO |
| 两时间尺度 | 分离价值函数和策略的更新速度 | Actor-Critic |
DQN为什么能工作?
DQN通过以下技术缓解了致命三元组:
- 经验回放:存储和随机采样经验,打破时间相关性
- 目标网络:使用独立的目标网络计算TD目标,减少自举的不稳定性
- 大批量训练:使用大批量样本平均掉噪声
但这些只是缓解,并不能完全解决问题。在某些任务上,DQN仍然可能发散。
8.3 延伸阅读
经典论文:
-
Watkins, C. J. C. H. (1989). Learning from Delayed Rewards. PhD Thesis. (Q-Learning原始论文)
-
Watkins, C. J. C. H., & Dayan, P. (1992). Q-learning. Machine Learning, 8(3-4), 279-292. (收敛性证明)
-
Sutton, R. S. (1988). Learning to predict by the methods of temporal differences. Machine Learning. (TD学习奠基论文)
-
Tsitsiklis, J. N., & Van Roy, B. (1997). An analysis of temporal-difference learning with function approximation. IEEE Transactions on Automatic Control. (致命三元组)
-
Rummery, G. A., & Niranjan, M. (1994). On-line Q-learning using connectionist systems. (SARSA相关)
推荐书籍:
- 《Reinforcement Learning: An Introduction》(Sutton & Barto)第5-7章, 第11章
- 《Algorithms for Reinforcement Learning》(Szepesvari)
- 《Neuro-Dynamic Programming》(Bertsekas & Tsitsiklis)
进阶主题:
- 函数逼近(Function Approximation):使用神经网络代替Q表
- 策略梯度方法(Policy Gradient):直接优化策略
- Actor-Critic方法:结合价值函数和策略梯度
- 梯度TD方法:GTD, GTD2, TDC
8.3 下一讲预告
第4讲将深入探讨深度Q网络(DQN),我们将学习如何将神经网络与Q-Learning结合,处理高维状态空间(如Atari游戏图像),并介绍经验回放、目标网络等关键技术。
如果你觉得本讲内容对你有帮助,欢迎点赞、收藏、评论交流。你的支持是我持续创作的动力!
更多推荐

所有评论(0)