1. 从动态规划到无模型控制为什么需要时序差分如果你已经啃过强化学习的基础部分大概率会有一种割裂感动态规划那一章讲得头头是道策略迭代、值迭代推导得严丝合缝但一合上书就发现一个致命问题——它要求你完全知道环境的转移概率和奖励函数。换句话说你得先把整个世界的规则手册拿到手才能开始算。可现实里哪有这种好事一个机器人走进陌生房间它不知道地板哪里会打滑、哪块区域是陷阱它只能靠一步步试探、一次次撞墙来积累经验。这就是无模型强化学习要解决的核心矛盾在不知道环境模型的前提下如何通过与环境的交互来学习最优策略。而时序差分方法正是打开这扇门的那把钥匙。我刚开始接触这部分内容的时候最大的困惑不是公式推不动而是搞不清楚Sarsa和Q-learning到底差在哪——两个算法的更新公式看起来几乎一模一样就差一个max符号凭什么一个叫同策略一个叫异策略后来踩了几次坑、跑了几轮实验才真正理解那个max符号背后代表的是两种完全不同的价值观。Sarsa是保守派它考虑的是“我实际会怎么做”所以它学出来的策略更安全Q-learning是激进派它考虑的是“理论上最优会怎么做”所以它学出来的策略更冒险但理论上更优。这个区别在悬崖行走这个经典环境里体现得淋漓尽致。这篇文章我会把时序差分控制这条线从头到尾捋一遍包括Sarsa、Q-learning、期望Sarsa、双Q学习这几个核心算法。不只是贴公式我会把每个算法为什么这么设计、实际跑起来会遇到什么问题、参数怎么调、坑在哪里都掰开揉碎讲清楚。如果你正在学强化学习或者准备用这些算法做项目这篇内容应该能帮你省下不少自己摸索的时间。2. 时序差分控制的核心思路拆解2.1 从预测到控制TD方法的两层逻辑时序差分方法其实分两个层面预测和控制。预测解决的是“给定一个策略它的价值函数是多少”控制解决的是“怎么找到最优策略”。很多人一上来就直奔Q-learning结果连TD误差是什么都没搞明白后面越学越糊涂。先说预测。TD预测的核心思想可以用一句话概括用当前的估计来更新当前的估计。听起来像循环论证但它之所以能work是因为每一步交互都会带来新的奖励信息这个信息就像一块拼图逐步修正你原来的估计。具体来说TD(0)的更新公式是V(S_t) ← V(S_t) α[R_{t1} γV(S_{t1}) - V(S_t)]其中R_{t1} γV(S_{t1})叫TD目标减掉V(S_t)叫TD误差。你可以把TD误差理解成“惊喜程度”——如果实际得到的比预期好误差为正价值往上调如果比预期差误差为负价值往下调。α是学习率控制每次调整的幅度。我第一次理解这个公式的时候觉得它跟蒙特卡洛方法很像都是拿实际回报来更新估计。但区别在于蒙特卡洛必须等到整个回合结束才能更新而TD方法每走一步就能更新。这个差别在实际项目中影响巨大——回合很长甚至没有终止状态的任务蒙特卡洛根本没法用TD方法却可以持续学习。2.2 控制问题的本质从价值到策略的映射预测只是手段控制才是目的。在强化学习里控制问题的核心是找到最优策略π*使得从任意状态出发的期望回报最大。对于表格型问题这个目标可以转化为找到最优动作价值函数Q*(s,a)然后在每个状态选择Q值最大的动作。这里有一个关键的设计选择到底是学状态价值V(s)还是动作价值Q(s,a)如果只学V(s)你还得知道环境的转移概率才能推导出策略因为你需要比较不同动作导致的下一状态价值这就又回到了有模型的老路。所以无模型控制方法几乎都选择直接学Q(s,a)因为Q值本身就包含了动作选择的信息——选Q最大的动作就行了不需要知道环境模型。这个选择看起来理所当然但它带来一个连锁反应Q表格的大小是状态数乘以动作数。如果状态空间很大或者动作是连续的表格就存不下了。这也是为什么后续会发展出DQN等深度强化学习方法——用神经网络来近似Q函数而不是用表格存储。不过在理解深度方法之前把表格型TD控制吃透是必须的因为所有深度方法的更新逻辑都是从这些基础算法延伸出来的。2.3 同策略与异策略一个max符号引发的分歧Sarsa和Q-learning的更新公式只差一个符号但这个符号背后是两种截然不同的策略评估逻辑。Sarsa的更新用的是实际执行的下一个动作的Q值Q(S_t, A_t) ← Q(S_t, A_t) α[R_{t1} γQ(S_{t1}, A_{t1}) - Q(S_t, A_t)]注意A_{t1}是下一步真正要执行的动作这个动作是根据当前策略比如ε-greedy选出来的。所以Sarsa在评估策略的时候把探索行为也考虑进去了——它知道自己在训练时会随机探索所以学出来的Q值反映的是“带探索的策略”的真实价值。Q-learning的更新用的是下一个状态中最大的Q值Q(S_t, A_t) ← Q(S_t, A_t) α[R_{t1} γ·max_a Q(S_{t1}, a) - Q(S_t, A_t)]这个max操作意味着不管实际下一步选什么动作Q-learning都假设未来会选最优动作。所以它学的是最优策略的价值哪怕当前行为策略还在探索。这个区别在悬崖行走环境里特别明显。Sarsa因为知道自己在探索时会随机走所以学到的最优路径是远离悬崖的安全路线Q-learning因为假设未来不会犯错所以学到的最优路径是紧贴悬崖的最短路线。实际跑的时候Q-learning在训练过程中掉下悬崖的次数明显更多但最终收敛到的策略理论上更优。3. 四个核心算法的细节与实操要点3.1 Sarsa保守但稳健的同策略控制Sarsa的名字来自它的更新依赖的五元组(S_t, A_t, R_{t1}, S_{t1}, A_{t1})。这个五元组正好是算法执行一次更新所需要的全部信息。算法流程是这样的初始化Q表格为零或者小的随机值然后对每个回合从起始状态出发用ε-greedy策略选动作A执行A得到奖励R和下一状态S再用ε-greedy在S选动作A然后用五元组更新Q(S,A)最后把S←SA←A继续循环直到回合结束。这里有一个实操细节很容易被忽略A必须在更新Q之前就选好。因为Sarsa的更新目标里包含Q(S, A)如果你先更新了Q再选A那用的就是更新后的Q值这就不对了。我一开始写代码的时候就犯过这个错误导致训练曲线一直不对劲后来逐行debug才发现顺序搞反了。Sarsa的收敛性依赖于策略的逐步改进。在ε-greedy策略下只要学习率满足一定条件比如逐渐衰减并且每个状态-动作对被无限次访问Sarsa就能收敛到最优策略。但实际中我们通常跑有限步数所以学习率的衰减策略就很重要。我一般用α从0.5开始每1000步衰减到原来的0.99倍这样前期学得快后期稳定。注意Sarsa在训练过程中的表现通常比Q-learning更稳定因为它把探索的随机性纳入了价值评估。如果你的应用场景对训练过程中的安全性有要求比如机器人导航Sarsa是更稳妥的选择。3.2 Q-learning激进但理论最优的异策略控制Q-learning的更新公式里那个max操作让它成为了异策略方法的代表。所谓异策略就是行为策略和目标策略可以不同。行为策略负责和环境交互、生成经验数据目标策略是我们想要评估和改进的策略。Q-learning的行为策略可以是任意探索策略比如ε-greedy但目标策略始终是贪心策略。这个特性带来一个巨大的好处经验回放。因为Q-learning不要求行为策略和目标策略一致所以它可以把过去产生的经验存起来反复拿来训练。这个思想后来直接催生了DQN——用深度网络近似Q函数加上经验回放机制在Atari游戏上取得了突破性成果。但Q-learning也有它的问题。最大的问题是max操作带来的过估计偏差。因为Q值本身是估计值存在噪声max操作会系统性地偏向那些被高估的动作。这个问题在动作数量多、奖励噪声大的环境下特别严重。我做过一个实验在10个动作的bandit问题上跑Q-learning发现估计的Q值平均比真实值高出15%左右。这个偏差会导致策略选择次优动作训练效果打折扣。实操心得Q-learning的学习率通常要比Sarsa设得小一些因为max操作放大了更新目标的方差。我一般用α0.1起步如果训练曲线震荡厉害就降到0.05。3.3 期望Sarsa用期望替代采样降方差期望Sarsa可以看作是Sarsa和Q-learning的折中方案。它不像Sarsa那样用实际选出的A来计算目标也不像Q-learning那样直接用max而是用当前策略下所有动作的期望值Q(S_t, A_t) ← Q(S_t, A_t) α[R_{t1} γ·Σ_a π(a|S_{t1})Q(S_{t1}, a) - Q(S_t, A_t)]这个期望是对策略π的如果是ε-greedy策略那期望就是(1-ε)·max_a Q(S_{t1},a) ε·mean_a Q(S_{t1},a)。当ε0时期望Sarsa退化成Q-learning当ε1时它退化成平均值的某种形式。期望Sarsa最大的优势是降低了更新目标的方差。Sarsa因为只用了一个采样动作A方差比较大Q-learning虽然不用采样但max操作引入了偏差。期望Sarsa通过计算期望既避免了采样方差又比max更温和所以在很多任务上表现更稳定。我个人的经验是在动作空间较大的场景下期望Sarsa的优势最明显。因为动作多了之后Sarsa的采样方差会急剧增大Q-learning的过估计也会更严重而期望Sarsa的计算代价虽然增加了需要遍历所有动作但换来的稳定性提升很值。3.4 双Q学习解决过估计的经典方案双Q学习的思路很巧妙用两个Q表一个负责选动作另一个负责评估动作。具体来说每次更新时先用Q1选出S处Q值最大的动作a*然后用Q2查这个a*的Q值作为更新目标下一轮反过来用Q2选动作用Q1评估。这样交替进行就打破了max操作中“选”和“评”的耦合从而减轻过估计。更新公式看起来是这样的Q1(S,A) ← Q1(S,A) α[R γ·Q2(S, argmax_a Q1(S,a)) - Q1(S,A)]注意argmax是在Q1上取的但取值是在Q2上取的。这个解耦操作让双Q学习在过估计严重的环境下表现明显优于标准Q-learning。双Q学习的代价是需要维护两个Q表内存翻倍计算量也增加。但在表格型问题里这通常不是瓶颈。在深度强化学习中Double DQN就是把这个思想搬到了神经网络上——用当前网络选动作用目标网络评估动作效果提升显著。注意双Q学习的两个Q表初始化时要独立不能设成一样的值否则前期会退化成普通Q-learning。我一般用不同的随机种子分别初始化。4. 完整实操流程与参数调优4.1 环境搭建与基础代码框架我以悬崖行走为例把整个流程走一遍。这个环境是一个4行12列的网格起点在左下角终点在右下角中间有一排悬崖。掉下悬崖奖励-100并回到起点每走一步奖励-1走到终点回合结束。先定义环境的基本参数import numpy as np class CliffWalking: def __init__(self, rows4, cols12): self.rows rows self.cols cols self.start (rows-1, 0) self.goal (rows-1, cols-1) self.cliff [(rows-1, i) for i in range(1, cols-1)] self.state self.start self.actions [0, 1, 2, 3] # 上、右、下、左 def step(self, action): r, c self.state if action 0: r max(0, r-1) elif action 1: c min(self.cols-1, c1) elif action 2: r min(self.rows-1, r1) elif action 3: c max(0, c-1) next_state (r, c) if next_state in self.cliff: return self.start, -100, False elif next_state self.goal: return next_state, -1, True else: return next_state, -1, False def reset(self): self.state self.start return self.state这个环境虽然简单但它包含了强化学习的核心要素状态转移、奖励信号、终止条件。而且悬崖的存在让不同算法的行为差异非常明显特别适合用来对比Sarsa和Q-learning。4.2 Sarsa的完整实现与训练曲线分析Sarsa的实现关键在于动作选择的顺序def sarsa(env, episodes500, alpha0.5, gamma0.99, epsilon0.1): Q np.zeros((env.rows, env.cols, 4)) rewards [] for ep in range(episodes): state env.reset() action epsilon_greedy(Q, state, epsilon) total_reward 0 while True: next_state, reward, done env.step(action) next_action epsilon_greedy(Q, next_state, epsilon) # Sarsa更新用实际选出的next_action td_target reward gamma * Q[next_state[0], next_state[1], next_action] td_error td_target - Q[state[0], state[1], action] Q[state[0], state[1], action] alpha * td_error state, action next_state, next_action total_reward reward if done: break rewards.append(total_reward) return Q, rewards跑500个回合之后Sarsa的每回合回报会稳定在-20左右。这个值看起来不高但考虑到每走一步扣1分最优路径大概需要13步加上探索的随机性-20左右是合理的。训练曲线会呈现一个特点前期下降较快后期比较平稳。这是因为Sarsa学到的策略本身就考虑了探索所以它的表现和最终策略的表现比较一致不会有太大的波动。4.3 Q-learning的实现与对比实验Q-learning的实现和Sarsa只有一行之差def q_learning(env, episodes500, alpha0.5, gamma0.99, epsilon0.1): Q np.zeros((env.rows, env.cols, 4)) rewards [] for ep in range(episodes): state env.reset() total_reward 0 while True: action epsilon_greedy(Q, state, epsilon) next_state, reward, done env.step(action) # Q-learning更新用max Q值 td_target reward gamma * np.max(Q[next_state[0], next_state[1]]) td_error td_target - Q[state[0], state[1], action] Q[state[0], state[1], action] alpha * td_error state next_state total_reward reward if done: break rewards.append(total_reward) return Q, rewards同样的参数下Q-learning的训练曲线波动明显更大。前期经常出现-100以下的回报掉下悬崖但后期收敛后的策略更优——最优路径紧贴悬崖只需要13步回报-13。把两个算法的训练曲线画在一起你会看到Sarsa的曲线更平滑但最终值略高-20 vs -13Q-learning的曲线更抖但最终值更低。这个对比非常直观地展示了同策略和异策略的区别。4.4 期望Sarsa与双Q学习的代码实现期望Sarsa的更新需要计算期望def expected_sarsa(env, episodes500, alpha0.5, gamma0.99, epsilon0.1): Q np.zeros((env.rows, env.cols, 4)) rewards [] for ep in range(episodes): state env.reset() total_reward 0 while True: action epsilon_greedy(Q, state, epsilon) next_state, reward, done env.step(action) # 计算期望值 q_next Q[next_state[0], next_state[1]] n_actions len(q_next) best_action np.argmax(q_next) expected_q (1 - epsilon) * q_next[best_action] \ (epsilon / n_actions) * np.sum(q_next) td_target reward gamma * expected_q td_error td_target - Q[state[0], state[1], action] Q[state[0], state[1], action] alpha * td_error state next_state total_reward reward if done: break rewards.append(total_reward) return Q, rewards双Q学习的实现需要两个Q表def double_q_learning(env, episodes500, alpha0.5, gamma0.99, epsilon0.1): Q1 np.zeros((env.rows, env.cols, 4)) Q2 np.zeros((env.rows, env.cols, 4)) rewards [] for ep in range(episodes): state env.reset() total_reward 0 while True: action epsilon_greedy(Q1 Q2, state, epsilon) next_state, reward, done env.step(action) if np.random.random() 0.5: # 用Q1选动作Q2评估 best_action np.argmax(Q1[next_state[0], next_state[1]]) td_target reward gamma * Q2[next_state[0], next_state[1], best_action] td_error td_target - Q1[state[0], state[1], action] Q1[state[0], state[1], action] alpha * td_error else: # 用Q2选动作Q1评估 best_action np.argmax(Q2[next_state[0], next_state[1]]) td_target reward gamma * Q1[next_state[0], next_state[1], best_action] td_error td_target - Q2[state[0], state[1], action] Q2[state[0], state[1], action] alpha * td_error state next_state total_reward reward if done: break rewards.append(total_reward) return Q1 Q2, rewards双Q学习在悬崖行走上的表现介于Sarsa和Q-learning之间——比Q-learning稳定比Sarsa最终策略更优。这个结果符合理论预期因为双Q学习减轻了过估计所以学到的策略不会像Q-learning那么激进。5. 常见问题与排查技巧实录5.1 Q值不收敛或震荡严重怎么办这是最常见的问题。我刚开始跑Q-learning的时候Q值经常在正负之间来回跳训练曲线像心电图一样。排查下来主要有几个原因学习率太大。α0.5在悬崖行走这种简单环境还能用但稍微复杂一点的环境就会震荡。我一般先用0.1试如果收敛太慢再往上调。有一个经验公式可以参考α的初始值可以设为1/平均每个状态被访问的次数。比如你有100个状态跑10000个回合那平均每个状态被访问100次α初始值可以设0.01左右。奖励尺度不合适。悬崖行走的奖励是-1和-100差距100倍。如果奖励差距太大Q值的更新幅度也会很大容易震荡。解决办法是把奖励归一化比如都除以100变成-0.01和-1。这样Q值的范围就控制在合理区间了。探索率衰减太快。ε如果从一开始就设得很小很多状态-动作对根本访问不到Q值自然学不准。我一般用ε从1.0开始每1000步衰减到0.99倍最低不低于0.05。这样前期充分探索后期稳定利用。排查技巧把Q值的热力图打印出来看看是不是有些区域的Q值明显异常。如果某个状态的Q值特别大或特别小很可能是那个区域访问次数太少导致的。5.2 Sarsa和Q-learning到底怎么选这个问题我被问过很多次。我的回答是看你的应用场景对训练过程安全性的要求。如果你在做机器人导航、自动驾驶这类训练过程中出错代价很高的任务选Sarsa。因为Sarsa学到的策略本身就考虑了探索的随机性训练过程中掉下悬崖的次数会少很多。虽然最终策略可能不是理论最优但安全第一。如果你在做游戏AI、推荐系统这类训练过程可以承受一定风险的场景选Q-learning。它的最终策略更优而且异策略特性让你可以用经验回放数据利用率更高。期望Sarsa和双Q学习可以看作是两种折中方案。期望Sarsa比Sarsa更稳定双Q学习比Q-learning更准确。实际项目中我经常先跑一轮对比实验看哪个算法在验证集上表现最好再决定。5.3 过估计问题怎么诊断和缓解过估计的典型症状是Q值持续增长但实际回报没有提升。你看到Q表格里的数值越来越大但agent在环境里的表现却停滞不前甚至变差。诊断方法很简单把学到的策略在环境中跑100个回合记录实际平均回报然后跟Q表格里对应状态的Q值比较。如果Q值明显高于实际回报那就是过估计了。缓解过估计有几个方案。最直接的是用双Q学习用两个Q表解耦选和评。如果不想改算法也可以调小学习率或者增加探索率让Q值估计更充分。还有一个技巧是奖励裁剪把奖励限制在[-1, 1]区间内这样Q值的范围也可控过估计的绝对幅度会小很多。我在一个动作空间为20的bandit问题上做过对比标准Q-learning的Q值平均高估18%双Q学习的高估只有3%左右。这个差距在动作空间大的时候会更明显。5.4 训练速度太慢怎么优化表格型TD控制的训练速度主要受两个因素影响每回合的步数和总回合数。如果每回合步数太多说明agent在绕圈子。这通常是探索率太高或者Q值初始化不好导致的。可以试试乐观初始化——把Q值初始化为一个较大的正值这样agent会倾向于尝试没访问过的动作减少绕圈。如果总回合数太多说明学习效率低。可以试试优先经验回放的思想把TD误差大的经验存起来多拿出来训练。虽然表格型方法不像DQN那样有专门的回放缓冲区但你可以手动实现一个简单的优先级队列每次从里面采样更新。还有一个容易被忽略的点是状态表示。悬崖行走的状态是(row, col)的元组直接用作字典的key没问题。但如果状态是连续值就需要离散化。离散化的粒度太细会导致状态空间爆炸太粗又会导致信息丢失。我一般先用较粗的粒度跑通流程再逐步细化。5.5 常见问题速查表问题现象可能原因排查方法解决方案Q值震荡不收敛学习率过大打印Q值变化曲线降低α从0.1开始试训练回报波动大探索率过高检查ε值逐步衰减ε最低0.05Q值持续增长但回报不涨过估计对比Q值和实际回报改用双Q学习或奖励裁剪训练速度慢状态空间太大统计状态访问次数粗粒度离散化或乐观初始化策略在训练时表现差同策略探索代价高对比训练和测试表现改用Q-learning或期望Sarsa某些状态Q值异常访问次数不足打印状态访问热力图增加探索或乐观初始化6. 从表格到深度这些算法的延伸价值把Sarsa、Q-learning、期望Sarsa、双Q学习这四个算法吃透之后你会发现它们不只是四个独立的算法而是四个设计维度的组合同策略vs异策略、采样vs期望、单估计器vs双估计器。这些维度可以自由组合衍生出更多变体。比如把Q-learning和期望Sarsa结合就得到了“期望Q-learning”——用max的期望替代max既保留了异策略特性又降低了方差。再比如把双Q学习的思想用到Sarsa上就得到了“双Sarsa”在同策略框架下减轻过估计。这些思想在深度强化学习里都有对应。DQN对应Q-learningDouble DQN对应双Q学习Expected DQN对应期望SarsaSARSA(λ)对应带资格迹的Sarsa。理解了表格型版本再看深度版本就只是把Q表格换成神经网络、把精确查表换成函数近似的问题了。我在实际项目里最常用的组合是双Q学习加经验回放。双Q学习解决过估计经验回放提高数据利用率两个加起来在中小规模问题上表现很稳。如果环境对训练安全性有要求就把双Q学习换成期望Sarsa牺牲一点理论最优性换训练过程的稳定。最后分享一个我踩过的坑不要一上来就调参。先把算法逻辑跑通用默认参数跑100个回合看看有没有基本的收敛趋势。如果完全不收敛那大概率是代码写错了不是参数问题。我见过太多人花几个小时调参最后发现是更新顺序写反了或者状态索引搞错了。先用小规模、简单环境验证逻辑再上复杂环境调参这个顺序不能反。