强化学习入门最容易卡住的地方:Q-learning和Sarsa到底哪里不一样
先说实话:当年我自己学强化学习的时候,Q-learning和Sarsa这对“双胞胎”让我怀疑人生。公式看起来就差了一个符号,老师讲的时候说一个是off-policy一个是on-policy,我点头点头,回头自己写代码跑出来结果完全不同,再一个个翻公式才明白——那个“max”和“实际动作”的区别,决定了两种完全不同的决策性格。
如果你也在学这两兄弟,或者已经在跑代码了但搞不清为什么结果有时候差很多,这篇文章就是写给你的。我会从二者的共同祖先讲起,拆到更新公式的每一位,再用一个经典实验把差距摆在桌面上,最后聊聊实际项目里到底该怎么选。
1. 先搞清楚它们从哪里来:时序差分、价值迭代和那张“不太聪明但好用”的Q表
很多教程一上来就扔两个公式,然后说“看,区别就在这里”,这是最蒙人的教法。你要不知道这两个算法为了解决什么而生,背再多次公式也白搭。
Q-learning和Sarsa都属于时序差分(Temporal Difference, TD)类算法。TD思想的精髓是一句话:用当前奖励加上对下一步的估计,去更新当前这一步的估计。它不需要像蒙特卡洛那样等整个回合结束才知道结果(太慢),也不会像动态规划那样必须知道完整的环境模型(大多数实际场景根本给不了)。它走一小步,就立刻回过头来修正自己的认知——有点像你开车时一边看后视镜一边修正方向盘,而不是等到了终点才回忆刚才哪个路口转错了。
这两兄弟共同的家,就是那张Q表。Q表记录的是“在某个状态下采取某个动作,未来能获得的累计折扣回报的期望”。强化学习训练的过程说白了就是反复跟环境交互,用反馈来回填这张表,让表里的估计值越来越贴近真实。表格型方法在真实项目里确实受限于状态空间爆炸,但它作为理解RL核心思想的最佳教具,地位至今没人能替代。
那么问题来了:既然TD思想定了,Q值的定义也定了,“更新Q表”这件事,到底应该用哪个值作为更新目标?这正是Q-learning和Sarsa分道扬镳的分岔口。
顺便提一句,这两个算法都遵循贝尔曼方程——就是那个把“当前价值”拆成“即时奖励”和“下一状态价值”的递归等式。你可以把贝尔曼方程理解为一张“价值地图”的拼图规则:每个位置的价值,等于你现在拿到的收益加上下一个位置的价值。Q-learning和Sarsa是拼图的两只手,拿着同一张规则,却用不同的方式找下一块拼图。
2. 一条公式的两种写法:max让它们分道扬镳
我们直接把两个更新公式摆在一起看。立刻就能找到那个起决定性作用的差异:
Q-learning的更新目标:
Q(s, a) ← Q(s, a) + α * [r + γ * max_{a'} Q(s', a') - Q(s, a)]Sarsa的更新目标:
Q(s, a) ← Q(s, a) + α * [r + γ * Q(s', a') - Q(s, a)]如果你第一次看,可能会说:这不就差一个max吗?对,就这一个小东西,让两个算法的逻辑性格天差地别。
我用一个生活化的比喻把这些讲透。假设你在玩一个无限重复的迷宫探险游戏,每次走到一个岔路口,都面临“继续往前”“回头”“左转”等选择。Sarsa走的是“我现在定下的策略是什么,下一秒就这么干”——更新Q值时用到的下一个动作,必须是按当前策略选出来的动作,比如按ε-greedy策略选出的那个具体动作。也就是说,Sarsa是在评估自己正在走的这条路到底值多少。
Q-learning则完全不管下一秒实际做什么——它只关心“下一步所有可能动作中,哪个能让我拿到的Q值最大”。哪怕当前策略是个探索很猛、瞎走很多的策略,Q-learning更新时依然死死盯着那个理论上最优的动作。所以它学习的是“假设接下来一直选择最优动作,这条路的值是多少”,而不等待你真正执行什么动作。
这就是那对关键术语的来源:
- Sarsa是on-policy(同策略):行为策略(用来选择和生成数据的策略)和目标策略(用来更新价值的策略)是同一个。更新公式跟实际行为绑定,怎么走就怎么算。
- Q-learning是off-policy(异策略):行为策略和目标策略是两码事。行为策略可以是任意灵活的探索机制,但它更新Q值时用的是“最优”,跟实际选择的动作脱钩。
这是整篇文章最重要的一个认知拐点。你一旦理解了这条,后面所有实验现象(Sarsa保守、Q-learning激进、悬崖行走那个经典对比)都不难解释。
2.1 为什么Q-learning的max是“激进的乐观派”
Q-learning里面那个max,实质上是把“后继状态的价值估计”推向最高的地方。这有两个直接后果:
第一,只要某个动作在未来有一个时刻存在获得高回报的可能,Q-learning就会倾向于认为这条路的价值很高。哪怕这条路中途会掉几次悬崖,只要“走过悬崖之后能到终点拿到大奖励”,它会觉得整条路都值得。这种乐观偏差在价值估计不准确的时候尤其明显——Q-learning对Q值的估计会系统性偏高。
这个现象在学术论文里有个专门的名字:过估计(overestimation)。它是max操作和函数近似误差一起导致的。啊对,你要是用神经网络而不是表格,过估计会被放得更大——这正是DQN后来需要Double DQN去修一个补丁的原因,后面我会提。
第二,因为总是取max,Q-learning的更新方向和更新幅度在训练早期非常跳跃。它敢于“畅想”一个还没实际验证过的好结果。这种性格在环境比较简单、动作空间清晰的任务里很管用,收敛快,能找到接近全局最优的路径;但在环境有随机性、有风险陷阱的任务里,它经常会做出“为了那个诱惑先赌一把”的选择。
2.2 为什么Sarsa是“跟随当前策略的保守派”
Sarsa的更新目标,是“当前策略实际会选择的下一个动作”的Q值。如果当前策略已经在某些状态探索得比较少,或者ε-greedy策略里探索率ε比较高,那么下一步很可能选到某个次优甚至比较差的动作——这个差动作的价值会被用来更新前一步的Q值。
换句话说,Sarsa在学习时把“当前策略可能会犯的错”也算进去了。这就让它的估计更贴近“如果我按照现在这个策略走下去,真实能得到多少”。因为会把策略的不完美算进折扣,Sarsa的Q值估计通常比Q-learning更保守、更贴近地面真实。
代价是:当环境允许走一条高风险高收益的捷径时,Sarsa常常会“不敢”学出那条捷径的价值。它宁可绕远路也不走悬崖边,因为只要探索策略让它跌过一两次,它就会把“悬崖边”这条路的估计压得比较低。
这么说吧,如果你把两个算法都训练成“走迷宫到终点”的智能体:Q-learning成绩表上经常是“最快路径但偶尔摔跤”,Sarsa则是“慢一点但几乎不掉坑”。
3. 用一个格子世界实验验证:悬崖行走里的两种驾驶风格
纯讲术语太虚,我们来跑个实验。经典用例是Cliff Walking(悬崖行走):一个4×12的网格世界,起点在左下角,终点在右下角,中间有一片悬崖区。智能体每走一步非终点非悬崖的格子,奖励为-1;掉进悬崖,奖励为-100并回到起点;到达终点,回合结束。最优路径是贴着悬崖边走过去——路径短,但一步失误就坠崖;安全路径是绕到最上面一行再绕下来——路径长很多,但稳定。
这正是检验两种算法性格的完美场地。来看核心代码,简化掉画图部分:
import numpy as np # 环境简化:4行12列,悬崖在最后一行1~10列 class CliffWalk: def __init__(self): self.row, self.col = 4, 12 self.start = (3, 0) self.goal = (3, 11) self.cliff = [(3, c) for c in range(1, 11)] self.reset() def reset(self): self.state = self.start return self.state def step(self, a): # 动作: 0上 1下 2左 3右 r, c = self.state if a == 0 and r > 0: r -= 1 if a == 1 and r < self.row - 1: r += 1 if a == 2 and c > 0: c -= 1 if a == 3 and c < self.col - 1: c += 1 state_ = (r, c) if state_ in self.cliff: return self.start, -100, True # 掉崖,重置回起点 if state_ == self.goal: return state_, -1, True return state_, -1, False def choose_action(Q, state, epsilon): if np.random.random() < epsilon: return np.random.randint(4) r, c = state return int(np.argmax(Q[r, c, :])) # 平局时argmax取第一个 def run(method, episodes=500, alpha=0.1, gamma=0.99, epsilon=0.1): env = CliffWalk() Q = np.zeros((env.row, env.col, 4)) ep_rewards = [] for _ in range(episodes): s = env.reset() done = False total = 0 if method == 'sarsa': a = choose_action(Q, s, epsilon) while not done: if method == 'q': a = choose_action(Q, s, epsilon) s_, r, done = env.step(a) total += r # 核心差异 if method == 'q': target = r + gamma * np.max(Q[s_[0], s_[1], :]) else: a_ = choose_action(Q, s_, epsilon) # 先按当前策略选好下一步动作 target = r + gamma * Q[s_[0], s_[1], a_] Q[s[0], s[1], a] += alpha * (target - Q[s[0], s[1], a]) if method == 'sarsa': a = a_ s = s_ ep_rewards.append(total) return Q, ep_rewards注意Sarsa的更新里面,a_的选取必须发生在更新之前,而且更新完之后你要真的把a_当作下一步的动作去执行,也就是那句sarsa分支里的a = a_。这正是on-policy的关键:你要用一个“按当前策略选好的下一步”来更新,然后真的走下去。
你跑完整个实验,会得到两个在很多场景下完全不一样的Q表:
| 对比项 | Q-learning | Sarsa |
|---|---|---|
| 学出来的路径 | 贴着悬崖边的最短路 | 绕到上方的安全长路 |
| 平均每回合掉崖次数 | 较高 | 很低,接近零 |
| 单回合累计回报 | 最优时约-13 | 稳定约-22到-30 |
| 学习早期方差 | 大,忽好忽坏 | 小,渐进改善 |
| 收敛后的最终策略 | 路径最短,最优 | 安全保守,有时未必最优 |
这里打一个醒:虽然表格里看起来Q-learning更“优”(回带更少),但它这种“优”是建立在环境允许掉崖重来这个前提下的。要是换成真实世界的机器人,掉一次悬崖可能就是摔一次跟头、烧一次电机,甚至出安全事故——那时候Sarsa的保守反而是工程上更理性的选择。
3.1 如果你想让它们都走捷径:衰减ε的策略会带来什么
实验里有个细节值得展开:如果你在训练后期把ε从一个比较大的值(比如0.3)逐渐衰减到接近0,Q-learning学出的最优路径依然会偏向悬崖边,因为它的max本身就是在主张“理论最优”;而Sarsa在ε比较大的时期已经积累了“走悬崖容易掉”的教训,后面ε降下来也很难洗掉这种保守倾向。
我有一次实验,把Q-learning的ε从0.1改成0.3,早期它每回合掉崖三四次,但三四百回合后收敛出来依旧是贴崖的最优路线。Sarsa在这个ε下几乎从始至终没有大规模贴近悬崖。这就是策略痕迹(policy footprint):Sarsa的Q值里留了太多“旧策略犯过的错”,想擦掉非常难。
4. 两个隐藏的工程差异:参数敏感性和过估计偏差
在实际项目中,大家用的往往不是表格,而是神经网络逼近Q函数。这时Q-learning和Sarsa的一些小差异会被放大成工程上致命的坑。这里单独列一节说清楚。
4.1 学习率α的敏感性不一样
Q-learning的更新目标里带了max,max操作本身对Q值的干扰比较大,所以它对学习率α比较敏感。α太大,Q值震荡得厉害;α太小,收敛慢得让人怀疑人生。不少项目里Q-learning都需要把α从0.1往下探到0.01甚至更小,尤其是环境带有随机奖励的时候。
Sarsa因为更新目标用的是“实际选到的动作的Q值”,少了一层max对噪声的放大作用,整体更新平稳许多。所以Sarsa通常能在稍大一点的α下稳定训练,也更容易配出不会发散的参数。
一个小建议:如果你在调参时发现Q-learning发散去,先别急着改网络结构,先往下降α,往往能救回来。
4.2 过估计:Q-learning的max是一把双刃剑
前面提过过估计。表格型Q-learning由于Q表是离散独立存储的,过估计相对可控;但一旦换到神经网络(比如DQN),max操作会让Q值系统性地高于真实价值,训练到后期经常看到“明明收益在变差,Q值却还在往上涨”的反常现象。
Sarsa没有这个Max困扰,但它也不因此就高枕无忧。当动作空间很大的时候,on-policy需要海量数据才能把策略覆盖完整,否则学到的东西会严重偏向“已经探索过的路径”,很多更优分支根本没机会被发现。这也就是高维控制场景(比如机器人连续控制)普遍更偏爱off-policy方法的根本原因——数据效率和信息覆盖上,off-policy天然占优。
4.3 数据来源:一个很少被提及但极其现实的差异
工程落地时常遇到一个问题:你不是真的能亲自控制环境,或者手里只有一段别人跑出来的历史数据。这时候Q-learning突然变成了几乎唯一的选择,因为它是off-policy,可以在别的策略采出来的数据上反复离线学习,不断更新自己的Q表。Sarsa是on-policy,它的更新目标依赖“当前策略”指定的下一步动作,拿历史数据来训练的时候逻辑上会拧巴:历史数据里的动作可能根本不是你当前策略会选的动作,那你还怎么用“当前策略选的a'”做更新?
这个差异在现实项目里意味着什么?意味着Q-learning可以拿存量数据、别家agent的轨迹做离线训练,Sarsa则必须是线上边试边学。做交易策略、自动驾驶决策这类有大量历史日志的领域,off-policy几乎是默认选择。你网上看到很多“从expert trajectory里学策略”的案例,底层基本都是Q-learning、DQN这一挂。
5. 到底选哪个?我给决策场景列了一个真实可用的选择清单
直接给结论容易误人,这里我按实际场景盘一盘。
优先选Sarsa的情况:
- 任务对单步失败容忍度极低。比如机器人行走、自动仓储车调度,摔一次代价很大,宁可绕远也不冒险。
- 环境随机性比较大,比如动作执行有噪声、奖励有噪声。Sarsa把噪声算进更新目标里,学出来的策略更贴地。
- 你打算长期在线训练、环境允许大量试错,但试错的单次成本不能太高。
- 你希望策略的行为方差小一点,方便做后期的安全性验证。
优先选Q-learning的情况:
- 你手里有离线数据、历史日志,想离线训练一个决策模块。这是off-policy的主场,Sarsa根本打不了。
- 任务有清晰的奖励结构,高回报路径明确,环境允许“失败后重置”(比如游戏、仿真器),可以将训练期的一些“翻车”视为可接受成本。
- 你后面计划往DQN、Double DQN等深度强化学习方向走。那本质上都是从这个max框架延伸出去的,底子打了不亏。
- 动作空间大、需要高数据利用率的任务。
如果你实在拿不准,我的土办法是:先用Sarsa搭一个安全保守的基线,再拿Q-learning往上冲最优解。两个算法一共就几十行代码,都跑一遍对比学习曲线,比拿着一堆理论纠结半天高效得多。
6. 从这对“双胞胎”出发,你的下一步该看什么
Q-learning和Sarsa不是你学习的终点,它们是整个RL算法树的根。弄懂这对双胞胎之后,你再看后面一串算法会发现全都是熟面孔:
- Expected Sarsa:把Sarsa里的
Q(s', a')换成对所有动作按策略概率加权求期望。它保留了Sarsa“按策略评估”的保守属性,但减少了单次采样带来的方差,线表上一般比Sarsa更稳。 - Double Q-learning:用两套Q表交叉更新,破解Q-learning的过估计问题。懂了对max的执念之后,再看Double Q-learning的思路就觉得非常自然——它不过是用两套账本互相纠偏。
- DQN:Q-learning + 神经网络 + 经验回放 + 目标网络,整个深度强化学习热潮的起点。你看DQN论文里那些“消除相关性”“防止过估计”的设计,每一条都能在Q-learning的短板上找到对应。
- SARSA(λ):在Sarsa前面加一个资格迹(eligibility trace),实现多步TD更新。它不人为等待回合结束,而是把最近走过的几步“记在账上”统一更新。这又是从“单步更新”到“多步更新”的全新维度。
我的建议是不要急着跳到DQN,先把表格版的Q-learning和Sarsa亲手写一遍。不需要用什么现成框架,纯numpy几十行就行,跑通那个悬崖实验,画两条学习曲线出来,你会有一种“这算法真的活了”的感觉。我就是靠这个实验,彻底告别了对着公式死记硬背的学习方式。
一段写在最后的心得
如果你认真把上面所有实验都跑过一遍,会发现一个很有意思的事实:Q-learning和Sarsa没有绝对的优劣,它们只是对“风险”有不同态度的两种决策者。一个盯着最优解攻坚,一个守着实操稳扎稳打。现实中调参调得多了,我反而觉得它们更像一对性格互补的伙伴——激进方案用来探上限,保守方案用来保下限。做项目也一样,先求安全落地,再一步步挑战更短的路径。理解了这层“性格差异”,你才算真正吃透了这对双胞胎。