看到“hindsight”这个标题如果你是做强化学习的第一反应大概率是那篇经典的Hindsight Experience ReplayHERAndrychowicz 等人在 2017 年发的那篇。但如果你不是这个领域的搜这个词可能会看到另外一些东西有讲“后见之明”认知心理学的有讲一个给 LLM 打标的开源项目的还有一家已经倒闭的互联网公司也叫这名字。我一开始就是被这种多义性坑过花了半天时间找资料结果发现搜到的内容和我想的完全不是一回事。这篇文章我按自己实际做过的项目来写目标是把 HER 这套“事后经验回放”的原理、代码实现、踩坑经验一次讲透。不管你是刚上手 goal-conditioned RL还是已经在用 DDPG、SAC 这类算法但苦于奖励稀疏训不动这篇应该都能给你一些能直接抄作业的东西。1. 先搞清楚hindsight 到底解决了什么问题1.1 稀疏奖励强化学习里的“一锤定音”先举个例子。你让一个机器人学推桌子上的一个小方块目标是把方块推到某个固定位置。如果方块最终恰好停在目标点上奖励 1否则奖励为 0。这就是典型的稀疏奖励环境整个 episode 里绝大多数时刻拿到的奖励都是 0只有最后那一下成功了才有反馈。听起来很合理对吧但问题在于一个完全没有中间反馈的任务智能体agent在一开始根本不知道“往哪个方向推是有意义的”。它随机探索大概率一整个 episode 下来奖励全是 0没有任何梯度信号可以学习。这就好比一个人从没打过篮球你直接让他投三分进了给 100 分不进给 0 分。他练了一下午一个球都没进他学到什么了什么都学不到连“力气用大了”还是“方向偏了”都无从判断。很多经典算法在这种情况下直接失效。DQN 这种基于值函数的方法在奖励全 0 的环境里所有状态的价值估计都是一样的根本没有可优化的梯度。策略梯度类方法也差不多因为 return 全是 0优势函数算出来也是 0参数更新方向是随机的。实际跑起来的表现就是 loss 一直在震荡或者干脆不变训练曲线是一条水平线。1.2 为什么“重放过去的经验”也不管用有人可能会说那我用经验回放Experience Replay不就完了把过去的状态转移存起来反复采样训练。问题在于如果你存下来的 transition 全是奖励为 0 的样本那你反复看这些样本也只是在反复学习“怎么做都不对”没有正向样本可以学等于一个人反复看自己投篮失败的录像但录像里没有一个球是进的他能改进个啥这就是 HER 要解决的核心矛盾稀疏奖励环境里正样本太稀少了少到几乎没办法驱动学习。但环境本身真的是完全无法学习吗不是。其实有很多次尝试虽然没达到预设目标但达到了某些其他状态——这些状态本身是有意义的可以作为“新的目标”来学习。1.3 一句话总结 HER 的思路HER 的核心思想特别朴素这条轨迹没达到你想要的目标但它达到了某个别的状态。那我就假装目标其实是被替换成那个状态然后把这个“伪成功”的经验存下来当作一次成功示范来学习。还是拿投篮举例。你本来想投进篮筐但球砸到了篮板右侧。在 HER 框架下你会把这个 episode 重新标记为“目标砸到篮板右侧”——而这个目标确实达成了于是这个 episode 就变成了一次成功经验可以从中学到“往那个方向、用那个力度能砸到篮板右侧”。哪怕这个新目标本身不是你最终想要的但它教会了智能体“状态空间里存在因果关系”。多次这样的学习之后智能体逐渐理解了这个环境的动力学最终真正的目标也能学会了。这个思路之所以叫“后见之明”因为它是在 episode 结束之后回头看这条轨迹的时候才知道“哦原来这次我其实完成了一个别的目标”。训练时引入这种后见之明的信息来扩充正样本就是 HER 的全名Hindsight Experience Replay。2. 核心原理拆解HER 是怎么改目标做回放的2.1 goal-conditioned RL 的基本形式要讲清楚 HER得先说清楚它的实验设定。HER 针对的是 goal-conditioned RL 任务。这个设定里每个 episode 会有一个额外的目标 (g)比如“把方块推到位置 A”“把门开到 90 度”“把机械臂末端移动到坐标点 P”。状态转移可以写成[ (s_t, a_t, r_t, s_{t1}, g) ]这里的奖励 (r_t) 通常是基于“当前状态是否达到目标”来算的最常见的形式是[ r_t -\mathbb{I}[s_{t1} otin g] ]也就是没达到目标就给 -1 或者 0达到了就给 0 或者 1。策略 (\pi(a|s, g)) 的输入除了当前状态还必须包含目标信息因为这决定了它该往哪使劲。大家用得最多的一组实验环境是 OpenAI 的 Fetch 和 Hand 系列比如 FetchReach、FetchPush、FetchPickAndPlace、HandReach。我自己的经验是FetchReach 相对简单HER 跑通很快FetchPickAndPlace 难度梯度明显更适合拿来验证算法实现是否正确。2.2 事后经验回放的 4 种采样策略HER 论文里提出了 4 种“如何选取替代目标”的策略实际效果差异很大值得单独拆开说。第一种叫final。这个最简单就是把一条 episode 的最终状态 (s_T) 拿出来当作这条轨迹的替代目标。如果这个 episode 的最终状态距离真正的目标很远那么这个替代目标其实也不近但这种做法最简单而且已经能超过普通 DDPG 一大截。第二种叫episode。就是从当前这条 episode 里随机挑一个状态用那个状态作为替代目标。比如这条轨迹走了 50 步里面有 50 个状态随机挑一个。这个做法的好处是目标状态至少是真实被访问过的状态不会选到一个根本不可能达到的想象状态。第三种叫future。这个策略是限定“从当前时刻往后的一段窗口里”随机选一个状态作为替代目标。为什么限定“往后”而不是“往前”因为 HER 要重写的是“我这一个 transition 想达到的那个目标”如果我选择了一个过去已经经历过的状态作为目标那这个 transition 其实已经知道这个目标达没达到了信息增益不大。而选一个未来的状态意味着这个 transition 确实是在“往那个状态前进”的过程中发生的因果关系更清晰。论文里的 k 值通常取 3 或 4意思是在未来 3 到 4 步的时间窗口内选目标。第四种叫random。就是从整个 replay buffer 里随机挑一个状态当作目标和当前这条轨迹没有时空关系。这个做法一般用作对比实验实际效果通常不如 future因为它可能选到一个完全不相干的状态导致“伪成功”太假。我自己的实验结果和论文结论一致future 策略表现最好episode 其次final 再次random 最差。但注意final 虽然效果不是最好的它有一个好处是极端简单适合做代码调试。在把 HER 接入新环境时我通常会先用 final 跑一遍通整个 pipeline再切换到 future 提升效果。2.3 future 策略的 k 值怎么选这个 k 值直接影响训练效果论文里说 3 或者 4 是很好的默认值但这不是随便拍脑袋定的。你想一下k 值代表“时间窗口的长度”。k 太小比如 k1那么你只能在 transition 的下一步状态作为替代目标这太保守了几乎没有扩展出多少新的正样本。k 太大比如 k10又可能选到一个太远的状态这个替代目标和当前 transition 的下一步状态差别太大学起来很吃力。在实际使用中我建议根据任务的平均 episode 长度来定。如果一步 episode 平均 50 步k3 就是在未来 3 步内选大约是轨迹长度的 6%如果平均 200 步同样比例下 k 就取 12 左右。不过大多数公开环境的默认 k4 都能正常 work没有特殊理由不需要动它。这里还有个细节HER 并不是把每条轨迹的每个 transition 都重写。实际操作时一条轨迹里每个 transition 会以概率 (p)通常取 0.8 或 1.0被重写重写时生成一个替代目标然后重新计算奖励。其余维持原目标不变。这样保证 buffer 里既有真实目标的样本也有替代目标的样本防止策略完全被“幻觉目标”带偏。3. 实操落地与代码实现3.1 你需要改造哪几个组件拿到一个现成的强化学习框架想接入 HER需要改三个地方。第一个是环境返回值。goal-conditioned 环境里env.step 返回的 next_state 通常包含两部分agent 自己的状态和 goal 状态。比如 Fetch 环境里 observation 是 25 维的其中前 10 维是机械臂自身状态后面 15 维包含 goal 和 achieved goal 的信息。但有些环境里 observation 是不含 goal 的goal 是单独返回的。这个必须先确认清楚否则后面代码会乱。第二个是replay buffer。普通 buffer 存的是 ( (s, a, r, s, done) )HER 的 buffer 要存的是 ( (s, a, r, s, g, ag, done) )其中 (g) 是原始目标(ag) 是这条 transition 发生的 step 之后实际达到的状态achieved goal。为什么要额外存 (ag)因为重写目标的时候要用它来当替代目标或者用来算替代目标对应的奖励。如果 buffer 里没存 achieved goal你后面根本没法做重写。第三个是采样逻辑。这是 HER 最核心的改动。从 buffer 里抽样时不能直接返回原始样本必须对一部分样本做“目标重写”操作重新计算 reward。3.2 核心代码HER 采样逻辑实现我用 PyTorch 风格的伪代码写一下 HER 的核心采样逻辑平时用的也就是这个套路def her_sample(buffer, batch_size, k4, replay_prob0.8): episodes buffer.sample_episodes(batch_size) transitions [] for episode in episodes: for i in range(len(episode) - 1): s, a, r, s_next, g, ag, done episode[i] if np.random.rand() replay_prob: # 从未来 k 步窗口内随机选一个 achieved goal 作为替代目标 horizon min(len(episode), i k 1) future_idx np.random.randint(i 1, horizon) g_new episode[future_idx][ag] r_new compute_reward(ags_next, goalg_new) transitions.append((s, a, r_new, s_next, g_new, s_next, done)) else: transitions.append((s, a, r, s_next, g, ag, done)) # 打乱后取 batch return collate(transitions[:batch_size])有几个细节你可能注意到了。第一future_idx是从i1开始取的也就是说替代目标只能是未来某个 step 的 achieved goal不能是当前 step 甚至过去的。第二r_new是调用环境的 reward 函数重新计算的而不是直接用 0 或 1 代替。Fetch 环境的 reward 函数是compute_reward(achieved_goal, desired_goal)内部算的是欧氏距离距离小于某个阈值比如 0.05就奖励 0否则奖励 -1。你这个写法必须和环境的判定逻辑完全一致不能自己想当然地写。还有一点我见过很多人把 HER 的重写逻辑放在环境返回时一次性做好存进 buffer 的就已经是重写后的样本。这种做法不推荐。原因有两条一是重写是有随机性的同一份 transition 这次可能重写下次可能不重写如果存进去就固定死了你损失了数据多样性二是你把因果关系搞错了——buffer 应该存“原始事实”这条轨迹达到了哪些状态重写是采样时根据“当前训练阶段的理解”来做数据增强。如果提前重写相当于你在训练还没开始时就替换掉了真实 goal 的信息这对后续学习真实 goal 是有害的。3.3 和 DDPG/SAC 结合的接口设计HER 只是一个经验回放机制它本身不限定用哪种算法。论文里用的是 DDPG但你完全可以用 SAC、TD3 甚至 PPO 的 off-policy 版本。我自己常用的是 TD3 HER 的组合比 DDPG 稳定不少。接口层面只需要注意一点actor 和 critic 的输入必须是 concat(state, goal)而不是把 goal 当作额外的东西绕开。很多人在这一步犯嘀咕“我的 state 里已经包含了 goal 信息还要再 concat 一次吗”这里要分清楚两个概念。observation space 里可能确实包含 goal 的数值但策略网络需要显式区分“哪些是当前状态、哪些是目标状态”如果你把它们混在一起当作一个扁平的向量交给网络网络虽然理论上也能学会但实际训练会慢很多而且容易过拟合到特定的 goal 分布。以 Fetch 环境为例observation 是 25 维但它内部已经包含了 goal 的 3 维坐标。我在实际实验里做了对比把 observation 直接给网络的版本和把 observation 拆成 state goal 再 concat 的版本后者训练速度差不多是前者的 2 倍左右。原因也不难理解显式区分后critic 的 Q 值函数才能更清晰地对“状态到目标的距离”建模。伪代码如下class Actor(nn.Module): def __init__(self, state_dim, goal_dim, action_dim): super().__init__() self.net nn.Sequential( nn.Linear(state_dim goal_dim, 256), nn.ReLU(), nn.Linear(256, 256), nn.ReLU(), nn.Linear(256, action_dim), nn.Tanh() ) def forward(self, state, goal): x torch.cat([state, goal], dim-1) return self.net(x)3.4 训练配置与超参数参考我把实际跑通过的一组超参数列出来环境是 FetchPickAndPlace算法是 TD3 HER超参数取值备注算法TD3相比 DDPG 更稳定目标重写概率0.8剩余的 0.2 保留原始 goalk 值4future 窗口buffer 容量1e6注意存储的是 transitionbatch size256比普通 DDPG 的 128 略大学习率1e-3Actor 和 Critic 同值动作噪声0.2训练时使用噪声衰减线性降到 0.05让策略逐渐确定性化网络结构256x256 ReLU不需要太深训练步数100 万步FetchPick 大约在 30-50 万步开始明显提升这组参数不需要调得很精细基本都能跑出不错的效果。实际跑的时候我建议不要一上来就开满整个实验先用 FetchReach 测 20 万步看能不能在很短的时间内达到接近 100% 的成功率。FetchReach 太简单了普通 DDPG 加一点 exploration 也能达到不错的效果但它适合用来验证你的 HER 重写逻辑是否写对了——如果最简单的环境都训不动那你的实现大概率有问题别急着换复杂环境。4. 实验结果与适用边界4.1 经典对比加了 HER 之后成功率变化有多大我直接说一组典型的实验结果环境是 FetchPush。用普通 DDPG跑了 100 万步成功率一直趴在 10% 以下几乎等于随机策略。换用 HER 之后大概 30 万步左右成功率开始爬升到 80 万步时可以达到 70% 到 80%。FetchPickAndPlace 难度高一些普通 DDPG 基本就是 0加 HER 之后能到 50% 到 70%。这组数据说明一个问题HER 不是“提升了一点效率”而是“从训不动变成能训动”。这两个量级的差距决定了在稀疏奖励的 goal-conditioned 任务里HER 几乎是标配而不是可选项。这也是为什么它在论文里用“sparse reward”这个关键词反复强调——它专门解决的就是奖励信号太少导致的有效学习样本稀缺问题。4.2 什么时候 HER 救不了你HER 不是万能的至少有两种情况它效果很差甚至没有效果。第一种你的任务不是 goal-conditioned 的。HER 的核心假设是“每个 episode 有一个明确可判定的目标且目标可以用状态空间中的某个状态来表示”。如果你的任务是“走迷宫到出口”目标确实是可判定的但“出口”不是一个连续的状态值而是一个离散的位置标签HER 也能用只是替代目标的定义需要额外处理。更典型的反例是“平衡倒立摆”这类连续控制任务。目标是“保持直立”没有“我这步达到了一个别的状态可以作为替代目标”的说法——要么直立要么倒下不存在“另一个直立目标”。这种任务里 HER 没有用武之地因为它的目标空间是单点的没有多样性。第二种你的奖励函数其实并不稀疏。有的环境虽然表面看起来只有 1/0但实际上 state 空间里的距离信息天然给了梯度。比如我用过一个机械臂环境它的 observation 里直接包含了“末端执行器到目标点的距离”这个值网络很容易从输入特征里学到梯度信号。这种情况下普通 DDPG 已经能训练了HER 加了虽然有好处但提升没那么显著反而因为重写逻辑增加了代码复杂度。做项目的时候先去理解你的环境到底是不是真的稀疏不要盲目套 HER。还有第三种场景就是探索本身才是瓶颈。HER 的重写逻辑解决的是“从失败中学习”但它不解决“如何探索到新的状态”。如果环境的状态空间极大而且随机策略几乎无法碰到有意义的区域比如一张地图 99.99% 的区域都是空白无意义的HER 也没办法凭空造出探索轨迹。这时候你需要的是更好的探索机制比如 curiosity-driven exploration 或者 intrinsic motivation而不是 HER。4.3 容易混淆的 hindsight 相关概念搜索的时候会遇到两个容易搞混的东西提前说一下。一个是Hindsight (Robotics/LLM 语义标注)。这是 2024 年前后有团队做的开源工具全称是Hindsight: Posterior-guided Training of Retrievers也不完全对。准确来说那个项目是用 LLM 给机器人操作视频里的状态做语义标注然后训练一个通用状态检索器。它借用了“hindsight”这个词的意象事后标注比实时标注更容易但它和 HER 算法除了名字和灵感来源有关联技术路线完全不同。如果你搜资料时看到的是这个别以为自己在看 HER 的变体。另一个是Hindsight Instruction Relabeling。这个是和 language-conditioned RL 结合的一个方向把“指令”也像目标一样在事后重写。比如机器人本来要执行“拿杯子”但失败了它实际做了“推开杯子”那么就把这个 episode 重写成“推开杯子”的成功样例。这是 HER 思想的自然推广思路是对的但和原文 HER 不是一回事。5. 踩坑记录与调试心得5.1 五个必踩的坑第一个坑是reward 函数重算不一致。我见过好几种实现在 HER 重写目标后奖励直接人为设定为 0因为“伪成功”了嘛。这个做法是错的。Fetch 环境的 reward 是有具体距离阈值的你手动设 0 意味着“替代目标对应的状态也恰好满足了真实的奖励条件”但实际上替代目标只是轨迹中的一个状态它和真实 goal 可能差了十万八千里。正确做法是调用环境自带的 reward 函数用替代 goal 重新计算。你只有保证这个判定逻辑和训练环境完全一致策略学到的才是真实环境里的奖励函数。第二个坑是buffer 里没存 achieved goal。如果只是在保存 transition 时存了s, a, r, s, g, done后面你会发现重写目标时无从下手——你没有记录每个 step 实际到达的状态。这个信息必须在保存时就要完整存下来否则回放时没有数据可用。这也是为什么我说 buffer 的格式要在写代码之前就定好而不是等后面补。第三个坑是done 信号的处理。HER 重写后done 应该怎么处理很多人直接复制原始 transition 的 done但这是不对的。如果替代目标被达成了这个 episode 在替代目标的意义上确实结束了那么 done 应该设为 1。如果替代目标没达成done 应该保持 0。我实际用的时候因为目标是重写的而替代目标几乎总是从未来状态里选的这个状态在轨迹中真实出现过所以理论上替代目标的 done 应该等于 1——但这只适用于当前这个 transition 恰好是轨迹中靠近末尾的情形。稳妥起见重写时用done_new (idx len(episode) - 1)来判断。这个坑比较隐蔽但它确实会影响 TD3 这种对 done 敏感算法的收敛质量。第四个坑是重写比例调得太高。有些人觉得重写概率越大越好直接设成 1.0 甚至把每条轨迹全部重写。但这样一来replay buffer 里几乎全部是替代目标的样本真实目标的信息反而被淹没策略会逐渐偏离去学那些“伪目标”。我的经验是 0.8 是很好的平衡点保留了 20% 的真实目标样本让策略始终记得自己最终要做的是什么。这个比例在论文里也是默认的。第五个坑是忽略了目标空间的分布。HER 重写时选择的替代目标是来自当前轨迹的因此它的分布和真实目标分布可能差异很大。比如一个 FetchPickAndPlace 环境真实 goal 在桌面高度附近但轨迹中的状态可能包含机械臂举到半空中的状态这些状态的 goal 值可能远离真实分布。如果策略过多地学习这些远离分布的目标最终迁移到真实目标时效果会打折扣。这个问题的缓解方式是把替代目标的采样范围适度限制比如只从未来一定窗口内采样同时保留一部分真实目标样本。5.2 调试技巧与判断依据我提供一个实用技巧把重写前后的奖励分布打印出来看。如果重写后的样本里正奖励成功样本比例明显高于原始 buffer说明 HER 在起作用。如果重写后还是大量负奖励那可能是替代目标的采样距离太远或者是自己的 reward 函数实现有问题。还有一个判断依据是看buffer 里正样本比例的曲线。普通 DDPG 在稀疏任务里这个比例是长期接近 0 的加了 HER 之后应该会逐渐升高最后稳定在一个比较高的水平。如果这个曲线一直不动说明重写逻辑有问题或者环境本身的成功判定和你设的不一致。最后如果你用的是 TD3 这类算法记得把critic 的 target 网络也加入噪声并且对动作进行 clip。这个技巧和 HER 无直接关系但很多 TD3 HER 实现里漏掉这点导致训练后期 Q 值过高策略退化。具体来说target 动作要加上一个 (\epsilon \sim N(0, 0.2)) 的噪声并 clip 到动作边界这是 TD3 稳定性的关键。6. 一些额外的心得我在自己的项目里做了个无聊但是有效的实验把同一份 HER 训练代码里的目标重写比例改成 0让它变成一个纯 TD3然后对比训练曲线。这个实验的意义在于它可以帮你确认“HER 到底贡献了多少”。如果你的环境和任务本来就是可训练的可能你会惊讶地发现 HER 只是锦上添花但如果任务真的是稀疏的这份对比会让你直观看到从 0 到能学的巨大差距。还有一点值得注意很多人会把 HER 和 curriculum learning 放在一起比较但其实它们解决的是不同维度的问题。Curriculum learning 是通过调整任务难度让模型逐步提升HER 是通过改写目标来利用失败样本两者可以叠加使用。我在抓取任务里用过“先让机器人学会靠近物体再学会抓取”的课程式训练配合 HER效果确实比单独用 HER 更好一些。最后如果你要复现论文的结果我建议仔细看一下 OpenAI 开源代码里util/mpi_utils.py和util/her.py这两个文件。虽然那套代码用的是 MPI 并行、TensorFlow 1.x环境也老旧了但里面的sample_her_transitions函数逻辑非常清晰比很多网上流传的简化版靠谱得多。我后来写自己的实现时就是以它为准而不是以论文里的伪代码为准——论文里的伪代码省略了一些边界条件直接照抄可能会写出 bug。HER 这个思路后来也启发出很多扩展比如 goal 的语义化重写、和自然语言指令结合、在 offline RL 里做数据增强等等。但万变不离其宗核心就是一句话回放时把“没做到的事”重新标记成“做到了的事”。理解透了这一层任何变体你都能很快看懂。
阅读完成 · 觉得有帮助?