1. 从“试错”到“学习”:理解SARSA算法的核心定位
在强化学习的广阔天地里,我们常常听到Q-Learning的大名,它以其简洁高效的“离线学习”特性,成为许多入门教程的首选。然而,当你真正开始动手实现一个智能体,尤其是在那些需要与环境持续交互、每一步决策都关乎即时安全与长期收益的场景下,你可能会发现,Q-Learning那种“大胆假设,小心求证”的更新方式,有时会带来意想不到的麻烦。比如,你训练一个机器人走路,Q-Learning可能会让它为了追求远处的一个高额奖励,而先尝试一个可能导致摔倒的危险动作。这时,另一种同样经典但理念迥异的算法——SARSA,就走进了我们的视野。SARSA代表的是State-Action-Reward-State-Action,这五个字母连起来,恰恰勾勒出了它最核心的运作逻辑:基于实际采取的行动序列进行学习。它不是旁观者,而是参与者,每一次价值更新都严格依赖于智能体“说到做到”的完整行动轨迹。这种“在策略”的学习方式,使得SARSA天生带有一种稳健和保守的气质,特别适合需要平衡探索与利用、对每一步风险都需谨慎评估的任务。
简单来说,如果你把智能体比作一个正在学习走迷宫的新手,Q-Learning像是一个在旁边观察并记录“从A点到B点最优走法”的教练,它可能告诉新手“理论上从这块砖跳到那块砖最快”,但不管新手实际敢不敢跳。而SARSA则更像是手把手带着新手走的导师,它说:“好,你现在站在这里,你决定往左走一步,拿到了这点奖励,并且发现左边是堵墙,那你下次在这个位置时,往左走的期望价值就应该这样调整。” SARSA学习的是当前策略下的行动价值,因此它的策略改进与策略评估是紧密耦合的。这使得它在诸如机器人控制、游戏AI(特别是需要避免即时死亡的游戏)、金融交易(避免单次巨大亏损)等领域,展现出独特的优势。理解SARSA,不仅是掌握一个算法,更是理解强化学习中“安全探索”与“策略跟随”这一重要哲学。
2. SARSA算法流程拆解:五步循环的奥秘
SARSA的名称已经揭示了其算法更新的核心要素。让我们把这个名字拆解开,一步步看它是如何在一个时间步中运作的。假设在时间步t,我们正处于状态S_t。
第一步:感知与决策 (State & Action)智能体首先观察当前环境状态S_t。然后,它需要依据某个策略(例如ε-greedy策略)来选择一个动作A_t。这个策略是基于当前的动作价值函数Q(S_t, a)做出的,它可能以大概率选择当前认为最好的动作(利用),也可能以小概率随机探索其他动作。
第二步:执行与反馈 (Action & Reward)智能体将选定的动作A_t发送给环境。环境接收到动作后,发生状态转移,进入下一个状态S_{t+1},并给予智能体一个即时奖励R_{t+1}。这个奖励是环境对动作A_t在状态S_t下效果的即时评价。
第三步:再次决策 (State & Action)关键的一步来了。在新的状态S_{t+1}下,智能体再次根据当前策略(注意,是同一个策略,例如同一个ε-greedy策略)选择下一个动作A_{t+1}。这里的选择是实实在在的,是智能体在S_{t+1}状态下“打算”执行的动作。
第四步:价值更新 (Update)现在,我们集齐了SARSA的五个字母:(S_t, A_t, R_{t+1}, S_{t+1}, A_{t+1})。SARSA算法使用这组序列来更新旧状态-动作对(S_t, A_t)的价值Q(S_t, A_t)。其更新公式如下:
Q(S_t, A_t) ← Q(S_t, A_t) + α * [ R_{t+1} + γ * Q(S_{t+1}, A_{t+1}) - Q(S_t, A_t) ]
让我们拆解这个公式:
α:学习率。控制本次更新对旧值的调整幅度。通常设置在0到1之间。[ ... ]:时序差分误差。这是驱动学习的核心信号。R_{t+1}:执行A_t后获得的即时奖励。γ * Q(S_{t+1}, A_{t+1}):对未来回报的估计。这里用的是在S_{t+1}状态下,根据当前策略实际会选择的动作A_{t+1}的价值Q(S_{t+1}, A_{t+1})。γ是折扣因子,衡量未来奖励的当前价值。Q(S_t, A_t):旧的价值估计。
- 整个公式的含义是:将
Q(S_t, A_t)向“即时奖励 + 折扣后的下一状态实际动作价值”这个新目标进行小幅调整。
第五步:状态与动作迭代将当前状态更新为S_{t+1},当前动作更新为A_{t+1},然后循环回到第一步,开始下一个时间步。
这个五步循环构成了SARSA的核心。与Q-Learning更新公式Q(S_t,A_t) ← Q(S_t,A_t) + α * [ R_{t+1} + γ * max_a Q(S_{t+1}, a) - Q(S_t, A_t) ]对比,最核心的区别就在于对未来价值的估计上:SARSA用的是Q(S_{t+1}, A_{t+1}),而Q-Learning用的是max_a Q(S_{t+1}, a)。这个细微的差别,导致了二者完全不同的行为特性。
注意:SARSA的更新必须发生在动作
A_{t+1}被选定之后。在代码实现中,这通常意味着我们需要为“下一个状态”S_{t+1}预选好动作A_{t+1},然后才能用这五个值进行更新,最后再将A_{t+1}真正执行出去。这是一个常见的实现细节。
3. 为什么是“在策略”学习?SARSA与Q-Learning的哲学分野
“在策略”这个术语是理解SARSA精髓的关键。所谓“在策略”,指的是算法用于评估和改进的是它当前正在执行的那个策略。在SARSA中,无论是选择动作A_t和A_{t+1},还是更新价值Q(S_t, A_t)时对未来价值的估计Q(S_{t+1}, A_{t+1}),都依赖于同一个行为策略(如ε-greedy)。
让我们通过一个经典的“悬崖漫步”网格世界例子来直观感受两者的区别。想象一个4x12的网格,最下面一行是“悬崖”,掉下去会获得-100的奖励并回到起点;起点在左下角,终点在右下角,每走一步获得-1的奖励(鼓励尽快到达)。
- Q-Learning(离线学习):它会学习到一条贴着悬崖边的最短路径,因为从价值函数
Q(s,a)的角度看,那条路径的累积负奖励最少(步数最少)。即使它使用ε-greedy策略进行探索,偶尔会掉下悬崖,但它的更新目标max_a Q(s', a)始终盯着“最优动作”的价值。因此,它最终学到的策略是“走悬崖边”。 - SARSA(在策略学习):由于它的更新依赖于实际采取的动作
A_{t+1},而它的行为策略(ε-greedy)有一定概率随机探索。当智能体处在悬崖边缘时,如果它根据策略选择了一个“非最优”的探索动作(比如向着悬崖方向),那么这次更新就会降低“在悬崖边向里走”这个动作的价值,因为下一个动作A_{t+1}可能是危险的。SARSA会“感知”到探索带来的风险。最终,SARSA学到的策略通常会选择一条远离悬崖的安全路径,尽管这条路可能更长(总奖励更负一些)。因为它评估的是在带有探索的策略下,每个动作的期望收益。
这个例子清晰地展示了两种算法的哲学:
- Q-Learning是乐观的。它假设智能体在未来总会做出最优选择,因此它学习的是“最优策略”的价值函数。它分离了行为策略(用于探索)和目标策略(用于更新),被称为“离线策略”算法。
- SARSA是保守的。它评估的是在当前的探索策略下,实际能获得的期望回报。它将探索的代价(如掉下悬崖的风险)考虑在内,因此学习到的策略通常更稳健、更安全。它的行为策略和目标策略是同一个,因此是“在策略”算法。
下表总结了核心区别:
| 特性 | SARSA | Q-Learning |
|---|---|---|
| 全称 | State-Action-Reward-State-Action | - |
| 策略类型 | 在策略:评估和改进的是行为策略本身。 | 离线策略:评估和改进的是最优策略,与行为策略可以不同。 |
| 更新目标 | R + γ * Q(S', A'),其中A'由行为策略产生。 | R + γ * max_a Q(S', a),即最优动作的价值。 |
| 学习目标 | 学习当前(可能带探索的)策略下的动作价值。 | 学习最优策略的动作价值。 |
| 行为特点 | 更保守,会将探索的风险考虑在内,策略通常更安全。 | 更激进,直接追求理论最优,可能忽略探索过程中的危险。 |
| 适用场景 | 需要安全探索、在线学习、动作有连续风险的任务(如机器人控制、交易)。 | 仿真环境、探索成本低、或可以大量离线训练的任务(如游戏AI、广告推荐)。 |
在实际项目中,选择SARSA还是Q-Learning,往往取决于你对智能体“冒险精神”的要求以及环境的特性。
4. 手把手实现:一个完整的SARSA算法代码剖析
理论说得再多,不如一行代码。让我们用Python来实现一个解决“悬崖漫步”问题的SARSA智能体。我们将使用gym库中的CliffWalking-v0环境。这个环境完美复现了我们之前讨论的场景。
首先,是环境准备和参数定义:
import gym import numpy as np import matplotlib.pyplot as plt # 创建环境 env = gym.make('CliffWalking-v0') n_states = env.observation_space.n # 48个状态 (4*12) n_actions = env.action_space.n # 4个动作 (0:上, 1:右, 2:下, 3:左) # 算法超参数 alpha = 0.1 # 学习率 gamma = 0.99 # 折扣因子 epsilon = 0.1 # ε-greedy策略中的探索概率 episodes = 1000 # 训练回合数 # 初始化Q表,形状为 (状态数, 动作数) Q = np.zeros((n_states, n_actions))接下来,我们实现核心的ε-greedy策略函数。这个函数将用于SARSA中选择动作A_t和A_{t+1}。
def epsilon_greedy_policy(state, Q, epsilon): """ 根据ε-greedy策略选择动作。 参数: state: 当前状态索引 Q: 当前的Q表 epsilon: 探索概率 返回: action: 选择的动作索引 """ # 以 epsilon 的概率进行随机探索 if np.random.random() < epsilon: return np.random.randint(n_actions) # 以 1-epsilon 的概率利用(选择Q值最大的动作) else: # 处理多个动作Q值相同的情况,随机选择一个 max_q = np.max(Q[state]) actions_with_max_q = np.where(Q[state] == max_q)[0] return np.random.choice(actions_with_max_q)现在,来到最关键的SARSA训练主循环。请注意我们是如何严格遵循S, A, R, S', A'的顺序来获取数据并进行更新的。
# 记录每个回合的总奖励,用于评估学习效果 rewards_per_episode = [] for episode in range(episodes): state, _ = env.reset() # 重置环境,获取初始状态S total_reward = 0 # 根据当前策略选择初始动作A action = epsilon_greedy_policy(state, Q, epsilon) done = False while not done: # 执行动作A,得到奖励R和下一个状态S' next_state, reward, terminated, truncated, _ = env.step(action) total_reward += reward done = terminated or truncated # **关键步骤**:在状态S'下,根据当前策略选择下一个动作A' next_action = epsilon_greedy_policy(next_state, Q, epsilon) # 如果回合未结束,使用S, A, R, S', A'进行SARSA更新 if not done: # SARSA更新公式 td_target = reward + gamma * Q[next_state, next_action] td_error = td_target - Q[state, action] Q[state, action] += alpha * td_error else: # 如果回合结束(到达终点或掉下悬崖),则下一个状态S'没有动作价值 # 此时更新目标仅为即时奖励 td_target = reward td_error = td_target - Q[state, action] Q[state, action] += alpha * td_error # 回合结束,跳出循环,无需再选择next_action # 将状态和动作向前推进:S <- S', A <- A' state = next_state action = next_action rewards_per_episode.append(total_reward) # 可选:每100回合输出一次进度 if (episode + 1) % 100 == 0: avg_reward = np.mean(rewards_per_episode[-100:]) print(f"Episode {episode+1}, Avg Reward (last 100): {avg_reward:.2f}") env.close()提示:在代码实现中,处理回合结束的情况需要小心。当
done为True时,next_state是一个终止状态,理论上其所有动作的Q值应为0。因此,更新目标简化为reward。这是时序差分学习中的标准处理方式。
训练完成后,我们可以可视化学习曲线,并测试学到的策略:
# 绘制学习曲线 plt.plot(rewards_per_episode) plt.xlabel('Episode') plt.ylabel('Total Reward') plt.title('SARSA Learning Curve on CliffWalking') plt.grid(True) plt.show() # 测试学到的策略(使用贪婪策略,即epsilon=0) test_episodes = 10 for test_ep in range(test_episodes): state, _ = env.reset() done = False steps = 0 path = [state] while not done and steps < 100: # 防止无限循环 action = epsilon_greedy_policy(state, Q, epsilon=0.0) # 纯利用 state, reward, terminated, truncated, _ = env.step(action) done = terminated or truncated path.append(state) steps += 1 print(f"Test Episode {test_ep+1}: Steps = {steps}, Path (last 5 states): {path[-5:]}")运行这段代码,你会观察到SARSA智能体在经过几百个回合的训练后,总奖励(从-100左右)逐渐提升并稳定在一个较高的值(比如 -20 到 -30)。通过测试路径你会发现,它大概率学会了一条远离悬崖顶部的安全路径,从上方绕行至终点。这正是其“在策略”保守特性的直观体现。
5. 参数调优与实战心得:让SARSA稳定工作的关键
实现一个能跑的SARSA很简单,但让它高效、稳定地学习却需要一些技巧。以下是我在实际项目中积累的一些关键参数调优经验和注意事项。
1. 学习率α:小步快跑还是大步迈进?学习率α控制了新信息覆盖旧记忆的速度。
- 值太大(如0.5以上):更新剧烈,Q值波动大,可能导致学习不稳定,甚至无法收敛。智能体像得了健忘症,新经验完全覆盖旧经验。
- 值太小(如0.01以下):学习速度极慢,需要大量经验才能收敛,训练时间成本高。
- 实践经验:通常从0.1开始尝试是一个不错的起点。对于状态-动作空间较小、问题相对简单的情况,0.1到0.3可能工作良好。对于更复杂的问题,可能需要更小的学习率(如0.01或0.05)来保证稳定。一个高级技巧是使用衰减的学习率,在训练初期用较大的α快速学习,后期用较小的α进行微调,例如:
alpha = initial_alpha / (1 + decay_rate * episode)。
2. 探索率ε:探索与利用的永恒博弈ε-greedy策略中的ε直接决定了智能体的探索欲望。
- 值太大(如0.5):智能体大部分时间在随机游走,无法有效利用已学到的知识,学习效率低下,最终策略性能差。
- 值太小(如0.01):智能体过于贪婪,可能过早陷入局部最优(比如在CliffWalking中,如果一开始随机探索没找到安全路,它可能就永远只尝试那条会掉下悬崖的“最优”短路径)。
- 实践经验:0.1是一个广泛使用的默认值。和α一样,使用衰减的ε是常见且有效的方法。例如,初始ε设为0.5或1.0(鼓励充分探索),然后随着回合数增加逐渐衰减到一个很小的值(如0.01或0.001),让智能体后期专注于利用学到的优秀策略。这模拟了人类“先广博后精深”的学习过程。
3. 折扣因子γ:目光有多长远?折扣因子γ决定了智能体对未来奖励的重视程度。
- γ接近1(如0.99, 0.999):智能体是“长远规划者”,非常重视未来的回报。这在回合制任务中很常见,比如下棋或走迷宫,最终结果远重于中间步骤。
- γ接近0(如0.1, 0.5):智能体是“短视者”,只关心眼前的即时奖励。适用于一些即时反馈非常重要的连续任务。
- 实践经验:对于大多数有明确终止状态的 episodic 任务,γ通常设置为0.9到0.99。你需要思考:对于你的任务,多步之后的奖励对当前决策还有多大意义?在CliffWalking中,我们设0.99,因为即使多走几步,最终安全到达的收益也远高于掉下悬崖的惩罚。
4. Q表初始化:从零开始还是乐观一点?我们通常将Q表初始化为零。但这在ε-greedy策略下会带来一个问题:所有动作初始价值相同,第一次选择完全是随机的。如果某个动作一开始不小心获得了负奖励,它的Q值会变负,在贪婪选择时就不再被选中,即使它可能是最优动作。这可能导致探索不足。
- 乐观初始化技巧:将Q表初始化为一个较小的正值(比如+5)。这样,所有动作在初始时都有被尝试的机会(因为它们的Q值都高于初始化的“基线”)。当某个动作获得负奖励后,其Q值下降,但可能仍是正数,仍有被探索的机会。这可以鼓励智能体在早期更均匀地探索所有动作。在CliffWalking中,乐观初始化有时能帮助智能体更快地发现安全路径。
5. 收敛判断与可视化不要只盯着最终的总奖励曲线。以下监控手段能帮你更好地理解学习过程:
- 查看Q表变化:定期(如每100回合)检查特定关键状态(如起点、悬崖边缘状态)的Q值,看它们是否趋于稳定。
- 观察策略变化:定期用贪婪策略(ε=0)测试一次,记录其路径和步数。你会看到策略从随机游走到逐渐稳定的过程。
- 分析探索率:如果你使用了衰减的ε,记录其变化,确保在训练后期探索率足够低,让策略得以收敛。
一个常见的坑是:学习曲线剧烈震荡,长期无法稳定。这通常是学习率α过大,或探索率ε衰减过快导致的。尝试降低α,或让ε衰减得更平缓一些。记住,强化学习训练本身具有一定随机性,多次运行取平均性能是更科学的评估方式。
6. 超越表格:当SARSA遇见函数逼近与深度学习
我们上面实现的SARSA被称为“表格型SARSA”,因为它用一个二维数组(表格)Q[state, action]来存储每一个状态-动作对的价值。这种方法简单直观,但有一个致命的缺陷:它无法处理大规模甚至连续的状态/动作空间。想象一下,如果状态是游戏的一帧图像(像素矩阵),或者是机器人传感器的连续读数,表格法将因为维度灾难而完全失效。
这时,我们需要引入函数逼近。其核心思想是,用一个参数化的函数Q(s, a; θ)来近似真实的动作价值函数,其中θ是函数的参数(比如神经网络的权重)。SARSA算法可以与各种函数逼近器结合,例如线性函数、决策树,或者如今最流行的深度神经网络,这就形成了Deep SARSA的雏形(尽管Deep Q-Network更常见,但思想相通)。
使用函数逼近后,SARSA的更新目标不再只是更新表格中的一个单元格,而是变成了一个监督学习问题:调整参数θ,使得Q(s, a; θ)的预测值更接近“目标值”y = R + γ * Q(s', a'; θ)。注意,这里的a'仍然是根据当前策略(由当前的Q(s, a; θ)网络定义)在状态s'下选择的动作。
其梯度下降更新规则为:θ ← θ + α * [y - Q(s, a; θ)] * ∇_θ Q(s, a; θ)
其中∇_θ Q(s, a; θ)是Q值对参数θ的梯度。
实现Deep SARSA的注意事项:
- 经验回放:与DQN一样,为了打破数据间的相关性,可以使用经验回放池。但需注意,SARSA存入回放池的经验是
(s, a, r, s', a')五元组。 - 目标网络:为了稳定训练,通常也会引入一个目标网络来计算
Q(s', a'; θ-),其参数θ-定期从主网络θ同步,而非实时更新。 - 策略的连续性:由于SARSA是“在策略”的,用于生成
a'的策略网络与正在更新的网络是同一个(或其延迟版本)。这意味着策略在训练过程中是连续变化的。这与DQN的“离线策略”特性不同,后者行为策略可以相对独立。
虽然Deep SARSA在实际应用中没有DQN那么普遍(因为其保守性和对策略连续性的要求),但在一些对安全性、策略平滑性要求高的连续控制任务中(如机器人 locomotion),基于Actor-Critic框架的在策略算法(如PPO、TRPO)更为流行,它们可以看作是SARSA思想在连续空间和高维问题上的高级演进。
从表格型SARSA到函数逼近,算法的核心哲学——依据实际采取的行动序列来评估当前策略——始终未变。理解了这个根本,你就能更好地把握强化学习算法家族中“在策略”这一重要分支的设计脉络,并在面对具体问题时,做出更明智的算法选择。