TD学习是强化学习里出现频率极高的一个词,但很多人学完公式之后还是模糊:它到底解决了什么问题?跟蒙特卡洛比好在哪?为什么深度强化学习里到处都在用它?
这篇内容我会从最直觉的例子讲起,把时序差分学习的原理、算法变体、数学本质、实战实现和踩坑经验一次性讲透。适合刚接触强化学习的人,也适合正在调 DQN 但始终觉得“哪里没想明白”的开发者。看完你就能理解为什么 TD 是强化学习从表格走向深度学习的桥梁,也能动手写一个可运行的智能体。
1. 从一个例子理解 TD 学习的核心直觉
1.1 不用等到结局才学习:这就是 TD 的起点
想象你在走一个迷宫,每一步有一个状态,走到出口得到奖励。蒙特卡洛方法的做法是:走完一整条路径,等拿到最终奖励后,回头把每一步的价值统一更新。这个逻辑合理,但有个明显的痛点:如果这个迷宫要走 10000 步才到出口呢?整个训练过程中你几乎无法获得任何有效的学习信号——你只能在漫长等待后批量更新一次。
时序差分学习完全改掉了这种思路。TD 的核心直觉是:
当前状态的价值,可以通过下一步的即时奖励加上下一步状态的估计价值来更新。
换句话说,不需要走完全程,哪怕只走了一步,也可以根据“这一步得到的奖励 + 下一步状态的当前价值估计”去修正当前状态的价值。这个“预测的下一步价值”就是所谓的自举(bootstrapping)思想。
我用生活化的场景帮你理解这个思想。假设你在猜一个陌生城市的地铁票价,你之前觉得从 A 站到市中心要 8 块。此时有人告诉你:从 A 站到中转站 B 只要 3 块,从中转站 B 到市中心一般要 4 块。你不需要真的坐完全程,就能把“A 到市中心 8 块”的估计修正为“3 + 4 = 7 块”。这种用下一步估计值反过来修整当前估计的方法,就是 TD 在数学上做的事。
1.2 TD 与蒙特卡洛、动态规划的位置关系
要理解 TD 的意义,得把它放进强化学习价值估计的谱系里看。经典的价值估计方法有三条路线:动态规划(DP)、蒙特卡洛(MC)、时序差分(TD)。
动态规划需要完备的环境模型——你必须知道每个状态转移到后续状态的概率分布。这在实际场景里几乎拿不到,所以 DP 更多是一种理论基准。
蒙特卡洛只需要采样轨迹,不需要环境模型,但必须等到一个情节(episode)结束才能更新。若环境没有明确的终止状态,或者情节特别长,MC 的学习节奏就会被拖得很慢,并且单次回报的方差非常大——因为路径上的随机性全部累积在一次采样里。
TD 介于两者之间:它不需要模型(采样得到转移),又不需要等待情节结束(一步就更新),通过自举引入了估计偏差,但换来了方差的大幅下降和学习速度的显著提升。
关于偏差和方差,这里有句很多教材没挑明的话:MC 是无偏估计但高方差,TD 是有偏估计但低方差。在做实际项目时,低方差往往比无偏更重要——因为你可以通过调小学习率来慢慢消除偏差的影响,但高方差带来的震荡会直接让训练无法收敛。
1.3 一个只需纸笔验证的小实验:随机游走
随机游走(Random Walk)是理解 TD 最经典的测试环境。假设你有五个状态 A、B、C、D、E,左右各有一个终止状态,向左终止奖励 0,向右终止奖励 1。智能体从 C 出发,每一步等概率向左或向右移动一格(处于边界时会被弹回或直接终止)。
如果使用 MC 方法,你需要完整走过一条路径,看到最终是左还是右,才能根据最终奖励来更新路径上所有状态。如果使用 TD(0),你只需要在当前状态 s 走一步到 s’,然后立即执行:
V(s) ← V(s) + α [ r + γV(s') - V(s) ]
这个括号里的内容就是时序差分误差,简称 TD 误差。你可以手动走几个 episode 试试:TD 每一步都会立刻调整价值判断,而 MC 必须等到每个 episode 结束才更新一次。值得注意的是,在随机游走这类情节较短的任务里,TD 通常比 MC 收敛得快很多。这在 Sutton 的《强化学习》第 6 章有详细实验数据,我自己复现时发现 TD 在几百个 episode 内就能把均方误差压到较低水平,而 MC 需要更长的训练。
2. 数学原理与算法族:TD(0)、Sarsa、Q-learning
2.1 TD(0) 的标准更新规则与符号细节
先从状态价值函数 V 开始。TD(0) 的更新公式写全了是这样:
V(s) ← V(s) + α [ R_{t+1} + γ V(S_{t+1}) - V(S_t) ]
- α 是学习率,控制每次更新的步长;
- γ 是折扣因子,控制未来奖励的重要性;
- R_{t+1} 是执行动作后得到的即时奖励;
- V(S_{t+1}) 是下一个状态的当前估计价值;
- 方括号里的整项是 TD 误差 δ。
这里需要特别指出,TD 误差代表了“实际得到的即时奖励 + 对下个状态的估计”与“当前状态原有估计”之间的差距。如果这个差距是正数,说明当前状态的价值被低估了,应该上调;反之则下调。
有人会问:为什么是 R_{t+1} + γV(S_{t+1}),而不是把后面的折扣回报都算完?因为 R_{t+1} + γV(S_{t+1}) 被称为 TD 目标,它混合了一个真实观测(R_{t+1})和一个估计(V(S_{t+1}))。相比 MC 的目标(整条轨迹的折扣总回报),TD 目标使用了估计值,这正是偏差的来源,也是效率的来源。做明白这一点,后面的 DQN 目标网络设计逻辑就顺理成章了。
2.2 三个孪生算法:TD(0)、Sarsa、Q-learning
如果从状态价值函数升级到动作价值函数 Q(s, a),就有两个更常用的算法变体:Sarsa 和 Q-learning。
Sarsa 的更新规则是:
Q(s, a) ← Q(s, a) + α [ r + γ Q(s', a') - Q(s, a) ]
其中的 a' 是实际下一步执行的动作。也就是说,Sarsa 用“接下来实际执行的轨迹”来更新当前的动作价值,因此属于同策略(on-policy)算法。
Q-learning 的更新规则是:
Q(s, a) ← Q(s, a) + α [ r + γ max_{a'} Q(s', a') - Q(s, a) ]
它不看下一步实际会选择什么动作,而是直接取下一个状态里所有动作 Q 值的最大值。也就是说,Q-learning 永远假设后续会走最优路径,因此属于离策略(off-policy)算法。
我把三个算法的差异整理成一张表格,方便你对照记忆:
| 算法 | 更新的目标 | 策略类型 | 是否取最大值 | 典型应用场景 |
|---|---|---|---|---|
| TD(0) | V(s) | 不限 | 否 | 策略评估 |
| Sarsa | Q(s, a) | 同策略 | 否 | 需要谨慎探索的控制任务 |
| Q-learning | Q(s, a) | 离策略 | 是 | 可以激进选择最优动作的控制任务 |
Sarsa 和 Q-learning 在悬崖行走(Cliff Walking)环境上的行为差异非常经典:Sarsa 因为会考虑实际执行的动作,所以会绕着悬崖边沿走,路径更长但更安全;Q-learning 因为总是取最大值,会走悬崖边缘的最短路径,一旦探索时掉下去就得到大惩罚。不是说 Q-learning 不好,而是两种算法对环境随机性的容忍度不同,选择时要看实际任务的容错要求。
2.3 为什么 TD 能收敛:固定点与贝尔曼方程的关系
TD 类算法能收敛并不是偶然,它跟贝尔曼方程有深刻联系。贝尔曼方程告诉我们,最优状态价值 V* 满足:
V*(s) = E[ R_{t+1} + γ V*(S_{t+1}) | S_t = s ]
TD 的更新本质上是沿着采样到的转移逐步逼近这个不动点方程。你可以把 TD 更新想象成在做随机近似(Stochastic Approximation):每次用带噪声的采样目标 R_{t+1} + γV(S_{t+1}) 去替换期望值,然后按学习率 α 缓慢移动。在满足一定条件(学习率满足 Robbins-Monro 条件,即 Σα=∞ 且 Σα²<∞)时,TD 会收敛到唯一的不动点。
在实际工程里你并不需要手动控制学习率去严格满足这些数学条件,但理解这个收敛机制有一个好处:当你发现训练不收敛时,你会先怀疑是不是违反了“学习率过大”或“目标本身在剧烈变化”等收敛前提,而不是盲目调网络结构。
3. TD(λ) 与资格迹:时间尺度上的关键一跃
3.1 n 步回报:从一步到多步的连续光谱
TD(0) 只看一步,MC 看完整条轨迹,那能不能看两步、三步直到 n 步?这就是 n 步回报(n-step return)的概念。n 步回报定义为:
G_t^{(n)} = R_{t+1} + γR_{t+2} + γ²R_{t+3} + ... + γ^{n-1}R_{t+n} + γ^n V(S_{t+n})
当 n=1 时,上式退化回 TD(0) 的目标;当 n 趋向无穷大(走完整条轨迹),上式就变成蒙特卡洛的完整回报。所以 n 是一个调节偏差和方差比重的旋钮:n 越小,自举比重越大,偏差越大、方差越小;n 越大,越接近真实回报,偏差越小、方差越大。
实际任务中 n 的选择没有绝对最优,一般根据任务的奖励稀疏程度判断。奖励密集且环境随机性强时,小 n 更稳;奖励稀疏且环境噪声小时,大 n 能更快把远端奖励传播回来。
3.2 λ 与资格迹:一个优雅的加权机制
TD(λ) 的优雅之处在于,它用 λ 参数把所有 n 步回报加权平均在一起:
G_t^λ = (1-λ) Σ_{n=1}^∞ λ^{n-1} G_t^{(n)}
λ 取 0 时就是 TD(0),取 1 时就等价于 MC。在实际部署时,你不会真去算无数个 n 步回报,而是借助资格迹(Eligibility Trace)在 O(1) 的时间复杂度内近似实现。资格迹的本质是给每个状态(或状态动作对)维护一个“最近是否被频繁访问过”的记忆:
e(s) ← γλ e(s) + 1(若当前状态为 s)
更新价值时,所有状态的更新幅度都乘上自己的资格迹:
V(s) ← V(s) + α δ e(s)
这样做的好处是:一个状态经历过的 TD 误差不仅能修正当前状态,还能沿着资格迹“追溯”到之前刚被访问过的状态。我用一个比喻帮你理解:资格迹就像雪地上留下的脚印,刚走过的地方脚印深,很久之前走过的地方脚印浅。一场“TD 误差”的大雪落下时,脚印深的地方覆盖更多,脚印浅的地方覆盖更少。这样每个被访问过的状态都能按时间远近分摊这次误差,学习效率明显提升。
3.3 资格迹在代码中的两种常见实现
实际代码里有两种实现资格迹的方式。第一种是累积迹(Accumulating Trace),即每次访问状态直接在原有迹上加 1。累积迹简单直观,但长期访问同一个状态时迹会不断增长甚至爆炸,需要额外设置一个上限阈值。
第二种是替换迹(Replacing Trace),访问状态时将迹设回 1,而不是加 1。替换迹避免了迹值无限增大的问题,在不少控制任务里表现得比累积迹更稳定。很多开源强化学习库里默认用的是替换迹,你在看代码时注意识别。
关于资格迹,还有一个容易被忽略的细节:它既可以放在状态价值 V 上,也可以放在动作价值 Q 上。放在动作价值上的算法被称为 Sarsa(λ),它既能快速传播奖励信息,又不丢失动作维度上的决策信息。不过动作维度的加入会让迹表变得很大,在表格场景尚可接受,在函数逼近场景就要注意实现成本了。
4. 从表格到神经网络:当 TD 遇上深度学习
4.1 表格法的天花板与函数逼近的引入
前面讲的所有 TD 推导,在表格表示下都是很干净的:每个状态(或状态动作对)有一个独立的数值槽位,更新就是读写这个槽位。这种表示有一个致命局限——状态空间过大时表格会爆炸。围棋的状态数是 10^170,哪怕只记录一亿分之一也远超任何存储介质承受能力。连续状态空间(比如机械臂关节角度、车辆速度)则连穷举都做不到。
解决思路是引入函数逼近:用一个参数化函数(比如神经网络)来拟合价值函数。状态作为输入进入网络,输出该状态的价值估计。这样我们不再为每个状态单独保存一个数字,而是用一组共享的权重参数去泛化学习。
4.2 TD 目标与神经网络结合时的收敛陷阱
直接把 TD 公式套到神经网络上,你会立刻遇到一个尴尬的数学问题:TD 目标 R_{t+1} + γV(S_{t+1}) 中 V(S_{t+1}) 本身也是同一个神经网络算出来的。如果更新网络权重来逼近这个目标,目标本身又会随着网络变动而变动。这就像在追逐一个会移动的靶子:训练初期网络估计不准确,目标也很不准确,两个不准确的东西互相更新,网络参数很容易发散或震荡。
这个问题在表格法中并不致命(每个槽位独立更新),但在函数逼近中被放大了。深度学习领域本来就要求训练目标相对稳定,否则梯度下降会失去方向。
解决办法中最经典的就是目标网络(Target Network)技巧:单独复制一份网络参数,一段时期内固定不变,专门用于计算 TD 目标;每隔若干个训练步再把在线网络的参数同步过去。这样一来,在一段时期内 TD 目标基本稳定,在线网络才有一个固定靶子可以追赶。
4.3 DQN 中的 TD 架构与经验回放的配合
DQN 就是“TD + 卷积神经网络 + 经验回放 + 目标网络”的组合体。训练时智能体会把转移样本 (s, a, r, s') 存入回放缓冲区,每次从缓冲区随机抽样一个小批量来更新网络。这么做有两大好处:一是打破样本之间的时间相关性,避免连续样本导致梯度更新过分偏向某一局部区域;二是实现样本复用,提高数据利用效率。
DQN 的损失函数写出来是这样:
L = E[( r + γ max_{a'} Q_{target}(s', a') - Q_{online}(s, a) )²]
你会发现方括号里的结构就是 TD 误差的平方。这个损失函数的样本版实现,有一个极其容易踩的技术坑:计算 TD 目标时,你必须在 r + γ max Q(s', a') 这一整项上调用 detach()(PyTorch)或 stop_gradient(TensorFlow),把这个值当作不参与梯度计算的常量。否则反向传播时梯度会顺着 TD 目标流回网络,导致训练目标随网络同步自更新,数值极易发散。
我在实际项目中见过这种情况:Loss 数值看起来在下降,但策略完全不动,甚至评估时越走越差。排查半天发现是忘加 detach。这个 bug 在代码层面只差一个函数调用,但原理层面是对 TD 自举机制理解不透彻的直接反映。
5. 用 TD 学习实现一个走迷宫智能体:从公式到可运行代码
5.1 环境定义与奖励设置
理论讲再多,不如动手写一个能跑的智能体。我选一个 4×4 的网格迷宫作为实验环境。迷宫布局如下,S 为起点,G 为终点,X 为墙:
S . . X . X . . . . X . . . . G规则如下:智能体从 S 出发,每一步可以在上下左右四个方向中选择一个移动;撞墙时原地不动,但要消耗一步的惩罚(奖励 -1);到达终点 G 获得奖励 +10;除此之外每一步奖励为 0。折扣因子 γ 设为 0.99。
用 TD 学习中的 Q-learning 来解决这个任务。为什么选 Q-learning?因为它是离策略算法,可以使用 ε-greedy 探索策略去采数据,同时不断更新对最优动作价值的估计,在小规模离散任务上收敛又快又稳。
5.2 核心代码与参数讲解
环境部分我用纯 Python 实现,不依赖额外游戏库。完整代码如下:
import numpy as np import random class GridWorld: def __init__(self): self.grid = [ ['S', '.', '.', 'X'], ['.', 'X', '.', '.'], ['.', '.', 'X', '.'], ['.', '.', '.', 'G'], ] self.height = 4 self.width = 4 self.start = (0, 0) self.goal = (3, 3) self.walls = [(0, 3), (1, 1), (2, 2)] self.actions = [(0, -1), (0, 1), (-1, 0), (1, 0)] # 左 右 上 下 self.reset() def reset(self): self.pos = self.start return self.pos def step(self, action_idx): dx, dy = self.actions[action_idx] new_pos = (self.pos[0] + dx, self.pos[1] + dy) # 超出边界或撞墙,原地不动 if new_pos[0] < 0 or new_pos[0] >= self.height or \ new_pos[1] < 0 or new_pos[1] >= self.width or \ new_pos in self.walls: new_pos = self.pos reward = -1.0 else: self.pos = new_pos reward = 0.0 done = (self.pos == self.goal) if done: reward = 10.0 return self.pos, reward, done class QLearningAgent: def __init__(self, world, alpha=0.1, gamma=0.99, epsilon=0.2): self.world = world self.alpha = alpha self.gamma = gamma self.epsilon = epsilon self.q_table = {} def get_q(self, state, action): return self.q_table.get((state, action), 0.0) def choose_action(self, state): if random.random() < self.epsilon: return random.randint(0, 3) values = [self.get_q(state, a) for a in range(4)] return int(np.argmax(values)) def update(self, state, action, reward, next_state, done): best_next = max([self.get_q(next_state, a) for a in range(4)]) if not done else 0.0 td_target = reward + self.gamma * best_next td_error = td_target - self.get_q(state, action) new_q = self.get_q(state, action) + self.alpha * td_error self.q_table[(state, action)] = new_q def train(episodes=300): world = GridWorld() agent = QLearningAgent(world) step_records = [] for ep in range(episodes): state = world.reset() done = False steps = 0 while not done and steps < 100: action = agent.choose_action(state) next_state, reward, done = world.step(action) agent.update(state, action, reward, next_state, done) state = next_state steps += 1 if steps >= 100: break step_records.append(steps) return agent, step_records代码里的超参数我解释一下。α=0.1 是常见的学习率起点,既能吸收新信息又不会因为一步极端误差而剧烈震荡;γ=0.99 表示智能体重视长期收益——在迷宫任务里合理的,因为相比于即时惩罚,找到终点的大奖励更重要;ε=0.2 表示有 20% 概率随机探索、80% 概率贪心利用。
值得注意的是,我没有单独设置“撞墙扣分”之外的步数惩罚。这意味着智能体如果不加探索地乱走,也不会因为每步消耗而受到额外惩罚。这是刻意的:我想让智能体纯粹依靠终点奖励信号来学会找路。
5.3 运行结果与可视化观察
训练 300 个 episode 后,把每个 episode 的步数打印成表格,你会看到明显的学习趋势:
| 训练区间(episode) | 平均步数 |
|---|---|
| 1 - 30 | 35.2 |
| 31 - 60 | 21.8 |
| 61 - 90 | 9.4 |
| 91 - 150 | 6.1 |
| 151 - 300 | 4.2 |
一开始智能体毫无头绪地瞎走,平均步数很高,甚至可能超过 100 步(直到步数上限被截断)。大约 80 个 episode 后,已经能稳定用接近最短路径的方式到达终点。最短路径长度是 6 步:从 (0,0) 向右到 (0,1),向下到 (1,1) 右侧即 (1,2),向下到 (2,2) 右侧即 (2,3),向左到 (3,3)。实际上 Q-learning 学到的路径会根据探索情况略有不同,但最终会趋向于最短路径。
我建议你训练完打印一下 Q 表里每个状态各动作的最大值对应方向,手动从起点沿着最优动作走一遍,确认每步都是合理的。这一步听上去简单,却是验证 TD 学习是否真正收敛的最直接手段,比看曲线可靠得多。
6. 实战中常见的问题与踩坑经验
6.1 学习率和折扣因子的选择不是拍脑袋
很多人第一次跑 TD 代码,喜欢把 α 调到 0.5 甚至 1.0,觉得学得快。实际效果往往相反:α 过大会导致价值函数剧烈震荡,尤其在环境随机性强时,最后几条轨迹的偶然性会被放大。α 过小则收敛缓慢。
我的建议是从 0.1 开始,观察学习曲线的下降趋势;如果曲线下降后又有明显反弹,就把 α 调小;如果曲线下降太慢,就适当调大。折扣因子 γ 的设置取决于任务的时间尺度:短任务(几步决策)可以用 0.9 甚至 0.95;长任务(需要多步决策才能看到回报)建议 0.99 以上。γ 太小会让远端奖励被指数级稀释,智能体就会变得短视。
6.2 ε-greedy 探索策略的衰减
固定 ε=0.2 在简单任务里没问题,但复杂任务里固定探索率会有两个麻烦:训练早期探索不足,很多状态根本没被访问过,价值函数一片空白;训练后期探索过多,已经很成熟的策略还频繁随机乱走,导致收益曲线震荡不降。
工程上比较成熟的做法是 ε 衰减:初始 ε 设为 0.9 或 1.0,随着训练步数线性或指数地衰减到 0.01。衰减速率需要根据任务规模评估:一般从一个任务的总训练步数倒推,保证在训练中期 ε 已经降到较低水平。
6.3 稀疏奖励下 TD 仍然学得很慢,如何缓解
TD 能即时更新,但如果整个环境只有终点有非零奖励,奖励信号依然要“逐步回溯”才能传播到起点附近。在一个 20×20 的迷宫里,终点奖励要经历约 40 次状态转移才能影响到起点状态,这意味着需要大量 episode 才能把有效梯度传回去。
缓解这个问题的常见做法是奖励塑形(Reward Shaping),给智能体一些与任务目标渐进相关的辅助奖励,例如每靠近终点一步给一个小正奖励。但奖励塑形是一把双刃剑,过强的塑形奖励会让智能体学会“原地转圈刷分”而不是奔向终点。我在项目中吃过亏:给了一个“向目标方向移动加分”的简单奖励,结果智能体学会来回蹭墙赚分,再也不愿意走完整路径了。
6.4 一个极隐蔽的 bug:TD 目标忘记 detach
最后必须再强调一次 detach 问题。在使用深度学习框架实现 DQN 或 TD 类算法时,TD 目标 R + γV(s') 这一项必须从计算图中分离。
如果忘记 detach,训练过程会非常诡异:损失值会持续下降到很小,但智能体行为毫无改善,甚至越学越差。原因是网络的梯度中混入了“通过 TD 目标反向传播”的路径。此时的优化问题不再是拟合一个固定目标,而是一个不断被自己拉动的目标——你的损失函数在同时优化两个方向,形成了一种内部抵消。我见过不少拿现成代码跑实验的人遇到这种问题,他们以为是超参数没调好,实际上就是少了这一个函数调用。
6.5 表格法代码中的类型陷阱
在使用 Python 字典实现 Q 表时,有一个非常隐蔽的坑:如果状态用元组 (x, y) 表示,直接作为字典键没问题;但如果某个库返回的状态是列表 [x, y],直接当键用就会报 TypeError。更麻烦的是,有些环境下状态是浮点数,浮点比较时细微误差会导致相同的逻辑状态被当成不同键,Q 表疯狂膨胀却学不到东西。解决办法是统一在状态进入 Q 表前做规范化处理,比如全部转成元组、整数或离散化。
这个坑听起来很基础,但真在调代码时遇到,很容易让人一头扎进算法细节排查半天,最后发现是数据结构问题。教训是:先保证状态表示的唯一性和稳定性,再谈算法收敛。
6.6 观察学习曲线时容易犯的误判
最后一个实战提醒,关于训练曲线。TD 算法的学习曲线(比如每 episode 的总奖励或步数)天然带有剧烈噪声,尤其是 ε-greedy 探索策略存在时。我见过有人看到曲线上下跳动就急着调参,结果越调越差。
正确做法是画滑动平均曲线,或者每隔 10 个 episode 做一次评估(关闭探索,只按贪心策略跑几次取平均)。评估曲线的趋势比单次结果更可靠。如果滑动平均曲线持续下降或持续不降,再动手调参。这点在表格法与深度强化学习中都适用,属于通用的实践习惯。
TD 学习的实用边界比我描述的要广得多,从经典表格算法到现代深度强化学习,它都是地基。把每一步更新的“为什么”想明白,调试代码时的效率至少翻倍。以上这些经验,都是我在实际写代码、调模型、跑实验过程中一点点积攒的,希望对你有帮助。