ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

时序差分学习:从TD(0)到DQN的核心原理与实战指南

时序差分学习:从TD(0)到DQN的核心原理与实战指南 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-learning2.1 TD(0) 的标准更新规则与符号细节先从状态价值函数 V 开始。TD(0) 的更新公式写全了是这样V(s) ← V(s) α [ R_{t1} γ V(S_{t1}) - V(S_t) ]α 是学习率控制每次更新的步长γ 是折扣因子控制未来奖励的重要性R_{t1} 是执行动作后得到的即时奖励V(S_{t1}) 是下一个状态的当前估计价值方括号里的整项是 TD 误差 δ。这里需要特别指出TD 误差代表了“实际得到的即时奖励 对下个状态的估计”与“当前状态原有估计”之间的差距。如果这个差距是正数说明当前状态的价值被低估了应该上调反之则下调。有人会问为什么是 R_{t1} γV(S_{t1})而不是把后面的折扣回报都算完因为 R_{t1} γV(S_{t1}) 被称为 TD 目标它混合了一个真实观测R_{t1}和一个估计V(S_{t1})。相比 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)不限否策略评估SarsaQ(s, a)同策略否需要谨慎探索的控制任务Q-learningQ(s, a)离策略是可以激进选择最优动作的控制任务Sarsa 和 Q-learning 在悬崖行走Cliff Walking环境上的行为差异非常经典Sarsa 因为会考虑实际执行的动作所以会绕着悬崖边沿走路径更长但更安全Q-learning 因为总是取最大值会走悬崖边缘的最短路径一旦探索时掉下去就得到大惩罚。不是说 Q-learning 不好而是两种算法对环境随机性的容忍度不同选择时要看实际任务的容错要求。2.3 为什么 TD 能收敛固定点与贝尔曼方程的关系TD 类算法能收敛并不是偶然它跟贝尔曼方程有深刻联系。贝尔曼方程告诉我们最优状态价值 V* 满足V*(s) E[ R_{t1} γ V*(S_{t1}) | S_t s ]TD 的更新本质上是沿着采样到的转移逐步逼近这个不动点方程。你可以把 TD 更新想象成在做随机近似Stochastic Approximation每次用带噪声的采样目标 R_{t1} γV(S_{t1}) 去替换期望值然后按学习率 α 缓慢移动。在满足一定条件学习率满足 Robbins-Monro 条件即 Σα∞ 且 Σα²∞时TD 会收敛到唯一的不动点。在实际工程里你并不需要手动控制学习率去严格满足这些数学条件但理解这个收敛机制有一个好处当你发现训练不收敛时你会先怀疑是不是违反了“学习率过大”或“目标本身在剧烈变化”等收敛前提而不是盲目调网络结构。3. TD(λ) 与资格迹时间尺度上的关键一跃3.1 n 步回报从一步到多步的连续光谱TD(0) 只看一步MC 看完整条轨迹那能不能看两步、三步直到 n 步这就是 n 步回报n-step return的概念。n 步回报定义为G_t^{(n)} R_{t1} γR_{t2} γ²R_{t3} ... γ^{n-1}R_{tn} γ^n V(S_{tn})当 n1 时上式退化回 TD(0) 的目标当 n 趋向无穷大走完整条轨迹上式就变成蒙特卡洛的完整回报。所以 n 是一个调节偏差和方差比重的旋钮n 越小自举比重越大偏差越大、方差越小n 越大越接近真实回报偏差越小、方差越大。实际任务中 n 的选择没有绝对最优一般根据任务的奖励稀疏程度判断。奖励密集且环境随机性强时小 n 更稳奖励稀疏且环境噪声小时大 n 能更快把远端奖励传播回来。3.2 λ 与资格迹一个优雅的加权机制TD(λ) 的优雅之处在于它用 λ 参数把所有 n 步回报加权平均在一起G_t^λ (1-λ) Σ_{n1}^∞ λ^{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_{t1} γV(S_{t1}) 中 V(S_{t1}) 本身也是同一个神经网络算出来的。如果更新网络权重来逼近这个目标目标本身又会随着网络变动而变动。这就像在追逐一个会移动的靶子训练初期网络估计不准确目标也很不准确两个不准确的东西互相更新网络参数很容易发散或震荡。这个问题在表格法中并不致命每个槽位独立更新但在函数逼近中被放大了。深度学习领域本来就要求训练目标相对稳定否则梯度下降会失去方向。解决办法中最经典的就是目标网络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_gradientTensorFlow把这个值当作不参与梯度计算的常量。否则反向传播时梯度会顺着 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, alpha0.1, gamma0.99, epsilon0.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(episodes300): 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 - 3035.231 - 6021.861 - 909.491 - 1506.1151 - 3004.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 一个极隐蔽的 bugTD 目标忘记 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 学习的实用边界比我描述的要广得多从经典表格算法到现代深度强化学习它都是地基。把每一步更新的“为什么”想明白调试代码时的效率至少翻倍。以上这些经验都是我在实际写代码、调模型、跑实验过程中一点点积攒的希望对你有帮助。
返回列表