第一次看到 hindsight 这个词,是在一份强化学习论文的标题里。当时我刚被一个稀疏奖励的机械臂抓取任务折磨了一周:环境里每一条轨迹几乎都是零奖励,网络训练跑了两天,成功率纹丝不动,TensorBoard 上的曲线像一条心电图直线。那种感觉,用过一句话形容就是“RL 后期最难的不是网络结构,不是目标函数,而是怎么在满屏零奖励里硬挤出一点梯度信号”。而 hindsight,对应的完整技术叫 Hindsight Experience Replay(HER,事后经验回放),正是专门治“智能体跑了半天,奖励纹丝不动”这种病的。
这篇文章不打算复述论文公式,我想把我在实际项目里用 HER 处理稀疏奖励的经验、踩过的坑、以及它真正的适用范围一次讲清楚。适合正在调 RL 环境的同学、做机器人抓取和仿真导航的朋友,以及所有被 reward=0 逼疯、甚至开始怀疑人生的人。读完你至少能明白三件事:HER 为什么有效、怎么把 HER 接到自己的训练代码里、以及哪些场景它救不了你。
1. 为什么一张全是 0 的奖励表会把智能体逼疯
1.1 稀疏奖励的本质:99% 的样本都是无效样本
先回到最基础的场景。假设你训练一个智能体走迷宫,终点放一个奶酪,只有碰到奶酪才给 +1 奖励,其他动作全部给 0。这条奖励函数看似简单,但你要知道,RL 优化的本质是“从数据里估计哪个动作更好”。如果一段 100 步的轨迹只有第 100 步有 +1,那么前面 99 步的 (state, action, 0, next_state) 样本,对 actor 来说几乎不携带任何“该往哪走”的信息,因为 TD 误差大部分时间都接近 0,梯度传回去非常微弱。
更致命的是探索问题。在连续动作空间里,比如一个 7 自由度的机械臂,动作取值范围是 [-1, 1]^7,随机初始化策略能“恰好碰到目标”的概率基本是零。也就是说,你采集到的绝大多数轨迹,全部都是“无效轨迹”——奖励从头到尾都是 0。这种情况下,无论用 DQN 还是 PPO,智能体学到的东西都约等于“不动最安全”,因为不动和乱动造成的后效完全一样。
这里可以做个生活化类比:让一个人蒙着眼睛在一个大房间里找一处开关,如果每走一步都没有任何反馈,他只能完全随机游走。但如果每次离开关近一点就听到“滴”一声,他很快就能摸过去。稀疏奖励的本质,就是省略了这声“滴”,让智能体连“我是不是在变好”都无法判断。
1.2 “事后诸葛亮”为什么是解药
hindsight 这个词的本意是“事后看来”。这正是问题的突破口。你还记得追公交车的经历吗?你拼命跑试图赶上 7 路车,结果它还是开走了。从“追上车”这个目标看,你失败了,这一段跑步轨迹全是负面经验。但如果把目标改成“在 20 秒内跑到公交站”,那么你刚才的行为就是一次完美的成功示范——你确实跑到了公交站,只是晚了几秒。
强化学习里的 HER 做的事是完全相同的一件事:一条没有达到设定目标的失败轨迹,我并不急着丢掉,而是从这条轨迹的后续状态里挑一个“实际到达的状态”,把它重新定义为新目标,然后把整条轨迹贴上新标签,当作一条成功轨迹放进回放缓冲区。(这在目标条件强化学习中叫做目标重标定 goal relabeling。)
换句话说,HER 的核心哲学是:失败不等于没有信息量,失败只是目标定错了。既然智能体来到了某个地方,那“到达这个地方”这件事本身就是一个可以学习的技能。我们只要让它在回放中学这个技能,它慢慢就能组合出一连串技能,最终够到原来的目标。
1.3 它的边界:能解决什么,解决不了什么
我得先泼一盆冷水:HER 不是银弹。它真正适用的场景必须同时满足三个条件:
- 任务是目标条件的(goal-conditioned):智能体的输入里包含一个明确的目标描述 g,策略可以写成 π(a|s, g)。
- 环境状态中能够提取出“实际达成目标”所需的量:比如机械臂的末端位置、物体的最终位置,或者游戏角色的最终坐标。
- 距离目标越近,行为就越接近成功路径:换句话说,任务存在“渐进路径”,只是奖励信号太稀疏。
如果任务压根没有目标概念,比如纯探索问题、或者奖励只取决于一个隐变量,HER 就很难发挥。比如训练一个智能体玩游戏,目标是解开谜题,但谜题的钥匙藏在随机房间,状态里并不直接体现“有没有碰到钥匙”。这种时候 relabeling 很难做,因为重标出来的目标可能根本不可达,或者语义上无意义。
所以别看到 HER 就以为能解决所有 reward 稀疏问题,它解决的是“有目标但够不着”的问题。接下来我们看它具体是怎么改的。
2. HER 到底在改什么:目标重标定的原理拆解
2.1 先搞清楚“目标”长什么样
要理解 HER,先要建立目标条件强化学习(Goal-Conditioned RL)的概念。普通 RL 的状态是 s,动作是 a,奖励是 r(s,a)。目标条件 RL 里多了一个目标变量 g,策略变成 π(a|s,g),奖励也变成 r(s,g)——只有当前状态“符合”目标 g 时才有正奖励。
举一个我经常用的例子。环境是 FetchReach(机械臂末端去碰一个目标点):
- 状态 s 里包含:机械臂各关节角度、末端位置、目标点位置(可能还包含物体位置等)。
- 目标 g 是一个三维坐标:期望的末端位置。
- 奖励函数很常见:如果末端和目标的欧氏距离小于阈值 0.05,奖励为 1,否则为 0。
这里有个关键点:目标 g 必须能跟状态 s 里的某个量直接对应起来,relabeling 才能执行。你从状态里提出“实际末端位置”,把它作为 g',这件事才有意义。如果目标本身是抽象的“把水烧开”,状态里没有任何可提取的中间量,那 HER 就没有抓手。
2.2 重标定流程:给失败轨迹重新贴标签
HER 的具体操作并不复杂,上午看论文下午就能实现。假设你有一条轨迹:
τ = (s_0, a_0, r_0, s_1, a_1, r_1, ..., s_T)
这条轨迹的真实目标是 g,但因为没达到,奖励全部是 0。HER 的处理方法:
- 对轨迹中的每个时刻 t,从时刻 t 之后的某个状态里选一个“未来状态” s_k,其中 k > t。
- 从这个状态中提取一个新的目标 g' = f(s_k),比如 s_k 里的物体位置,或者机械臂末端位置。
- 把轨迹上每个转移的奖励重新算一遍:r' = r(s_{t+1}, g')。
- 将新的转移样本 (s_t, a_t, r', s_{t+1}, g') 投进回放缓冲区。
因为 g' 是从未来的状态提取的,对于 t 到 k 这段区间来说,智能体确实“成功达成了目标 g'”,所以后半个轨迹的奖励会从 0 变成 1。一条原本毫无价值的失败轨迹,经过重标定之后,变成了一段携带丰富正反馈的成功轨迹。
这里最容易踩的误区是:重标定时要用未来状态,而不是用当前状态。如果用当前状态作为目标,那智能体在 t 时刻“立刻就在目标上”,这种样本同样没有学习意义。所以 HER 的标准做法是“从后续时刻采样目标”,而且越靠后的状态越有价值。
我当年实现的时候,第一次写反了,把目标设为当前状态,结果训练出来一个只会原地发呆的策略,K 值怎么调都没用。后来打印 buffer 里的 reward 分布才发现,重标过的样本全部都是“原地成功”,智能体被训练成了“站着别动就是满分”的废物。这个坑后面我会在排查部分再展开。
2.3 重标过的样本为什么能帮助“真实目标”
有人会问:你换个目标让智能体“成功”,但它学会的是“到达别的地方”,这跟原来的目标 g 有什么关系?
关键在于策略是条件的:π(a|s,g) 不是一个只会奔向某个固定点的策略,而是一个给目标就能执行的策略。你训练它“让物体移动到位置 A”,它学会了推的动作;再训练它“让物体移动到位置 B”,它又学了一个推的动作。这些动作内在机制是共享的——都要学会“怎么推”。HER 等于给你提供了一大批“各种位置的成功示范”,这些示范覆盖了真实目标附近的状态空间。策略在这些重标目标上学会的感知和运动能力,最终会迁移到真实目标 g 上。
再打个比方:你想训练一个球员射进门框右上角这种极高难度的死角球。如果只让他对着这个死角射,他一天也进不了几个球,教练也难以纠正动作。HER 的干法,就是让他在多个不同角度的球门位置反复射门,每个位置都允许他成功几次,让他积累“调整脚法、瞄准、发力”的经验。当这些一般化能力足够强时,再让他去射那个死角,成功率就上来了。
这也是 HER 为什么能跟 DQN、DDPG、TD3、SAC 这类 off-policy 算法完美配合的原因。回放缓冲区里除了原始轨迹,还有大量重标轨迹,它们一起参与了 Q 值和策略的更新。梯度不再稀缺,Q 函数的估计也就更平稳。
2.4 三种取目标策略,到底该选哪个
HER 论文里提出了几种策略来选择重标定的目标,实际用得最多的是这两种,但完整对比一下会让你理解更准:
| 策略 | 做法 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| final | 只用轨迹最终状态作为新目标 | 实现最简单,目标离真实起点最远,长程信息强 | 最终状态往往离原始目标很远,中后段才出现正奖励,前半段梯度弱 | 目标空间小、状态即目标的任务(如点到达) |
| future | 对每个 t,从 t 之后随机采一个状态作为新目标 | 覆盖范围广,整条轨迹都有正反馈,训练稳定 | 采样逻辑稍复杂,目标数量大 | 绝大多数连续控制任务,首选 |
| random | 从整个回放缓冲区里随机抽一个状态作为目标 | 目标分布多样 | 大部分抽样目标与当前轨迹无关,正奖励比例低 | 不适合单独用,一般做补充 |
| episode | 把整条轨迹所属 episode 的最终目标作为新目标 | 语义贴近任务 | 等价于不重标,提升有限 | 极少用,多数情况不如 future |
我在实际项目中全部用 future,并且 K 值取 4。也就是说,每一条原始轨迹,额外生成 4 条重标轨迹。这样缓冲区里真实目标样本和重标样本的比例基本是 1:4,既保证了数据多样性,又没有把真实目标淹没掉。如果 K 太大,比如 16,回放缓冲区里几乎全是重标样本,智能体学出来的策略会变得“太擅长去各种地方”,但对真实目标的定向能力反而弱。
3. 亲手把 HER 接进训练流程:从选型到跑通
3.1 前置条件:别拿 on-policy 算法硬上
HER 依赖经验回放,因此必须在 off-policy 算法上使用。DQN、DDPG、TD3、SAC 都没问题。如果你用的是 PPO、A3C 这类 on-policy 算法,直接套 HER 会非常别扭:on-policy 要求训练数据来自“当前策略”的采样分布,而重标轨迹改变了分布,策略梯度公式里的重要性权重根本算不准,训练容易直接爆炸。
这不是说你不能用 HER 的思想去改进 on-policy 算法,而是说你需要走 EPO(利用 off-policy 数据做 on-policy 修正)这类更复杂的路径,普通项目不值得折腾。我的建议很简单:想用 HER,就用 TD3 或 SAC 当基底,亲测稳定省心。
3.2 环境接入:你的 env 需要暴露什么
gym 里的 Fetch 系列环境已经把接口设计好了,你自己写环境时至少要实现这几样东西:
- reset() 返回的 obs 中必须包含与目标相关的字段,通常是一个 dict,例如 {'observation': _, 'achieved_goal': _, 'desired_goal': _}。
- step(action) 返回 next_obs、reward、done、info,其中 next_obs 里的 achieved_goal 会随状态变化。
- 一个独立的 compute_reward(achieved_goal, desired_goal, info) 静态方法,专门用来事后计算任意“已达成”状态相对任意目标的奖励。
- 一个从状态里提取目标字段的函数 extract_goal(obs),我用它从未来状态里抽出 g'。
别小看 compute_reward 独立出来的意义。训练时你既要算真实目标的奖励,又要算大量重标目标的奖励,如果奖励函数藏在环境内部的某个私有方法里,重标逻辑根本没法写。
3.3 完整训练循环骨架
下面给一个极简但完整的 HER 训练循环伪代码,基于 DDPG 的思想,但把细节剥开让你看到 HER 插在哪里:
import numpy as np import random def train_with_her(env, agent, replay_buffer, k=4, epochs=1000): for epoch in range(epochs): obs = env.reset() episode = [] # 保存一整条轨迹 goal = obs['desired_goal'] # 1. 用真实目标去环境里交互,攒出一条轨迹 for t in range(env.max_steps): action = agent.select_action(obs) next_obs, reward, done, info = env.step(action) episode.append(Transition( obs['observation'], action, reward, next_obs['observation'], goal, obs['achieved_goal'], float(done))) obs = next_obs if done: break # 2. 原始轨迹直接进 buffer for transition in episode: replay_buffer.add(transition) # 3. HER 重标定:对每个时刻,额外生成 k 条重标样本 for t, trans in enumerate(episode): for _ in range(k): future_idx = random.randint(t, len(episode) - 1) new_goal = episode[future_idx].achieved_goal new_reward = env.compute_reward( trans.achieved_goal, new_goal, None) replay_buffer.add(Transition( trans.state, trans.action, new_reward, trans.next_state, new_goal, trans.achieved_goal, trans.done)) # 4. 从 buffer 中采样并更新 actor / critic for _ in range(40): batch = replay_buffer.sample(256) agent.update(batch)代码里第 3 步就是 HER 的全部秘密。注意我加了achieved_goal这个字段保存“当步实际上到达的状态”,compute_reward 完全基于这个字段算奖励,而不是基于原始目标。这也是我犯过的错——重标样本的 reward 还按真实目标去算,那就等于没重标。
3.4 我认为最关键的超参数
光有代码还不够,参数调不好照样白搭。HER 相关的关键参数,我整理成一张表:
| 参数 | 建议取值 | 说明 |
|---|---|---|
| K(重标轨迹数) | 4 | 论文消融结果:K=0 到 4 提升巨大,4 到 8 趋于平缓,超过 16 反而有害 |
| 重标目标采样方式 | future | 绝大多数任务首选,曲线上涨最稳定 |
| 奖励阈值 | 根据物理尺度定 | 不要用 0.001 这种极高精度,机械臂一般 0.03~0.1 就够了 |
| 缓冲区容量 | 50 万到 100 万 | HER 需要足够多样化的重标分布,buffer 太小效果大打折扣 |
| 每轮更新次数 | 20~40 | 重标样本增加后,每轮多更新几次能明显加快收敛 |
| 探索噪声 σ | 0.1~0.3 | 探索太少,buffer 里的状态覆盖不足,重标样本也会缺乏多样性 |
这里我想特别强调奖励阈值的选择。很多新手喜欢把稀疏奖励的阈值设得非常高精度,觉得“这样才算正确”。但阈值设成 0.001 之后,重标得到的“成功”样本几乎全集中在目标点附近极小区间,奖励分布依然稀疏。我的经验是:阈值应该接近任务本身的物理精度,比如机械臂任务设 0.05(5 厘米),这就已经足够区分“够没够着”。先用宽松阈值把技能学起来,再逐步收紧,比一步到位更容易成功。
3.5 从简单到复杂的三步落地路线
如果你从来没跑过 HER,我强烈建议别一上来就碰真实机器人或者复杂仿真,按下面三步走:
第一步,跑通 OpenAI Gym 的 FetchReach。这是最简单的点到达任务,目标空间只有三维,HER 加持下通常几百个 epoch 就能达到 90% 以上成功率,用来验证代码和参数没毛病。
第二步,换成 FetchPickAndPlace 或者 FetchPush。这两个任务引入了物体操作,缓冲区里的重标目标变成了“物体最终位置”,你会发现 HER 依然有效,但需要的训练轮数明显上升,此时开始学会看 buffer 里的 reward 比例。
第三步,再上你自己的定制环境。此时你已经知道 HER 的接口长什么样,调试时可以直接复用我下面要讲的排查技巧。
4. 调参路上的那些坑:症状、原因、对策
4.1 “train loss 降了,成功率却不涨”
这是我在多个任务上遇到频率最高的问题。先看是不是重标样本把真实目标淹没了——当你把 K 设成 20、30 的时候,缓冲区里 95% 都是重标样本,策略被训练成“到达随机地方”,反而忘记主要任务。对策是把 K 回调到 4,并且确保原始轨迹永远完整进入 buffer,不要为了省空间丢原始样本。
另一种可能是奖励函数被算错了。我建议你在训练过程中打印最近 100 条真实目标的 reward 分布,如果发现真实目标的 reward 几乎全是 0,说明策略学偏了;但如果连重标样本的 reward 也都全是 0,说明 extract_goal 提取的目标本身就不合理——很可能你从状态里提的是动作指令而非实际位置,导致“新目标”根本不可达。
4.2 Q 值爆炸、策略震荡
HER 会显著增加正样本密度,Q 值网络在大量“刚刚够到目标”的样本上容易高估。特别是在目标空间非常大、重标目标离当前状态很远的场景,智能体偶尔“瞎猫碰到死耗子”成功了一次,这个样本会被回放很多遍,Q 值被过度放大,之后策略开始抖。
解决办法:
- 把 K 降到 1 或 2,减少同一轨迹的重复重标样本。
- 配合 TD3 的 target policy smoothing,给目标 Q 加噪声,缓解高估。
- 如果还是震荡,考虑把奖励阈值稍微放宽一点,让“成功”不再那么苛刻,降低方差。
- 检查是否在同一轮训练里反复用同一条轨迹重标多次导致过拟合,如果是,把 HER 重标上限 K 调小。
4.3 智能体变成一个“原地刷子”
这是我的一个典型翻车现场。训练 FetchReach 时,前端曲线看着挺正常,loss 稳步下降,但实际 rollout 成功率只有 2%,策略就只会把末端移到一个固定位置然后原地转圈。原因是什么?缓冲区里重标的“成功样本”几乎都集中在初始状态附近——因为未来采样会从轨迹后半段选目标,但轨迹前半段的初始状态离这些目标很远,所谓“成功”其实只是从很远的地方开始靠近。
那为什么只动了那么一小下?因为智能体发现,只要稍微移动一点,就会进入“下个状态离目标更近”的奖励区间,于是它就把“稍微动一下”当成了最优策略,不再敢大幅度移动。这种保守化在稀疏奖励任务里非常常见。
对策是在环境交互阶段加大探索噪声,或者用一个简单的内在奖励(比如对状态访问次数计数)鼓励它多逛不同区域。HER 负责从失败中挖掘成功,探索负责产生足够多样的失败——两者缺一不可。如果探索度不够,HER 就只能在一小片状态空间里打转。
4.4 在复杂任务上没效果:不是 HER 的错
如果你的任务是“把抽屉里的螺丝拧到某个扭矩”这种多阶段任务,HER 直接套用效果会很差。原因是目标空间不再是简单的几何坐标,而是一个复合语义目标:打开抽屉、拿起螺丝刀、对准、拧动。HER 重标出的子目标可能语义不连贯,导致策略学出一堆互相矛盾的行为。
我的建议是:复杂任务先用分层思想拆解,把每个子任务单独做成一个 goal-conditioned 环境,各自套 HER;或者采用课程学习,先学第一阶段,再学第二阶段。也可以关注 HER 的后续变体,比如 CHER(Curriculum-guided HER),让重标目标的选择更智能,从“随机选未来状态”变成“选择难度适中的目标”。但要做好心理准备,这些变体的实现复杂度不低。
4.5 我自己一直保留的排查清单
最后分享一个我每次训练都会过一遍的检查清单,看起来简单,但救了我很多次:
| 症状 | 排查方向 | 对策 |
|---|---|---|
| 成功率始终为 0 | 奖励阈值是否太严格 | 放宽阈值到物理尺度 0.05~0.1 |
| loss 下降但 rollout 不涨 | 重标样本是否淹没真实样本 | K 回调到 4,检查真实目标 reward 分布 |
| 策略原地不动 | 探索噪声太小,状态覆盖不足 | 增大噪声,加 intrinsic reward |
| Q 值忽高忽低 | 同轨迹重标过于集中 | K=1,开 target smoothing |
| 换任务后完全失效 | 目标不可提取或语义复杂 | 重新设计目标空间,考虑分层 |
| 训练极不稳定 | env 的 done 信号设计不合理 | 检查 done 不要过早在半路触发,导致轨迹截断 |
我自己的习惯是每隔一段时间就打印一小批 buffer 里的重标样本,看一眼 goals 的分布和 reward 的比例。如果 goal 分布太集中,说明状态覆盖差,得调探索;如果 reward 的比例超过 80%,说明重标目标太简单,得调阈值。这种“看一眼原始数据”的土办法,往往比盯着 loss 曲线有用得多。
顺带说一句题外话:HER 的思想后来也影响了我做项目复盘的方式。以前我总觉得跑失败的实验就是纯亏,数据和日志都懒得再看。后来我养成了“给失败实验换指标重新看”的习惯——换一个评价维度,失败样本里常常藏着成功路径。技术上这叫 relabeling,工程上这叫复盘。两者本质上是一个道理:别急着定义什么是失败,先看看你能从这次经历里学到什么目标。