人的大脑里住着一个马后炮:题做错了,看完答案觉得“我本来会做”;系统线上出了故障,复盘时每个人都拍着大腿说“我早提醒过”。这种事后聪明在心理学里叫 hindsight bias,往难听里说叫马后炮,绝大多数时候遭人嫌弃。但到了强化学习(RL)领域,事情反过来了。有研究者把“事后聪明”做成了算法的核心训练信号,名字起得毫不避讳:Hindsight Experience Replay,简称 HER,中文通常译为“后见经验回放”。
我第一次真正被这个算法击中,是 2017 年底做一个机械臂抓取项目。目标很朴素:从桌面上抓到一个随机位置的杯子。听起来不难,但当你跑起训练就会发现什么叫稀疏奖励下的绝望——环境只在你成功抓取那一刻返回 +1,其余上千个 episode 全程奖励为零。策略网络在这种信号下几乎是纹丝不动,损失函数像块石头。后来我把 OpenAI 那篇《Hindsight Experience Replay》翻出来,看完整个思路,有种后脑勺被拍了一下的感觉:既然抓不到目标杯子,那智能体这一百步里总归把手伸到了某个位置、碰到了某个物体、把机械臂挪到了某个坐标,这些信息难道就该被当成垃圾丢掉吗?HER 的核心,就是把这些“没达成原始目标、但达成了某个替代目标”的经验捡回来,重新赋予奖励信号,然后再拿去训练。
这篇文章,我会结合自己从零复现和实际部署 HER 的经历,把这个算法的原理、代码接入方式、训练中的坑、边界条件和调参心得一次讲透。适不适合你,取决于你是否也在做机器人控制、稀疏奖励任务、或者任何“成功次数极少、但环境能在事后判断成不成功”的强化学习问题。
1. 我们都依赖“事后聪明”,但 AI 一直学不会这个——稀疏奖励的根本困境
1.1 当奖励全是零,梯度也归零
要理解 HER 为什么重要,先得理解稀疏奖励为什么难。
强化学习的常规更新公式里,策略梯度大致正比于“这个动作带来的累计回报”。如果训练时所有 episode 的奖励都是 0,那策略梯度中每一项都趋近于 0,网络几乎收不到任何有效的“推一把”的信号。你可能会问:不是还有探索噪声吗?是,探索噪声可以让智能体随机尝试不同的动作,但一个维度足够高的连续动作空间里,靠随机噪声撞到精确目标位置的概率低到可以忽略。我做抓取实验时统计过:500 个 episode 里能碰到杯子的不到 3 次,而真正把杯子抓起来并保持稳定的次数是 0。这不是算法的锅,是信号本身就没有。
1.2 常规解法为什么绕不过去
面对稀疏奖励,主流思路无非以下几种,但每条路都带着额外成本。
- 奖励塑形(Reward Shaping):手写一个稠密奖励函数,比如“离目标越近奖励越大”。听起来直接,但现实任务里很难构造一个好的距离度量。机械臂抓取,是算指尖到杯口的欧氏距离,还是算手腕角度差加距离?手写的奖励往往会被智能体钻空子,比如绕着目标转圈也能累计距离奖励,最后学会一个极丑且不稳定的动作。
- 课程学习(Curriculum Learning):从简单目标开始,逐步提高难度。这很有效,但需要你提前设计一套任务难度序列,在真实环境里还要动态调整。说白了,又多了个人工调度器。
- 更聪明的探索(如 ICM、RND):用内在奖励引导探索。这类方法能缓解问题,但引入了一套额外的预测网络和超参,稳定性和通用性在稀疏 reward 下都不好保证。
这些方法都有一个共同的前提假设:任务的目标是固定的、不可更改的。HER 的切入角度不太一样——它问了一个很“人类”的问题:既然“这次失败了”,那我们能不能换个目标,把这次失败看成一次成功?
1.3 重新定义“成功”
在 HER 眼里,每个 episode 的价值不只有“达成原始目标的概率”,还有“达成其他目标的信息量”。
比如机械臂本来想去位置 A,结果手伸到了位置 B。从任务 A 的角度看,这是失败,零奖励。但如果把任务从“移动到 A”改写成“移动到 B”,那这条轨迹就是一条完美演示:智能体从起点一路运动,最终准确停在 B,奖励应该是 +1。这条经验对任务 B 来说有多珍贵?它相当于一份免费的专家演示,而传统算法直接把它扔了。
关键来了:目标 B 不是拍脑袋想出来的,它就藏在轨迹的末端状态里。“智能体最终到了哪里”,这个信息在 episode 结束时天然可得,不需要额外标注。HER 本质上是在说:我们不需要为每个失败去猜一个有用的替代目标,直接把到达的最终状态当成目标就行——这就是“后见之明”。你做到了你没打算做的事,那就把这件做到了的事记下来,以后可能会用得上。
2. HER 的核心玩法:把失败轨迹“改签”成成功轨迹
2.1 前提知识:Goal-Conditioned RL 和 Off-Policy
HER 不是凭空长出来的,它依附在一套已经存在的框架上,有两个前提你得先有。
第一个是goal-conditioned RL(以目标为条件的强化学习)。也就是说,你的状态输入里必须包含一个“目标”信息,网络要根据这个 goal 来做决策。环境返回的 reward 也不是一个固定函数,而是由当前状态和目标共同决定的:只有当前状态满足目标条件时,reward 才为 1,否则为 0。第二个是off-policy 算法,典型代表是 DQN、DDPG、SAC。只有 off-policy 才允许你事后修改经验里的 reward 和 goal,然后再放进回放池反复训练。on-policy 算法(比如 PPO)一条数据只能用一次,事后改签再训练就行不通了。
2.2 重标定:不是改动作,是改目标
HER 对常规经验回放的改造,核心就一步:在 store 一条 transition 时,额外生成一个或多个替代目标(hindsight goal),然后用替代目标重新计算这条经验的 reward,再把“原目标版本”和“替代目标版本”一起存进样本池。
这里有一个容易混淆的点:HER 不会修改原始状态和动作,因为动作和状态是环境真实反馈的结果,不能造假。它修改的只是“目标”和“奖励”。Link 一下你的人生经验:你没考好,但你的错误答案暴露了你哪一章没学懂。你不会去篡改考卷上的答案,但你可以把“学会这一章”当作新的目标来复习。HER 做的就是这个。
具体的重标定过程可以用伪代码表达:
# 假设一个 episode 已经跑完,收集了 list of transitions # 每条 transition 结构: obs, action, reward, obs_next, goal, done def relabel_episode(transitions, strategy="future", k=4): new_transitions = [] n = len(transitions) # 对每个 transition,额外生成 k 个替代目标版本 for i, trans in enumerate(transitions): # 添加原始版本 new_transitions.append(trans) if strategy == "final": # 最简单:用整个轨迹最终状态作为目标 hindsight_goal = transitions[-1].obs_next # 取最终观测中的目标部分 # 重新计算奖励:如果当前 obs_next 满足 hindsight_goal,则 r=1 new_trans = relabel_one(trans, hindsight_goal) new_transitions.append(new_trans) elif strategy == "future": # 更常用:从轨迹后续时间步中随机采样 k 个状态作为替代目标 for _ in range(k): # 随机选一个在 i 之后的时间步 future_idx = random.randint(i, n-1) hindsight_goal = extract_goal(transitions[future_idx].obs_next) new_trans = relabel_one(trans, hindsight_goal) new_transitions.append(new_trans) return new_transitionsrelabel_one要做的也很简单:把目标字段替换成 hindsight_goal,然后调用环境的奖励函数,判断obs_next是否满足新目标,满足给 +1,不满足给 0。奖励函数本身不用改,环境已有的判断逻辑直接复用。
2.3 future 采样策略:为什么“用未来状态当目标”最香
你可能会问:替代目标为什么不直接用轨迹末端状态(论文叫 final 策略),而是要随机从后续状态里采样?
早期 HER 论文实验过多种策略:final(只用终点)、episode(用本 episode 内随机时间步)、future(只用当前步之后的时间步)、random(任意时间步)。实验结果非常明确:future 策略通常最好,k=4 时性价比最高。原因也不难理解:
- 如果用最终状态,那么整条轨迹只有靠近结尾的那几个 transition 是“达标的”,前半段的替代奖励全是 0,区别不大。
- 如果用当前步之前的某个状态当目标,会出现一个时序矛盾:智能体在 t 时刻还没有“到达”那个未来状态,但目标报的却是那个状态的位置,这样反而不利于学习动态性。
- future 策略从当前时刻之后的状态里采,相当于给每个 transition 都安排了一个“只要我继续走下去就能到”的目标。对于轨迹的中期和后期,它都能构造出足够多的 +1 样本,学习信号密集得多。
所以我的习惯是:k=4、future 策略。如果你的任务轨迹很长(几百步以上),可以考虑 k=8,但要注意回放池里的冗余样本比例会上升,训练速度变慢。
2.4 免费的正样本:把失败变成多任务学习
理解 HER 有效性的另一个视角,是把它当成多任务强化学习。假设一个环境里有一堆不同的 goal,每个 episode 开始你会随机抽一个 goal 让它去完成。传统 RL 拿到的样本,绝大多数都失败,稀疏得离谱。但 HER 发明了一个巧妙的 trick:每个失败 trajectory,对于它实际到达的那个 goal 来说,是一条成功轨迹。所以它是“免费的专家演示”——因为那个实际到达的 goal 在训练之前根本不用预先枚举,只需要事后标注就行。
这样一来,你既不需要人工设计稠密奖励,也不需要精心安排课程,只需要一个能判断“目标是否达成”的判定器,就能让网络从失败中持续学到“如何把状态从 A 变到 B”的运动技能。这正是 HER 能成为稀疏奖励领域常青树的原因。
3. 从论文到仓库:HER 在 DDPG 上的接入方式与关键代码
3.1 环境侧要做的三处改动
不是随便一个 RL 环境都能接 HER,你需要先做三处改造。这里拿我常用的 Gym 类机械臂环境举例。
- 状态要包含 goal 信息。通常的做法是把 observation 拼接成
[state, goal]的形式,其中 state 是机械臂的关节角度、末端位置等物理量,goal 是目标位置向量。DDPG 的 actor 和 critic 输入都要同时包含这两部分。 - reward 必须由 goal 和状态动态算出。不能像 CartPole 那样返回一个固定公式。比如:
reward = 1 if np.linalg.norm(achieved_goal - desired_goal) < threshold else 0。 - 环境要能返回“已达成目标”。这通常意味着 step 函数里要解析出
achieved_goal,比如末端执行器当前位置。没有它,HER 就不知道用什么来做重标定。
3.2 训练循环:采样器也要跟着改
HER 和普通 off-policy 训练循环的最大区别,在于回放池存的是完整 episode,而不是单步 transition。因为重标定需要整条轨迹的全部未来状态,单条 transition 脱离轨迹后无法 future 采样。
我的训练循环结构大致如下:
# 伪代码,展示 HER 在 DDPG 训练循环中的位置 replay_buffer = EpisodeBuffer(capacity=100000) for episode in range(total_episodes): obs = env.reset() episode_transitions = [] for t in range(max_steps): action = policy(obs) + exploration_noise obs_next, reward, done, info = env.step(action) episode_transitions.append((obs, action, reward, obs_next, done)) obs = obs_next if done: break # HER 核心:整条轨迹入库前做重标定 relabeled = relabel_episode(episode_transitions, strategy="future", k=4) replay_buffer.store(relabeled) # 普通 off-policy 更新 for _ in range(updates_per_episode): batch = replay_buffer.sample(batch_size) ddpg_update(batch)注意一个细节:训练更新频率通常是一个 episode 结束后做多步更新。因为 HER 一次性会膨胀出大量重标定样本,你在一个 episode 结束就该立刻抽样训练,而不是像 DQN 一样每步更新一次,否则 buffer 里积攒的 0 奖励样本太多,训练效率会打折扣。
3.3 CRITIC 的网络输入:不要漏掉 goal
DDPG 接入 HER 时,最容易被忽略的是网络结构里 goal 的注入方式。常见做法是把[state, goal]直接 concat 后输入 MLP。但在抓取这类任务里有个经验:把 state 和 goal 分别编码,再拼接成一个中间层,往往比直接 concat 效果更好。原因很简单,state 和 goal 的量纲、语义差异很大,直接拼在一起会让网络前期花更多时间在 “对齐语义” 上。我在复现时用了两层 encoder 分别处理 state 和 goal,再送入同一套全连接层,收敛速度明显提升。
3.4 少量关键超参配置
这里给一份我跑 FetchPush 环境时验证过的配置参考,基于 DDPG:
| 超参 | 值 | 说明 |
|---|---|---|
| actor 学习率 | 1e-3 | 用 Adam,与 critic 解耦 |
| critic 学习率 | 1e-3 | 保持一样的量级即可 |
| polyak 系数 | 0.95 | target 网络软更新参数 |
| gamma | 0.98 | 任务步数短,gamma 太高意义不大 |
| 探索噪声 | N(0, 0.2) | 训练中期需要手动衰减到 0.05 |
| future 采样 k | 4 | HER 默认推荐 |
| batch size | 256 | 不要低于 128,否则重标定样本多样性不够 |
| buffer 容量 | 1e6 | 注意内存,episode buffer 比单步 buffer 吃内存 |
4. 复现 HER 时踩过的坑:目标归一化、future 采样与网络结构
4.1 第一个绊脚石:观测归一化不做,直接 NaN
这是我第一次跑 HER 时踩的头号大坑。机械臂环境的观测里,同时包含关节角度(大概 0 到 180 度)、关节角速度(可能上千)、末端位置(0 到 1 之间)、目标位置(0 到 1 之间)。这些东西量纲差了几个数量级,直接喂给 critic 网络,Q 值很容易爆炸,训练几百步后 loss 变成 NaN。
解决办法不是调小学习率,而是对观测做归一化。最简单的做法是维持一个 running mean / running variance,把每一个维度都缩放到均值 0、方差 1。如果你用的环境已经自带 normalized obs,那也要注意:重标定出来的 goal 必须用同一套归一化统计量,否则 train 和 relabel 时的分布不一致,critic 学了错误映射。
4.2 future 采样和“信息泄漏”的迷思
群里讨论 HER 时,经常有人问:重标定用了轨迹未来的状态,这会不会造成信息泄漏?答案是不会。因为未来的状态只是被用作一个“事后目标”,它并没有参与动作生成——动作序列是在原始目标下执行出来的。从数据生成角度,这相当于“事后给一条轨迹贴上新的标签”,不影响策略执行的因果性。
但要注意一个真正的坑:future 采样必须限定在当前时间步之后,而且必须来自同一条轨迹。如果采样时跨了 episode,或者选了当前步之前的某个状态,那么重标定出来的“成功”会包含未来信息,训练时 critic 可能学出一种“我明明什么都没做就已经成功”的幻觉。用代码表达就是:random.randint(i, n-1),下标一定要从 i 开始,不能从 0 开始。
4.3 网络层数不是越深越好
HER 常用环境里的状态空间很低维,比如 Fetch 系列大多数是 25 维左右。这个体量下,三到四层全连接(每层 256)足够,再深反而容易过拟合重标定样本里的噪声。我试过用 ResNet 风格的结构去跑,训练更慢,且没有带来任何精度提升。MLP 你不需要用太激进的调参,老老实实 256-256-256 就够了。
4.4 评测指标:小心目标分布偏移
HER 训练时,目标是在每个 episode 开始时随机采样的,这就意味着模型学的是“在任意随机目标上都能成功”的能力。但实际部署时你往往关心的是某一个固定目标。这就带来一个评测陷阱:如果测试时只测少数几个固定目标,得到的结果可能虚高或虚低,取决于训练时目标分布和测试目标的匹配程度。
我自己的做法是:先用均匀采样目标方式训完,再用一个固定的、中等难度的目标列表做评测,跑 100 个 episode 取成功率。另外,训练时给目标加一点噪声(目标位置小幅扰动),能显著提升在固定目标上的稳健性,代价是收敛速度稍微变慢。
4.5 大 buffer 的内存压力
HER 需要存整条轨迹,所以它的 buffer 比普通 DDPG 大得多。一个 50 万容量的 episode buffer 在 30 维观测下大概要占几个 GB 内存。这时候有两个选择:一是把 buffer 容量调小一点,比如 20 万;二是用字典存储而不是 list。反正别省内存把容量压到 5 万以下,否则重标定的多样性会严重不足,稀疏任务基本学不动。
5. HER 的边界在哪里:什么任务它救不了
5.1 有效的任务长什么样
HER 非常适合的四类特征:奖励稀疏但可判定成功;目标可以从观测中提取且是连续值;任务本质上是“把状态从一个地方移到另一个地方”;用的是 off-policy 算法。典型的就是机械臂抓取、推动、到达,以及某些导航任务。
5.2 失效的场景:HER 不是银弹
HER 也有明确的短板。第一类是目标不可从观测中直接得出的任务。如果任务的目标是“把螺丝拧到特定扭矩”这种高精度操作,轨迹末端状态并不能代表成功的扭矩值,重标定无从谈起。第二类是需要精确的操作序列的任务,比如倒水必须经历“拿起杯子—倾斜—对准—倒出”这个次序,你不能简单用一个最终状态来描述成功。第三类是目标维度太高的任务,比如目标是整张图片,重标定后目标空间太大,训练样本覆盖率严重不足,HER 基本无能为力。
我自己在做一个“抓取后放到指定区域”的任务时,就发现单纯的 HER 只能学到“抓起来”,学不会“放准”——因为抓取和放置的组合目标维度过高,重标定的成功标准很难用最终状态精确描述。这时需要的是分层结构:上层任务规划,底层用 HER 学单步操作。
5.3 变体和改进方向
HER 作为一个 2017 年的方法,生命力在于它太好缝合了。你可以把它和 RND 结合,用内在奖励弥补重标定之外的探索;可以把它放进 SAC 的框架里,比 DDPG 更稳定;也可以做一个分层 HER:上层学“子目标序列”,下层用 HER 完成每个子目标。如果需要目标自动生成,也可以考虑用生成模型对目标空间做采样,而不是单纯随机采样。
6. 在真实项目中使用 HER 的个人经验
6.1 复现路线:别一上来就挑战高难度
如果你也是第一次接触 HER,我的建议是:先从 FetchReach 开始。它是最简单的 HER 任务,目标就是让机械臂末端到达一个点,action 维度低、成功判定简单,一晚就能训出高成功率。跑通了再升级到 FetchPush,体会一下接触物体带来的动力学变化。等你有把握了,再上 FetchSlide 或真实机械臂。
6.2 调试利器:画一张“重标定目标分布图”
用过一段时间后我养成了一个习惯:每个 episode 结束后,把重标定目标的坐标画在散点图里,和原始目标的分布叠在一起看。如果你发现重标定目标过于集中在一个很小的区域,说明策略已经过早收敛到单一行为模式,此时需要加大探索噪声或提高目标采样均匀性。这种调试手段比盯 loss 曲线直观得多,也更容易定位问题。
6.3 最后想说的一点体会
HER 这个名字起得很诚实,整个算法就是教 AI 学会“事后聪明”。但真正做过一遍你才会懂,它教给我们的远不止一个算法:当一个任务长期得不到正向反馈时,与其硬撑着等那个遥不可及的成功,不如回头看看自己已经做到了什么,把这个“已经做到的”变成一个新的起点。这个思路放在训练智能体上有效,放在做项目上其实也一样。跑 HER 这些年,我最深的体会就是:不要只盯着你还没拿到的奖励,先看看你手里已经积累了什么可用的经验——这往往才是最被低估的宝藏。希望这篇文章能帮你少踩几个坑,也期待看到你在自己的任务上跑出漂亮的曲线。