
看到“hindsight”这个关键词我第一反应就是 OpenAI 那篇《Hindsight Experience Replay》。如果你也在做机器人控制类任务比如让机械臂把方块抓起来放进盒子里你一定体会过稀疏奖励带来的绝望环境只在你成功完成整个任务的那一刻才给出正向回报其他所有尝试全部是零。普通 off-policy 算法硬训跑个三百万步成功率照样纹丝不动。而 hindsight 这招本质上就是教会算法“事后复盘”没抓到想要的方块但抓到了旁边的杯子那就把“去抓杯子”临时当成这次尝试的目标于是这条失败经验立刻变成了一条有正奖励的样本。这篇博文会把 HER 的原理、采样策略、代码实现和我在 Gym 里踩过的坑一次讲透适合正在做强化学习基线实验或者被稀疏奖励折磨的读者。1. 理解 HER 前的第一关稀疏奖励到底有多“毒”1.1 一个让 DDPG 彻底失效的任务拿 OpenAI 的 FetchReach 举例。机械臂末端要从起点移动到一个随机生成的目标点阈值是 0.05 米动作空间是三维连续力。环境每一步都会调用compute_reward如果末端离目标小于阈值返回 0否则返回 -1。每个 episode 最多 50 步所以一条完全失败的轨迹总回报是 -50。问题就在这目标点是随机采样的机械臂前几步基本都在乱飞绝大多数 transition 的 reward 都是 -1。Critic 网络试图估计 Q 值但所有失败样本的回报都差不多它根本学不出“哪个动作更好”。Actor 网络则一直在输出随机动作今天涨一点明天跌一点成功率曲线就是一条平坦的直线。我在第一次跑 DDPG 时给这个任务训了 100 轮最终成功率 0%围观的人都觉得我代码写错了其实这就是稀疏奖励的日常。相比之下如果改用稠密奖励比如每一步给负的欧氏距离就算策略很差critic 也能从“距离缩短了一点”里获得梯度所以大家一开始都会下意识去做 reward shaping。但 reward shaping 是有代价的你得手工设计势函数而且设计不好会诱导策略钻空子比如绕远路消磨时间。HER 提供了一条完全不同的路——不改奖励改目标。1.2 人是怎么用“后见之明”学习的你会投篮吗我第一次投三分发力太大球过了篮筐撞到篮板弹了回来。这个尝试失败了但如果我重新定义目标为“把球打到篮板上沿”那我其实成功了。强化学习里这叫 credit assignment 的问题智能体已经做出一串动作产生了实际后果但我们因为“没有命中原始目标”而把这些动作全标成无效。后见之明就是把这些无效动作重新解释成对另一个目标的成功操作。HER 的核心假设是目标其实是一种条件变量。我们不需要让智能体在开始前就知道唯一正确的目标而是让它在探索后说一句“我原本想达到的其实是它实际到达的那个状态”。对随机策略来说任何轨迹都会以一定概率“成功”地碰到某个状态只要这个状态可以被当成目标。这样失败轨迹就被转化成了成功轨迹而且这些成功轨迹是智能体靠自己的动作真实走出来的不是假数据。1.3 目标重标注的数学表达假设一条轨迹里有个原始 transitiona当前状态 (s_t)b执行动作 (a_t)c转移到 (s_{t1})d原始目标为 (g)e奖励 (r_t reward(s_{t1}, g))如果 (s_{t1}) 没有达到 (g)那么 (r_t -1)这条样本对学习没有帮助。HER 的做法是从这条轨迹的后续状态中挑一个实际到达过的状态 (g)比如第 (t) 步的机械臂末端位置然后用 (g) 替换原始目标重新计算奖励(r_t reward(s_{t1}, g))因为 (g) 确实是轨迹后面的真实状态(s_{t1}) 通常离 (g) 不会太远因此 (r_t) 很容易变成 0成功。于是我们得到了一条新的 transition((s_t, a_t, s_{t1}, g, r_t))这条样本可以被放进 replay buffer 给 off-policy 算法使用。为什么这个 trick 有效因为任务是随机采样目标但智能体实际到达的状态分布也是随机的任何一条轨迹都可能对上某个“更容易的目标”。HER 相当于自动生成课程先从容易达成的目标学起再逐步逼近真实目标。2. HER 的四个采样策略和关键超参数2.1 future、final、episode、random 到底怎么选从论文里可以找到四种重标定目标的采样策略这里我把它们的区别整理成了一张表策略新目标的来源特点适合场景final整个 episode 最后一个状态的 achieved_goal目标固定简单容易学但可能离原始目标很远快看看训练管道是否跑通future当前 transition 之后的某个状态的 achieved_goal目标可达性最强难度适中论文推荐绝大多数稀疏奖励任务episode整个 episode 中随机一个状态的 achieved_goal比 final 更随机覆盖范围广目标状态分布复杂时random从已经见过的所有 achieved_goal 里随机选依赖 buffer 的多样性效果通常最差当你有大量真实目标样本时我自己的经验是future 策略是默认首选。它选择 (j \geq i)也就是说新目标来自当前时间步之后的某个状态这保证了“当前动作确实能把状态推向目标”这个因果方向是对的。final 策略常常让目标太“远”试几次之后你会发现成功率上不去random 策略则完全丢掉了 trajectory 的顺序信息效果不稳定。2.2 k 值一条经验要重标定几次k 表示每条原始 transition 额外生成几条 HER 样本。原始论文里试过 k2、4、8我复现下来的感受是FetchPush 这类任务 k4 就已经很稳k8 在训练后期会因为重标定样本过多导致模型对真实目标“关注度不够”反而出现成功率波动。这里有个容易被忽略的点k 只是每条 transition 生成的重标定样本数量。假如 k8那么 buffer 中每 1 条原始样本就对应 8 条 HER 样本采样时遇到 HER 样本的概率高达 8/9。如果策略一直学“任意目标都能到达”最后可能变成“为了到达而到达”原始任务目标反而被稀释。所以我建议在实现里做一层保护采样时强制 50% 的样本来自原始目标 transition剩下 50% 来自 HER 样本。这样做比单纯调 k 更直观有效也能缓解下面的 4.2 问题。2.3 离线学习是 HER 的地基HER 重标定是在历史轨迹上做的这些轨迹并非当前策略在线产生因此必须搭配 off-policy 算法。DDPG、TD3、SAC 都能用而在线策略算法比如 PPO、A2C直接用 HER 会有很严重的分布漂移问题因为重标定后的样本已经不服从当前策略的采样分布。我见过有人直接用 PPO HER结果还不如不用 HER。这不是 HER 的错而是算法特性不匹配。如果你非要在 on-policy 框架里用“后见之明”的思想一般只能做成 reward shaping 或 goal relabeling 后再用重要度采样修正复杂度会高很多。对于绝大多数场景老老实实选 DDPG 或 TD3 就够了。3. 实操手记给 DDPG 加上 HER在 Gym 上跑通 FetchPush3.1 环境与代码结构准备我用的是老版本的gym环境名是FetchPush-v1。安装的时候注意机器人相关的环境在gym[robotics]里面需要额外安装mujoco。如果你环境装不上至少可以减少一个维度换成FetchReach-v1这个环境更简单适合验证代码逻辑。FetchPush-v1的观测是一个字典核心字段有三个observation机械臂关节角、速度、物体位置等achieved_goal当前实际实现的目标比如方块位置desired_goal任务想要的目标HER 之所以能实现完全依赖于观测空间里区分achieved_goal和desired_goal。重标定时我们只需要改掉desired_goal其他字段保持不变。所以在设计自己的自定义环境时一定要把“实际状态”和“期望目标”分到两个独立字段否则后见之明无从下手。3.2 重写 ReplayBuffer核心就这么十几行HER 的 replay buffer 和普通 DDPG 不一样它需要按“episode”来存储因为重标定必须知道整条轨迹的后续状态。下面是一份简化但能跑的核心代码import numpy as np class HERBuffer: def __init__(self, capacity1000000, k4, strategyfuture): self.capacity capacity self.k k self.strategy strategy self.episodes [] def _store(self, transition): if len(self.episodes) self.capacity: del self.episodes[0] self.episodes.append(transition) def add_episode(self, episode): length len(episode) for i, trans in enumerate(episode): # 先保存原始 transition self._store(trans) # 再生成 k 条 HER transition for _ in range(self.k): if self.strategy future: j np.random.randint(i, length) elif self.strategy final: j length - 1 elif self.strategy episode: j np.random.randint(0, length) else: # random目标从所有已经见过的真实目标中选这里简化用整集 j np.random.randint(0, length) new_goal episode[j][achieved_goal] new_reward self._compute_reward( trans[next_obs][achieved_goal], new_goal ) new_done 1.0 if new_reward 0.0 else 0.0 self._store({ obs: trans[obs][observation], action: trans[action], reward: new_reward, next_obs: trans[next_obs][observation], done: new_done, goal: new_goal, }) def sample(self, batch_size): # 真正的实现里需要拼接 goal最终输入是 concat(obs, goal) # 这里只给逻辑不展开每一行拼接代码 transitions np.random.choice(self.episodes, batch_size) return transitions这段代码有一点很重要重标定后的 reward 是用实际转移到的 achieved_goal 和新目标算出来的而不是用原始 reward。如果你忘了这一步等于没有 HER。3.3 奖励与 done 的正确重标定在原版 Fetch 环境里compute_reward返回 -1失败或 0成功。重标定时直接用同样的函数计算新 reward 即可。代码如下def _compute_reward(self, achieved_goal, desired_goal): # 二值稀疏奖励 return -1.0 if np.linalg.norm(achieved_goal - desired_goal) 0.05 else 0.0但这里有一个隐蔽的坑done 标志。环境返回的 done 表示原始任务是否成功而重标定后的“成功”是假的。如果直接把new_done当作真实 episode 结束交给 Q-learning bootstrap会让 critic 误以为智能体可以“控制何时结束”导致价值函数失衡。我在实验里比较过两种做法done new_done训练初期的确更容易看到 Q 值上涨但容易高估价值后期震荡。done 0训练稍慢但更稳最终成功率更高。我现在的习惯是HER 样本的 done 统一设成 0原始目标样本仍然使用环境的 done。如果你用的是 TD3它对 done 的敏感度相对低一些用new_done也没太大问题如果用的是原始 DDPG建议保守一点。3.4 一套可以照抄的训练配置我经常用这套配置跑 FetchPush成功率能稳定达到 90% 以上参数取值算法DDPG或 TD3策略结构Actor / Critic 都是 256 x 256ReLU采样策略futurek 值4replay buffer1e6batch size128actor / critic 学习率1e-3目标网络 soft update 系数 tau0.05探索噪声OU noise 或高斯噪声std0.2每轮 episode 数50每 episode 最大步数50每轮更新次数40训练流程是每个 epoch 先跑 50 个 episode把整条 episode 塞进 buffer然后从这个 buffer 里采样 40 次去做梯度更新。我在 FetchPush 上跑大概 8 到 15 个 epoch 就能看到成功率从 0 跳到 80%到了 25 个 epoch 左右稳定在 95% 上下。4. 踩坑实录HER 训练中的五个常见问题4.1 训练半天不收敛十有八九是 reward 没设计对HER 最标准的奖励函数是二值稀疏奖励即成功给 0失败给 -1。这不是巧合而是因为重标定后的新目标是从真实轨迹里抽出来的如果 reward 是稠密的距离函数那么“接近任意目标”也会获得高的奖励会让 critic 对“是否真正达成目标”失去区分度。我一开始就犯过这个错把 reward 写成- distance结果成功率一直卡在 10% 左右。后来改成二值奖励同样超参没几轮就上了 80%。如果你的任务不是二值奖励至少也要保证 reward 函数是以“阈值”为核心的比如0 / -1而不是平滑的负距离。4.2 重标定样本淹没原始目标我前面提到 k8 时 HER 样本占比过高会导致策略“学偏”。最典型的表现是策略确实学会了移动机械臂但指定目标在左它偏要去右因为右侧在训练数据中出现的频率更高。解决方法有两个把 k 降到 4 或 2在采样函数里固定分配比例比如 50% 原始目标样本 50% HER 样本。第二种方法更稳健因为即使 k 调大原始目标也不会被完全淹没。4.3 Q 值发散和过高估计HER 本身不解决 Q 值过高估计。DDPG 在 FetchPush 上训到中后期我经常看到 critic loss 突然飙到几百然后成功率为零。常见原因有两个一个是done标志乱用导致价值回传过强另一个是目标网络更新太快。对付这个问题的组合拳是使用 TD3双 critic 取 min能大幅抑制过高估计或者把 tau 从 0.05 降到 0.005critic 学习率从 1e-3 降到 3e-4加 gradient clipping限制 critic 梯度的 L2 范数不超过 10。在 HER 代码里调这些超参比换一个复杂的 reward shaping 方案简单多了。4.4 成功率曲线出现“U 型反转”最讨厌的情况是前期已经训到 80%再跑几个 epoch 突然跌回 0。我踩过之后总结了背后的原因replay buffer 中早期随机样本太多随着训练推进策略分布逐渐偏离这些旧样本但 buffer 采样时仍会抽到大量旧数据导致 critic 对当前策略的价值判断严重偏置。缓解办法减少 buffer 容量从 1e6 降到 2e5让旧数据自然淘汰每个 epoch 增加更新次数从 40 次调到 80 次让策略更快跟上新数据或者给探索噪声加退火训练后期让动作更确定降低分布漂移。如果你看到成功率呈锯齿状忽上忽下基本就是 buffer 中旧数据比例太高优先调整 buffer 容量。4.5 快速定位表这里整理一张速查表帮你在训练过程中快速定位问题症状可能原因解决方案从一开始就收敛不了reward 不是二值、done 标志混乱改稀疏 0/-1 奖励HER 样本 done 置 0学了一点就卡住k 太小、没有用 future 策略k 设为 4策略切换为 future后期掉回零buffer 中旧样本占比太高减小 buffer增加更新次数Q 值爆炸过高估计、目标网络更新过快换 TD3调低 tau 和学习率目标被忽略HER 样本占比过高采样时保底 50% 原始目标样本4.6 独家小技巧再分享一个网上文档里不常写的东西HER 与随机目标采样联合使用时最好做 goal normalization。Fetch 系列的 goal 是三维坐标范围基本在 [-1, 1] 之间问题不大但如果你自定义环境的目标范围很大比如 [0, 100]HER 重标定出的目标会严重偏离训练分布。我建议在把 goal 喂给网络之前先减均值再除标准差让目标分布在 0 附近。这个操作对收敛速度的提升非常明显。5. 写在最后的一段个人体会拿了 hindsight 这个名字做关键词写这篇文章是因为我自己真的被它救过。去年做一个机械臂抓取项目环境只在抓起并且放到托盘成功时才给 reward我用稀疏奖励的 DDPG 训了两天成功率纹丝不动。后来加了 HER一天之内就看到了可用策略。我实际跑下来最稳定的一套组合是future 策略 k4 二值奖励 HER 样本 done 置 0 50% 原始目标保底。训练 FetchPush 约 20 轮就很稳FetchPickAndPlace 需要 50 轮以上但总算可以从容等待训练曲线上升而不是干瞪眼。总之真正给我留下印象的不是 HER 这个技巧本身有多么花哨而是它把“失败”变成了“训练资源”。如果你也在跟稀疏奖励对抗不妨先别急着设计花哨的 reward shaping给现有的 off-policy 算法加上 hindsight也许第一天你就会看到不一样的成功率曲线。