
做强化学习的人十有八九都被稀疏奖励折磨过。智能体在环境里横冲直撞跑了几万步奖励信号永远是同一个数你根本不知道它在学还是没有学。这时候如果有个思路告诉你既然没抓到目标那就干脆把“抓到的那个位置”当作真正目标去学。这个思路就是 hindsight也就是 Hindsight Experience ReplayHER里那个“事后诸葛亮”的含义。我第一次看到这个算法时觉得它有点自欺欺人但真正在 Fetch 类环境里跑通之后才发现这个朴素的 trick 有多能打。这篇文章我就顺着 hindsight 这个关键词把 HER 的来龙去脉、算法细节、踩坑记录和调参经验一次讲透。1. hindsight一个靠“事后回想”工作的强化学习算法1.1 为什么智能体需要“事后诸葛亮”先拿个最直观的例子说事。假设你训练一个机械臂去推一个方块到桌面上的某个固定目标点标准做法是把“方块离目标还有多远”换算成一个稠密奖励比如距离越近奖励越大。但很多真实场景根本给不出这种奖励你只有两个输出成功或者失败。机械臂乱推一通方块根本没到目标那就只能给一个“失败”信号可能是 -1也可能是 0。这种情况下整个轨迹里没有任何一个 rewarded 过渡DQN、DDPG、SAC 这些偏置策略算法全都卡在起点。这里的问题不是算法不够强而是样本里全是“失败经验”失败经验在标准的经验回放里直接被当成噪声丢掉了。但 hindsight 的视角完全不同每一次失败其实都隐藏着一次“成功”只不过成功的是那个“方块最终到达的位置”而不是你预设的目标。如果能把“实际到达的位置”重新定义成本轮的目标那么原本一条零奖励的轨迹就可以被改写成一条充满正样本的成功轨迹。这个思路绕开了奖励塑形也不需要额外的人工知识纯粹是改变了经验回放的“目标视角”。我自己体感很深的点是HER 这类方法特别适合那种目标状态可观测、且目标状态就是环境状态一部分的任务。如果目标本身不可观测比如“用户心里的某个偏好”那 HER 用起来就要打折扣。反过来说只要目标是可以采样的状态就能用这套 re-labeling 思想给训练过程注入“伪成功经验”。1.2 HER 到底改了什么从失败轨迹里挖经验HER 的全称是 Hindsight Experience Replay直译过来就是“事后经验的回放”。它和普通经验回放最大的区别只在于一个环节采样到一条 transition 后除了保留原始目标还有概率给它重新分配一个新的目标再用新目标重新计算奖励。这里有个关键概念你得拎清楚状态里通常包含两个部分一个是 desired_goal也就是我们想让智能体达成的目标另一个是 achieved_goal表示智能体此刻实际已经站在了哪里。在 Fetch 环境里achieved_goal 就是方块当前的 3D 坐标。HER 做的事就是把某条历史经验里的 desired_goal 换成这条经验里真实出现的某个 achieved_goal然后重新计算奖励。换成新目标之后原本“没有到达目标”的失败经验就变成了“成功到达了某处”的成功经验。比如你本来想让方块去 (1, 0, 0)结果一回合下来方块停在 (0.3, 0.2, 0.1)如果你把目标改成 (0.3, 0.2, 0.1)那这条轨迹的所有动作序列就是一条能成功到达目标的轨迹。这样一来每一个动作都有了正向的信用分配。这个方法不解决“策略一定能把方块推到任意目标”的问题但它给了策略一个可优化的梯度方向让智能体至少能先把“到达自己面前的位置”这个能力练出来再逐步往外泛化。我一开始觉得“换目标”就是在作弊反正目标改了肯定成功。但真正跑完才发现这套逻辑之所以成立是因为它利用了奖励信号的结构稀疏奖励下成功的定义是“状态与目标足够接近”。把目标改到实际达成的位置等价于把探索过程中偶发的“有效结果”挖掘出来转化成密集的学习信号。它没有改变环境的物理规则只是改变了回放样本的标注方式让算法从同样的数据里多学了一倍以上的东西。2. HER 的核心机制与设计亮点2.1 经验回放里的目标替换re-labelingHER 的操作看起来简单但实现时要踩的细节相当多。先看一条 transition 在代码里的组织结构在 OpenAIGym 的 Fetch 环境里观测值长这样obs { observation: array([...]), # 机械臂自身状态 achieved_goal: array([0.3, 0.2, 0.1]), # 当前方块实际位置 desired_goal: array([1.0, 0.0, 0.0]) # 预设目标 }DDPG 或者 SAC 这类 actor-critic 结构在更新时需要把整条 transition 从回放缓冲区里抽出来obs, action, reward, next_obs, done buffer.sample(batch_size) # 对每个样本决定是否做 re-labeling for i in range(batch_size): if random.random() her_replay_prob: # 关键选择换一个什么样的新目标 new_goal sample_new_goal(episode, i) # 重新计算 reward distance np.linalg.norm(next_obs[i][achieved_goal] - new_goal) new_reward 0.0 if distance threshold else -1.0 # 把新目标写回观测 obs[i][desired_goal] new_goal next_obs[i][desired_goal] new_goal这里最重要的一点是两个观测位置的目标都必须改。obs 里的 desired_goal 是当前时刻看到的目标next_obs 里的 desired_goal 是下一步看到的目标策略网络在计算目标 Q 值时要用到 next_obs 的目标作为状态输入如果只改一个网络就会面对不一致的输入训练直接崩掉。这个错误我犯过不止一次每次都能浪费一天时间。re-labeling 的概率一般设为 1也就是采样到所有经验都有机会被改写。论文默认的是每次采样后对每条 transition 有 50% 到 100% 的概率替换目标。在实际跑 FetchPush 时我用 100% 替换也没有问题因为原始目标轨迹仍然存在于其他样本里并不会造成“忘记真正任务”的情况。2.2 四种目标采样策略对比HER 论文提出了四种从轨迹里采样新目标的方式实际用的时候差别非常大。下面这张表是我自己整理的习惯用法。策略采样方式特点适用场景final取轨迹最后一个状态作为新目标最稳定但样本多样性差任务简单、轨迹短比如 FetchReachrandom从整条轨迹里随机取一个状态多样性好但早期很多目标太远学习慢需要增加目标覆盖范围时episode从整条轨迹里随机取一个状态等价于 random但通常指未加额外约束介于 final 和 future 之间快速验证 HER 是否生效时future取当前时刻之后的状态作为新目标目标与动作存在时序一致性效果最好大部分 Fetch 系列任务k 通常取 4future 策略之所以是最优解从因果角度看很合理一条轨迹里t 时刻的动作只会影响 t 之后的状态不会影响 t 之前的状态。如果新目标是 t 之前某个时刻的 achieved_goal那么当前动作本质上和这个目标没关系学出来的梯度是错的。如果取 t 之后第 k 个时刻的状态作为目标那么当前动作至少和目标状态之间存在一条物理可达的路径信用分配更合理。我在 FetchPickAndPlace 环境里对比过 final 和 future 的表现差距不是一点半点。只用 final 时机械臂很容易陷入“把方块抓到最高点”这个局部解因为轨迹终点对应的 achieved_goal 往往都是行程末尾的位置目标分布永远集中在末端。改用 future 策略后目标范围覆盖到了轨迹中段策略学到的行为就丰富多了推动方块、抬高机械臂、调整姿态这些子技能都被激活了。2.3 HER 为什么必须搭配离策略算法理解 HER 的适用范围要先说清楚它和 off-policy 算法之间的关系。HER 改的是回放缓冲区里的样本标签而回放缓冲区本身是离策略算法的地基。DQN、DDPG、TD3、SAC 都是从历史经验里反复采样子集来更新网络因此它们都天然兼容 HER。相反PPO 这类在策略算法是每批数据只用来更新一次数据用完就丢你就没有地方去做 re-labeling目标替换也就没有意义了。我自己的习惯是优先搭配 SAC 或者 TD3。SAC 的熵调节机制在稀疏奖励下能维持不错的探索力度HER 补足奖励密度之后SAC 很容易就学会抓取任务。如果你用的是 DDPG那就要小心DDPG 对奖励尺度敏感HER 重写出的奖励往往是稀疏的 -1/0DDPG 学起来会有点脆通常需要配合更快的学习率或者更大 batch size。跑通一个 FetchReach 用 DDPG 和 HER 也够了但一旦上到 FetchPickAndPlaceSAC 的优势就明显出来。这里还要强调一下HER 不改变损失函数的形式也不改变网络结构它只是在数据侧做手脚。这带来的工程好处是你可以把 HER 封装成一个采样代理插进任何已有的 off-policy 算法流水线里不需要修改模型代码。这也是为什么很多框架里 HER 都是作为 ReplayBuffer 的一个可选特性存在的而不是独立算法。3. 从零实现 HER 的实操记录3.1 选环境Fetch 系列为什么是标配如果你想最快看到 HER 的效果最省事的路子是直接用 OpenAI Gym 的 Fetch 系列环境。这个系列的设计初衷就是为稀疏奖励研究提供一套标准测试集。FetchReach 控制机械臂指尖到达目标点FetchPush 需要推方块到目标点FetchPickAndPlace 需要抓取并搬运方块FetchSlide 则涉及摩擦和滑动更考验对动力学误差的容忍度。这些环境统一使用 MuJoCo 物理引擎观测空间是一个 dict里面天然分好了 observation、achieved_goal、desired_goal 三个字段。这个结构对 HER 来说非常友好你不需要自己去想办法从观测里解析“实际目标状态”环境直接给了。我建议新手一定从 FetchReach 开始因为它只需要控制指尖移动到目标点没有复杂的物体交互HER 几分钟就能看到成功率往上走。跑通了之后再切到 FetchPush这时候你才会感受到 sparse reward 的残酷。安装环境的时候要注意版本对齐。gymnasium 0.28 之后Fetch 环境分为 gymnasium robotics 和 gymnasium 的 legacy 版本接口差别主要在 reward 和 done 的定义上。我自己用的是 gymnasium-roach 体系里的 Fetch 环境但如果你用的是老版本 gym可以直接 from gym.envs.robotics import FetchPushEnv。代码层面差别不大但 reward threshold 的默认值可能不一样最好在动手前先打印看一眼环境的 reward_range 和 success state 判断方式。3.2 关键代码结构与训练流程HER 的完整实现代码量并不大但结构上必须做对三件事轨迹分段存储、目标替换、奖励重算。下面是一个我能直接跑起来的伪代码流程你照着搭基本不会错。# 伪代码示意 1. 初始化环境 env gym.make(FetchPush-v2) 2. 初始化 actor-critic 网络比如 SAC 3. 初始化回放缓冲区 buffer容量 1e6 4. episode_buffer [] # 临时存一条完整轨迹 5. for episode in range(total_episodes): obs, _ env.reset() episode_buffer.clear() while not done: action policy(obs) next_obs, reward, terminated, truncated, info env.step(action) # 原始 transition 也存入 part episode_buffer.append((obs, action, next_obs, done, info)) buffer.add(obs, action, reward, next_obs, done) obs next_obs # 走完一条轨迹后对 episode_buffer 做 re-labeling for t, (obs, action, next_obs, done, info) in enumerate(episode_buffer): if random.random() P: new_goal sample_future(episode_buffer, t, k4) new_reward compute_reward(next_obs[achieved_goal], new_goal) replace_goal(obs, next_obs, new_goal) buffer.add(obs, action, new_reward, next_obs, done) # 更新策略 for _ in range(num_updates): batch buffer.sample(batch_size) update_actor_critic(batch)这个流程里有个容易迷惑的点为什么一条轨迹要存两次第一次是原始目标版本第二次是替换目标版本。原因是原始目标的经验仍然有价值尤其是当策略已经能偶尔成功时原始目标轨迹是“真正完成任务”的信号。如果你只存重标定版本算法会越来越偏于优化“任意可达目标”而丢失对指定目标的服从性。我在实现里处理的另一个细节是 terminate 的判断Fetch 环境的 done 分成了 terminated 和 truncated如果是超时截断轨迹其实并未失败不能简单地把整条轨迹扔进失败堆。HER 在这里有一点天然优势即使超时截断我们也能把最终位置当作目标重标定从而让这条轨迹变成有效训练样本。这个特性在实际的机器人生成场景里非常有用很多真实任务就是个“超时即失败”的设定HER 可以变废为宝。3.3 核心参数设置与调参心得HER 看起来只有一个“替换概率”要调但真正跑起来你会发现参数间是互相拉扯的。我把自己常用的一套参数列在下面这套配置用 SAC 跑 FetchPush 大约 40 万步就能把成功率拉到 0.7 以上。参数推荐值说明her_replay_prob1.0每条采样都做 re-labeling但 buffer 里同时存在原始样本不会丢失原目标信息future_k4future 策略里往后看 4 步K 越大目标越接近终点K 小则目标更接近当前buffer_size1e6需要存放足够多的轨迹片段太小人会导致重新标记后的样本过早被覆盖batch_size256 或 512SAC 下 256 起步DDPG 建议 512因为 DDPG 方差更大actor/critic lr3e-4SAC 的标准配置一般不需要额外调整gamma0.98 或 0.95Fetch 任务轨迹比较长gamma 太大会让价值估计不稳定update_per_step1 或 2每步环境交互后更新网络次数增大能加速收敛但不要超过 4否则经验利用过度关于 future_k 这个参数我多说几句。论文里给的建议是在 {1, 2, 3, 4, 5, 8, 10} 里选大部分环境 4 是稳健选择。K 值太小新目标离当前状态太近网络学到的几乎是“维持当前姿态”这种无聊行为K 值太大新目标离当前状态过远负载过重回传的梯度太稀疏。如果你发现训练曲线长期不动不妨把 K 从 4 降到 2 试试。曾有一个朋友跑 FetchSlide 一直失败最后把 future_k 改成 2明显好转因为滑动任务里当前几步的动力学影响最关键。还有一个很容易被忽略的参数是 reward threshold。Fetch 环境判定成功时直接检查 achieved_goal 和 desired_goal 的欧氏距离是否小于 0.05。做 re-labeling 时如果你也用 0.05 这个距离判定成功那么被替换目标后“成功”的样本会非常苛刻只有恰好轨迹时刻点与目标点距离小于 0.05 的才算成功。这会让正样本比例远低于预期。我的做法是把 re-labeling 的判定阈值稍微放宽到 0.06 或者 0.08这样在不破坏任务语义的前提下能多产生一些边界正样本。注意这只是训练时用评测时仍然用环境的原始阈值否则会把评测指标搞脏。4. 训练 HER 时踩过的坑和排查清单4.1 奖励计算错误最容易翻车的点HER 的代码量不大新手首次跑通时最容易挂在奖励重算上。我见过最多的写法是直接复用环境中 reset 时给出的 seed goal 来算奖励结果 curve 直接一条直线。最典型的错误有两类一类是计算距离时用的是 achieved_goal 和 desired_goal 之间的差但忘记对差取范数另一类是奖励符号反了本该 -1 给成了 1结果智能体拼命往远离目标的地方跑。我自己在实现时强制给自己加了一个断言函数专门检查转换后样本的成功率。做法是从 buffer 里抽 1000 条 re-labeling 过的样本统计它们的 new_reward0 的占比如果占比低得离谱比如小于 0.1%说明新目标选得太严或者距离阈值太小。这个检查 30 秒就能完成能拦下大量肉眼难以发现的错误。还有一类更隐蔽的错误next_obs 里的 achieved_goal 已经被环境更新到了下一个状态但你还是拿当前 obs 里的 achieved_goal 去算新目标和新奖励。正确逻辑是拿 next_obs 里的 achieved_goal 当“实际结果”因为这条 transition 的动作做完之后结果状态就是 next_obs。拿到后再和 new_goal 比较算奖励。如果你拿错了状态所有正样本都会被“我自己到底在哪”这个噪声污染训练结果非常诡异。4.2 训练不收敛、loss 爆炸的常见原因HER 在稀疏奖励下其实比普通算法稳但如果你发现训练完全不动先别急着调网络结构。用排除法检查第一步看 buffer 里有没有正样本第二步看 re-labeled 样本占比是否正常第三步看 Q 值是否发散。我自己踩过的坑之一是网络更新次数过多。SAC 在一个 step 里更新 4 次以上配合 HER 重写的奖励Q 值会开始出现周期性尖峰然后网络漂移成功率从 0.6 直接回到 0。把 update_per_step 降回 1 之后曲线又稳定下来。第二个常见问题是 actor 和 critic 的更新频率不匹配。HER 重造了大量“伪成功样本”这些样本会让 critic 的 target 快速变化如果 actor 的更新步长跟不上策略网络还在输出随机探索式的动作价值网络已经认定“随便动一动就能成功”于是整个训练进入一种假性停滞。这个问题在 DDPG 上尤其明显换成 SAC 之后因为策略自带熵正则稳定性会好很多。第三个原因我很少在文档里看到但实测快了目标替换后的 next_obs 里包含的是重新标定过的 desired_goal但是 reward 有时候用的是原环境自带的奖励公式这个公式可能会因为 obs 字段里加了一些额外信息而出 bug。Fetch 环境自带 reward 函数在 gym 某个版本以后会直接读取 done 的布尔值如果你在环境内部硬改 desired_goal结果变得不可控。我的建议是不要依赖环境自带的 reward 函数自己写一个def sparse_reward(achieved_goal, desired_goal, threshold0.05): dist np.linalg.norm(achieved_goal - desired_goal) return 0.0 if dist threshold else -1.0这样所有奖励都在你的控制下可复现性也更强。4.3 怎么确认 HER 真的在工作很多人跑完代码只看最终成功率但这不够。要确认 HER 是否真的在起作用可以盯两个指标。第一个是 buffer 里“替换成功后样本”的比例这部分比例会从 0 慢慢爬升中间会有一段硬化说明 re-labeling 在不断产生新正样本。第二个指标是 actor 损失的下降曲线它不应该一路振荡到底而是会在前几千步内快速下降然后进入缓慢优化区。另一个更直观的验证方式是做一个“消融对照”把 HER 开关关掉只用原始目标经验训练同一个网络。在 FetchPush 上基线基本是 0 成功率HER 能达到 0.7 以上。如果你的基线和 HER 都有差不多的成功率那大概率是你的环境任务还不够稀疏或者是奖励本身已经有很多信息泄漏了这时 HER 的增益自然不明显。我还养成了一个习惯每隔 100 个 episode 就手工录一段 rollout 视频并在画面上把 desired_goal 和 achieved_goal 画成两个不同颜色的小球。这样我能直观看到智能体到底在追哪个目标、有没有出现目标来回横条的情况。很多时候成功率曲线没变化但行为已经在悄悄改变视频比曲线更容易暴露问题。下面是几个高频问题与排查路径的速查表现象优先排查项解决方案训练完全没进展成功率保持 0检查 re-labeled 样本是否进 buffer确认 trajectory 分段存储逻辑确认替换概率设置的时机奖励曲线一开始上升随后暴跌Q 值发散降低更新频率调小学习率检查 reward 尺度训练到 30 万步后才突然起飞future_k 偏大或 buffer 太小调低 k 到 2扩大 buffer 到 2e6策略目标变成了追着任意点跑替换概率过高且原始样本不足把 her_replay_prob 降到 0.5 或 0.8保留更多原始目标样本动作剧烈抖动targeted 网络更新过强调整 polyak 系数到 0.995或降低 critic 学习率5. 从 hindsight 出发还能往哪里走5.1 把“事后目标”思路用到真实机器人场景HER 在仿真环境里很能打但迁移到真实机器人时会遇到一个很实际的问题真实环境中 achieved_goal 很难被精确定义。比如你想让机械臂把螺丝拧进孔里什么算是“实际达成”的目标位置、角度、力矩都可能是目标的一部分而且传感器噪声会让 achieved_goal 不稳定。我自己在真实机械臂平台上的经验是先抽象出可观测的“目标状态向量”把力觉、视觉、关节角拼成一个高维观测然后把目标定义成“期望观测向量”。这样 HER 的 re-labeling 依旧可以在物理状态粒子上操作。真实环境里还有一个好处你可以通过 re-labeling 把“尝试但失败”的实体轨迹变成训练数据这比单纯靠人工演示收集正样本廉价得多。当然真实环境的数据效率问题依然存在。一条真实轨迹要跑十几秒甚至几十秒HER 只能换目标不能减少实体交互次数。所以真机落地时通常会让 HER 先在仿真里启动策略学到初步技能后再迁移到真机配合域随机化微调。我的体会是HER 不是万能药但它可以极大减少真机调试时“策略撞了几小时仍然零反馈”的无意义等待。5.2 与随机网络蒸馏、经验加权等方法的搭配HER 的 hindsight 思想还可以和其他解决探索挑战的方向叠加。比如 RNDRandom Network Distillation负责在状态空间里找新奇区域HER 负责把到达新奇区域的路径重标定为正样本两者互补。实际操作中你可以把 RND 的探索奖励和 HER 的重标定奖励同时提供给 agent但要注意奖励的尺度平衡否则探索奖励会把 HER 的稀疏成功信号完全淹没。另一个方向是给 re-labeled 样本做重要性加权。不是所有重标定样本都同样有用早期探索阶段的重标定样本可能包含大量随机动作质量很低。我试过按“新目标与原目标之间的距离”来加权距离越远的样本权重越低相当于限制了重标定目标的跨度效果在某些环境里比一视同仁更好。不过这个方案每次都要调权重系数收益不算稳定。从更长远的视角看hindsight 这个词所代表的哲学——“用实际发生的结果来纠正原本不切实际的目标”——并不仅限于强化学习。在模仿学习里它对应着失败演示的重建在离线强化学习里它对应着保守估计与伪标签的平衡。理解了这个思想你会自然而然地设计出更符合自己场景的训练方案而不是硬套一个开源仓库。我自己在项目里后来的做法是把 HER 当成默认选项而不是一个需要额外论证是否该用的实验特性。只要是 off-policy 算法只要有可观测的目标状态我都会先把 HER 的 re-labeling 接上再考虑别的更花哨的组件。因为它带来的收益非常确定成本却低得惊人。最后分享一个我在调试时一直保留的小习惯在回放缓冲区里同时保留原始目标和重标定目标两组数据训练时每隔一段时间人工看一眼“原始目标经验的成功率”和“重标定目标经验的成功率”这两条曲线。如果原始目标成功率在稳定上升说明算法在真正解决原任务如果只有重标定目标在涨说明策略可能是在利用重标定的漏洞刷奖励。这个区分看起来简单却是我判断训练是否健康最直接的依据。希望这篇文章能帮你少走点弯路也欢迎你在交流区一起讨论 HER 在你任务里的表现。