强化学习实战:用Python手搓Sarsa和Q-Learning,在悬崖漫步里看谁更“怂”
强化学习实战:Python实现Sarsa与Q-Learning在悬崖漫步中的策略差异
从游戏视角理解强化学习核心算法
想象你正站在一个4×12的网格世界起点,目标是到达右下角的终点。但中间有一片"悬崖"——任何踏入都会让你回到起点并承受巨大惩罚。每走一步都会消耗体力(负奖励),而安全到达终点则获得奖励。这就是经典的"悬崖漫步"环境,也是理解强化学习基础算法的绝佳试验场。
本文将带你用Python实现两种最基础的时序差分(TD)算法——Sarsa和Q-Learning,通过直观的代码实验揭示它们在策略安全性上的本质差异。不同于单纯的理论讲解,我们将:
- 使用Gym库搭建可视化环境
- 从零编写算法代码
- 调整关键参数观察策略变化
- 可视化智能体的"走位"决策过程
import gym import numpy as np import matplotlib.pyplot as plt # 创建悬崖漫步环境 env = gym.make('CliffWalking-v0') print(f"状态空间大小: {env.observation_space.n}") print(f"动作空间: {env.action_space.n}") # 0:上, 1:右, 2:下, 3:左1. 环境解析与Q表初始化
悬崖漫步环境是一个典型的离散状态-动作空间问题,非常适合表格型方法。环境特点:
- 状态:48个离散位置(4行×12列)
- 动作:4个方向移动(上、右、下、左)
- 奖励:
- 每步:-1
- 掉崖:-100并重置
- 到达终点:0
我们先初始化Q表,这是两种算法共享的数据结构:
class CliffWalkingAgent: def __init__(self, env, alpha=0.1, gamma=0.9, epsilon=0.1): self.env = env self.alpha = alpha # 学习率 self.gamma = gamma # 折扣因子 self.epsilon = epsilon # 探索率 self.q_table = np.zeros((env.observation_space.n, env.action_space.n))2. Sarsa算法实现:保守派的路径选择
Sarsa是一种on-policy算法,其名称源自更新公式中使用的状态-动作序列:(S, A, R, S', A')。核心特点是当前策略决定下一个动作,导致更保守的行为模式。
2.1 算法原理
Sarsa的更新公式:
Q(S,A) ← Q(S,A) + α[R + γQ(S',A') - Q(S,A)]其中:
- α:学习率
- γ:折扣因子
- A':根据当前策略在S'状态下选择的动作
class SarsaAgent(CliffWalkingAgent): def update(self, s, a, r, s_, a_): # Sarsa更新公式 td_target = r + self.gamma * self.q_table[s_, a_] td_error = td_target - self.q_table[s, a] self.q_table[s, a] += self.alpha * td_error def train(self, episodes=500): rewards = [] for _ in range(episodes): s = env.reset() a = self._choose_action(s) episode_reward = 0 done = False while not done: s_, r, done, _ = env.step(a) a_ = self._choose_action(s_) self.update(s, a, r, s_, a_) episode_reward += r s, a = s_, a_ rewards.append(episode_reward) return rewards2.2 策略可视化与结果分析
训练完成后,我们可以可视化学习到的策略:
def plot_policy(q_table): arrows = ['↑', '→', '↓', '←'] policy = np.argmax(q_table, axis=1).reshape(4,12) for row in range(4): line = [] for col in range(12): if row == 3 and (0 < col < 11): line.append('×') # 悬崖 else: line.append(arrows[policy[row,col]]) print(' '.join(line))典型输出:
→ → → → → → → → → → → ↓ → → → → → → → → → → → ↓ → → → → → → → → → → → ↓ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ →可以看到Sarsa倾向于选择远离悬崖的安全路径,尽管这条路更长。这是因为:
- 在靠近悬崖的状态下,随机探索可能导致掉崖
- 算法会将这些高风险状态的Q值降低
- 最终策略会避开这些高风险区域
3. Q-Learning实现:冒险家的最优路径
Q-Learning是off-policy算法,其更新公式为:
Q(S,A) ← Q(S,A) + α[R + γmax_a' Q(S',a') - Q(S,A)]关键区别在于使用最优未来价值而非实际采取的动作。
3.1 代码实现
class QLearningAgent(CliffWalkingAgent): def update(self, s, a, r, s_, done): # Q-Learning更新公式 if done: td_target = r else: td_target = r + self.gamma * np.max(self.q_table[s_]) td_error = td_target - self.q_table[s, a] self.q_table[s, a] += self.alpha * td_error def train(self, episodes=500): rewards = [] for _ in range(episodes): s = env.reset() episode_reward = 0 done = False while not done: a = self._choose_action(s) s_, r, done, _ = env.step(a) self.update(s, a, r, s_, done) episode_reward += r s = s_ rewards.append(episode_reward) return rewards3.2 策略对比
Q-Learning的典型策略:
→ → → → → → → → → → → ↓ → → → → → → → → → → → ↓ → → → → → → → → → → → ↓ ↑ → → → → → → → → → → →Q-Learning倾向于贴着悬崖的最短路径,因为:
- 它学习的是最优策略的价值估计
- 不考虑探索时的随机动作影响
- 理论上当探索率ε→0时能收敛到最优路径
4. 参数实验:探索率ε的影响
探索率ε是控制算法行为的关键参数。我们通过实验观察不同ε值对策略的影响:
epsilons = [0.01, 0.1, 0.3] results = {} for eps in epsilons: agent = SarsaAgent(env, epsilon=eps) results[f'Sarsa(ε={eps})'] = agent.train() agent = QLearningAgent(env, epsilon=eps) results[f'Q-Learning(ε={eps})'] = agent.train() # 绘制学习曲线 plt.figure(figsize=(10,6)) for label, rewards in results.items(): plt.plot(np.convolve(rewards, np.ones(50)/50, mode='valid'), label=label) plt.legend() plt.xlabel('Episode') plt.ylabel('Average Reward') plt.title('Performance under Different Exploration Rates') plt.show()实验发现:
- Sarsa:高ε值导致更保守的策略
- Q-Learning:高ε值增加掉崖风险但保持最优路径倾向
5. 算法本质差异解析
两种算法在悬崖漫步中表现差异的根本原因:
| 特性 | Sarsa | Q-Learning |
|---|---|---|
| 策略类型 | On-policy | Off-policy |
| 更新目标 | 当前策略的动作 | 最优可能动作 |
| 安全性 | 高 | 低 |
| 路径长度 | 长但安全 | 短但有风险 |
| 适用场景 | 高风险环境 | 确定性环境 |
用驾驶比喻:
- Sarsa:新手司机,考虑实际可能采取的操作(包括失误)
- Q-Learning:理想化驾驶,假设总能做出完美决策
6. 进阶技巧与优化
6.1 动态探索率
class DecayEpsilonAgent(CliffWalkingAgent): def __init__(self, env, epsilon_start=1.0, epsilon_end=0.01, epsilon_decay=0.995): super().__init__(env) self.epsilon = epsilon_start self.epsilon_end = epsilon_end self.epsilon_decay = epsilon_decay def _choose_action(self, s): self.epsilon = max(self.epsilon_end, self.epsilon*self.epsilon_decay) return super()._choose_action(s)6.2 可视化价值函数
def plot_value_function(q_table): v = np.max(q_table, axis=1).reshape(4,12) plt.imshow(v, cmap='hot') plt.colorbar() plt.title('State Value Function') plt.axis('off') for i in range(4): for j in range(12): plt.text(j, i, f"{v[i,j]:.1f}", ha='center', va='center', color='blue')7. 工程实践建议
在实际项目中应用这些算法时:
- 环境敏感性分析:先理解环境特性再选择算法
- 超参数调优:系统性地探索参数空间
- 可视化调试:价值函数和策略可视化至关重要
- 基准测试:与随机策略或规则策略比较
- 稳定性处理:考虑使用滑动平均评估性能
# 性能评估函数 def evaluate(agent, runs=100): total_rewards = [] for _ in range(runs): s = env.reset() done = False rewards = 0 while not done: a = np.argmax(agent.q_table[s]) # 使用贪婪策略 s, r, done, _ = env.step(a) rewards += r total_rewards.append(rewards) return np.mean(total_rewards), np.std(total_rewards)通过这个完整的实现和实验过程,我们直观地验证了:
- Sarsa的保守特性使其适合安全关键应用
- Q-Learning在充分探索后能找到最优路径
- 探索率对策略安全性的显著影响
- 可视化在强化学习开发中的重要性
