
hindsight 这个词有意思。在日常生活里它说的是“事后聪明”——事情都结束了才觉得“我早该想到”一般带点贬义。但在强化学习里这个词代表的是一个非常能打的技术Hindsight Experience Replay也就是事后经验回放通常缩写成 HER。我第一次看到这个缩写还以为是“她的”那个 she/her 的 her后来才意识到是 hindsight 的前半截。这个技术解决的是强化学习里最让人头疼的问题之一稀疏奖励。很多任务里机器人做完一整条轨迹得到的奖励全是零只有最后一刻才知道成没成。这种环境让算法几乎学不到东西而 HER 用一种“事后重新解读失败”的思路把这个困境绕过去了。这篇文章写给正在做强化学习、跑机器人仿真、或者用 SAC/DDPG 这类 off-policy 算法但苦于奖励设计的同学。我会从 HER 的动机、核心设计、代码实现、踩坑经验一路讲下去尽量把每一步的“为什么”也说清楚。不管你是刚入门 RL 的新手还是已经跑过几个环境的老手这篇文章里应该都有你能直接拿走的东西。1. 为什么一个“事后聪明”能成为强化学习的突破口1.1 先聊一个直觉失败里本来就有成功想象一个场景你让一个机械臂去抓桌子上的杯子它伸过去没抓稳杯子倒了但还是碰到了杯子。从目标的视角看这次试验失败了因为没有“成功抓取”。但如果把目标改写成“伸手碰触”那这次轨迹后半段其实是完美地完成了这个子目标。这就是 HER 最核心的直觉一条轨迹没有实现它原本的目标但它一定以某种方式实现了某个别的、实际达成的状态。与其把这条轨迹扔掉不如把目标替换成“实际达到的状态”重新标一下奖励这条轨迹就变成了一条高质量的成功示范。算法从失败里翻出了成功经验然后继续学这比让它在纯零奖励的轨迹里挣扎要高效得多。这个思路跟人类的学习方式也有点像。我们复盘一次失败的操作经常不是单纯说“失败了重来”而是会拆成“哪个动作其实是有效的、哪个环节出了问题”。HER 相当于把这个复盘过程自动化了每次试验结束自动把轨迹改写成多个版本每个版本对应一个“事后看来确实达成的目标”然后全部喂给算法学。1.2 稀疏奖励到底难在哪先看两个常见环境。假设我们要训练一个四足机器人走到某个坐标点状态空间是一堆关节角度动作是关节力矩。如果这个任务的奖励只在“离目标点 0.1 米以内”时给 1其他时候一律给 0那么 99.99% 的探索轨迹都是全程零奖励。DDPG 这类算法靠的是时序差分误差来更新 Q 网络而时序差分误差来自奖励的差异。全零奖励的轨迹没有任何差异性Q 网络根本不知道该往哪个方向调。你可能说那给个稠密奖励不就行了比如每一步奖励等于“当前距离减去上一步距离”或者直接加负的距离项。这确实能解决一部分问题但稠密奖励非常考验设计功力。距离奖励会让机械臂“朝着目标方向动”就能拿小分于是它可能学会快速靠近然后卡在障碍物前面因为只要减少距离就有奖励障碍物挡住也不管。你可能还得加惩罚项去纠正这类投机行为结果奖励工程越做越复杂换一个环境全部又要重来。课程学习和好奇心驱动也能缓解稀疏奖励问题但前者需要人工设计课程顺序后者虽然鼓励探索却不保证探索方向能对解决任务有帮助。HER 的优雅之处在于它不改环境、不设计课程、不引入额外探索信号只是换一种方式利用已有的失败数据。它针对的是一个很大的任务类别——多目标任务。1.3 什么样的任务能适用 HERHER 不是什么环境都能用它对任务有一个结构上的要求存在一个可以判断的“目标”。具体来说每个 episode 有一个期望目标(goal)智能体每步的观测里包含或者可以同时得到“当前实际达到的状态”(achieved goal)且在 episode 结束后环境还能告诉你这个 episode 最终实际达到了什么状态。导航、机械臂抓取、推箱子、迷宫寻路、机器人插孔、这些任务天然满足这个结构。相比之下打游戏这种没有显式目标的场景就没法直接套 HER。你在 Pong 里想达到的“目标”是得分但你很难定义一个“实际达到的某种 game state”作为虚拟目标因为游戏状态千变万化而且用一个 game state 当虚拟目标也没有意义。2. Hindsight Experience Replay 的核心设计与直觉2.1 一次失败试验的“重新解读”我们具体演示一下 HER 在一条轨迹上做了什么。简化起见假设机械臂的目标是把方块推到二维坐标 (10, 10)轨迹一共 50 步最终方块停在 (8, 7)。原始的采样结果是每一步奖励都是 -1为了鼓励尽快达成我给未成功步都设 -1最后一步也没有变成 0因为没达到 (10, 10)。这条轨迹放进 replay buffer全部都是同样的 -1Q 网络学不出东西。HER 处理完这条轨迹以后会额外生成一批新经验。比如把整个轨迹的目标改成“(8, 7)”也就是这条轨迹最终实际达到的状态。那么最后一步的 achieved goal 正好是 (8, 7)意味着任务成功奖励给 0。倒数第二步的方块位置是 (7.9, 7.1)跟最终目标 (8, 7) 还差一点奖励是 -1。再把轨迹的目标改成“(7.5, 6.8)”这是第 30 步方块所在的位置。于是第 30 步及之后的每一步都因为“已经达到这个目标”而被标记为成功奖励 0第 30 步之前的每一步仍然是 -1。这样一条原本毫无学习价值的失败轨迹被变成了两条“越走越接近目标”的有效轨迹。Q 网络在更新时会收到这样的信号在这个状态下做这个动作最终能获得“达到某个状态”的结果。它开始理解动作与状态变化之间的关系不再是无头苍蝇。2.2 替代目标与替代奖励的计算方式HER 的重新标记过程在数学上很简洁。原轨迹是 \(s_0, a_0, s_1, a_1, \dots, s_{T-1}, a_{T-1}, s_T\)原始目标为 \(g\)每一步的奖励是 \(r_t R(s_t, a_t, g)\)。因为 \(g\) 一直没达成所以 \(r_t\) 基本全为 -1 或 0。重标记时先选一个新的目标 \(g\它必须来自这条轨迹上实际出现过的某个状态或者未来状态比如最后状态 \(s_T\) 或者轨迹中随机选的某个后继状态 \(s_k\)。然后对于每个时刻 \(t\)新的奖励是\[ r_t R(s_t, a_t, g) -\mathbb{1}_{\|\phi(s_t) - g\| \epsilon} \]这里的 \(\phi(s_t)\) 是“从状态里提取出来的 achieved goal”\(\epsilon\) 是成功判定阈值。简单说如果当前状态已经“够到了”新目标 \(g\)奖励就是 0否则是 -1。二值奖励让目标变得非常明确——达到就是成功没达到就是失败。不过这里有个容易忽略的细节重标记的目标 \(g\) 不能包含“未来信息”。回想一下轨迹在时刻 \(t\) 的转移是 \((s_t, a_t, r_t, s_{t1})\)。如果我现在把 t 时刻的转移重新标记成目标 \(g s_T\)那 t 时刻的策略真的“知道”未来会到 \(s_T\) 吗答案是否定的。所以 HER 在重放时有一个约定虽然我们把整条轨迹的目标替换成更晚时刻的状态但后续的贝叶斯更新时这条经验对应的策略分布其实是在“以 \(g\) 为目标”的新设定下产生的严格来说这会造成 off-policy 偏差。实际中大家测下来这种偏差并没有坏处反而极大地提高了学习效率这也是 HER 论文的一个理论贡献点。注意如果你在调试 HER 时发现训练异常不稳优先检查是不是重标记过程中某个环境把 episode 结束标志done处理错了。老版本一些实现里重标记这条经验时直接复用原轨迹的 done 标志这会导致时序目标算错。正确做法是如果重标记后的目标是某一步已经达到的状态那么从那一步开始后续都应该视为 doneTrue因为“以这个目标来看任务已经完成了”。2.3 四种目标采样策略final / future / episode / randomHER 论文主要测试了四种从轨迹中选择替代目标的策略这几种策略影响非常大直接决定了重标记的经验质量。final只拿整条轨迹的最终状态作为替代目标把每条轨迹额外生成一份“以最终状态为目标”的版本。future对轨迹中的每个转移从未来的某个状态里随机挑一个作目标然后重新标记。比如在第 t 步随机选 tk 步的状态当目标。episode和 final 类似但更极端一点把整条轨迹的最后一个状态作为该轨迹中所有转移的替代目标。random从整个 buffer 里随机抽取一个已经出现过的目标状态作为替代目标。论文里跑下来future 的策略往往效果最好也是后来大多数实现里的默认值。原因也直观final 只给每条轨迹一个“最终目标”生成的额外样本量少random 选出来的目标跟当前状态可能离得太远重标记后一堆转移都还是失败帮助有限future 则是在每个时间步都生成一个“未来的局面”对每个转移都提供了一个“下一步以后会达到哪里”的目标数据利用率高很多。我在实际复现的时候也验证过同样跑 FetchReachfuture 策略两百万步能到接近 100% 成功率final 策略大概在 80% 左右徘徊再往上就慢了。当然具体差距跟任务复杂度有关任务越难future 的优势越明显。2.4 为什么 HER 有效从数据分布到学习信号那 HER 本质上到底改变了什么我理解是两件事。第一它把“稀疏奖励”变成了“相对稠密的二值奖励”。不是物理意义上的每一步都有连续奖励而是说很多曾经的失败轨迹会被重新解释为“阶段性成功经验”这些成功经验里含有正的时序差分误差可以反向传播。Q 网络不再面对一整片都是 -1 的平原而是有了起伏和梯度。第二它隐式地改变了采样分布。原始 off-policy 算法里replay buffer 里攒了一堆稀疏奖励的样本采样时抽到的多数是没信息量的。而 HER 在采样时混入了相当比例的重标记样本这些样本的目标分布跟真实目标分布是不同的它们教给 Q 网络的是“状态到状态”的动力学关系。这个信息对最终的策略学习是很有用的——它让 Q 网络知道“在当前状态采取这个动作有多大可能达到另一个状态”然后策略网络再利用这个状态转移知识去靠近真正的目标。有人把 HER 的机制跟“逆强化学习”类比它其实是在从失败轨迹中隐式地推理出“哪些子目标已经实现”。不过它绕开了逆强化学习那些复杂的奖赏推断步骤直接用了一个非常简单粗暴的重标记。3. 从论文到代码HER 的实现要点与实操细节3.1 环境与算法选型HER 该配什么算法HER 依赖经验回放机制所以它天然适合 off-policy 算法DQN、DDPG、TD3、SAC 都可以。DDPG 因为早期与 HER 结合得最紧密、在 OpenAI 的 Fetch 系列实验里成名成了默认搭档。后来的 Stable-Baselines3 里官方就把 HER 做成了一个独立的 HerReplayBuffer能直接跟 SAC、DDPG、TD3 搭配使用我实际用下来 SACHER 或者 TD3HER 通常比 DDPGHER 更稳。选算法时有几个经验如果动作空间是连续的、目标空间也是连续向量首选 SACHER 或 TD3HER。如果动作是离散的、目标是离散的比如“到达某个房间”DQNHER 也能工作但要注意目标编码方式。不推荐用 on-policy 算法PPO、A2C直接配 HER因为 HER 重标记得来的样本不是当前策略产生的on-policy 算法用起来会有偏差效果不好。3.2 一个最小可跑示例二维点到达任务为了演示核心流程我自己写了个超小的实验环境一个质点在一个 2D 平面上移动状态是 (x, y)动作是 (dx, dy) 的小位移目标是到达一个随机采样点。奖励如果当前坐标离目标点小于 0.1奖励 0否则 -1。每步有一个小位移限制50 步为上限。这个环境简单到几行代码但它完整保留了 HER 要处理的全部要素稀疏奖励、随机目标、明确的可判定目标。在这个环境上调通了再去跑 FetchPickAndPlace 这类复杂环境会从容很多。import numpy as np class SimplePointEnv: def __init__(self, max_steps50, goal_radius0.1): self.max_steps max_steps self.goal_radius goal_radius self.action_dim 2 self.obs_dim 2 def reset(self): # 起点固定在左下角目标是随机一个点 self.pos np.array([0.0, 0.0]) self.goal np.random.uniform(-1.0, 1.0, size2) self.step_count 0 return self._get_obs() def step(self, action): action np.clip(action, -0.1, 0.1) self.pos self.pos action self.step_count 1 dist np.linalg.norm(self.pos - self.goal) reward 0.0 if dist self.goal_radius else -1.0 done (dist self.goal_radius) or (self.step_count self.max_steps) return self._get_obs(), reward, done, {} def _get_obs(self): # 观测 当前位置 当前目标 return np.concatenate([self.pos, self.goal])注意_get_obs返回的是[pos, goal]目标被直接拼进了观测里。HER 重标记时需要修改的也是这个部分把 obs 中的 goal 字段替换成替代目标。3.3 核心训练循环存储、采样、重组经验真正的 HER 实现核心在于 replay buffer它不光存储 transition还存了每条轨迹的完整信息方便事后重标记。下面给一个简化但功能完整的实现思路。先定义要存什么。每个 transition 存五样东西观测、动作、奖励、下一观测、是否结束。但注意这里判断“是否结束”依赖目标所以不能只存一个 done必须存下对应的 goal等重标记后再重新计算 done。所以 buffer 里每个样本我存的是transition { obs: obs, # 包含原始 goal action: action, reward: reward, # 原来按原始目标计算的奖励 next_obs: next_obs, done: done, goal: current_goal, # 这个 transition 的原始目标 achieved_goal: achieved_goal, # 当前实际达到的状态 }采集一条完整轨迹之后除了把原始 transition 一条条放进 buffer然后用 future 策略生成额外样本。比如我从这条轨迹中随机选几个时间步把这个时间步对应的 achieved_goal 当作替代目标重新计算奖励和 done拼成新的 transition 再塞进 buffer。这里有一个非常关键的点重标记时一定要重新计算奖励不能只改目标不改奖励否则这条样本就废了。奖励重新计算的规则是看“当前状态是否已经达到新目标”也就是看achieved_goal与新目标g的距离。def relabel_transition(obs, action, next_obs, achieved_goal, new_goal, success_threshold0.1): # 替换目标注意 obs 和 next_obs 的拼接格式 new_obs np.concatenate([obs, new_goal]) new_next_obs np.concatenate([next_obs, new_goal]) # 判断当前状态是否已经达到了 new_goal dist np.linalg.norm(achieved_goal - new_goal) reward 0.0 if dist success_threshold else -1.0 # 是否结束达到目标或超过步数上限 done (dist success_threshold) return new_obs, reward, new_next_obs, done在整合进训练循环时我的做法是环境 rollout 完一整条 episode先把原始样本全部放进 buffer然后额外生成 N 个重标样本。N 一般设为 episode 长度的 1 到 2 倍。比如 episode 长度为 50我每条轨迹额外生成 50 个重标样本总样本量翻倍。3.4 超参数与实现中的注意点HER 本身超参数不多但有几个地方值得细调。一个是重标样本与原始样本的比例。比例太低相当于没吃到 HER 的福利比例太高会让策略过度追求“达到随便哪个状态”反而忽略了原始目标。我自己的经验是buffer 里重标样本和原始样本数量控制在 1:1 左右比较合适。Stable-Baselines3 的 HerReplayBuffer 也有一个n_sampled_goal参数它控制每条经验重新采几个虚拟目标我一般设 4相当于 1 份原始样本会额外产生 4 份重标样本但如果 buffer 里原始轨迹数量本身就很大这个比例可以调低。还有一个容易踩的坑目标归一化。如果目标空间范围很大比如坐标从 -100 到 100那么基于距离的奖励计算会非常不稳定。HER 的奖励是二值的阈值不好定。这个时候需要先对目标做归一化或者缩放不然 success_threshold 这一个参数就够你调半天。网络结构方面早期实现里最常见的是把 obs 和 goal 直接拼接成一个长向量输给网络。这在目标维度低的时候没问题但目标很复杂时比如图像目标直接拼接会让网络很难把两个信息分离开。更先进的做法是分别编码 obs 和 goal在 Q 网络里用某个交互层组合起来。不过对于大多数机器人控制任务concat 就够用了别一上来就想搞复杂的架构。训练可视化方面我强烈建议记录两个指标一个是最新策略在真实目标下的平均成功率另一个是重标样本里的“虚拟成功率”。虚拟成功率太高比如到了 90% 以上说明重标样本大多是从“已经成功”的 trajectory 里来的反而不一定能帮助最终目标虚拟成功率太低比如长期低于 5%说明替代目标选得太远重标样本也学不到东西。合理区间大概在 20% 到 60% 之间。不过这个区间跟环境和阈值有关系主要还是看相对变化。4. 训练中的常见问题与排查技巧实录4.1 训练不收敛或学习缓慢这是我见到的最常见问题而且很多人第一时间会怀疑是 HER 没用。其实十有八九是重标样本没正确进入 buffer或者进入 buffer 的比例太低。我排查的顺序一般是先打印一条轨迹重标记后的奖励分布看看是否出现了非 -1 的奖励。如果整条轨迹重标后还是全是 -1说明替代目标选得离当前状态太远或者成功阈值设得太严。检查 buffer 采样时重标样本的占比。我用一个计数器实时统计每个 batch 里重标样本的数量发现如果 ratio 低于 0.2训练几乎不动。检查 done 标志。很多人重标时直接沿用原始轨迹的 done导致所有重标样本的 done 都为 False这样一来最后一步的 Q target 永远加上了未来折现估值偏大学习也就偏了。如果以上都没问题但还是很慢可以考虑加大重标样本数。另外如果用的是 DDPG可以顺便检查下 exploration noise 的初始值。HER 重标后的数据分布跟策略分布有偏差噪声太小不容易探索出新状态我一般会把 noise 初始设大一点0.3 左右训练中再退火。4.2 Q 值过估计与“虚假成功”HER 会让 Q 网络产生一个很微妙的问题由于重标目标可以任意选取Q 网络可能学到“随便什么状态都能快速达到”进而高估所有状态-动作对的 Q 值。这个问题在 DDPG 上特别明显TD3 和 SAC 因为机制上缓解了过估计会好很多。我在调试的时候会周期性打印一批随机状态下的 Q 值分布。如果所有 Q 值都集中在 50、 100 这种明显不合理的区间那就是过估计了。处理方法换 TD3 或者 SAC用 Clipped Double Q-learning 天然防止过估计。降低学习率DDPG 的 critic 学习率我一般调到 1e-4 以下。适当增加重标样本的“惩罚”在重标样本里如果一个状态距离新目标还很远但动作又比较大这种样本包含的信息量低可以考虑用重要性权重降低它在 batch 中的比重。不过这个操作在标准实现里不多见属于我试验过的野路子效果有限仅供参考。4.3 future 策略里的时间一致性 bugfuture 策略要求在重标时选的目标必须是“当前时刻之后”某个状态。很多人实现时直接用整条轨迹的所有状态随机挑这其实变成了 random 和 future 的混合体而且更严重的问题是会引入信息泄漏在 t 时刻的转移被赋予一个“过去某个时刻的状态”作为目标这在时序上是不合逻辑的因为 t 时刻以后的动作根本不依赖于那个过去状态。正确的 future 实现是遍历轨迹时对每个时间步 t从 t1 到 T 之间随机选一个 k把第 k 步的 achieved_goal 作为目标。这样重标后的样本里动作序列的因果关系是自洽的——策略在 t 时刻做出动作然后未来确实达到了那个状态。这个 bug 在代码里很容易写错因为随机选状态这个动作看起来很简单一不小心就把整条轨迹的状态池直接拿来抽了。我当时在这个地方卡了两天训练出来的模型在仿真里能成功但换一个初始分布就不行了后来定位到是重标目标里混入了过去状态。4.4 HER 与其他算法结合的实战经验拿 Stable-Baselines3 为例官方实现里HerReplayBuffer是挂在DDPG、SAC、TD3之下的你要做的只是传一个replay_buffer_classHerReplayBuffer参数。但这不代表开箱即用有几个参数必须认真设n_sampled_goal每条原始经验额外生成几条重标目标。官方默认给的是 4我试过 2 到 8。任务越复杂需要的越多但越多的重标样本也意味着越重的计算负担8 的耗时是 2 的两倍以上收益却不总是线性。goal_selection_strategy默认是 future没问题别改成 random效果差很多。online_sampling官方支持在 rollout 时立即做重标也可以在采样时在线做。我建议打开在线重标True这样 buffer 里始终有足够的新鲜重标样本。另外要注意HerReplayBuffer的保存格式比较特殊它把经验暂存在 GPU 之外如果你用 GPU 训练注意数据搬运的耗时。之前有人抱怨说用了 HER 之后训练慢了一倍结果一看是 replay buffer 的数据类型是 Python list 而非 numpy 数组样本采样时做了大量没必要的拷贝。把 buffer 的存储数据结构优化好性能差距非常明显。5. 从机械臂到更广阔的领域HER 的扩展应用5.1 真实机器人操作与仿真到现实的迁移HER 最早的成名场景就是机械臂操控OpenAI 那套 Fetch 环境里HER 让机械臂直接从完全随机探索开始学会了推方块、抓方块这在稀疏奖励设定下几乎是不可能完成的任务。后来很多真实机器人项目也采用了两阶段策略先在仿真里用 HER 训练一个模型再用 sim-to-real 技术迁移到真实机械臂上。因为 HER 训练出的模型天然对目标泛化能力强——它对“各种可能的目标”都训练过而不是只对某个固定目标过拟合——所以迁移到真实世界时面对新的目标位置也能做出合理的反应。不过实际部署时有个坑真实机器人的状态观测有噪声HER 重标时用的 achieved_goal 不准确奖励计算就会出错最终学到的策略会很奇怪。我建议在真实系统上做 HER 时对 achieved_goal 做一次平滑滤波或者用视觉系统输出一个置信度过滤掉不可靠的目标状态。5.2 多智能体与分层任务中的变体HER 的思想不局限于单智能体强化学习。在多智能体场景下一个智能体没有完成原始目标但它可能帮助队友推进了某个子目标这时候同样可以用 HER 的方式重新标记。具体做法是把目标替换成“队友在某个时刻实际所在的位置”然后用这个重标目标去更新这个智能体的 Q 网络。这种用法在通信受限的协同任务里出奇地有效因为它避免了智能体之间因为相互依赖导致的奖励稀疏问题。分层强化学习里也有 HER 的影子高层策略给低层策略下发一个子目标低层没完成这个子目标但完成了一个接近的替代目标HER 就能把这个替代目标反馈给高层让高层学会“清楚当前能实际完成什么”。这种用法我还只试过半成品但思路是成立的。5.3 一点个人实操体会HER 是个看着简单、实际非常有纵深的技术。我一开始觉得它不过就是“把目标改成 achieved goal”但跑过几个环境、踩过几个 bug 之后才意识到它的精妙之处在于如何结构性地利用失败数据。它不是让算法更努力而是让算法更聪明地看待已有的经验。如果让我给刚接触 HER 的人一个建议我会说先在最简单的二维点环境里把重标流程的核心代码彻底跑通打印出每一步的 obs、goal、achieved_goal、reward、done对照着看再上 Fetch 这种复杂环境。不要一上来就直接套 Stable-Baselines3因为封装得太好了你反而不会理解里面发生了什么。等到你在简单环境上能预测下一步输出再切到成熟库你会发现参数调起来心里有底多了。HER 不是银弹它只解决“稀疏奖励下的多目标任务”这一类问题但这一类问题在机器人领域实在太多了学它绝对是值得的。