Hindsight 这个词,中文直译是“后见之明”,说白了就是我们常说的“回头看”。做开发这些年,我越来越觉得“回头看”是个被低估的能力。代码写多了会形成惯势,踩坑踩多了也会有路径依赖,真正能带来突破的,反而常常是事后复盘时那个“早知道当时换个目标就好了”的念头。而在强化学习里面,这个念头被做成了一个正经算法,名字就叫 Hindsight Experience Replay,也就是 HER。这篇博文想聊的,就是 HER 到底解决什么问题、核心机制怎么拆解、放到自己项目里怎么落地,以及我在实际操作中踩过的那些坑。如果你是做机器人控制、目标条件强化学习,或者正被稀疏奖励折腾得焦头烂额,这篇内容应该能给你一些非常具体的参考。
1. Hindsight 是什么?一个关于“事后重标”的故事
1.1 稀疏奖励为什么是绕不过去的坎
先站在一个实际问题面前。假如你让一个机械臂去抓取桌面上的杯子,动作空间是连续的四维或七维,状态空间动辄几十维。绝大多数尝试里,机械臂连杯子的边都没碰到,你该给它什么奖励?如果给 0,那一整条轨迹里所有转移没有任何学习信号,策略梯度或者 Q 值的更新都等于在做布朗运动。如果给一个基于距离的稠密奖励,比如末端执行器和目标点之间距离的负数,那你又要调距离函数的尺度、权重、以及“距离很近但方向不对”的惩罚逻辑。
稀疏奖励的真正代价不是“没奖励”,而是奖励完全无法覆盖绝大多数状态。对于强化学习来说,没有信号的地方就是一片死区。传统解法里,奖励塑形是最直接的思路,但你等于把对任务的先验理解全部塞进 reward function。这种手工设计不但耗时,而且很容易被 agent 钻空子,比如只优化距离但不关心抓取姿态。课程学习是另一条路,让 agent 从简单目标逐步过渡到难目标,但需要人为划分难度等级,换一个场景就得重做一套课表。
HER 的出现让我觉得很有启发的点在于,它把“探索”和“理解目标”解耦了。它不要求环境给你一个完善的奖励函数,也不要求人为设计课程,而是从已经跑出来的失败轨迹里,自己挖掘出有意义的成功信号。本质上,它利用了“后见之明”这个人类身上很常见的能力:哪怕这次没抓到杯子,但我在某个瞬间其实已经把机械臂推到了一个特定位置,那我把这个位置当作目标重新学习一遍,是不是就有信号了?
1.2 从“奖励塑形”到“事后重标”的思维切换
为了搞清楚 HER 为什么有效,得先理解它和常规做法在思维层面的差别。奖励塑形是“事前的”,它试图在轨迹发生之前就把每个状态的优劣定义好。HER 是“事后的”,它不在优美地设计 reward,而在轨迹发生之后,把已有的转移数据重新赋予新的目标,让原本看起来失败的转移变成“针对另一个目标成功的转移”。
这样做的意义非常实际:一个是把目标条件化建模,另一个是自动生成伪成功样本。在目标条件强化学习(Goal-Conditioned RL)里,策略是带输入参数的,即 π(a|s, g)。你关心的是策略能不能在给定不同目标 g 的情况下完成对应行为。HER 做的就是把那些“原本朝着目标 g 失败的轨迹”重新标注成“朝着另外一个后来实际达到过的状态 g′”的成功轨迹。
千万要注意,HER 不是改环境的奖励,它改的是训练时重放缓冲区里的数据。这意味着它和环境的判定逻辑完全解耦,环境可以继续用自己的稀疏奖励去评估成功率,而训练数据里却充满了密集的有效反馈。我常用一个比喻:它像一个复盘教练,不改变比赛规则,只看录像时告诉你“你刚才这个跑位虽然没接到球,但如果把球传向另一个空位,就能接住”。这种重新解释数据的能力,让学习效率提升了一个量级。
1.3 它适合用在什么场景
我总结下来,HER 特别适合三类场景。第一类是真机或仿真机器人操控,比如抓取、推箱子、插销,这类任务天然有明确目标状态和稀疏的成败判定。第二类是那些目标空间和状态空间几乎一致的任务,这样“把某个状态当作新目标”才自然合理。第三类是已经有 off-policy 算法的项目,比如 DDPG、SAC、TD3,因为 HER 本质是数据重放技巧,需要往现成的经验回放机制里插入数据重标注环节。
如果你只是想跑一个静态分类或者网格世界搜索,HER 并不是对症的药。它的价值在于目标条件化的连续控制任务,而不是普适的强化学习加速器。搞清楚这个适用范围,比学会代码实现更重要。
2. 核心机制拆解:HER 的四个关键设计点
2.1 经验元组的重构:谁被替换了
普通 off-policy 强化学习存储的是五元组 (s, a, r, s′, done) ,状态、动作、奖励、下一状态、结束标志。HER 在训练时会把它改写成一个六元组视角的样本 (s, a, r′, s′, g′) ,其中 g′ 是一个重新设定的虚拟目标,r′ 是按照这个新目标重新计算的奖励。
这里有个非常容易误解的细节:我们不是把整条 episode 重新标成一个新目标,而是逐条 transition 地重标。为什么要这么做?因为如果只把整条轨迹的目标换成最后状态,那么中间很多动作并不是最优的,但至少在目标条件化框架里,这些动作可以让 agent 学会“先往某个方向移动”的行为。逐条重标的粒度更细,能够提供大量稳定的梯度信号。
举个例子。一个轨迹是机械臂从初始位置出发,尝试去抓杯子,但最后一刻杯子滑落了。原始目标“杯子在 A 点”失败了。HER 会从这条轨迹的中间状态里选择一个状态,比如机械臂在 t=10 时刻到过的位置坐标,把它当作虚拟目标 g′。然后,对每一步重新计算奖励:如果当前状态离 g′ 越来越近,奖励就是正向的。于是这条本来失败的轨迹,变成了“成功接近 g′”的优质教学数据。
2.2 目标重标策略:final、future、episode 和 random
虚拟目标不能随便挑,挑得不好会引入噪声甚至误导训练。目前主流的四种策略,我整理成一张表供你对照:
| 策略 | 重标目标来源 | 特点 | 适用情况 |
|---|---|---|---|
| final | 轨迹最后一步的状态 | 最简单,提供整条轨迹一致的虚拟目标,但样本多样性差 | 任务轨迹短、状态空间连续 |
| future | 当前时间步之后随机一个状态 | 最常用,兼顾多样性和因果合理性 | 大多数连续控制任务 |
| episode | 整条轨迹任意一步的状态 | 多样性最强,但可能选到和当前动作无关的状态 | 探索性较强、任务不敏感 |
| random | 从经验池中随机取目标 | 简单,但容易造出无意义组合 | 目标空间极大且随机探索友好 |
我实际用得最多的是 future 策略。原因是 “future” 这个词的定义保证了虚拟目标是在当前时刻之后某个时间步达到过的状态。从因果上看,既然后续真的到达了那个状态,那么前面若干步的动作行为是有指导意义的。final 虽然更简单,但整条轨迹只用一个虚拟目标,样本多样性不足,训练后期容易过拟合到少数几条轨迹。episode 策略多样性最强,但会选出一些和当前动作毫无关系的状态,需要更大容量去消化。random 除非有其他考量,否则我一直不太推荐,因为随机目标一旦落在不与轨迹空间重合的区域,就等于凭空制造了不可解样本。
2.3 稀疏奖励的替换:从距离到布尔值
重标目标确定之后,下一步是重新计算奖励。最常用的两类是布尔型和距离型。
布尔型奖励在很多公开代码里长这样:如果当前状态和虚拟目标之间的误差小于某个阈值,奖励为 0,否则为 -1。有时候也反过来,成功给 1,失败给 0。关键在于,绝对不能沿用原始环境的大稀疏奖励,否则重标的意义就没了。在连续控制问题里,我倾向于把布尔型替换成带距离度量的奖励,比如说:
reward = -np.linalg.norm(achieved_goal - desired_goal, axis=-1)这样的好处是每一步都有一个连续的负距离信号,即使当前离虚拟目标很远,梯度也指向“缩小距离”的方向。需要注意的是,距离函数的选择要和状态空间的尺度匹配。如果状态空间里位置坐标是米,角度是弧度,直接把它们拼成一个向量求范数,可能导致角度的微小变化被位置的大尺度淹没。我踩过这个坑,后面会在问题章节展开说。
2.4 为什么必须搭配 off-policy 算法
HER 从模型设计上就和 off-policy 算法绑定。原因在于,它的核心动作是“修改经验池里面的历史数据”,再随机采样去训练。这要求算法本身允许从历史经验中反复采样,也就是需要经验回放机制。像是 DDPG、TD3、SAC、DQN 这些 off-policy 方法天然适合。而 policy gradient 类的 on-policy 方法,比如 PPO、TRPO,每一轮数据用完就要丢弃,HER 就无从施展了。
实操中我会优先选 DDPG 或 SAC 作为底层算法。DDPG 简单直接,和 HER 组合很经典;SAC 则额外引入熵正则化,探索性更强。如果你用的是 TD3,也可以无脑套 HER,因为 TD3 本身就是 DDPG 的稳定版,逻辑上完全兼容。值得提醒的是,如果是 off-policy 算法但目标空间和状态空间不一致,需要保证重标的目标能够合法映射回状态空间,否则会出现“训练时用一个状态,测试时拿不到这个状态”的窘境。
3. 实操落地:从环境准备到超参设定
3.1 第一步:准备一个适合 HER 任务的目标条件环境
刚开始跑 HER 的时候,很多人会直奔 OpenAI Gym 里的 Fetch 系列环境。Fetch 系列确实是把 HER 发扬光大的基准,但它们对环境依赖较多,跑一次训练也要不少时间,新手很容易在环境安装上卡住。我的建议是先用一个自定义的简单环境验证核心流程,确认 HER 或适用于你的场景后,再切换到重负载环境。
一个简单的目标条件环境可以设计成:二维平面上有一个小球,动作是水平与垂直方向上的位移增量,目标是到达某个坐标点。状态是当前位置坐标,目标是目标坐标,稀疏奖励定义为“到达阈值 0.1 以内给 0,否则 -1”。这个环境小而清晰,非常适合观察 HER 是否生效。下面是一个精简的环境定义片段:
class SimplePointEnv: def __init__(self): self.state = np.zeros(2) self.goal = np.zeros(2) self.threshold = 0.1 def reset(self): self.state = np.random.uniform(-1, 1, size=2) self.goal = np.random.uniform(-1, 1, size=2) return self.state, {'achieved_goal': self.state.copy(), 'desired_goal': self.goal} def step(self, action): self.state = self.state + np.clip(action, -0.1, 0.1) reward = 0.0 if np.linalg.norm(self.state - self.goal) < self.threshold else -1.0 done = False info = {'is_success': reward == 0.0} return np.concatenate([self.state, self.goal]), reward, done, info注意这里返回的 state 我把当前状态和目标拼在了一起,这样便于对接传统 off-policy 算法。HER 重标时会直接操作 env_info 里的 achieved_goal 和 desired_goal,而不是操作拼接后的 state,这是一个非常重要的约定,很多人在自定义环境时把这块搞混,导致重标后样本完全乱掉。
3.2 第二步:实现 future 目标重标逻辑
以最常用的 future 策略为例,重标发生在一条完整 episode 结束后。我们需要保存这条轨迹里的状态、动作和 achieved_goal 序列。重标的代码大概长这样:
def relabel_episode(episode, k=4): new_transitions = [] T = len(episode['actions']) for t in range(T): for _ in range(k): future_t = np.random.randint(t, T) new_goal = episode['achieved_goal'][future_t] new_reward = compute_reward(episode['achieved_goal'][t], new_goal) new_transitions.append(( episode['observations'][t], episode['actions'][t], new_reward, episode['observations'][t + 1], new_goal, False )) return new_transitions这个片段有两个细节值得强调。第一个是np.random.randint(t, T),它保证只从当前时刻之后采样,符合 future 策略的定义。第二个是重标后的 done 标志我统一设成了 False,因为虚拟目标并不是环境的真正终止条件,如果置成 True,算法会误以为后续没有状态,Q 值的 bootstrapping 就会出错。
如果你用的是 OpenAI Baseline 里 deepq 或 ddpg 的代码风格,通常做法是维护一个临时 buffer,把原始轨迹和重标后的轨迹同时塞进去。原始轨迹的奖励还是按原目标算,重标轨迹则按新目标算。两者在缓冲区里共存,训练时统一被采样。
3.3 第三步:训练主循环如何衔接经验池
训练主循环的逻辑其实不复杂。每跑完一个 episode,我们先不做任何训练,把轨迹暂存。等 episode 结束,把原始轨迹写入经验池,再从这条轨迹生成 K 条重标轨迹也写入经验池。整体结构可以抽象成下面这组伪代码:
for epoch in range(epochs): episode = [] obs = env.reset() done = False while not done: action = policy.select_action(obs) next_obs, reward, done, info = env.step(action) episode.append((obs, action, reward, next_obs, done, info)) obs = next_obs replay_buffer.add_episode(episode) # 原始经验 replay_buffer.add_her_samples(episode, k) # 重标经验 for step in range(total_steps_per_epoch): batch = replay_buffer.sample(batch_size) algorithm.update(batch)经验池的容量值得你刻意调大。因为 HER 会额外生成 K 倍的重标样本,如果容量太小,旧数据很快被新数据冲掉,会导致重标经验来不及被充分学习。我常用的配置是原始容量乘上(1+K),例如本身打算用 50 万条经验,那 HER 场景下我会开到至少 200 万条。
3.4 超参数设定:K 取多大最合理
K 是每个原始 transition 额外生成的重标样本数,这是 HER 最核心的超参数。论文里的标准设定是 K=4,我的实际经验是 K=4 在多数连续控制任务上就是“甜点区间”。如果 K 取 1,虚拟样本太少,稀疏奖励问题不能有效缓解。如果 K 直接上到 20,缓冲区内重标样本严重过剩,模型反复学习那些“容易完成”的伪目标,反而降低了真实目标的拟合能力。
这里有个逻辑可以帮你理解:K 的本质是在真实数据和伪数据之间取一个平衡。K=4 意味着每个真实转移伴随 4 个伪目标转移,模型每看一条真正轨迹,就要看 4 条“后见之明”轨迹。这个比例既给了足够的恢复信号,又不至于让模型忽略真实目标。
训练步数建议排在 100 到 200 epoch 左右观察收敛趋势。如果你用的是 Fetch 之类复杂环境,需要耐心一点,前 50 个 epoch 里 reward 曲线很可能是平的,因为 agent 还没构造出有效的策略。我自己见过不少人在前 10 个 epoch 看到奖励没变化就跑去调参,实际上 HER 的收益通常要在探索积累一定量之后才显现。
4. 常见问题与排查技巧实录
4.1 奖励尺度不匹配导致训练发散
这是我最常遇到的问题。如果你自定义环境时状态由多个物理量拼接而成,比如位置、速度、姿态,直接用它们的欧氏距离作为奖励,尺度大的分量会主导梯度,尺度小的分量几乎学不到。举个例子,位置范围在 [-1,1],角度范围在 [-π, π],角度稍一变,距离变化可能很大,机械臂就会优先学“转动关节”而不是“接近目标”。
一个有效的排查方法是把训练前期的虚拟目标奖励打出来,看数值分布。如果幅度突然从 0.1 跳到 5,说明尺度不协调。解决办法有三个:一是分别计算物理量各自的距离,再加权求和;二是对状态做归一化;三是在 reward 外面加一个适当的缩放系数。我惯用的是第二种,先把观测和目标的每一维都缩放到同一量级,再去算距离。
4.2 虚拟目标选得“太容易”或“太难”
HER 说白了是靠重标样本引导学习,但如果虚拟目标分布不当,也会适得其反。太容易的情况是虚拟目标就在当前状态附近,距离很近,奖励几乎是 0,梯度非常小,模型学不到什么。太难的情况是选择的虚拟目标来自一条极长的轨迹末端,远超当前步可达范围,模型要预测的回报跨度太大,Q 值估计极不稳定。
future 策略天然规避了一部分问题,因为它从当前之后的状态里采样,目标总是“未来能到达的”。但如果你把 K 调得很大,或者 episode 长度过长,future 采样出来的虚拟目标也可能距离太远。我处理这类问题的方法是在重标时加一个距离过滤,只选择与当前状态距离在合理范围内的未来状态作为虚拟目标,超出范围的丢弃。这个过滤项不会显著增加代码复杂度,却能明显稳定训练曲线。
4.3 成功判定与奖励函数不一致
很多任务通过 info 里的 is_success 判断是否成功,但训练时用的 reward 可能是距离型。这两个概念一旦不一致,就会出现评估已经成功但奖励还在负值的情况,或者奖励显示接近目标但 is_success 始终为 False。测试时我们看 success 率,训练时 Q 函数优化的是 reward,两者跑偏会让曲线看起来毫无进展。
举个例子,位置误差阈值设成 0.1,但距离奖励用的是原始欧氏距离数值,模型优化到 0.05 时已经可以成功,但 reward 还在 -0.05,它还会继续优化;反之,如果模型停在 0.12,reward 相对较好,却因为没有进入成功阈值导致 success 率一直上不来。这个问题的标准化解法很简单:要不把成功判定当作奖励雷同的规则,要不奖励和 success 使用同一个阈值和同一种度量。
4.4 训练前期的伪成功率假象
HER 让人高兴得太早的假象也发生过。因为训练数据里充斥着重标后的伪成功样本,模型在缓冲区上表现出很高的“成功率”,但如果换到真实环境,这个成功率往往对不上。原因很好理解:重标样本的目标是后来才选定的,并不代表 agent 在一开始就主动朝着它走。模型只是学会了“朝某个已到达过的状态修正”,而真实环境里没有这种事后修正机会。
因此我强烈建议你把评估和训练彻底分开。训练时用 HER,评估时绝不加载重标数据,必须让 agent 在真实验证环境里跑完整 episode,直接记录 is_success。另外,评估环境里不能开启重标逻辑,否则测出来的指标就是自欺欺人。这个坑很隐蔽,因为你肉眼看着训练 loss 在降,可能会误以为一切正常。
5. HER 的扩展方向与场景思考
5.1 抓取、推动与移动机器人
HER 最直接的应用是在机械臂抓取与操控任务里。无论你用的是 Fetch、Franka 还是 UR 机械臂,目标通常是六自由度的抓手位姿或者物体的位置。在这些任务里,稀疏奖励问题极其严重,因为机械臂必须完成一整套接近、对齐、闭合抓手的流程才算成功,中途任何一步偏差都会导致失败。HER 的价值在于把一次失败轨迹拆解成多个“接近某个位姿”的成功片段,让策略逐步掌握整个运动链。
移动机器人导航也可以用到 HER。目标点是二维坐标,轨迹中车辆虽然没到达最终目的地,但途经了很多中间位置。把这些途经点当作虚拟目标,机器人就能学到“先移动到中间点”的子策略,最终逐步逼近终点。这个思路和分层策略不同,它不需要预先定义子目标,一切都从数据里自动涌现。
5.2 与 DDPG、SAC、TD3 的组合配置
如果你用 DDPG + HER,我建议把探索噪声保持在一个适中的水平。噪声太小,探索不足,重标样本的多样性不够;噪声太大,轨迹自己都偏离方向,虚拟目标也会变得杂乱。我习惯用 OU 噪声但把 std 设得比普通任务略小,因为 HER 已经在数据层面增加了探索。
换成 SAC 的话,熵系数初始值建议比常规任务略高一点。SAC 本身鼓励探索,配合 HER 可以快速覆盖更多状态。TD3 和 HER 的组合也比较顺滑,因为 TD3 的 target smoothing 可以抑制 Q 值的高估,正好对冲 HER 重标样本可能带来的 Q 值偏差。
5.3 对稀疏奖励问题的方法论启发
如果你从 HER 抽离出来看,会发现它带来的启发其实很朴素:失败数据里往往藏着可被重新解读的信号。传统强化学习把 rewards 当成客观事实,但 HER 告诉我们,在目标条件化场景下,目标可以是在事后被调整的。这种信息重用的思想可以迁移到很多领域,比如模仿学习里给示范数据重新定义意图,或者逆强化学习里从多条轨迹推断潜在目标。
做项目时,遇到稀疏奖励问题,我不建议第一时间就上 HER,而是先梳理清楚你的状态、目标、动作三者之间的关系。只有当目标状态可以被表达成状态空间里的点,HER 才有用武之地。如果你的目标是一个抽象概念,比如“完成任务”或者“让用户满意”,那 HER 帮不上忙,因为无法定义“把哪个状态当作目标”。
我个人实际操作下来的体会是,HER 之所以有效,不完全是因为它增加了数据,而是因为它改变了模型对“成功”的定义方式。训练一套从失败中提取有效信息的算法,远比单纯调大经验池有意义。把虚拟目标的重标当作数据增强来理解,你可能觉得它平平无奇;但真正在你的机器人任务上跑起来,看到原本纹丝不动的 success 率开始爬升时,那种“原来是这么回事”的感觉还挺值得体验的。最后再分享一个小技巧:如果你在复杂环境里跑 HER 发现训练不稳,别急着调网络结构,先重新检查一下你的 achieved_goal 是从哪里取的。很多人的问题出在 info 里传的是当前观测而不是真实验证后的目标状态,这一步错了,HER 再强也白搭。