
第一次看到 hindsight 这个词是在 2017 年 OpenAI 那篇《Hindsight Experience Replay》的论文标题里。当时第一反应是“事后诸葛亮强化学习也能用这招”后来真正在机器人控制任务里跑通以后我才明白这个词选得有多准——它解决的是强化学习里最让人头疼的稀疏奖励问题当智能体怎么试都尝不到成功甜头的时候与其把失败轨迹一股脑扔掉不如“事后”给这些轨迹换个目标让它们从看似失败的样本中学到真东西。这篇文章我不会只复述论文而是会把我复现 HER 的思路、代码细节、调参经验和踩过的坑完整捋一遍。适合的人群是已经跑过 DQN 或 DDPG想解决稀疏奖励问题却不想一上来就上复杂分层强化学习的同学。你不需要有很强的数学底子只要能理解“目标状态”这个概念就足够读完并复现整个算法。1. 为什么需要 hindsight稀疏奖励下的学习困境1.1 一个让算法崩溃的经典场景假设你让一个机械臂学会推桌子上的方块目标是把方块推到某个红色圆点上。如果方块没有到达圆点奖励为 0只有到达了奖励才为 1。这就是一个典型的稀疏奖励任务。问题来了机械臂的动作空间是连续的包含位置变化、角度变化等至少 4 到 7 个维度。在没有任何引导信号的前提下随机探索恰好把方块推到那个小圆点的概率低到可以忽略。你会发现训练了几十万步累计奖励始终是 0Q 函数给出的估值也在原地打转。用生活类比来解释这相当于让一个孩子做一道完全超纲的数学题只告诉他“对”或“错”却不给任何步骤分。孩子试了无数次都是“错”他没有任何信息来调整思路自然永远学不会。传统强化学习算法遇到这种情况基本只有两条路一是精心设计密集奖励函数比如“离目标越近奖励越大”但这需要大量领域知识还容易让智能体学会钻奖励的漏洞二是靠人工演示通过模仿学习先让智能体有个基本策略代价是需要收集专家数据。这两条路都太贵了HER 就是冲着第三条路来的。1.2 失败样本里的隐藏信息我们换个角度想那条“没推到位”的轨迹真的毫无价值吗不是的。轨迹里记录了完整的“状态-动作-下一状态”序列只是因为我们给它设定的奖励信号全是 0所以算法读不出任何梯度。如果只关心“目标在红点上”这个单一目标那这条轨迹确实没有正反馈。但如果我们把视角放宽承认“目标的定义是我们可以随时修改的”情况就完全不同了。比如这条轨迹实际上把方块推到了蓝点上那么我们完全可以把这次经验重新标记为“目标蓝点成功”变成一个 reward1 的完美回合。这就是 hindsight 的核心直觉失败不是没有价值而是你缺少发现其中价值的视角。就像你出门想买苹果结果买到了橘子如果你坚持“橘子不是苹果”就一无所得但如果你把目标改成“买水果”这次的经历就是一次成功采购。1.3 算法的灵感来源重新定义目标英文里 hindsight 直译是“后见之明”“事后聪明”听起来像贬义词但在强化学习里恰好是个非常有用的操作。既然这次尝试没能实现原定目标那我就“事后诸葛亮”地把实际达到的状态当作新目标然后告诉算法你看我们有了一条成功经验。这不是自欺欺人的心理安慰而是有算法支撑的在多目标强化学习框架里策略本身是同时以“状态”和“目标”为输入的。同一个状态-动作转移在不同目标下可以有不同的奖励标签。既然策略需要泛化到多个目标上那“实际达到的状态”就是一个合理合法的目标样本。这个思路最妙的地方在于它不需要任何额外的监督信号不需要手工设计奖励也不需要专家演示只需要在采样时多做一步“重新标注”。代价非常小收益却极其可观。所以 HER 能成为稀疏奖励场景下的一个标配算法不是因为原理复杂而是因为它契合了强化学习“从经验中学习”的本质。2. HER 的原理拆解把失败变成成功经验2.1 多目标强化学习的形式化描述HER 是建立在多目标强化学习Goal-Conditioned RL之上的。为了把流程说清楚我们先把符号捋一遍。在一个目标条件化的马尔可夫决策过程里我们有一个状态空间 S一个动作空间 A还有一个目标空间 G。每次环境初始化时会从 G 里采样一个目标 g。策略 π 的输入不再仅仅是状态 s而是状态和目标拼起来的 (s, g)。奖励函数也不再是固定形式而是取决于当前状态和目标rg(s, a) 1[达到目标]或者说“到达目标状态集合则奖励 1否则 0”。这样设计的好处是一个策略可以被训练来达成多个目标。训练时我们不断地给它不同的目标它就可以学习“给定一个目标我该怎么动作”的通用技能。理解了这一点HER 的逻辑就顺理成章了我们原本有一个还没实现的目标原始目标执行完一段轨迹后发现轨迹末尾的真实状态是 s。如果我们把“目标替换成 s”那么这条轨迹的每一个奖励标签都变成 1整段轨迹从“失败的探索”变成了“成功的示范”。2.2 四类目标替换策略OpenAI 论文里提出了四种选择替代目标的方式简单来说就是用轨迹里的哪个状态来替代原始目标。我先把这四种方式列成表然后再逐个说明。替换策略替代目标的选取方式特点与适用场景final轨迹最后一个状态最直接对应“最终到达哪里就当作目标”实现最简单future轨迹当前位置之后某个时刻的状态每个转移都能获得一个未来可达的目标样本效率最高episode轨迹中随机一个状态目标多样性好但可能与当前转移距离较远random从经验池随机抽一个状态多样性最强但目标可能与当前轨迹完全无关学习效率偏低实际工程里最常用的是 future 策略其次是 final。我自己的项目里一般默认 future因为它对每个状态转移都能给出一个“可达目标”保证样本既多样又不会太离谱。episode 策略可以作为补充增加目标空间的覆盖度random 策略则很少单独使用通常是在目标空间本身比较丰富时才会作为辅助。2.3 为什么替换后的目标一定能学到东西有人会问把失败轨迹强行标成成功等价于说“只要我最后到了某处我就算达成了某处”这难道不会让策略变得自欺欺人吗关键在于替代目标并不是用来骗 Q 函数的而是用来给策略一个“在某个目标下得到正奖励”的真实样本。这个样本在经验池里被反复采样Q 函数会学习到“在状态 si 下采取动作 ai能达到状态 s”这个真实的转移关系。策略随后会朝着“更大概率达到这些已实现状态”的方向更新。打个比方一个新手投篮目标是把球投进篮筐但每次都投偏了。HER 的做法是在每次投篮后记录球实际落点然后把目标暂时改成“投到那个落点”于是这次投篮就被标成了成功。下一次训练时策略会更倾向于重复“将球送到那个落点”的动作。随着落点逐渐靠近篮筐策略也就逐步逼近真正的目标。难点在于HER 并不能凭空创造“原目标下的成功经验”它只是把探索到的动态信息利用了起来。如果探索阶段根本没见过某个区域的任何状态HER 也无法学习到那个区域相关的转移信息。所以 HER 依赖随机探索的覆盖度而这也解释了为什么在动作空间巨大且探索效率极低的环境中HER 依然可能失效。3. 实操落地从零实现 HER3.1 环境选型与基准动手实现 HER建议直接使用 OpenAI Gym 的 Fetch 系列环境它们是评估 HER 的标准测试集也是最方便验证算法是否写对的场景。环境名称任务描述动作维度目标维度FetchReach机械臂末端移动到目标点43FetchPush把方块推到目标位置43FetchSlide把方块撞击滑行到目标位置43FetchPickAndPlace抓取方块并放到目标位置43我个人建议先从 FetchReach 开始验证管线正确性因为这个环境足够简单HER 往往在几万步内就能达到接近 100% 的成功率。它通过之后再上 FetchPickAndPlace 这类更复杂的任务才能区分是“代码写错了”还是“任务本身难”。Fetch 环境的 observation 是字典结构包含 observation、achieved_goal 和 desired_goal 三个字段。其中 achieved_goal 就是 HER 做目标替换时最关键的字段它代表当前状态中“实际达到的目标可达状态”。采样时我们通常把 observation 和 goal 拼接成一维向量作为网络输入。3.2 算法底座选择DDPG 还是 SACHER 是一个经验回放技巧不是独立的强化学习算法所以需要搭配一个 off-policy 算法使用。OpenAI 论文里用的是 DDPG但如果让我给实战建议我会直接说优先选 SAC。原因有三点。第一DDPG 对超参数非常敏感尤其是 actor 学习率和 critic 学习率的比例稍微偏一点就容易发散SAC 有熵正则项能自动平衡探索与利用稳定性好很多。第二SAC 内置了更加稳健的 target 网络更新机制配合 HER 做替换目标时Q 函数的波动明显更小。第三SAC 的随机策略天然带着探索属性正好弥补 HER 对状态空间覆盖度的依赖。当然如果你的项目里已经有的代码库是基于 DDPG 的也没有必要强行迁移。DDPG HER 的组合依然能跑出论文里的水平只是你要付出更多耐心去调参。3.3 replay buffer 改造HER 核心代码实现HER 的核心工作都集中在采样阶段。传统经验池存 transfer 元组 (s, a, r, s, g)HER 额外记住每个转移对应的 achieved_goal并在采样后执行目标替换。下面是一段简化但可直接使用的采样逻辑def sample_her_transition(episode_transitions, sample_index, strategyfuture, k4): # episode_transitions: 一条完整轨迹中的转移列表 # 每个转移包含 (obs, achieved_goal, action, reward, next_obs, next_achieved_goal, desired_goal) transition episode_transitions[sample_index] obs transition[obs] action transition[action] next_obs transition[next_obs] original_goal transition[desired_goal] if strategy final: new_goal episode_transitions[-1][achieved_goal] elif strategy future: # 在 sample_index 之后随机选一个时间步的状态作为目标 future_index np.random.randint(sample_index 1, len(episode_transitions) 1) future_index min(future_index, len(episode_transitions) - 1) new_goal episode_transitions[future_index][achieved_goal] elif strategy episode: new_goal episode_transitions[np.random.randint(0, len(episode_transitions))][achieved_goal] elif strategy random: new_goal np.random.uniform(...) # 从整个目标空间采样 else: raise ValueError(fUnknown strategy: {strategy}) # 根据新目标重新计算奖励 reward compute_reward(achieved_goalnew_goal, desired_goalnew_goal, info{}) if reward 1.0: reward 0.0 else: reward -1.0 # 替换转移中的目标字段 transition[obs] np.concatenate([obs, new_goal]) transition[next_obs] np.concatenate([next_obs, new_goal]) transition[desired_goal] new_goal transition[reward] reward return transition采样时我们并不是只把替换后的转移存进经验池。标准做法是原始目标下的转移存一份然后额外生成 k 个替换目标的转移也存进去k 通常取 4。这样经验池里同时包含原始目标样本和替换目标样本既保留了原始探索信息又提供了大量密集的成功信号。3.4 训练超参数参考超参数直接影响 HER 的成败。我把论文默认值和我在实践中推荐的值放在一起对比方便你直接抄作业。超参数论文默认值实践推荐值说明每轮最大步数5050太长会导致轨迹稀疏性增加经验池容量1_000_0001_000_000足够大保证样本多样batch_size256256 或 512较大 batch 能稳定 Q 更新k替换目标数44过大容易让策略过度偏向已达成状态actor 学习率1e-33e-4SAC 配更小学习率更稳critic 学习率1e-33e-4与 actor 保持一致折扣因子 γ0.980.98Fetch 任务步数短不需要太高软更新系数 τ0.050.005论文的 0.05 会带来较大的目标网络波动目标网络更新频率每步更新每步更新SAC 风格 soft update 即可奖励形式稀疏sparse1/0HER 下不需要额外密集奖励我最想强调的是 τ 这个参数。论文里用 0.05实测在部分环境里会导致目标网络更新过快Q 值震荡。我后来统一改成 0.005训练稳定性明显提升。原因很简单HER 替换目标后同一批样本里目标分布差异较大目标网络如果更新太快Q 目标容易忽高忽低。4. 工程踩坑与问题排查实录4.1 rollout 和训练线程的时序问题HER 对采样和训练的时序要求比普通 off-policy 算法更严格。原因在于一条完整轨迹必须“结束”之后我们才能知道 trajectory 里的最终状态也才能执行 final 或 future 策略的目标替换。如果你一边 rollout 一边训练就必须保证“一条轨迹采集完成后再进入训练队列”。我在最初实现时图省事每个转移单独存池再立刻训练导致未来状态替换时还没拿到整条轨迹最后只能退化成 random 策略成功率暴跌。正确做法是先收集一整条 episode 的所有转移做一次 HER 扩展把扩展后的转移批量写入经验池然后再从池子里采样训练。顺序错了整个算法就失效了。还有一个容易被忽略的细节未来状态替换需要“当前步之后的某个状态”。如果你在一轮 rollout 中实时做替换需要预先知道整条轨迹的所有未来状态这就逼迫你必须分段处理。建议把所有 episode 保存在内存里等轨迹收集完再统一做目标替换处理。4.2 目标替换比例与过拟合k4 是论文里给出的经验值但并不是所有任务都适合。实际使用时我发现如果任务的目标空间比较小比如 FetchReach加大 k 到 8 甚至 16 会加速收敛但如果目标空间很大比如 FetchPickAndPlacek 过大会让经验池里成功样本的比例过高导致策略过早收敛到“少数已达成目标”上泛化性变差。这个问题在训练曲线上体现为前期成功率涨得很快但到后期停滞不前怎么调学习率都没用。这时候先别怀疑模型容量把 k 降回 4或者引入一部分原始目标的稀疏样本往往就能缓解。我在实际项目中通常采用“future episode”混合替换的采样方式70% 的样本用 future 策略生成30% 用 episode 策略生成。这样既能保证目标的可达性又能保持目标空间的多样性。4.3 训练不收敛的排查清单HER 训练不收敛绝大多数情况不是 HER 本身的问题而是配套组件没配好。我踩坑踩出一个排查顺序希望你在怀疑算法之前先按这个列表检查一遍。症状排查方向常见原因与修复累计奖励一直为 0经验池是否混入已替换样本检查 HER 替换代码是否正确执行观察 sampled 样本中 reward1 的比例训练前期正常后期 Q 值发散软更新系数过大把 τ 从 0.05 降到 0.005或减小 actor/critic 学习率成功率上不去但 Q 值很高reward scale 导致 Q 过估计检查稀疏奖励是否用了 0/1 或 0/-1避免使用 ±200 这类大尺度奖励接入 SAC 后动作输出异常熵系数被奖励淹没把奖励切换成 0/1 二值automatic entropy tuning 才能正常工作rollout 速度极慢每步都要重新生成 HER 样本确认是否每步都调用了 np.concatenate 平铺大数组建议预分配缓冲区另外我想特别提醒一个新手容易踩的坑HER 的奖励函数不是从环境返回的原始奖励而是根据“替换后的目标”重新计算出来的。如果你忘记重新计算奖励直接沿用原始目标下的 0 奖励那替换就白做了。4.4 HER 的变体与扩展方向HER 表面看起来很简单但它作为“重新标注经验”的思想延伸出了不少有价值的变体。理解这些变体能帮你在自己的项目里找到更适合的切入点。第一个方向是课程化的 HER。比如 Curriculum HER它不是在整个目标空间里均匀采样替代目标而是从易到难地组织目标序列让策略先在“容易达成的状态”附近建立正确的 Q 估计再逐步向困难目标扩展。这特别适合目标空间存在明显难易差异的任务。第二个方向是把 HER 与多任务强化学习结合。当你同时训练多个形状相似但目标不同的任务时HER 天然适合作为统一经验回放的策略因为不同任务的目标替换可以共享同一个经验池。第三个方向是离线强化学习场景。很多离线数据集里充满了失败的轨迹HER 提供了一种“不用额外收集数据就能把失败数据转换为正反馈样本”的思路。在离线场景中HER 会和 CQL、IQL 这类算法结合用来缓解稀疏奖励下的保守估计问题。如果你想在 2024 年之后的思路上继续深入可以关注 goal-conditioned transformer 和 decision transformer 与 HER 的结合。这类模型天然以“目标历史轨迹”为输入HER 的替换思想可以直接作为数据增强手段让离线数据集的效果显著提升。最后分享一些个人心得我在实际项目中跑了不下十个 HER 相关实验最大的体会有两个。第一个是做稀疏奖励任务先别急着设计复杂的奖励函数先上 HER看随机探索能不能覆盖到目标附近。很多时候环境本身并不难只是奖励信号太稀HER 的重新标注机制就足以解决。第二个是把 HER 当“数据增强”而不是“算法核心”心态会轻松很多。你就把它当作给失败轨迹换个合适的标签然后让后面的 Q 学习更好做。如果你刚开始复现建议按“FetchReach 跑通 → FetchPush 调 k 和 τ → 换比赛或真实场景”这个节奏走。第一步只求跑通第二步再体感调参第三步才谈得上规模化应用。另外一个小技巧训练时把每一步的目标和 achieved_goal 打印出来确认替换后的目标确实来自轨迹中的真实状态很多诡异问题一眼就能看出来。HER 这个方向本身已经不再新鲜但它提出的“重新审视失败经验”的思路在今天的离线强化学习、多任务学习和机器人操作任务里依然有很强的生命力和参考价值。希望这篇文章能帮你减少一点试错成本。