看到 hindsight 这个词,我的第一反应不是词义辨析,而是前年调机械臂抓取实验时那条无限平稳的 loss 曲线。DDPG 跑了四十万步,成功率始终是零,每次 reset 后机械臂都在原地打转——那时候我还没听说过 Hindsight Experience Replay(HER),只能在 reward shaping 和 curriculum 之间反复折腾,事倍功半。直到后来把"后见之明"这四个字落实成一行 relabel 代码,我才意识到:强化学习里真正稀缺的从来不是"成功轨迹",而是"把失败轨迹看懂的能力"。
这篇文章想写的,就是这个叫 hindsight 的算法思路。它的全名是 Hindsight Experience Replay,由 OpenAI 在 2017 年提出,专门解决稀疏奖励场景下 off-policy 算法学不动的问题。不管你是刚入门 RL 的学生,还是已经在跑机器人控制任务的工程师,只要被"奖励全是 0、策略不更新"折磨过,这篇文章应该能帮你把原理、实现和坑一次性理清楚。
1. 稀疏奖励的困局:为什么经典 RL 在这里集体失灵
1.1 稀疏奖励:一次任务只有 0 和 1 两种信号
我经常给刚进实验室的同学打一个比方:稀疏奖励的环境,等于一场只看最终成绩的闭卷考试。你从头到尾做一百道题,交卷后老师只告诉你"及格了"或"没及格",中间没有任何过程分。反馈极少,但每一步的选择都有意义。机器人领域的抓取、推箱子、插销,游戏里的寻宝、迷宫,本质上都是这种"要么全对、要么全错"的反馈模式。
在数学上,稀疏奖励可以写成一个非常简单的判断函数:目标达成给 1,其余时刻给 0。比如机械臂抓取,只要抓手在目标物体附近且物体被成功抓起,这一整条轨迹里最后一步的奖励才变成 1。问题在于,前面几十步、几百步的动作,无论多么接近正确答案,奖励一律是 0。于是策略梯度类算法算出来的梯度几乎处处为零,整个网络像冻结一样不更新。
1.2 随机探索为什么救不了场
也许有人会想:既然没信号,那就多随机试几次,总有一次成功吧?这个想法在离散小状态空间里勉强成立,但在连续控制任务里基本是幻想。
我们可以做一个很粗糙的估算:假设机械臂每步动作以 0.9 的概率"方向正确",任务需要连续 50 步都保持在正确轨道上才能碰到目标,那么整条轨迹成功的概率大约是 0.9 的 50 次方,约等于 0.005。也就是说平均两百条轨迹才可能拿到一次正反馈。如果方向正确概率降到 0.5,这个数字会变成 0.5 的 50 次方,几乎等于 10 的负 15 次方。
而在 DDPG/TD3 这类算法里,一次成功轨迹的正反馈会被几百条失败轨迹的零奖励稀释,Q 函数在初期学到的基本是一个"永远接近零"的常数。等到某次随机探索真的运气爆棚,信号能不能在信噪比极低的情况下传回网络,又是另一个问题。
1.3 传统补救方案的局限
社区为解决稀疏奖励尝试过不少思路:
- reward shaping:人为设计中间过程的奖赏,引导机器人逐步靠近目标。但它需要很强的领域知识,而且设计不好会改变最优策略——机器人可能学着"原地抖动刷奖励",而不是真正完成任务。
- curriculum learning:从简单任务逐步过渡到难任务。限制在于课程通常要人工设置,且策略在课程切换时容易出现"灾难性遗忘"。
- 模仿学习:直接学人类专家的演示。问题在于演示数据的采集成本高,而且专家轨迹覆盖不到的状态空间,策略依然无从学起。
这些方法都忽略了一件很反直觉的事情:那些"失败"轨迹,其实并不是没有信息。只是因为我们一直用最终目标去评判它,才让它们看起来毫无价值。HER 正是从这个视角出发,把"后见之明"变成了一种可复用的训练数据。
2. 同一条轨迹,换一个目标定义,它就是一次成功
2.1 "事后诸葛亮"在强化学习里的正确用法
hindsight 的英文原意就是"后见之明",中文里有个更接地气的说法叫"事后诸葛亮"。第一次接触 HER 论文时,我觉得这个命名特别巧妙,因为它真的把人类复盘时的思维过程搬进了算法。
想象一个学生考试。目标本来是 90 分,结果他只答对了三道题,总分 30,考试失败。但他复盘时发现,这三道题其实是他知识范围内完全掌握的。那么问题来了:如果把目标改成"答对这三道题",这场考试对他个人来说是不是一次成功的、可以复用的经验?答案当然是。他至少确认了这类题目的解题路径是有效的。
机械臂也是一样。它没能把红色方块抓到指定位置,但它的确伸出了手臂、推了一下方块、让方块发生了位移。这些都是"事实上已经达成"的状态。把目标从"把红色方块放到位置 A",替换成"把手臂移动到它实际到达的那个点",或者"把方块推到它实际停下来的那个点",这一整条原本失败的轨迹,立刻就变成了一条完成目标的成功专家轨迹。
2.2 形式化:目标条件策略与目标重标记
要支撑这个直觉,需要把强化学习的环境定义扩展一下。我们得引入目标条件马尔可夫决策过程(Goal-Conditioned MDP):
- 环境除了状态 s,还额外有一个目标 g;
- 策略是条件策略 π(a | s, g),也就是说智能体要根据不同目标决定行为;
- 奖励 r(s, a, g),目标达成时给 1,否则给 0。
在这个框架下,HER 的重标记过程非常干净。假设我们采集到一条轨迹:
τ = (s_0, a_0, s_1, a_1, ..., s_T)
它原本对应的目标是 g,但因为没达成,所以每个时刻的 r 都是 0。现在我们不去修改环境,只修改这条轨迹在训练时"名义上要完成的目标"——从这条轨迹里挑一个实际达到过的状态 s' 当作新目标 g',重新计算每个 transition 的奖励:
r'_t = r(s_t, a_t, g')
由于 g' 是轨迹里真实到达过的状态,新的奖励序列里大概率会出现 1。然后我们把整条轨迹以 (s_t, a_t, r't, s{t+1}, g') 的形式写进 replay buffer。
2.3 这不算作弊吗?为什么策略不会被"教坏"
每次讲 HER,几乎都有人问同一个问题:把失败轨迹标成成功,难道不会让智能体学到自欺欺人?
答案是不会,关键在于我们改变的是"训练数据的标签",而不是"环境中的真实奖励"。在环境里,红块没抓到就是没抓到,测试时该给 0 还是给 0。但在训练阶段,HER 教给策略的其实是另一种能力:如果你到达了某个状态,你就应该学会"如何到达这个状态"。换句话说,这条轨迹原本要学的是"如何抓红块",重标记之后学到的是"如何到达手臂现在的位置""如何把方块推到它停住的位置"。
这些技能单独看毫无意义,但当目标条件策略同时学习大量这类"到达过的地方"的 relabel 样本后,它就慢慢建立起一个目标空间里的运动技能库。等到测试目标恰好落在这些已学会的路线上时,策略自然能泛化过去。这也是为什么论文里反复强调:HER 必须配合 goal-conditioned 网络结构,没有目标作为输入,relabel 就失去了意义。
2.4 最小可读的核心伪代码
如果只保留 HER 最核心的东西,其实就是一段重标记函数。我用 PyTorch 风格的伪代码写一下:
def her_relabel(episode, original_goal, goal_dim, k=4): transitions = [] horizon = len(episode) for t in range(horizon): obs, action, _, next_obs, _ = episode[t] # 每个原始 transition 保留 1 份原目标样本 if t % (k + 1) != 0: goal_for_this = original_goal reward = sparse_reward(obs, action, original_goal) else: # 从未来时间步中随机挑一个状态作为新目标 future_idx = random.randint(t + 1, horizon - 1) goal_for_this = episode[future_idx].next_obs[:goal_dim] reward = sparse_reward(obs, action, goal_for_this) transitions.append((obs, action, reward, next_obs, goal_for_this)) return transitions实际工程里,不会真的按 t 间隔来切分原始样本和 relabel 样本,而是按比例批量采样。后面讲数据管线时我再展开。
3. 目标从哪来:final、future、episode、random 四策略对比
3.1 四种重标记策略
论文里给出了四种选择新目标 g' 的策略,很多人看公式半天没感觉,我来一个个解释:
- final:直接用轨迹终点状态 s_T 作为新目标。实现最简单,但一条轨迹只产出一个替代目标,信息量有限。
- future:从当前时刻 t 之后的状态里随机挑一个,作为该 transition 的新目标。这是论文实验里效果最好的策略。
- episode:从整条 episode 的任意状态里随机挑,不限过去还是未来。实现方便,但可能采到"时间上已经过去"的目标,导致前后不一致。
- random:从预先生成的目标池或者与当前 episode 无关的状态里随机挑一个。这种策略几乎不携带"这条轨迹做到了什么"的信息,效果通常最差,论文里更像一个对照组。
这里需要注意,future 策略里的"未来"是指相对于当前 transition 的时间戳 t 而言。常见写法是从 [t, T] 或 [t+1, T] 里均匀采样。
3.2 策略效果对比表
我做了一张粗粒度的对照表,方便你快速理解:
| 策略 | 新目标来源 | 信息量 | 实现难度 | 实验表现 | 适用场景 |
|---|---|---|---|---|---|
| final | 轨迹终点状态 | 低 | 最低 | 中 | 短轨迹、伪代码验证 |
| future | 当前步之后的状态 | 高 | 低 | 最高 | 大部分稀疏奖励控制任务 |
| episode | 整条轨迹任意状态 | 中 | 低 | 中上 | 长轨迹且目标空间连续时 |
| random | 随机目标池 | 极低 | 低 | 差 | 几乎不建议单独使用 |
3.3 为什么 future 在实验里一骑绝尘
OpenAI 论文里用 Bit-Flipping 环境做了系统对比,future 策略在 20 位二进制翻转任务上明显优于 final 和 random,也比 episode 好。我的理解是两点:
第一,future 目标与当前轨迹的实际走向强相关。它选出的目标状态,是这条轨迹自己即将到达或最终到达的某个地方。这种目标天然满足"轨迹真实可达",relabel 出的成功样本分布与真实状态分布高度一致。而 random 目标基本来自目标池的均匀采样,和当前轨迹没有任何关系,即使 relabel 成"成功",也只是强行粘贴一个标签,训练出的策略在实际面对这种目标时大概率崩掉。
第二,future 的时序语义干净。它保证目标来自未来,不同于 episode 可能采到过去的状态。如果目标在时间上已经"发生过了",还会带来一种诡异的耦合:策略需要完成的目标本身依赖于之前的行为,梯度传播路径会被污染。future 虽然也引入一定程度的自相关性,但相比 episode 更可控。
3.4 我自己的选型经验
在短 horizon 的控制任务上,我基本无脑选 future,K 取 4 或 8。长 horizon 任务(比如需要几百步才能完成导航)里,我会把目标池限制在一定半径内,避免采样出过于遥远、不可能一次学会的目标。另外可以在 future 基础上混合少量原始目标样本,用来维持对真实目标的敏感度——实际效果比纯 relabel 更稳。
4. 把 HER 装进 DDPG/SAC:数据管线的改造
4.1 为什么 HER 天然适合 off-policy
很多初学者问:HER 能不能和 PPO 一起用?理论上可以,但工程上很不顺手。原因在于 HER 的本质是"重新利用旧数据",而 on-policy 算法恰恰要求参与更新的数据都来自当前策略。你一旦对旧轨迹做目标重标记,数据的"策略新鲜度"就被破坏了,更新出来的梯度是有偏的。PPO 的 importance sampling 本身处理的是动作概率差异,没有能力纠正目标标签替换带来的偏置。
反过来,DDPG、TD3、SAC、DQN 这类 off-policy 算法用 replay buffer 缓存历史数据,本来就允许"旧数据参与训练",HER 只是把重标记这一步插在"采样 transition"和"计算梯度"之间,完全符合 replay 的哲学。所以 HER 不是一套独立算法,而是一个数据增强插件,默认搭配 off-policy 方法使用。
4.2 Replay Buffer 的数据结构改造
传统 replay buffer 里每条数据是一个四元组 (s, a, r, s')。接入 HER 后,单位不能再是单条 transition,而要是整条 episode。因为 relabel 需要知道整条轨迹最终走到了哪里、某个时间点之后有哪些状态可选。
我的做法是建一个环形 buffer,元素是 episode 对象:
class EpisodeBuffer: def __init__(self, max_episodes): self.max_episodes = max_episodes self.episodes = deque(maxlen=max_episodes) def push_episode(self, episode): # episode: [{obs, action, reward, next_obs, done}, ...] self.episodes.append(episode) def sample_transition_with_goal(self, batch_size, k=4, strategy="future"): batch = [] for _ in range(batch_size): episode = random.choice(self.episodes) t = random.randint(0, len(episode) - 1) trans = episode[t] if random.random() < 1.0 / (k + 1): goal = trans["original_goal"] else: goal = sample_relabel_goal( episode, t, strategy ) reward = sparse_reward(trans["obs"], trans["action"], goal) batch.append((trans["obs"], trans["action"], reward, trans["next_obs"], goal)) return batch注意,这里的 reward 必须在重标记目标之后再计算,不能直接读取原始 reward。这是新手最容易漏掉的一步。
4.3 DDPG + HER 的完整训练循环
结合 DDPG 的话,整体循环长这样:
for episode_idx in range(total_episodes): goal = sample_goal() obs = env.reset() episode = [] for t in range(max_steps): action = actor(torch.cat([obs, goal])) + noise() next_obs, reward, done = env.step(action) episode.append({"obs": obs, "action": action, "next_obs": next_obs, "reward": reward, "original_goal": goal}) obs = next_obs if done: break buffer.push_episode(episode) if len(buffer) > warmup: for _ in range(train_steps): batch = buffer.sample_transition_with_goal(BATCH_SIZE, k=4) obs_b, act_b, rew_b, next_obs_b, goal_b = unpack(batch) # 训练 critic target_q = critic_target(torch.cat([next_obs_b, goal_b])) y = rew_b + gamma * target_q critic_loss = mse( critic(torch.cat([obs_b, goal_b]), act_b), y ) # 训练 actor actor_loss = -critic( torch.cat([obs_b, goal_b]), actor(torch.cat([obs_b, goal_b])) ).mean() # 更新目标网络...这个循环里最值得注意的点是:actor 和 critic 的输入,全部要带上 goal。目标网络也一样,要把 goal 拼进去。很多复现失败的 case,就是把 goal 漏在了某个分支外面。
4.4 目标信息在网络里的注入方式
最简单也是最常用的做法是把 obs 和 goal 直接拼接后送入网络。对 Fetch 这类状态维度不高的任务,用 concat 就是够的,效果也很好。
如果你遇到的是高维观测、目标很复杂的情况,可以考虑更结构化的注入:
- FiLM 层:用 goal 生成缩放和平移系数,调制中间特征。在部分视觉任务里比 concat 稳定。
- 自注意力:目标和观测分别编码后做 cross-attention,适合目标与对象位置高度相关的场景。
- 双流结构:观测和 goal 先各过一个 MLP,再在某个层汇合。增加参数,但有时能改善解耦。
我的建议是:先按 baseline 用 concat 跑通,再根据任务需要逐步升级。不要在没跑通 baseline 的时候直接上复杂结构。
5. 复现 HER 最容易踩的五个坑
5.1 目标没有归一化
我第一次跑 FetchPush 时,Q 值一直在正负之间剧烈震荡,还以为是网络结构问题。排查到最后发现,是把观测归一化到了 [-1, 1],但目标还是原始的三维坐标。critic 在拼接处直接看到了两个尺度完全不一致的输入,梯度方向混乱。
处理办法:无论 obs 还是 goal,进入网络前统一归一化到同一个量级。若使用相对目标(如"当前位置与目标的差值"),也要保证差值维度的量级一致。这个预处理看着不起眼,却直接影响整个训练过程的稳定性。
5.2 把三值奖励换成距离奖励再配合 HER
在稀疏奖励任务里,很多人嫌 0/1 信号太单调,主动改成"到目标的距离"作为奖励。这个想法本身没有错,但和 HER 放在一起时会出问题——HER 的重标记目标是"轨迹实际到达的状态",当你使用距离奖励时,relabel 后的奖励会从连续分布里取值,数据分布和原始三值奖励的分布完全不一样。我在实验中发现,这种混用会让学习曲线前期极其不稳定,且难收敛。
我的建议:先复现论文的 0/1 三值奖励,确认 HER 本身的机制生效了,再考虑 reward shaping。如果你确实需要距离奖励帮助探索,也要把距离奖励限制在一个较小的值域里,并观察 relabel 样本的 reward 分布。
5.3 future 策略的时间下标写错
future 采样必须保证目标来自"当前时刻之后"的状态。如果偷懒写成从整条 episode 随机采(episode 策略),实验结论往往会和论文对不上。
更隐蔽的 bug 是索引错位:future 采的是 next_obs 而非 obs。比如第 t 步的状态是 s_t、动作 a_t、下一状态 s_{t+1},目标应该从 s_{t+1}, ..., s_T 里采样,因为动作 a_t 真正影响的第一个未来状态是 s_{t+1}。如果把 s_t(当前状态)也纳入候选,会引入一步"虚假成功":策略什么都没做,目标却已经达成了。
我踩过这个坑后写了个注释来提醒自己:
# 正确:目标从 t+1 步之后的状态里采样 candidate_states = episode[t + 1:] # 错误:目标从当前步开始采样 candidate_states = episode[t:]提示:future 策略采样范围应该从下一个状态开始,而不是当前状态。这一点直接影响训练稳定性,是最容易被忽视的实现细节。
5.4 网络拼接维度没对齐
DDPG 的 actor 和 critic 输入维度必须严格一致。描述起来很蠢,但真的会犯:obs 是 (B, 17),goal 是 (17,),拼接时 broadcast 出错;或者 actor 里 concat 了、critic 里忘了 concat,导致 critic 永远看不到 goal。
调试技巧很简单:在第一次 forward 前打印每个分支的 shape,确认 goal 维度和 obs 维度都对齐了。再把 goal 全部置零跑一步,loss 不报错不代表逻辑正确,只能说明维度对。真正要验证的是:固定 obs,改变 goal,看 critic 输出是否随之变化。如果 Q 值对 goal 的变化无动于衷,说明目标信息根本没有进到网络里。
5.5 忽略探索噪声与随机种子
HER 负责"从失败轨迹里挖价值",但它不负责"主动探索新状态"。如果策略前期几乎随机、没有系统性的探索,HER 也只是在反复重标记同一个小状态空间里的轨迹,收益有限。
Bit-Flipping 这类离散任务里,我建议动作加足够大的 epsilon 噪声,甚至在前几千个 episode 用完全随机策略收集初始数据。连续控制任务里,DDPG 的 action noise 初始值要设得足够大,比如 OU 噪声 sigma 取 0.2 以上,后期再逐步衰减。另外,HER 对随机种子的敏感度不低,同一个任务不同种子的试验成绩差异可能超过 20 个百分点。报告实验结果时务必跑多个种子取中位数,不要被单种子结果误导。
6. 从 Bit-Flipping 到真实机械臂:HER 的边界和拓展
6.1 经典实验结果回顾
OpenAI 论文里最让人印象深刻的图,是 Fetch 系列环境下 HER+DDPG 的成功率曲线。以 FetchPush 为例,普通 DDPG 跑几百万步成功率还在低位徘徊,加 HER 后几十万步就能逼近 100%。FetchSlide 这种需要精确控制滑动距离的任务稍微难一些,HER 也能把成功率拉到 60% 以上。
这些环境的特点值得注意:目标空间与状态空间完全一致(目标就是某个物体的位置或关节角度),并且奖励判定是稀疏的。这正是 HER 最理想的发挥空间。如果你的任务不属于这一类,比如目标是文本指令或抽象语义,那么 HER 没办法直接上,得先想办法把目标嵌入到与状态可比较的空间中。
6.2 HER 的短板:陌生区域与探索死结
HER 有一个很本质的限制:它只能重标记"策略已经到过"的状态。如果受限于探索能力,机械臂从未出现过某些区域的状态,HER 就没有关于这些区域的成功样本。它能把已知区域的路越来越熟,但对完全未知的区域无能为力。
这个问题的常见解法包括:
- 将 HER 与课程学习配合:先让智能体在简单目标上积累状态覆盖,再逐步提高目标难度;
- 增大探索噪声或使用多策略混合:让数据收集阶段更"野"一点;
- 使用 HGG(Hindsight Goal Generation)这类变体:主动分析当前策略的能力边界,生成恰好在"可完成边缘"的目标,逐步扩大覆盖区域。
6.3 relabeling 思想的更大版图
HER 的意义已经超出了抓取任务本身。目标重标记这个思路,在多个方向都有延伸:
- 模仿学习也可以用 relabeling:把专家演示轨道的目标改成它实际完成的任务,让策略学"如何完成专家做过的任何事",比单一任务模仿更稳健。
- Offline RL 里,很多方法开始用 relabeled goal 来生成正样本,缓解 batch 内数据稀疏问题。
- 多任务学习里,把任务间轨迹互相重标目标,本质上也是一种数据增强。
这些方向听起来很散,但底层都是同一个智慧:与其执着于"这条轨迹是不是完成了它本该完成的任务",不如问"这条轨迹究竟完成了什么任务"。
6.4 我的实践建议
如果你也想复现,我的建议路径很明确:先在 Bit-Flipping 这种玩具环境上跑通 HER 的核心逻辑,确认 relabel、采样比例、奖励重算这些细节都对;再上 FetchPush 这类机器人仿真环境,把维度、归一化和噪声调明白;最后再考虑真实机械臂。真实环境里还需要处理状态估计误差、执行延迟和随机扰动,HER 在仿真里的收益不一定能完全复现。
我个人在实际项目中的体会是:HER 并不是万能钥匙,它解决的是"稀疏奖励下如何复用失败数据"这一具体问题。它让我最受益的一点,是改变了看待数据的习惯——训练数据里没有绝对的垃圾,只有还没被正确标记价值的轨迹。如果你手头正好有一个奖励稀疏、策略学不动的任务,不妨先用 HER 把主线跑通,再回头看其他花哨技巧,你会发现很多所谓"高级解法"其实都是在给 HER 打辅助。