
单看 hindsight 这个词大多数人下意识会想到“后见之明”——事情发生之后一切看起来都那么理所当然。放在强化学习圈子里这个词还有另一层更硬核的含义2017 年 OpenAI 提出的 Hindsight Experience Replay后见经验回放简称 HER把“事后诸葛”这个通常被认为负面的认知偏差变成了一套让智能体从失败轨迹里硬生生学出正样本的算法。我最近重新完整跑了一遍 HER 在机械臂稀疏奖励任务上的训练流程从原理、代码到调参踩坑整个过程值得拿出来好好拆一遍。不管是刚入门 RL 的新手还是被稀疏奖励问题卡住的老手这篇应该都能给出一些能直接用上的东西。1. hindsight 再认识从认知偏差到强化学习算法1.1 后见之明偏差为什么人总是“事后聪明”心理学里有个著名的概念叫 hindsight bias翻译过来就是后见之明偏差。实验场景很常见让一群人预测一场比赛的结果等到比赛结束、结果公布之后再让他们回忆自己当初的预测大部分人都会高估自己“当时就猜对了”的概率。股票、球赛、项目复盘里到处都能看到这种影子——事后看一切都清晰事前却没人拍板。这个偏差常被当作认知缺陷来批判但从工程视角看它其实揭示了一个深刻的事实人类能从结果回推原因能够在知道答案之后重新校准自己的判断。问题只在于这种“回看”大多数时候发生在人的大脑里没有系统化。而强化学习在 2017 年做了一件很有意思的事它把这种“事后之明”写成了算法让智能体也学会从失败中回头找经验。这个东西就是 HER。1.2 HER把后验成功当作学习信号HER 的全称是 Hindsight Experience Replay中文通常译作“后见经验回放”。它解决的核心问题非常具体当环境只给稀疏奖励时智能体几乎学不到东西。传统强化学习里一条轨迹只有达到预设目标才算成功否则整条轨迹都是零奖励或负奖励。HER 提出了一个反直觉的操作如果预设目标是位置 A智能体跑到了位置 B那就把这条轨迹的目标临时改成 B重新算一遍奖励。听起来像作弊但它不是。把目标改成 B 之后这条轨迹在“以 B 为目标”的评判标准下确实是成功轨迹而智能体通过这种方式获得了大量“成功经验”。这些经验被存进回放缓冲区策略就有机会从中学到“如何到达一个状态”的通用技能。论文作者 Andrychowicz 等人在多个机械臂操作任务上验证了这一点同样是稀疏二值奖励不加 HER 成功率几乎为零加了 HER 之后能达到 80% 以上的成功率。现在 stable-baselines3 这类主流库已经直接内置了 HerReplayBuffer说明这个思路经过了大规模工程验证。2. 稀疏奖励困境与 HER 的解题思路2.1 稀疏奖励下智能体学不到东西的根因要理解 HER 的价值得先搞清楚稀疏奖励为什么难。想象一个机械臂需要把末端移动到随机生成的目标点状态是几十维的关节角、末端位置加上目标位置动作空间是连续力矩。奖励函数只有一条末端与目标距离小于阈值给 1否则给 0。这种情况下随机策略碰到一次成功目标就像中彩票。即使偶尔碰到一次单条成功轨迹的梯度信号也被淹没在大量零奖励样本里Q 函数几乎没有可学习的梯度。更麻烦的是成功了这一次也不代表学到了什么——因为高维连续动作空间里复现一次成功同样困难。打个比方让一个从没打过篮球的人直接投三分只告诉他“进了”还是“没进”没有弧度、力度、站位这些中间反馈他练上一整天也找不准手感。这不是网络容量不够的问题是反馈信号太稀疏学习过程根本没有方向。许多算法在这种任务上的表现都接近于随机猜测而 HER 改变的不是网络结构而是“样本的标签”。2.2 目标重标记如何让失败轨迹增值失败轨迹在传统框架里是废料在 HER 眼里却是金矿。一条轨迹记录了智能体从初始状态开始一路经过的连续状态变化——它实际到达了哪里、离哪个位置近、最后停在哪里这些信息全都在。原来的目标是 A智能体跑到 B传统强化学习会觉得整条轨迹都是失败。但 HER 会问一个问题如果目标不是 A 而是 B轨迹末端的智能体是不是已经成功了答案往往是“是”。于是同一条轨迹通过重标记目标被转化成了大量带正奖励的经验。策略看到“当你想要到达 B并且你确实到了 B 附近时当时的动作为什么是有效的”从而学到可迁移的到达技能。重标记的次数越多人工构造的成功样本就越多稀疏奖励的“稀疏”就被软化了。这里要注意一个容易误解的点HER 并不是让智能体欺骗自己说失败是成功而是把一个目标设定问题转化成了目标泛化问题。一个能够理解“目标 B 且到达 B”的策略稍加泛化就能理解“目标 A 且到达 A”。失败轨迹提供的是“如何到达某个状态”的运动学经验而不是“如何完成当前任务”的终极答案。2.3 适用边界HER 不是所有任务的银弹HER 有效的前提是目标可以被定义为状态空间中的一个点或一个可计算的集合而且奖励可以基于“与目标状态的距离”来判断。机械臂到达、推箱子、倒水这类任务天然满足。但遇到语义目标就麻烦了——比如目标是“把红色杯子放到蓝色盘子里”状态里没有一个明确的坐标可以直接衡量成功与否。再比如严格顺序任务煮饭必须先洗菜再切菜再下锅单纯到达某个中间状态不代表已经把任务往前推进了一步HER 的重标记逻辑就会失效。此外HER 对不可逆任务也有局限。如果环境一旦进入某个错误状态就无法恢复而目标重标记生成的正样本又都基于这种错误状态策略可能学到一些“看起来优秀、实际上没用的技能”。所以实践里我一般先用 HER 处理位置到达型稀疏奖励任务遇到语义目标就换其他方法比如学习奖励函数或引入课程学习。3. HER 算法原理拆解目标重标记的实现机制3.1 状态、目标与二值奖励的形式化定义形式化来看HER 建立在带目标的目标条件马尔可夫决策过程上写成元组就是 (S, A, G, p, r, γ)。与普通 MDP 最大的区别是奖励函数多了一个目标参数r(s, a, g)。对于 FetchReach 这类任务典型奖励就是r(s, a, g) 1, if ||sg - g|| ε 0, otherwise这里的 sg 是状态中“已经达到的目标”部分g 是期望目标ε 是成功阈值。同时策略 π(a|s, g) 和价值函数 Q(s, a, g) 都必须把目标 g 作为输入这就是 Universal Value Function Approximator 的思想。换句话说训练出来的不是一个只会完成固定目标的策略而是一个理解“目标”本身的策略。这个设定非常关键正因为策略吃进了目标和状态HER 把目标从 A 换成 B 之后重标记样本依然落在策略能力范围内。如果策略只能处理固定目标重标记样本根本无法学习。所以 HER 和 UVFA 是绑在一起的。3.2 重标记的具体流程与采样策略对比HER 的训练流程可以拆成五步使用当前策略在环境中跑完一整条轨迹得到状态序列、动作序列和真实奖励。对于轨迹中的每个时间步 t先按原始目标正常存入经验缓冲区。根据某种策略为这条轨迹重新选择一个新目标 g。用新目标 g 重新计算从状态 s_t 执行动作 a_t 之后的奖励。把重标记后的经验 (s_t, a_t, r, s_{t1}, g) 也存入缓冲区供后续采样更新。第 3 步选择新目标的策略直接决定了算法的表现。论文里对比了四种常见策略我用一张表总结采样策略新目标来源特点与适用场景final取整条轨迹最终状态实现最简单正样本比例高但目标分布偏简单复杂任务容易让策略退化future从当前时刻之后的未来状态里随机选一个目标难度梯度自然前期学习稳定论文最推荐的策略episode从整条轨迹里随机选一个状态目标分布更广但有时状态之间关联性弱学习方差偏大random从其他 episode 的最终状态里随机选跨轨迹泛化更好但早期难度太高一般不单独使用我实际测下来future 是最稳的。原因也不难理解选未来状态作为新目标意味着目标离当前状态不会太远重标记后的样本既带有正奖励信号又没有完全脱离智能体当时的动作能力范围。final 在简单任务上收敛快但在 FetchPickAndPlace 这种复杂任务上只看最终状态会让智能体丧失学习中间阶段运动技能的机会。3.3 支撑 HER 的理论基础UVFA 与 off-policy 训练HER 在工程上的成功还得益于 off-policy 训练框架。重标记产生的样本并不严格来自当前策略的数据分布如果使用 PPO 这类 on-policy 算法这些样本根本不能直接用。但 DQN、DDPG、TD3、SAC 这些算法都有经验回放机制天然允许利用历史样本甚至允许利用“部分伪造标签”的样本。这也是 stable-baselines3 里 HER 默认跟 DDPG 或者 SAC 配合的原因。之前有人尝试在 PPO 上硬接 HER效果很不理想本质上是数据分布假设被破坏了。理解了这一点在选算法搭配时就不会踩这个坑。4. 从零写一个 HER 最小训练框架4.1 环境与依赖选择代码层面我选择先用一个简单的点到达环境验证逻辑再切到机械臂环境中看真实表现。Gymnasium 生态里可以直接安装 gymnasium-robotics环境包括 FetchReach、FetchPush、FetchPickAndPlace 和 FetchSlide。我的环境配置是 Python 3.10 PyTorch 2.1 gymnasium 0.29 gymnasium-robotics 1.2显卡用不用其实都行FetchReach 这种低维状态任务纯 CPU 也能跑出结果。选环境有个小建议一开始不要直接上 FetchPickAndPlace优先级是 FetchReach 先跑通逻辑再上 FetchPush最后再挑战 PickAndPlace。稀疏奖励任务本身训练不稳定如果环境还选得太难很难判断是算法问题还是环境问题。我在 FetchReach 上先把框架跑通再切到 FetchPush 调参。4.2 带目标重标记的 Replay Buffer 代码HER 的核心改动集中在 Replay Buffer。这里我给出一个尽量简化的 PyTorch/NumPy 实现重点展示重标记逻辑import numpy as np class HindsightReplayBuffer: def __init__( self, capacity1_000_000, future_k4, threshold0.05, goal_dim3 ): self.capacity capacity self.future_k future_k self.threshold threshold self.goal_dim goal_dim self.buffer [] self.pos 0 def _compute_reward(self, achieved, goal): dist np.linalg.norm(achieved - goal) return 1.0 if dist self.threshold else 0.0 def _add(self, obs, action, next_obs, goal, reward): transition (obs, action, next_obs, goal, reward) if len(self.buffer) self.capacity: self.buffer.append(transition) else: self.buffer[self.pos] transition self.pos (self.pos 1) % self.capacity def add_episode(self, obs_list, action_list, achieved_list, original_goal): T len(action_list) for t in range(T): achieved_next achieved_list[t 1] # 1) 原始目标样本照常存入 r self._compute_reward(achieved_next, original_goal) self._add( obs_list[t], action_list[t], obs_list[t 1], original_goal, r ) # 2) 以概率生成一条重标记样本 # future_k / (future_k 1) 使得平均每条经验额外生成约 future_k 条 if np.random.rand() self.future_k / (self.future_k 1): future_idx np.random.randint(t 1, T 1) new_goal achieved_list[future_idx] r_future self._compute_reward(achieved_next, new_goal) self._add( obs_list[t], action_list[t], obs_list[t 1], new_goal, r_future ) def sample(self, batch_size): idxs np.random.randint(0, len(self.buffer), batch_size) obs, action, next_obs, goal, reward zip(*[self.buffer[i] for i in idxs]) return ( np.array(obs), np.array(action), np.array(next_obs), np.array(goal), np.array(reward), )这个实现刻意简化了一点真正的论文实现里每条原始经验通常会固定生成 k 条重标记样本而这里用概率方式生成 0 或 1 条。从期望值来看当 future_k4概率为 0.8平均每条经验会多出 0.8 到 1 条重标记样本量级差不多。实测下来学习曲线差异不大但代码逻辑清楚了很多。有一个细节必须提醒add_episode 里传入的 achieved_list一定是从环境返回的obs[achieved_goal]字段中收集的状态而不是完整观测向量。Fetch 环境的观测中同时包含observation、achieved_goal、desired_goal三部分重标记用的是 achieved_goal这是新手最容易弄混的地方。4.3 训练主循环与 DDPG 衔接要点有了重标记缓冲区剩下的训练循环就和标准 off-policy 算法差不多。以 DDPG 为例核心更新逻辑可以写成for epoch in range(n_epochs): obs_list, action_list, achieved_list [], [], [] obs, info env.reset() goal info[desired_goal] terminated, truncated False, False while not terminated and not truncated: action actor(torch.cat([obs[observation], goal])) noise next_obs, reward, terminated, truncated, info env.step(action) obs_list.append(obs[observation]) action_list.append(action) achieved_list.append(obs[achieved_goal]) obs next_obs achieved_list.append(obs[achieved_goal]) buffer.add_episode(obs_list, action_list, achieved_list, goal) for _ in range(num_gradient_steps): obs_b, action_b, next_obs_b, goal_b, reward_b buffer.sample(batch_size) with torch.no_grad(): target_q reward_b gamma * critic_target( next_obs_b, actor_target(next_obs_b, goal_b), goal_b ) critic_loss F.mse_loss(critic(obs_b, action_b, goal_b), target_q) # critic 更新、actor 更新、目标网络软更新按 DDPG 常规流程走注意 actor 和 critic 的输入都把 obs 和 goal 拼在一起了。很多参考代码里网络输入只写了 state忽略了 goal导致目标重标记起不到任何作用。这个不起眼的细节决定了 HER 到底有没有真正生效。5. 训练调参与工程化避坑实录5.1 四项目标采样策略的实测效果我分别在 FetchReach 和 FetchPush 上测过四种目标采样策略。表现如下final 在 FetchReach 上收敛快但在 FetchPush 上成功率到 40% 左右就停滞了原因是最终状态离初始状态太近重标记出来的正样本全都是“接近末尾”的样本策略缺少对中间过程的探索。episode 策略训练曲线波动很大因为随机选一个中间状态作为目标时有时目标远、有时目标近难度节奏不可控。random 单独用效果最差早期几乎完全学不动。future 是综合最优的选择。它选的是“未来某个时刻的状态”这意味着目标永远在当前状态之后的前方不远处策略只需要在当前动作基础上略微调整就能接近目标天然形成了一条难度递进的课程。这个结论和论文里的实验一致但这几年的开源项目里仍然有不少人为了省事只用 final结果任务稍复杂就报错说“HER 没用”——其实并不是 HER 没用是目标采样策略选错了。5.2 网络、学习率、buffer 容量等超参数推荐给出我自己在 FetchPush 上比较稳定的超参数组合可以作为起步配置参数推荐值说明隐藏层[256, 256]两层 MLP 足够不需要更深激活函数ReLU简单稳定必要时隐藏层加 LayerNormactor 学习率1e-4调大容易动作发散critic 学习率1e-3比 actor 大一阶是常规操作gamma0.98连续性任务 0.99稀疏任务 0.98 更好训batch_size256越大越稳但速度下降buffer 容量1e6Fetch 类任务建议至少 50 万future_k41 也有效8 提升有限且占内存探索噪声N(0, 0.1)前期可以放大到 0.3后期衰减目标更新 tau0.05比默认 0.005 略激进能加快学习训练时最关键的一点别盯着训练 reward 看。因为重标记样本拉高了整体平均奖励训练 reward 没有真实的物理意义。我通常在固定间隔跑 10 个测试 episode把“成功率”作为唯一核心指标。如果训练 reward 在涨但 success rate 没动那大概率是智能体学会了在重标记样本上刷分并没有真正掌握到达目标的能力。5.3 Fetch 类环境中容易踩的五个坑坑一混淆 achieved_goal 和 desired_goal。重标记的新目标必须是实际达到的状态而不是环境给的期望目标。代码里写反了奖励永远为 0HER 完全失效。坑二future 采样索引越界。采样范围必须是[t1, T]如果不小心取到当前时刻 t那么目标是当前状态所有样本都会变成正奖励策略会变得过度乐观、毫无区分度。坑三重标记比例过高。有一回我把 future_k 设到 16buffer 里几乎全是人工成功样本策略确实收敛很快但换成新初始位置后成功率惨不忍睹。过度依赖人工成功样本会牺牲泛化能力4 到 8 是比较合理的区间。坑四观测不归一化。Fetch 环境的关节角、位置坐标量纲不一样如果直接拼到 obs 里喂给网络训练波动会相当大。建议用 running mean / running std 做归一化或者至少对所有维度做减均值除方差。坑五只在成功轨迹上做重标记。这是初学者最容易犯的误解。HER 的全部意义就在于处理失败轨迹——成功轨迹本身已经能提供正样本压根不需要重标记。如果你只在成功轨迹上调用重标记逻辑那失败轨迹仍然全是零奖励算法退化成普通 DDPG。6. 常见问题速查表与排查思路6.1 典型问题速查表我把实际跑 HER 时遇到过的典型问题整理成了一张速查表现象大概率原因排查方向训练很久成功率始终为 0奖励分布全是 0或噪声过大打印 buffer 中正样本比例确认重标记是否生效训练 reward 在涨但成功率不动过度依赖重标记样本泛化能力不足降低 future_k增加随机目标混合比例训练后期频繁震荡学习率偏高或目标网络更新太快降低 actor/critic 学习率调小 tauloss 突然变 NaN观测未归一化或梯度爆炸检查状态范围加入归一化和梯度裁剪换随机初始状态后效果明显变差训练时固定了 seed 或初始目标确认 rollout 时每次 reset 都随机化初始状态成功率始终卡在 40%-60%阈值 ε 设置不合理或 future 采样失效适当放宽阈值到 0.1确认重标记代码路径被执行6.2 一套快速排查顺序遇到 HER 训练不理想时我建议按这个顺序排查。先看 buffer 里的正样本数量——如果 sampled batch 中 reward1 的比例低于 5%说明重标记逻辑可能有问题或者轨迹太短导致 future 选不出好目标。其次看奖励分布是否合理重标记样本和原始样本的比例是否接近 future_k 的设定。然后检查网络输入维度确认 goal 拼接进去了。最后再看超参数尤其是探索噪声和阈值 ε。这套顺序能覆盖大多数问题因为多数坑不在算法理论而在数据流。HER 本质上是一个数据加工方法数据流一旦出错训练结果必然出问题而网络结构再怎么调都救不回来。7. 从算法到方法论hindsight 思维的泛化价值7.1 为什么说 HER 是“事后聪明的工程化”花了一段时间理解 HER 之后我越来越觉得它不仅仅是强化学习里的一个 trick还带有很强的工程方法论色彩。心理学里 hindsight bias 通常被定义为认知缺陷指人在知道结果后高估自己事前判断。但 HER 反而把“事后知道答案”这件事变成了一种学习机制既然失败轨迹已经摆在这里与其懊恼没有成功不如重新定义目标从已经发生的结果中构造正反馈。这种思路放到日常项目复盘里同样适用。很多团队做完一次失败的实验结论往往只有一句“结果不符合预期”然后所有数据都成了废料。但换个角度想这次实验至少确定了哪些方向是无效的哪些参数区间是值得进一步探索的这些信息本身就是“重新定义目标后的正样本”。HER 教会我们一件事不要轻易丢弃失败数据失败数据中往往藏着一个可以被重新定义的、接近成功的标签。7.2 个人体会与可扩展方向我现在拿到一个稀疏奖励任务第一反应已经不再是花大量时间设计 reward shaping而是先把 HER 架起来跑一个 baseline。原因很朴素reward shaping 需要领域知识而且很容易引入局部最优HER 几乎不需要领域先验只需要定义好“目标状态是什么”以及“成功阈值是多少”基本盘就很稳。在这个 baseline 上再去叠加课程学习或者更细的奖励设计效果调优通常更快。顺着 HER 这条路有几个扩展方向我一直在关注。一是自适应目标生成让重标记的目标分布随着训练进度动态变化避免早期目标太难、后期目标太简单二是 HER 与课程学习结合用环境自动生成从易到难的目标序列三是基于模型的 HER用学习到的动力学模型生成虚拟轨迹再做重标记能进一步提升样本效率。我的理解是HER 不会让困难任务变简单它只是让每一次失败都变得有价值。对于做强化学习的人来说这种心态和算法本身同等重要。现在我每次看到一条 zero-reward 轨迹都会下意识想如果我把目标改一下这里面有多少可以被救回来的经验这个思维习惯就是从跑通那套 HER 代码开始的。