时序差分(TD)方法是我在学习强化学习过程中觉得最有“灵性”的一类算法。它既不像动态规划那样需要环境模型,也不像整轨迹回放式的方法那样必须等到回合结束才更新——它把两者巧妙地拧在了一起:每走一步,就用当前能拿到的最新鲜的反馈去修正估计。这篇文章我会把TD方法的来龙去脉、核心公式、同策略与异策略的分岔以及代码层面的落地方案串起来讲,适合已经了解强化学习基本概念、准备深入算法细节的读者。
如果你看过本系列前面的几篇,应该还记得我们讲过两种路线:一种靠数学模型推导出最优策略,另一种靠反复跑完整条轨迹再回头统计修正。这两条路各自有硬伤,而时序差分方法出现后,才真正把“实时更新”和“无需模型”同时变成现实。这篇文章会用尽量直白的语言,把TD方法里那些看起来抽象的概念掰开揉碎,顺便附上一个可以跑起来的简单实验,方便你边看边动手。
1. 动态规划和回合更新法各自缺了什么
在没有时序差分之前,强化学习的两条主要路线其实都很别扭。搞懂它们各自的问题,才能理解TD方法到底“补”在了哪里。
1.1 回合更新法的延迟与高方差
回合更新法(一整条轨迹跑完再更新)几乎所有初学者都能想明白:让智能体从起点走到终点,记录下每一步获得的奖励,等这个回合彻底结束之后,再把每一步的实际总回报算出来,用来更新这一路上所有状态的价值估计。
这个思路很直观,但两个问题特别头疼。第一是延迟,智能体在第一步获得的那个奖励到底有多重要?要等到走到终点、甚至跑完几百步之后才能反馈回来。如果环境特别复杂、单个回合特别长,那学习效率低得让人着急。比如我一开始在做一个迷宫任务,智能体走一步得等几百步以后才知道这次走得到底好不好,中间的关联早就被后续大量动作冲淡了。第二是方差,整条轨迹上随机因素太多了,每一步的动作、环境的随机转移、初始状态的差异,全都会累积进总回报里。也就是说你用同一套策略跑十遍,最后得到的总回报可能差别很大。用这样大起大落的信号去更新价值估计,不光是慢,还会反复震荡。
1.2 动态规划对模型的硬依赖
另一条路线是动态规划。动态规划的思路是:我不需要去真正地走轨迹,只要我知道环境的状态转移概率和奖励函数,就能直接从数学模型迭代出每个状态的价值。
问题在于“只要”这两个字。真实环境里,状态转移概率和奖励函数往往是不知道的。你让智能体控制一个机械臂,你说它的下一个关节角度会以多少概率落到某个范围?这个精确模型很难得到。就算能近似建出来,一旦环境本身有变化,模型又得重新建。更重要的是,动态规划依赖的自举递推关系在理论上很漂亮,真正落地的时候却经常卡在“没有模型”这个第一道坎上。我当时做一个小玩具的时候就想:能不能有一类方法,既不像动态规划那样必须要模型,又不像回合更新法那样非得苦等整条轨迹结束?
时序差分方法就是冲着这个需求来的。
2. TD目标与TD(0)更新公式:一步预测如何平衡误差和方差
TD方法的全部秘密,其实就藏在一句话里:我只走一步,然后立刻用现有的估计去修正现有的估计。听起来有点循环论证?实际操作起来反而非常有效。
2.1 从预测误差说起
假设你现在要估计某个状态的价值。在回合更新法里,你这个估计要等到回合结束时拿真实总回报来校准。而TD方法想得很直接:我现在走出了这一步,拿到了一步的即时奖励r,还看到了下一个状态s'。既然我手头已经有一个价值函数,那s'的价值估计V(s')是多少?我可以用 r + γV(s') 来作为对当前状态s的一个新“参考值”。
请注意,这里 r + γV(s') 并不是最终真相,它里面同样包含了我们自己的估计。但因为下一步状态s'离终点更近,这个参考值通常比原来的V(s)要靠谱一些。于是我们拿着这个新参考值和旧估计对比,看差多少,然后往这个方向修正一点点。这就是预测误差的思想:不要等整场比赛打完再评价这脚传球怎么样,而是球传出去之后就立刻根据现在的局面做一次小幅度修正。
2.2 拆解TD(0)更新公式
TD(0)的更新公式用状态价值函数写出来是这样:
V(s) ← V(s) + α [ r + γV(s') - V(s) ]
方括号里这一整项就是时序差分误差(TD误差)。它由三部分组成:
- r:这一步刚刚拿到的真实奖励,是环境给出的硬信息。
- γV(s'):对走完这一步之后、从下一状态出发还能拿到多少回报的估计。
- V(s):更新之前对当前状态的旧估计。
这个式子的本质就是:把“新观察到的信号”和“旧估计”做一次加权平均。如果新信号比旧估计大,就把估计往上调一点;如果比旧估计小,就往下调一点。调节的幅度由学习率α控制。
举个例子,假设你在和一个朋友玩猜比分,你原来判断某球队能赢30分,结果第一节打完领先了35分,你并不会直接把预测改成最终结果,而是稍微上调一点,改成31分——因为你心里清楚,第一节领先只是整场比赛的一个信号,不是最终真相。TD更新也是一样的逻辑,用一局中的一小步作为信号去修正总估计,而不是把这个信号当成最终回报。
2.3 为什么TD的误差比回合更新更“温柔”
回合更新法用一个回合的真实总回报来更新,这个总回报方差大,但几乎是无偏的。TD方法只基于一步真实奖励加一步估计,偏差可能会大一些,但方差小得多,因为每一步的真实奖励本身变化范围有限,不像整条轨迹那样累积了大量随机性。
两者之间其实是偏差和方差的权衡。TD(0)站在“几乎完全靠估计自举”的一端,每一步都在修正,修正幅度不大,但是修正频率很高。也正因如此,它不需要等待回合结束,天然适合在线学习。你刚踏入某个状态,走出一步,下一秒就能用上这个修正信号。这一点在机器人控制、实时交易、游戏对战这类不能等整个回合跑完再学习的场景里,价值极高。
3. 从状态价值到动作价值:SARSA和Q-learning的同策略与异策略取舍
TD方法可以直接用来更新状态价值V(s),但光有状态价值还不够——智能体做决策时得知道“在这个状态下做什么动作最好”,所以我们需要动作价值函数Q(s, a)。于是TD的思路自然迁移到Q值的更新上。但在这个迁移过程中,出现了一个非常重要的岔路口:同策略和异策略。
3.1 为什么必须有动作价值函数
如果你只掌握V(s),你可以找到一个策略吗?一般情况下不太行,因为在不知道环境模型的时候,光知道当前状态多好,你不知道哪个动作能把你带到更好的状态去。你当然可以用好奇心去试探,但最终形成稳定策略时,还是需要Q(s, a)这样“状态-动作”级别的估计。一个简单的判断标准是:如果已经学好了Q值,那决策只需要看argmax Q(s, a)就行了,连环境模型都不需要。这也是从TD状态价值走向TD动作价值最直接的动机。
3.2 Q-learning:只看最好的下一步
Q-learning的更新规则是这样:
Q(s, a) ← Q(s, a) + α [ r + γ·max_a' Q(s', a') - Q(s, a) ]
注意看目标部分:r + γ·max_a' Q(s', a')。意思是走了一步之后,在下一个状态s'的所有可能动作里,我来挑一个让Q值最大的,用它来作为未来回报的估计。问题来了——我下一步在s'里真的会去选那个最优动作吗?不一定。因为我的探测策略是ε-贪心,有可能为了探索去选一个随机动作。但Q-learning不管这些,它在更新的时候假设自己永远选择最优的那个动作。
这就叫异策略(off-policy):行为策略(我实际用来做决策的那些动作,可能带着随机探索)和学习策略(我更新Q值时假定的那个最优动作)可以不是同一个策略。好处是Q-learning可以一边试探一边贴着“最优策略”的方向学习,收敛后得到的是逼近最优动作价值的估计。代价是在探索比较激烈的时候,更新信号里可能包含了比较乐观的幻想。
3.3 SARSA:实际走哪条路就按哪条路学
SARSA换了思路。它的名字就是 State-Action-Reward-State-Action,每个词对应更新公式里要用的一个元素:
Q(s, a) ← Q(s, a) + α [ r + γ·Q(s', a') - Q(s, a) ]
这里的a'不是“s'里最好的动作”,而是“我马上真的要在s'里执行的那个动作”。也就是说,如果你现在的策略在s'会随机乱走,SARSA就会把这个随机走的结果算进目标里,更新出来的Q值反映的是“在我当前策略下,这个动作到底能拿多少回报”。
这就叫同策略(on-policy):更新价值和执行动作用的是同一个策略。SARSA学到的,是对“当前这套带探索的行为模式”的价值估计,而不是对“假设全程最优”的价值估计。所以SARSA通常比Q-learning更保守、更稳妥,尤其在环境中存在高危险区域的时候,这种“把自己随机的下一步也考虑进去”的机制,反而能保护智能体不做出太冒进的决策。
3.4 悬崖行走对比:两种性格的差异
我在一个经典的悬崖行走类环境里做过对比实验:一条窄路通向目标,路边就是深坑。Q-learning学出来的策略常常是贴着悬崖边走,因为它更新时默认下一步总能走到最优动作,但现实中它带着探索概率,很可能中途一步随机动作就直接掉坑里。SARSA则不一样,它把探索随机性也计入了Q值,于是学出来的路线会离悬崖边远远的,绕一个圈子到达终点。
这个对比挺有意思:Q-learning上线表现一开始会比较差,但它学到的更接近理论最优策略;SARSA在线执行更安全,但它的最终策略里带着探索痕迹的折中。重要的不是争谁更好,而是你必须知道自己所在场景的需求:如果在线执行期间犯错代价极高,那就偏向SARSA;如果你主要关心离线训练之后的最终策略质量,Q-learning会成为更常见的选择。
4. 动手实验:用Q-learning走通一个4x4网格世界
理论说得再多,不写代码总觉得没落到实处。下面我带大家做一个4x4网格世界的最小实验,用Q-learning让智能体学会从起点走到终点,顺便看一下SARSA和Q-learning之间那种微妙的性格差异。
4.1 搭建最小环境
这个环境不需要任何现成强化学习库,直接用几行代码定义就行。16个格子,起点在左上角,终点在右下角,全程走一步得到微小负奖励,踩中障碍则回到起点且受到更大惩罚,到达终点则获得正奖励。
import numpy as np grid_size = 4 start = (0, 0) goal = (3, 3) obstacles = {(1, 1), (2, 2)} # 分别对应 下、上、右、左 actions = [(1, 0), (-1, 0), (0, 1), (0, -1)] action_names = ['down', 'up', 'right', 'left'] def step(state, action_idx): x, y = state dx, dy = actions[action_idx] nx, ny = x + dx, y + dy if not (0 <= nx < grid_size and 0 <= ny < grid_size): nx, ny = x, y if (nx, ny) == goal: return (nx, ny), 1.0, True if (nx, ny) in obstacles: return start, -1.0, False return (nx, ny), -0.01, False注意撞墙的处理我选择留在原地,没有额外惩罚。这会稍微改变障碍附近的动力学,但对学习过程无伤大雅。如果想让探索更有挑战性,也可以给撞墙加一个负奖励,看智能体能不能学会绕开边界。
4.2 核心更新代码
Q表用三维数组表示:格子x坐标、y坐标、动作索引。下面这段是Q-learning的核心循环:
Q = np.zeros((grid_size, grid_size, len(actions))) alpha = 0.1 # 学习率 gamma = 0.99 # 折扣因子 epsilon = 1.0 # 初始探索率 eps_min = 0.05 eps_decay = 0.995 def select_action(state): if np.random.rand() < epsilon: return np.random.randint(len(actions)) return int(np.argmax(Q[state[0], state[1]])) for episode in range(500): state = start epsilon = max(eps_min, epsilon * eps_decay) total_reward = 0 done = False while not done: action = select_action(state) next_state, reward, done = step(state, action) target = reward + gamma * np.max(Q[next_state[0], next_state[1]]) Q[state[0], state[1], action] += alpha * (target - Q[state[0], state[1], action]) state = next_state total_reward += reward if episode % 50 == 0: print(f"episode {episode}, total_reward {total_reward:.2f}")SARSA的差别就在更新那一行。Q-learning用的是max,SARSA则需要你先按照当前的探索策略选出a',再把Q(s', a')放进去:
# SARSA 版本的更新核心 next_action = select_action(next_state) target = reward + gamma * Q[next_state[0], next_state[1], next_action] Q[state[0], state[1], action] += alpha * (target - Q[state[0], state[1], action]) state, action = next_state, next_action代码结构几乎一样,但这一行改动就足以造成完全不同的学习行为。我建议你把两个版本都跑一下,记录每一回合的总奖励,曲线差异会非常直观。
4.3 实验结果和可视化
跑完500个回合之后,Q-learning基本能学会一条稳定的路径。你可以把每个格子的最优动作打印出来,形成一张简单策略图。方法很简单,对每个状态取argmax Q,就可以画出类似下面的策略方向:
right right down down up right down down up #下坑 right down up right right goal实际输出中,障碍(1,1)和(2,2)附近会出现一些有趣的绕路动作,这取决于探索过程中的随机性。有一点我想提醒:网格世界很小、很干净,所以几百回合就能收敛。如果换一个更复杂的环境,收敛曲线的形状和震荡幅度都会变大,那时你就要去调节α、ε、γ这些参数了。
4.4 调参观察
我试过的几组参数组合,简单总结如下:
- 学习率α从0.01到1.0:α太小收敛太慢,太大则曲线剧烈震荡。0.1附近在这个小环境里表现不错。
- 折扣因子γ从0.9到0.999:γ越接近1,智能体越把终点奖励和早期的微小惩罚放在同一尺度上衡量;γ过小会显得目光短浅,但在这个环境里性能差别不算大。
- ε衰减速度:如果ε降太快,智能体过早固化策略,可能错过更好路线;如果降太慢,收敛慢且在线表现差。我用的0.995每回合衰减,大概在几百回合后降到0.05附近,节奏比较合适。
强烈建议你动手改一下这几个参数,特别是把ε衰减关掉、固定成0.5,你会看到智能体在障碍附近反复横跳,那就是探索过度的样子。亲手看到震荡曲线,比读十遍理论都管用。
5. 表格版TD落地时的参数选择与踩坑经验
表格版的TD算法看起来简单,真正跑起来还是有不少小陷阱。很多问题不是数学上的,而是工程和初始化习惯上的。我把这几年里印象比较深的经验整理一下。
5.1 学习率和折扣因子的搭配
学习率和折扣因子经常被分开调,但我觉得它们是一对搭档。γ决定“未来的奖励值多少钱”,α决定“这次的新信号值多少钱”。如果γ非常接近1,那么TD目标里的V(s')占比很高,信号里“来自未来估计”的成分就很大,此时α太大很容易造成来回震荡,因为目标本身还不够稳定。如果γ很小,目标更依赖眼前的真实奖励r,α就可以稍微大胆一些。
我的建议是:先固定γ为0.99,在一个小环境里调α;等行为基本正确之后,再尝试调节γ。每次都只动一个变量,否则出问题你根本定位不到是哪个参数引起的。
5.2 探索率的衰减节奏
探索率ε的衰减通常被公式化地处理:每回合乘以一个衰减系数,或者线性递减。但实际上它的理想节奏和环境难度强相关。环境简单,比如4x4网格,衰减快点无所谓;环境复杂,最优策略藏在深层的探索路径里,ε衰减太快就容易陷入次优解。
一个很实用的经验是不要直接调一个固定衰减系数,而是先跑一遍观察整个训练过程,在哪个回合策略基本稳定,然后让ε在“稳定回合数”的百分之七十处降到0.05左右。这样既保证前期有足够的探索,又不会让智能体长期处于乱走状态。
5.3 初始值里的大学问
很多人初始化Q表时直接全零,这在很多场景下会带来问题。如果所有Q值都是0,智能体开始时会倾向于选择那些已经拿到正奖励的动作,而那些从来没被尝试过的动作还是0,不会主动去碰。有些环境里好状态必须绕远路才到,一上来全零初始化会导致智能体一直摸不到那边。
最简单的技巧叫作乐观初始化:把Q表初始值设成一个比可能最大回报还高的数。这样智能体早期尝试每个动作,都会发现实际回报比预想低,于是被“失望”推着去探索更多动作。代价是探索阶段会变长,但往往能显著提升最终策略质量。遇到“智能体一直原地踏步”的情况,可以优先检查是不是初始Q值太冷淡了。
5.4 TD(λ):从一步回望整条轨迹
当你把TD走明白之后,一个自然的困惑就来了:一步更新是不是太“近视”了?如果我刚走出的那一步很特殊,后续几步其实都很顺利,一步TD会不会把坏信用错算到动作头上?
为了平滑这个问题,才有了n步TD和TD(λ)。n步TD的思路是走n步之后再回头更新,取一个介于“一步自举”和“整条轨迹统计”之间的平衡点。TD(λ)则更进一步,用资格迹把每一状态的贡献按时间差加权叠加,实现了一步和整条轨迹的连续插值效果。也就是说它既可以像TD(0)那样在线更新,又能利用较远未来的信息。这个扩展我在实际项目中用得比较多,尤其是在回合比较长但无法跑完的在线场景里。等把表格版的TD(0)弄熟之后,建议一定再往TD(λ)走一步。
另外值得提一句的是俞强调,现在大多数深度强化学习框架(比如各种DQN类算法)都是从Q-learning这个异策略TD框架里长出来的。所以即便你最终目标是上手深度强化学习,把表格版Q-learning的更新逻辑打磨熟练,绝对值得。
我在实际项目里感受到:那些看起来理论性很强的算法,真正好吃透它的方式,就是调参、对比、看曲线,亲手撞一两个坑之后再回头看公式。TD这个家族尤其如此,它用最朴素的“走一步修正一步”思想,解决了动态规划和整轨迹更新各自的问题。下次当你面对一个不能等回合结束才能更新的场景时,或许第一反应就该是:能不能换成时序差分试试。