先说一个我实际遇到过的情况:用随机策略去控制一个六轴机械臂,让它在桌面推动积木,目标是到达某个红点位置。在连续动作空间里,这个“随机撞上目标”的概率低到可以忽略——六个关节角度连续取值,稍微偏一点落点就天差地别。于是整个训练过程陷入一个怪圈:没有任何成功样本,奖励全是0,策略网络收不到梯度,不管迭代多少轮都在原地打转。这个怪圈有个专门名字,叫稀疏奖励问题(sparse reward problem),而今天要聊的 hindsight——更准确地说是 Hindsight Experience Replay(HER,后见经验回放)——就是专门用来撬开这个怪圈的方法。
“hindsight”中文通常译作“后见之明”,通俗讲就是“事后诸葛亮”。在决策科学里,事后复盘常被认为带有认知偏差,但在深度强化学习里,把它变成一种系统化的训练信号后,效果出乎意料地好。这个思路最早来自 2017 年 OpenAI 团队的论文,之后迅速成为多目标强化学习、机器人操作任务里最常用的技巧之一。顺带说一句,hindsight 这个词在工程监控领域也有一款同名工具,但今天只聊强化学习里的 HER,不展开那个方向。
这篇内容适合谁看?一个是对强化学习有一点基础、正在做稀疏奖励任务的开发者;另一个是想在机器人仿真环境里快速跑通目标导向任务的工程同学。读完你至少能理解 HER 为什么有效,怎么把它接进自己的训练代码,以及哪些坑我替你先踩过了。
1. 为什么强化学习会在稀疏奖励里卡死
1.1 稀疏奖励不是“没奖励”,而是“没梯度”
先说清楚什么是稀疏奖励。以机械臂推积木为例:任务目标是“把积木推到红点中心”,状态是机械臂关节角度和积木坐标,动作是六个关节的目标角速度。奖励函数往往写得很简单:
- 积木与目标点的距离小于阈值,r = +1;
- 否则,r = 0。
这个奖励定义很清楚,没有任何歧义。但问题在于:在几十万步随机探索里,策略几乎一次都碰不到那个 +1。于是经验池里全是 r = 0 的样本,时间差分误差虽然在算,可目标值也都是 0,Q 网络学到的全是“无论怎么做都没好处”,策略自然也就学不到东西。
用生活话讲,这就像一个学生做了两个月的题,但所有题批改结果都是零分,而且老师不告诉他离及格差多少,只给“过”或“没过”。他连往哪个方向改都不知道。稀疏奖励的本质不是“奖励少”,而是“学习信号里没有梯度方向”。
我早期做过一个实验,在没有 HER 的情况下训练一个推积木任务,训练了 100 万步,成功率恒为 0。查看每个 epoch 的平均奖励时,那条曲线是一条贴地的直线,一点起伏都没有。那一刻你会真切理解什么叫“梯度消失”在非神经网络层面的体现。
1.2 常见的破解套路,为什么总差点意思
面对稀疏奖励,教科书里通常给出的方案有四类,每类都有它的代价:
奖励塑形(reward shaping)是最直接的思路:把 r = 0/1 改成 r = -distance_to_goal,这样每一步都有了连续梯度。但麻烦在于,如果你把“距离缩短”作为奖励,策略很容易停在“靠近但不能完全到达”的局部最优;而且塑形函数的系数极其敏感,调不好就会出现绕着目标点打转却拿不到最终奖励的怪象。
课程学习(curriculum learning)也很常见:先让目标出现在离机械臂很近的位置,训练成功后逐步拉远。听起来合理,但你需要手动为每个任务设计“从易到难”的序列,任务一换,课程也得跟着换。这属于人工先验成本很高的方案。
内在奖励(intrinsic motivation)是另一条路,典型代表是 ICM、RND 这类“好奇心”机制,鼓励智能体探索未见过的新状态。这类方法有效,但状态空间一大,计算开销明显上升,而且有时会鼓励智能体沉迷于随机噪声区域。
演示学习(learning from demonstration)思路是直接给一些专家轨迹,让模型模仿。问题是这需要额外获取专家数据,在很多物理环境里,采集一条可用的示教轨迹并不比训练策略便宜。
这四类方案都有一个共同点:它们都在“如何产生更多有效探索”上做文章。而 HER 的思路完全不同——它不改探索,也不改奖励函数,它改的是“已经发生的失败经验”的标签方式。这个视角差,是它最聪明的地方。
2. HER 的核心思想:用“后见之明”给经验重新贴标签
2.1 失败轨迹里其实藏着大量“成功片段”
让我用一个更具体的例子展开。机械臂推积木的任务中,一次完整的 episode 可能是这样的:
- 目标位置 g = (0.3, 0.2, 0.05);
- 策略从初始状态出发,推了 50 步;
- 积木最终停在 (0.15, 0.4, 0.05),离目标点差距很大;
- 整条轨迹没有获得任何奖励。
如果以“原始目标 g”来标记这条轨迹,它就是一条彻底的失败样本,对训练没有任何正向贡献。但如果换个角度:积木虽然没到 (0.3, 0.2),可它确实从初始位置被推到了 (0.15, 0.4)。那么请问,如果目标一开始就是 (0.15, 0.4),这条轨迹是不是一条成功的轨迹?最后一步是不是就应该拿到 r = +1?
HER 做的就是这个事:把轨迹里“实际达到的状态”拿出来,作为新的目标,重新给这条轨迹的每一步计算奖励。原来那条零奖励的失败轨迹,经过 relabel 之后,至少最后一段变成了有正奖励的成功经验。这就是“后见之明”——我事先不知道目标在哪里,但事后我可以说,“既然你到了这里,那这次的目标就是这里”。
实际算法里,HER 并不会把整条轨迹的每一步都变成成功经验,只有真正到达新目标的那个时刻以及之后的状态才是 +1。但哪怕只是给训练注入一个正样本,也足以让 Q 网络尝到“甜头”,开始往正确方向调整。
2.2 为什么可以随便改目标?因为目标本来就是输入
很多第一次接触 HER 的人会问一个很关键的问题:把目标从 g 改成 g’,相当于篡改训练数据,这真的没问题吗?这不会让策略学到了错误的目标吗?
答案在于强化学习任务的定义方式。传统单目标任务里,目标被写在奖励函数里,是环境的一部分,改不得。但 HER 适用于 goal-conditioned 任务,也就是“给定一个目标,输出对应动作”的设定。在这类设定中,目标 g 不是环境属性,而是作为网络输入的一维向量存在:
- 状态空间:state = concat(observation, g);
- 策略:π(a | observation, g);
- 奖励:r = 1{φ(s_next) 与 g 的距离 < δ}。
也就是说,目标 g 是条件信号,同一个状态转移可以搭配不同的 g 使用。比如一条“从 A 点推到 B 点”的状态转移序列,在目标为 C 时是失败样本,在目标为 B 时就是成功样本。HER 只是把原来那条经验从“g=C 的失败样本”重新打包成“g=B 的成功样本”,并没有改变状态转移本身的物理逻辑。
这个设计真正学出来的能力,不是“推到某个特定点”,而是“给定一个目标点,我能学会把积木推向它”的通用映射。这就像刷题时,你不仅学习“解某一道题”,而是通过大量题目学习“解某一类题”的方法。HER 的 relabel 相当于变相扩充了训练题目集,而且这些题目都是策略自己“做出来”的,不需要人工标注。
2.3 关键细节:新目标从哪里采样
目标可以改,但改成什么是有讲究的。HER 原始论文里对比了几种新目标来源,这也是我后来才发现差别极大的地方:
- final:把轨迹的终点状态作为新目标;
- future:在轨迹中当前时刻 t 之后的状态里随机选一个作为新目标;
- episode:在同一个 episode 的未来状态里随机选;
- random:从整个经验池的任意状态里随机选。
读论文时我觉得 final 和 future 应该差不多,但实际测试下来差异不小。final 的缺点是:一条轨迹只有一个终点,整条轨迹 relabel 后,只有最后几步能拿到正奖励,中间大部分经验仍然是零奖励,信息增益有限。而 future 策略可以在轨迹的不同位置多次采样,把中段的状态也变成潜在目标,相当于把一整条轨迹里的“中途成就”都挖掘出来了。
论文推荐的默认配置是 future 策略 + K=4。K 的含义是:每一条真实经验,额外生成 4 条 relabel 经验存入 buffer。也就是说,经验池里原始经验只占 1/5,relabel 经验占 4/5。这个比例听起来激进,但实验下来确实效果最好。我个人的经验是,如果训练任务的目标空间特别大,K=4 是稳健起点;如果目标空间比较小,K 减到 2 也能快速收敛,还能省内存。
3. 从想法到代码:HER 的实现细节
3.1 整体架构:无需改探索,只改经验处理
HER 最大的工程优势是侵入性很小。它的前提是训练算法本身是 off-policy 的,也就是有经验池的算法,比如 DQN、DDPG、SAC 都行。你不需要改策略网络结构里的探索部分,也不需要改 env 的奖励函数定义,只需要在“数据入池”时多存一份信息,在“采样训练”时多做一次目标替换。
完整的数据流是这样的:
- 策略在环境中跑一个 episode;
- 每一步都保存 observation、action、reward、next_observation、achieved_goal、done;
- episode 结束后,把整条轨迹的原始经验存入 buffer;
- 同时按 K 值,为这条轨迹生成额外 K 份 relabel 数据,也存入 buffer;
- 训练时从 buffer 里随机采样 batch,正常算 Q 更新。
在这个流程里,探索策略本身完全没变。这意味着你可以把 HER 当成一个“后处理插件”挂在任何 off-policy 算法后面。我第一次接进 DDPG 的时候,整个改动大约只有几十行,训练效果完全是另一个量级。
3.2 核心代码逻辑:relabel 怎么实现
我用 Python 伪代码说明最核心的两个函数。第一个是“判断是否到达目标”的奖励函数:
def compute_reward(achieved_goal, desired_goal, threshold=0.05): # 用欧几里得距离判断是否到达目标 dist = np.linalg.norm(achieved_goal - desired_goal) return 1.0 if dist < threshold else 0.0注意这里的 achieved_goal 是从 observation 里分离出来的子向量。在机械臂任务里,observation 通常包含机械臂关节角度、积木位置、目标位置等多个信息,其中积木位置就是 achieved_goal,目标位置就是 desired_goal。一定要在环境设计时把这两个字段独立出来,否则后面无法做 relabel。
第二个是 relabel 的核心逻辑:
def relabel_trajectory(trajectory, original_goal, k=4): # trajectory 是 [(obs, action, achieved_goal, next_obs, done), ...] relabeled = [] for step_idx, transition in enumerate(trajectory): obs, action, achieved_goal, next_obs, done = transition # 原始经验:保留一份 relabeled.append({ "obs": obs, "action": action, "goal": original_goal, "reward": compute_reward(achieved_goal, original_goal), "next_obs": next_obs, "done": done }) # 额外生成 k 份 relabel 经验 for _ in range(k): # future 采样:从当前步之后的状态里随机选一个新目标 future_idx = np.random.randint(step_idx, len(trajectory)) new_goal = trajectory[future_idx].achieved_goal relabeled.append({ "obs": obs, "action": action, "goal": new_goal, "reward": compute_reward(achieved_goal, new_goal), "next_obs": next_obs, "done": done }) return relabeled这里有一个非常容易踩的坑:relabel 时不仅要改 goal,还要重新计算 reward。很多人第一次实现时只改了 goal,忘了改 reward,结果经验池里出现大量“目标已经换了但奖励还是老的”的错误数据,训练直接乱掉。
另外,done 标志也需要注意。在稀疏奖励任务里,如果新目标是轨迹终点,那最后一步确实到达了目标,done 可以设为 True,用来启动自举(boostrapping),给 Q 网络一个明确的终止信号。但如果新目标是从中间状态采样的,后续还有几步才到终点,那 done 应该保持 False。最简单稳妥的做法是:除了最后一步到达“新目标”的情况,其余一律 done=False。我甚至建议在早期验证阶段直接强制所有 relabel 数据的 done 都是 False,先确认其他环节没问题再说。
3.3 超参配置:照着抄也能稳的版本
我把自己在仿真环境里验证过的一组配置列出来,环境是 OpenAI Gym 的 Fetch 系列任务,算法是 DDPG+HER:
| 参数项 | 我的配置 | 说明 |
|---|---|---|
| replay buffer 大小 | 1,000,000 | 必须大,relabel 会放大经验数量,buffer 太小容易覆盖早期经验 |
| batch size | 256 | 适中即可,太大并不会显著加速 |
| relabel 比例 K | 4 | 论文默认,大多数任务都合适 |
| 新目标采样策略 | future | 比 final 稳定,中段信息利用充分 |
| actor 网络结构 | 三层 MLP,每层 256 | 目标条件策略,输入 concat(observation, goal) |
| critic 网络结构 | 三层 MLP,每层 256 | 同样输入 concat(observation, goal, action) |
| 动作噪声 | OU 噪声或高斯噪声,标准差 0.2 | 稀疏奖励任务需要较长时间探索,噪声不能太小 |
| 优化器 | Adam,学习率 1e-3(critic)、1e-4(actor) | critic 学习率略高一点收敛更稳 |
| 目标网络软更新系数 | 0.05 | DDPG 常用范围 0.01~0.1 |
| 折扣因子 γ | 0.98 | 稀疏奖励任务中建议略低,避免长期信用分配压力过大 |
有几个细节值得展开说说。第一,buffer 容量一定要给足。relabel 会把经验放大到原来的 K+1 倍,意味着 1e5 的 buffer 实际能存的有效原始经验只有 2 万条左右,稀疏奖励任务本身又需要大量探索样本,buffer 太小会频繁覆盖掉稀有的正样本。第二,我在 relabel 时有意识地保留了原始经验,而不是全部替换。虽然 relabel 经验对学习“达成任意目标”很有帮助,但原始目标经验也不能完全丢,否则策略在真正需要推到指定点的时候,会缺少“朝指定目标优化”的信号。保留原始经验的比例我通常控制在 1/(K+1) 左右,正好和 K=4 的设定匹配。
还有一个容易忽视的细节:目标向量和 achieved_goal 向量的维度必须一致,而且物理量纲最好也一致。如果你的目标是用三维坐标表示,而 achieved_goal 是机器人末端的四元数姿态,这两个向量就不能直接替换。我在一个任务里吃过这个亏:目标空间是位置坐标,网络输入里却拼接了姿态角,结果 relabel 出来的新目标在语义上根本不对应,训练怎么跑都不收敛。后来把所有目标统一成“位置坐标”后才正常。
3.4 奖励函数与状态空间的匹配问题
HER 能工作的前提是:环境的状态里必须显式包含“当前任务达成到什么程度”的信息,也就是 achieved_goal 必须从 observation 中完整读取出来,且不能有歧义。
举个例子:判断一个积木是否到达目标位置,你需要知道积木当前的位置坐标,这在 observation 里通常是现成的。但如果你想让机械臂学习“把瓶盖拧紧”,achieved_goal 应该是“瓶盖当前旋转了多少弧度”,如果 observation 里只有一个 RGB 相机图像,那你就得先训练一个感知模块从图像里估计瓶盖状态,HER 的 relabel 才能做下去。感知误差会直接污染 relabel 后的目标,这是很多真实机器人任务里 HER 效果打折的原因之一。
所以在设计状态空间时,我的建议是:尽可能把任务相关的低维状态量直接暴露给策略。不要痴迷于端到端从像素学习,在大部分控制任务里,先用真值状态把控制逻辑跑通,再考虑感知部分,工程上要稳得多。
4. 实测效果与踩坑记录
4.1 加了 HER 之后,训练曲线到底长什么样
我在 MuJoCo 的 Fetch 系列环境里做过几组对比实验,不带 HER 的 DDPG 在 FetchReach 和 FetchPush 上基本不收敛,成功率曲线是一条贴地的直线。这话我说得很绝对,因为事实就是如此:随机探索撞不上目标,没有正样本,Q 网络学不到东西。
接入 HER 之后,曲线形态非常有意思,不是平滑上坡,而是“长平台+跳变”的结构。一开始很长一段时间成功率还是 0,但经验池里 relabel 出来的正样本在悄悄积累。然后某个时刻突然出现一些成功率大于 0 的试探性尖峰,接着成功率在几十个 epoch 内抬升到一个平台。这个“突然抬升”的阶段,正是 Q 网络第一次理解到“目标距离越近,价值越高”的时刻。
如果你画的曲线长时间没有任何跳变迹象,先别急着调网络结构,大概率是 relabel 逻辑有 bug,或者 achieved_goal 字段提取错了。我个人会先用 FetchReach 这种简单任务做代码验证,它只有单步到达目标,没有物体交互,跑通很快,能快速排除大部分实现问题。
4.2 六个常见问题速查表
我在反复折腾 HER 的过程中,整理了一张排查表,分享出来可能帮你省几天时间:
| 现象 | 可能原因 | 排查与解法 |
|---|---|---|
| 加了 HER 依然完全不收敛 | relabel 时没有重新计算 reward,或 achieved_goal 与 goal 维度不匹配 | 单独写一个 test 函数,打印 relabel 前后某条数据的 goal 和 reward 是否匹配 |
| 训练前期震荡特别大 | buffer 太小,正样本被过早覆盖 | 加大 buffer,优先保证原始经验越多越好 |
| 前期能涨但总是卡在某个成功率上不去 | K=4 的 relabel 经验太多,挤占了原始目标经验 | 把 K 降到 2,或者保留更多原始经验的比例 |
| 偶尔出现一波成功率高,但随即跌回 0 | 稀疏奖励天然会有较大的方差 | 用滑动平均看趋势,不要用单次 epoch 的最高值判断效果 |
| 同样的配置换一个种子结果差异巨大 | 稀疏奖励下随机种子影响非常敏感 | 每个配置至少跑 5 个种子,修炼“多试几次”的心态 |
| 用 SAC+HER 时训练不稳定 | SAC 自动调节熵的机制和 relabel 后的目标分布有交互 | 可以临时固定熵系数 α,先验证 HER 本身是否工作正常 |
第二个问题我特别有体会。在一组实验里我把 buffer 设成 1e5,结果训练前期出现了“刚刚学到一点、马上忘掉”的反复,后来把 buffer 加到 1e6 才稳住。经验池大小在普通连续控制任务里可能没那么敏感,但稀疏奖励任务里,正样本太稀缺,任何一点有效经验都非常宝贵。
4.3 关于实操心态的几个建议
第一,HER 不是万能药。它解决的是“经验标签稀疏”的问题,如果探索方向本身就完全错误——比如机械臂往远离目标的方向运动,而动作空间又限制了它无法回头——HER 也救不了你。这种情况下要回头检查状态表示、动作空间设计,或者引入一些先验知识来约束初始探索方向。
第二,验证 HER 代码时,不要一上来就跑大任务。先用一个你能用肉眼判断正误的简化环境:比如一个 2D 点形机器人,环境只有一个点需要到达目标位置。如果连这种环境都不收敛,说明代码有问题,而不是任务太难。我在一个高维任务上足足排查了两天,最后发现只是 future_idx 的采样范围写错了,在单点环境里这种 bug 一眼就能看出来。
第三,训练过程中建议定期保存模型,并且把每个阶段的 checkpoint 都留一份。HER relabel 后的经验池会让模型在训练中后期变化很快,有时候一个 checkpoint 看起来快收敛了,但继续训练一段后反而变差。有备份的话,可以回退到最好的版本继续微调,而不是从头再来。
我自己在机器人操作任务里的整体感受是:HER 是我遇到过的,性价比最高的稀疏奖励解决方案之一。它不要求你设计复杂的探索策略,也不依赖专家数据,只需在数据流上做一层“事后改写”,就能把大量看似无用的失败轨迹变成训练燃料。这种思路本身也值得记在心里:有时候问题解决不了,不是缺新数据,而是旧数据里有太多被浪费掉的信息。