稀疏奖励下强化学习如何破局:HER事后经验重放原理与DDPG/TD3工程实践 刚开始做机器人抓取这类任务时我踩过最疼的一个坑就是训练了几百万步策略网络还是一副原地罚站的样子。环境里明明有目标agent 却完全不知道该往哪走因为整个 episode 结束之前reward 永远是 0没有任何中间信号告诉它你离得近了一点。后来我去翻了 hindsight 相关的资料才发现这个方向的思路不是在奖励函数上继续加调料而是直接改变目标的定义——把失败轨迹重新解释成成功样本。这篇文章就围绕 hindsight 的核心思想也就是 HERHindsight Experience Replay把我从理论到落地踩过的坎、试过的配置、还有我认为最容易翻车的地方完整梳理一遍。1. 稀疏奖励是强化学习落地中的第一道坎hindsight 正好从这里切入先聊聊我当时为什么会碰上一个叫hindsight的东西。强化学习在仿真环境里看起来无所不能但一放到真实任务上最先暴露的问题往往不是模型容量不够而是reward 太稀疏。拿机械臂抓取来说真实世界里你需要控制七八个关节目标是让夹爪到达某个三维坐标并成功闭合。这个过程里你很难设计出一个每一步都合理的奖励函数。最常见也是最省事的做法就是成功抓到给 1其他情况给 0。可这样一来agent 在初期几乎完全随机探索几百步里能恰好蒙到一次成功就算运气爆棚而这段时间里它什么有用信号都收不到。传统上有三条路来解决这种稀疏奖励问题奖励塑形Reward Shaping给每一步都设计一个渐进奖励比如离目标越近分数越高。听起来美好但工程上非常容易引入局部最优而且很多任务你根本不知道什么样的中间状态才算更好。课程学习Curriculum Learning从简单任务开始练然后再逐步提高难度。问题在于简单任务到复杂任务的过渡策略很难设计而且每个任务阶段都要手动调参数工作量非常大。模仿学习和行为克隆依赖人类专家演示数据标数据成本高而且一旦策略偏离演示分布模型很容易崩溃。这三条路我都试过怎么说呢能用但都很脆。直到我理解了hindsight 这个视角它把失败变成了一种可以利用的数据。虽然没有达成我原来想要的目标但我确实达到了某个别的状态。那能不能把这个状态当作新的目标重新构造一条成功轨迹这就是事后重新解释也是 HER 的核心。HER 的全称是 Hindsight Experience Replay最早是在 2017 年由 OpenAI 的研究团队提出来的主要用在多目标强化学习Multi-Goal RL场景。它的出发点特别朴素一个失败的 episode 里往往包含大量已经发生过的真实达成的目标只要把目标换掉这条轨迹立刻从全零奖励变成有成功信号的成功样本。对于任何一个被稀疏奖励折磨过的人来说这种思路几乎是一瞬间就能理解的但它真正要落地还要解决一连串问题比如目标怎么重标、经验怎么存、和哪种算法搭配、训练稳定性怎么保证。下面我一个个细讲。2. HER 的核心机制用事后视角重标目标把失败轨迹改写成学习样本2.1 先把多目标 Markov 决策过程理清楚理解 HER 之前必须要先接受一个设定我们不是在训练一个完成单一任务的 agent而是在训练一个能完成一堆目标中任意一个的 agent。用数学一点的语言说我们面对的是一个带目标条件的马尔可夫决策过程。状态空间是 S动作空间是 A目标空间是 G。在每个 episode 开始的时候环境会采样一个目标 gagent 的策略 π(a | s, g) 要根据当前状态和目标共同决定动作。奖励函数也不再是固定的 r(s, a)而是 r(s, a, g)——同样是碰到一个位置目标不同奖励不同。这个设定非常关键因为 HER 的本质就是在同一个状态转移数据上通过替换 g生成不同的奖励信号。举个例子假设机械臂从初始位置出发最终停在坐标 (0.3, 0.2, 0.1)而你给它设定的原始目标在 (0.8, 0.5, 0.4)。用原始目标来评判这条轨迹是失败的所有 step 的奖励都是 0。但是HER 会做一件事把目标 g 从 (0.8, 0.5, 0.4) 换成最终状态 (0.3, 0.2, 0.1)。这样一来这条轨迹上的最后一步就变成了成功奖励为 1前面几步也变成了接近目标的状态。于是一条原本没有任何学习价值的轨迹变成了有成功、有渐进信号的优质样本。2.2 事后经验的四种重标策略HER 在具体实现时并不是机械地只把目标换成 episode 的最终状态。论文里给出了四种常见的重标策略我在实际项目里也都试过策略做法我的评价final把目标替换成轨迹最后一个状态最简单但信息量少只提供一个成功样本future从当前时间步之后的某个状态中随机选一个作为新目标最常用信息密度高同一轨迹能产生多个成功样本episode从整条轨迹的任意状态中选一个作为新目标会产生一些过去状态的目标时间一致性略差random从经验池里随机选一个状态作为新目标数据多样性好但目标可能和轨迹无关噪声比较大我个人的经验是future 策略的性价比最高。原因很直观在时间步 t 时agent 已经往某个方向走了一段路之后它又继续走了一段。如果拿之后某个时刻的状态当目标那么从 t 到那个时刻之间的状态转移都天然地构成了逐渐接近目标的过程奖励曲线是渐进向上的非常适合训练价值网络。2.3 重标不会破坏数据分布吗这里有个很自然的疑问你改了目标那原来的状态转移还有效吗策略 π(a | s, g) 是依赖目标的如果目标变了同一个动作还会是合理的吗答案是不一定合理但没关系。因为 HER 不是让 agent 学着在这个状态下为了曾经那个目标要这么动而是让它学到在这个状态下为了这个新目标这条转移路径是可以成功的。它本质上是拓宽了策略的可用数据覆盖范围。原来一条轨迹只能教 agent 一种行为重标之后它可以教 agent 多种状态-目标组合下的行为。这也解释了为什么 HER 能显著提升样本效率一份环境交互数据被多次使用每一份都注入了额外的学习信号。在真实机器人上每多采一条轨迹都要花真金白银的场景里这种数据复用度的提升是极其宝贵的。3. HER 搭配哪种算法最合适我为什么选择 off-policy 的 DDPG 而不是 PPO一开始我以为 HER 只是个目标重标工具应该什么算法都能接。后来实测下来发现算法选型直接决定 HER 能不能发挥出效果这里面的核心约束是off-policy。3.1 为什么 off-policy 是硬性要求HER 的操作流程是先跑一个完整的 episode把它存进 replay buffer然后取出这条轨迹重标目标再拿去更新网络。这意味着数据产生时用的策略和更新时的策略已经不一样了——这正是 off-policy 学习的前提。如果用的是 PPO 这类 on-policy 算法每个 step 的数据用完就丢不允许从历史 buffer 里取额外的样本反复更新那么 HER 根本没有施展空间。你最多只能做到用重标后的轨迹再跑一次重要性采样但那样方差会非常大稳定性很差。所以我最终选择的是DDPGDeep Deterministic Policy Gradient以及它的改进版 TD3。DDPG 本身就是一个标准 off-policy 算法用 replay buffer 存储历史转移再用一个确定性策略网络输出动作配合一个 Q 网络评估动作价值。它天然地适配一条轨迹被反复使用这种玩法。3.2 DDPG 里和 HER 相关的几个关键组件DDPG 的完整管线包括四套网络actor 当前网络、actor 目标网络、critic 当前网络、critic 目标网络。在加 HER 之前我先把 DDPG 本身跑稳定然后才在它的 buffer 层动手脚。HER 对 DDPG 的重写发生在两个地方数据的存储粒度普通 DDPG 存的是 (s, a, r, s) 四元组HER 版本需要存的是 (s, a, r, s, g)也就是除了状态动作之外还要记录这个轨迹背后的目标。更进一步为了做 future 重标我们不能在每个 step 立刻丢进 buffer而是先缓存整条 trajectory等 episode 结束后统一处理。采样的扩展从 buffer 里采一个 batch 时每个样本都带着它的原始目标。我们会按一定比例比如一半一半混合原始目标和重标目标。原始目标保证策略不偏离原任务重标目标提供额外的学习信号。3.3 TD3 比 DDPG 更稳我在实际跑 FetchReach、FetchPush 这类环境时直接裸 DDPG 有时候会出现 Q 值过高估计的问题表现为训练中期 critic loss 不降、actor 开始抖。后来我把 DDPG 换成 TD3主要加了两个东西双 critic 网络取最小值两个 Q 网络独立估计更新目标时用两者中的较小值降低过估计风险。目标策略平滑给目标动作加上一个小的噪声相当于对 Q 函数做正则化防止策略陷入尖峰。TD3 加 HER 的组合在标准 benchmark 上基本是默认选项。如果某个项目里你看到有人用HER SAC那也是合理的SAC 是 stochastic 版本的 off-policy 算法同样能吃 HER 这碗饭。只是 SAC 的熵调节项在部分稀疏奖励任务里会让探索方向太散我个人还是喜欢 TD3 那种偏保守的风格。4. 动手实现 HER一个可运行的伪代码级流程与参数配置理论知识讲完直接上实操。因为手头没有真实机械臂我用 OpenAI Gym 的 Fetch 系列环境做验证特别是FetchReach-v1和FetchPush-v1一个简单一个难正好能看出 HER 的增益幅度。我先把整体流程拆成三步收集轨迹 → 重标目标 → 更新网络。下面是一段核心伪代码基本还原了我代码仓库里 HER 缓冲区的逻辑去掉了一些工程细节只保留主干。import numpy as np class HindsightReplayBuffer: def __init__(self, capacity, replay_strategyfuture, replay_k4): self.buffer deque(maxlencapacity) self.replay_strategy replay_strategy self.replay_k replay_k def store_episode(self, episode): # episode 里包含每个 step 的 obs, action, reward, next_obs 以及原目标 goal # 先按原始目标存一份 for trans in episode.transitions: self.buffer.append(( trans.obs, trans.action, trans.reward, trans.next_obs, trans.goal )) # 再按 replayed 目标存 K 份 for _ in range(self.replay_k): replayed_goal self._sample_replayed_goal(episode) for trans in episode.transitions: new_reward compute_reward(trans.next_obs, replayed_goal) self.buffer.append(( trans.obs, trans.action, new_reward, trans.next_obs, replayed_goal )) def _sample_replayed_goal(self, episode): if self.replay_strategy final: return episode.final_goal # 最后一个状态的 goal 表示 elif self.replay_strategy future: # 随机选一个时间步 t再随机选 t 之后的一个状态做目标 t np.random.randint(len(episode.transitions)) future_idx np.random.randint(t, len(episode.transitions)) return episode.transitions[future_idx].next_obs[achieved_goal] elif self.replay_strategy episode: idx np.random.randint(len(episode.transitions)) return episode.transitions[idx].next_obs[achieved_goal] elif self.replay_strategy random: return random_goal_from_buffer(self.buffer)4.1 采样比例与 buffer 大小的经验值HER 不是把每一条轨迹都全部替换成重标目标那样 agent 会忘掉原始任务。通常的做法是原始目标 for 每条轨迹的重标副本。我项目里的标准配置是replay_k 4每条原始轨迹额外生成 4 条重标轨迹。buffer 容量1e6Fetch 系列环境的 state 维度不高1e6 基本够用太大了反而让新数据占比太低收敛变慢。每次采样时原始目标样本和重标目标样本各占一半或者让重标样本略多一点比如 0.6/0.4。这个比例我试过从 0.2 到 0.8 区间内的多个值发现 0.5~0.6 之间差别不大但如果重标比例太高训练后期会出现agent 在原始任务上精炼度不足的问题表现为成功率曲线到 90% 附近就开始震荡上不去。4.2 网络结构与奖励函数的坑Fetch 环境的观测包含三部分observation机械臂关节状态、desired_goal期望目标、achieved_goal实际达到的位置。在构造 state 时不能只用observation必须把observation和desired_goal拼接起来作为网络输入。这一点我一开始漏了结果 agent 接收到的全是等价于无目标的状态训练了很久成功率都是 0。奖励函数方面Fetch 系列默认用的是距离阈值判断如果 achieve 和 goal 的欧氏距离小于某个阈值奖励为 0成功否则为 -1。注意这里不是 0/1而是 -1/0HER 之所以在这种奖励下有效是因为它能把大量 -1 的轨迹变成部分 0 奖励的轨迹相当于把一张全黑图片上逐块点亮value network 终于有梯度可以传了。4.3 一个完整的训练循环骨架再贴一段训练主循环方便你对照着搭自己的项目for episode_idx in range(total_episodes): obs env.reset() episode_transitions [] goal obs[desired_goal] while True: action actor.select_action(obs) # 加上探索噪声 next_obs, reward, done, info env.step(action) episode_transitions.append(Transition(obs, action, reward, next_obs, goal)) obs next_obs if done: break # 存原始轨迹 重标轨迹 replay_buffer.store_episode(episode_transitions) # 更新网络 if len(replay_buffer) batch_size: for _ in range(update_steps): batch replay_buffer.sample(batch_size) td3_update(batch, actor, critic, target_actor, target_critic)有个细节要注意Fetch 环境的done并不是靠成功来触发的而是跑满最大步数。所以即便某条轨迹成功了done也可能是 False。好在我们的目标是在整个 episode 结束后做重标这并不影响逻辑只是别把done当成任务成功的标志。5. 踩坑记录重标目标别把未来的自己变作弊器以及这几个边界问题HER 看起来简单实现起来坑不少。有些坑是代码 bug有些是理解偏差我把最典型的五个列出来每个都是我实际撞过墙的地方。5.1 future 策略里的时间方向不能乱future 重标的意思是拿当前时刻之后的状态作为新的目标。如果代码里不小心把np.random.randint(t, len(episode))写成了np.random.randint(0, t)就是拿过去的某个状态当目标这会出大问题。为什么因为一条轨迹本身就包含了从过去到现在的状态演化如果你把一个已经走过的状态当作未来目标那么从它之后的所有转移都变成了背离目标的动作重标出来的奖励会是一连串的 -1反而污染数据。我在代码 review 时盯过这个问题好几次它不会报错但训练曲线会莫名变差排查起来很费劲。5.2 目标空间和状态空间不一定同构HER 论文里有个隐含假设achieved_goal 和 desired_goal 来自同一个空间可以直接替换。但真实任务里这个假设经常不成立。比如对话系统里目标是完成用户意图实际达成的是生成了某句话两者就不在一个向量空间里不能简单替换。如果遇到这种情况我的做法是在重标之前训练一个状态到目标的隐空间映射把所有观测映射到一个统一的目标向量空间再做替换。这会引入额外开销但总比硬替换导致训练发散强。5.3 奖励阈值和距离度量的匹配Fetch 环境的成功判断用的是欧氏距离阈值而我在写compute_reward时一开始直接用了 squared L2 距离导致阈值行为完全变了原本该判成功的样本被判失败。重标后的奖励计算必须和原始环境的成功判定完全一致否则学出来的策略和真实评估标准会对不上。顺带提一个技巧如果你要自定义任务最好把成功判定封装成一个独立函数HER 重标和环境之外都调用同一个函数避免两边逻辑漂移。5.4 重标轨迹的优先级权重问题我一度把所有重标轨迹都当成和原始轨迹同等重要的样本结果发现训练早期 Q 值会偏高因为成功样本里混了很多实际上并不容易达成的目标。后来我引入了简单的优先经验回放给重标轨迹里的成功转移一个小的优先级加成但不要加成太大。重标数据是补充信号不是主角让原始目标数据保持稳定的存在感。5.5 网络更新步数要匹配数据收集速度HER 让每条轨迹被重复使用但如果你每个 episode 之后更新网络的步数太多很快整个 buffer 里的样本都会被反复学习出现过拟合表现为训练中后期 loss 不再下降但成功率也不涨。我的做法是每个 episode 后更新 40~50 步而不是一口气更新几万步。总的来说更新的节奏要跟数据流的速度保持匹配这不是 HER 独有的问题但在 HER 场景下更明显因为单条数据的信息量被放大了。6. 我更愿意把它当作归因工具而不是花招对基线和业务落地的思考最后聊一点认识层面的东西。HER 之所以吸引我不只是因为它能提升 benchmark 上的成功率而是它提供了一种非常自然的归因视角一个 agent 虽然没有达到你指定的目标但它确实达成了一些别的东西。放在真实业务里这种事后重新解释的思路其实到处都能用。比如推荐系统里用户没有点击我们推荐的商品但他点击了另一个推荐位上的商品。站在原始目标引导点击某商品的角度这是一次失败但如果把目标换成让用户点击任意感兴趣的商品这条行为轨迹立刻变成了正样本。这本质上就是 HER 在非 RL 场景下的精神延续。又比如机器人技能学习里抓取任务失败但机械臂把物体推到了另一个位置这个新位置也许正是下一个技能需要的起始状态。把任务链拆开每个失败动作都可能成为另一个环节的输入。不过也要泼一盆冷水HER 不是银弹。它对那种目标空间连续、容易定义成功判定的任务效果最好比如机械臂控制、导航、操控但如果是抽象目标、语义目标或者目标空间和状态空间差异很大的任务HER 的收益就会明显下降你需要在目标表示上做额外的设计。根据我的个人经验HER 和自动课程学习搭配是更值得关注的方向让 agent 在训练过程中自动生成当前能力边界附近的目标然后用 HER 快速吸收这些边界内的成功经验相当于给 agent 划了一条始终贴着能力边缘的学习路径。我最近在几个任务里用这个思路做实验比单纯跑 HER 收敛速度快了不少等数据再扎实一点我再单独写一篇展开。说到底hindsight 这个名字起得真好。它提醒我们强化学习很多时候并不是在跟环境对抗而是在跟最初的意图较劲。失败的数据摆在那里关键是你有没有能力换一个视角把它们重新利用起来。这个思路不管最后用不用 HER 这个具体的算法都值得每个做 RL 的人认真想一遍。