1. 项目概述:这个“后见之明”到底解决了什么问题
做强化学习(RL)的人,十有八九都经历过这种绝望时刻:智能体在环境里跑了几个小时,奖励一直是 0,什么也没学到。我接手的第一批稀疏奖励任务就是这种状态。
那时有个机械臂抓取仿真,目标是把桌上的杯子推到固定位置。如果杯子没到目标,奖励就是 -1;到了,奖励才是 0。训练出来的策略从头到尾都是“原地不动”,因为哪怕随机探索碰巧把杯子推近了一点,策略也感知不到“变好了”,奖励信号里没有任何中间阶梯。
后来我接触到 hindsight 这个项目,准确说是一套算法思想:Hindsight Experience Replay,中文一般叫“后见经验回放”,或者更直白点——“事后聪明经验回放”。它当时发表在 NeurIPS 2018,现在已经是处理稀疏奖励问题的标配武器之一。这套思路的核心非常反直觉:如果你在某个目标上失败了,那就把这个失败结果重新标记成另一条目标轨迹的成功样本,用这条“人为构造的成功经验”去训练。说白了,就是让智能体学会从“本来想做的事没做成,但顺手完成了另一件事”里吸取经验。
这篇文章我打算围绕 hindsight 这个项目,从原理讲到实际复现,再把我踩过的坑和参数调优心得一起倒出来。适合谁看?刚上手 RL 的读者可以把它当作稀疏奖励入门第一课;已经在跑 PPO、DDPG、SAC 的工程师,也值得花十分钟看看这个“失败样本重新利用”的思想,因为它在很多实际场景里比调奖励函数更省力。
我先说结论:如果你遇到训练不收敛、奖励函数怎么设计都稀疏、或者探索阶段智能体完全不动弹的情况,hindsight(HER)是你工具箱里最值得先试的方案之一。下面我们从“为什么稀疏奖励难搞”讲起。
2. 核心痛点拆解:为什么稀疏奖励会让强化学习直接“躺平”
2.1 稀疏奖励下智能体的困境:随机成功概率太低
强化学习最基本的循环是:动作、反馈、更新策略。问题就出在反馈上。稀疏奖励环境里,智能体绝大部分情况下什么反馈都拿不到,或者说拿到的是同一个负常数。
以机械臂抓取任务为例,目标位置的精度要求可能就 5 厘米,但机械臂的关节自由度有 7 个。随机初始化策略去推杯子,杯子最终停在目标半径 5 厘米内的概率,我实测下来大约在千分之一以下。这意味着,跑 1000 个 episode,运气好才有一个 episode 拿到非零奖励。PPO 这类算法在这种环境下基本退化成了随机搜索,甚至比随机搜索还差,因为策略网络会逐渐坍缩到某个固定动作,连探索多样性都保不住。
生活化类比一下:你让一个从来没有下过厨的人,在不给提示的情况下,尝试把一道红烧肉做到“咸淡适中”的标准。他每做一锅都自己尝,但只有在“完全正好”的那一刻才能得到一个“成功”标记。大多数时候他根本不知道自己差了多远,是咸了还是淡了,于是只能原地踏步。HER 要做的事情,就是在这个人做出一锅偏甜的肉时告诉他:“虽然你没能做出标准红烧肉,但你做出了一锅不错的甜口红烧肉,记住这次操作。”于是下次他至少能稳定做甜口的,而对咸淡的把握也会逐步改善。
2.2 传统奖励塑形为什么治标不治本
看到这你可能想说:那我干脆设计一个稠密奖励——离目标越近奖励越高——不就没这个问题了?
有道理,但实际做过的都知道这里有三个坑:第一,奖励函数的设计高度依赖人工先验,你得定义“距离”,选择距离度量方式,还要权衡不同维度的权重;第二,设计不当会导致奖励黑客(reward hacking),智能体会找到刷奖励的捷径,比如原地抖动把距离传感器刷低;第三,真实环境里往往根本没有一个现成的距离函数可以给你用。比如训练一个对话系统回复客户问题,你拿什么定义“离正确答案的距离”?根本没法定。
HER 的巧妙之处在于:它不要求你设计稠密奖励,也不需要任务自然的距离度量,它直接从“已经产生的轨迹”里自动构造稠密的学习信号。这是它和奖励塑形本质不同的地方。
2.3 “后见之明”的数学合法性:从 0 概率到非零概率
为什么“把失败样本重新标记成另一种目标下的成功样本”在数学上是合理的?
关键在于目标条件的策略形式。我们训练的策略是 \(\pi(a_t | s_t, g)\),即“给定当前状态和当前目标,输出动作”。原本设定的目标是 g,但真正执行完这条轨迹后,智能体实际到达的终点状态是 g'。如果我们把目标从 g 替换成 g',那么这条轨迹的奖励序列就从原来的“全是 -1”变成了“最后一个时步至少是 0(或者按稀疏定义变成成功)”。
如果每次只把轨迹重标记到 g',那么新目标空间里的成功样本覆盖率会迅速从一个极低概率提升到接近 1,因为每条实际完成的轨迹终点都会被当作一个目标。这样,在目标空间中原本稀疏的成功样本,就变得稠密了。策略更新的梯度不再是一片死寂,而是真实的、可学习的信号。
这套逻辑确实有点“事后聪明”的味道:它不问“当初想做什么”,只问“现在做到了什么”。而正是因为目标条件策略的存在,这个“做到什么就学什么”的循环才能自洽闭环。
3. 核心技术细节:HER 机制与多目标重放的三大要点
HER 的原理听起来简单,实现起来有相当多细节,任何一个位置偷懒都可能让效果大打折扣。下面我把影响成败的至少三个关键点逐一拆开讲明白。
3.1 目标的重放方式:future 策略为什么通常最稳
HER 不是把“重新标记的目标”随便选一个就行,它有几套候选策略,经典论文里归纳为四种:
- final:直接把整个 episode 的最终状态当作新目标。
- future:从轨迹中某个时间步之后随机选一个状态作为当前样本的新目标。
- episode:从轨迹中某一个时间步的状态作为当前样本的新目标。
- random:从经验池里随机抽取一个非本轨迹的状态作目标。
我这里直接给结论:绝大多数任务里,future 策略是稳赢的,final 次之,random 效果最差。原因是 future 采样出来的目标与当前状态之间存在合适的“难度梯度”:目标越靠近轨迹后面的位置,离当前状态越远,任务越难;而 sample 对应的动作序列在语义上确实是在向那个方向移动的,于是“当前状态 -> 目标”构成了一段真实存在过的轨迹片段。
打个比方,一个人从起点出发,途中经过了 A 点和 B 点,最后走到终点 C。如果我们想让他学会“从 A 走向 C”,那 A 到 C 的后半段路程他确实走过;就算他当初的意图是去更远的 D,但至少 A 到 C 这段经历在他的运动记忆里是真实存在的,是可学习的。future 策略就是随机截取“已经走过的后半段”作为学习目标,让这些片段都变成有效训练数据。
我实际测试下来,future 策略里还有一个超参,叫“未来步数选择窗口”,一般取未来 0 到 k 步内随机。在机械臂类任务里,k 取未来时间步总长的 20%~50% 时表现最好,窗口太大容易包含已经偏离很远的轨迹段。
3.2 目标重放比例:经验从哪里 deserve 再训练
HER 在采样时会同时保留两种经验:原始目标下的失败经验,以及重新标记目标后的“人造成功经验”。这个比例直接决定了训练的稳定性。
论文推荐的默认做法是,每次从 batch 里取一部分原始经验、一部分重标记经验,具体比例往往用超参 \(K = \frac{N_{fake}}{N_{real}}\) 来控制,常见的值是 8。我的个人经验是:K 太小(比如 1-2)时,学习速度会明显变慢,因为绝大部分梯度依然来自稀疏的原始成功样本;K 太大(比如 32 以上),智能体会过度自信——它看到的全是“成功经历”,面对原始目标时反而容易产生过于激进的尝试,Q 值严重过估计。
所以 K 是一个需要根据任务稀疏程度调节的旋钮。如果你发现智能体在目标附近反复振荡、成功率高但退不出来,试试降低 K;如果你发现它从头到尾毫无进展,先提高 K。默认 8 是绝大多数连续控制任务能直接下锅的安全参数。
3.3 新目标与实际动作序列的关系:别把“事后聪明”变成“指鹿为马”
HER 能 work 还有一个隐含前提:重标记的新目标必须和这条轨迹中的动作序列有对应的“因果链条”。也就是说,新目标状态 g' 必须是这条轨迹里真实到达过的一个状态,而不是随意编造的。
如果我们在机械臂任务里把目标改成“桌子另一侧的点”,但动作序列从头到尾都是在往左边推,那么这条轨迹作为“从右往左推的目标成功经验”去训练,就是在教策略错误映射,反而干扰了策略。
这一点在实现里对应一个容易被忽略的细节:你重标记的目标必须来自轨迹本身的经验缓冲,不能直接取当前 batch 的随机生成点,否则会引入大量离线策略噪声。我在早期复现时图省事,直接从环境GOAL采样空间随机抽目标,结果训练曲线完全失真,后来对照原始代码才发现问题出在这。
4. 实操示例:从头实现一个 HER + DDPG 的稀疏奖励推箱子环境
我想用一个具体的迷你环境来演示整个实现流程。环境设定很简单:一块 5x5 的网格世界,智能体从左上角出发,目标是把“箱子”推到某一个指定位置。奖励函数设置为:只有箱子到达目标格时给出 +1,其余所有步都是 0。
在这种环境中,随机探索成功概率大约在 0.01% 量级,绝大多数策略根本学不会。我们在此之上加一层 HER,并用 DDPG 作为底层 off-policy 算法。为什么选 DDPG 而不是 PPO?因为 HER 本质上需要 off-policy 回放池,而 DDPG 天然符合;PPO 也可以配 HER,但工程上需要在采样时额外做轨迹缓存与重标记,多不少麻烦。
4.1 环境定义与稀疏奖励设定
网格世界很小,但足够展示所有关键逻辑。状态是二维坐标 (x, y),动作是上下左右四个方向,箱子初始位置固定为 (2, 1),任务目标位置(goal)随机生成,不在箱子初始位置即可。
代码里环境返回的状态 obs = (agent_x, agent_y, box_x, box_y),goal 单独作为一个 dict 传入。稀疏奖励实现起来就三行:
def step(self, action): self._apply_action(action) reward = 1.0 if (self.box_pos == self.goal).all() else 0.0 done = (reward > 0) return obs, reward, done, {}注意这里的 done 只在成功时置 True。之前见过有人把“回合最大步数到了”也设为 done,然后 reward 置 0,这样会对终止状态的价值估计引入偏差。正确做法是,时间到了强制结束时返回一个截断标志(truncated),但value估计仍按未终止来处理。
4.2 经验重放与后见重标记核心代码
HER 最核心的就是“样本重标记”这一步。伪代码逻辑如下:
def her_replay_buffer_add(self, episode, goal): # 原始的 episode 照常存入回放池 for t, (obs, act, rew, next_obs, done) in enumerate(episode): self.store_transition(obs, act, rew, next_obs, done, goal) # 生成一个重标记目标:从 episode 里随机挑一个非初始状态 future_time_step = np.random.randint(self.t_begin, len(episode)) new_goal = episode[future_time_step].next_obs # 用该状态的观测当新目标 for t, (obs, act, rew, next_obs, done) in enumerate(episode): her_goal = new_goal # 重新计算奖励:新目标下这条轨迹是否成功 if self.is_success(next_obs, her_goal): rew = 1.0 done = True else: rew = 0.0 done = False self.store_transition(obs, act, rew, next_obs, done, her_goal)几点说明:is_success 函数在不同环境里完全不一样。网格世界里可以直接比较坐标;机械臂环境里则常见距离阈值判断。重标记时要注意 done 的设置:只要到达了新的“后见目标”,就算 done=True,哪怕此时距离原始目标还差很远。这是因为对未来的策略而言,新目标下这步已经成功了,能获得终止信号。
另外,future_time_step 的抽取范围不能包括 0 时刻,否则目标退化成初始状态,等于没构造有效信息。我早期写代码时没注意这个边界,导致部分重标记目标与初始状态重合,训练明显变慢,后来修正后收敛速度提了近 40%。
4.3 DDPG + HER 的训练循环与参数选择
DDPG 由四个网络构成:actor、target_actor、critic、target_critic。每次更新从回放池采样一个 batch,分别计算 critic 和 actor 的 loss,再用软更新方式同步 target 网络。
for _ in range(train_steps): batch = buffer.sample(BATCH_SIZE) # 包含原始样本和重标记样本 next_q = target_critic(next_obs, target_actor(next_obs, next_goal)) y = reward + (1 - done) * gamma * next_q critic_loss = mse(critic(obs, act, goal), y) actor_loss = -critic(obs, actor(obs, goal), goal).mean() # ... 反传、soft update ...这里的 goal 是以向量形式拼进输入的,在网格世界里就是二维坐标,在机械臂任务里则是目标位置的向量表示。除了网络输入不同,整套流程和常规 DDPG 并无差异。
超参方面,我给一份能直接跑通的配置表:
| 超参数 | 取值 | 备注 |
|---|---|---|
| 回放池容量 | 500000 | 越大越稳,但注意内存 |
| batch size | 256 | off-policy 算法不宜太小 |
| actor/critic 学习率 | 0.001 / 0.001 | 可尝试 decay |
| 折扣因子 gamma | 0.95 | 网格短任务可以偏低 |
| K(HER 比例) | 8 | 每 1 条原始样本配 8 条重标记样本 |
| future 窗口 k | 30 步内 | 低于总轨迹长度的 50% |
| 软更新 tau | 0.05 | 太大会不稳,太小收敛慢 |
| 探索噪声 | OU 噪声或高斯噪声 | 网格世界用 epsilon 探索也可 |
实际训练大约 300 个 episode 后成功率能冲到 80% 以上,500 个 episode 后稳定在接近 100%。如果去掉 HER 只保留 DDPG,我让它在同等步数下跑了三次,成功率始终徘徊在 3% 以下,这就是 HER 的效果差距。
5. 常见问题与排查技巧实录
5.1 重标记目标导致 critic 过估计,成功率虚高但策略不稳定
这是我遇到最多的问题。现象是训练曲线上成功率看着不错,但实际 rollout 时智能体动作混乱。
原因往往是重标记样本比例过高,把 critic 训练成了盲目的乐观派。排查方法:先检查回放池中“原始成功样本”与“重标记成功样本”的比例。我建议训练日志里同时记录两类样本各自的 TD-error。如果发现重标记样本的 TD-error 长期远低于原始样本,不是好事——说明 critic 对虚构目标过于熟练,而真实目标建模不足。
缓解办法:提高原始样本在 batch 中的权重,或者减少 K。另一个小技巧是,在计算 target Q 值时对 next_goal 的目标向量加入微小的随机噪声,让 critic 对目标的微小扰动不那么敏感,能明显提升稳定性。
5.2 HER 不是万能的:当任务目标与状态分布差异极大时仍然失效
有一些任务,比如“目标是生成一段特定音色的语音”,状态空间和目标空间不直接对应。HER 需要的条件是:目标必须能够作为状态的一个可测投影,并且网络能直接从观测中推断目标信息。
我踩过的真实场景是离线机械臂装配任务:目标是一个复杂装配位姿,但状态里只有末端执行器坐标,没有零件位姿信息。这时候重标记目标根本无法与状态一一对应,HER 基本失效。如果你遇到类似情况,先检查观测设计,想办法把“目标相关量”显式加进状态,再上 HER 才有意义。
5.3 训练速度慢到不可接受:注意目标编码的表示方式
HER 需要把目标向量和状态向量一起喂进网络,很多人直接把原始坐标数值拼接。这在网格世界没事,但在高维连续控制里,目标坐标物理尺度如果和状态尺度差异太大(比如状态是毫米级坐标,目标是米级坐标),会导致网络刚开始训练时 loss 直接爆炸。
解决办法是先对目标做归一化,或者对状态和目标分别过一个小型 MLP,再拼接进主网络。我推荐后者,因为鲁棒性更好,只增加一行代码的复杂度,收益却很大。另一个替代方案是让 actor/critic 的输入层对目标做 LayerNorm,效果略逊,但处理最快。
5.4 一个提高复现成功率的隐藏参数:轨迹缓存容量
HER 重标记要求保留整条轨迹,而不仅是单步样本。这意味着回放池写入逻辑和普通 DDPG 完全不同。我见过很多复现仓库只是在核心代码里加了 her_replay_buffer_add,但轨迹缓存循环写错,导致重标记目标来自另一条轨迹,训练完全失效。
排查这个 Bug 的最好办法是加一个断言:重标记的 new_goal 必须是一个出现在该轨迹观测集合里的状态。形式如下:
assert any((transition.next_obs[:4] == new_goal).all() for transition in episode)如果断言突然不通过,你就能立刻定位是轨迹缓存索引错位的问题。
6. 扩展方向:HER 在此之后的几个大杀器用法
HER 本身的价值已经被证明,但它更大的价值在于可以和别的算法叠加,产生 1+1>2 的效果。我在这里列几个自己试过、方向靠谱的扩展路径,给做工程的读者一点参考。
6.1 优先经验回放与 HER 的配合:困难样本二次挑选
普通 HER 从经验池均匀采样,但并不是所有重标记样本难度都一样。很多样本可能是“太简单”的——比如新目标和当前状态几乎一样,样本能提供的梯度几乎为零。
类似 PER(优先经验回放)的思路,可以按 TD-error 绝对值对重标记样本做优先级采样。但注意不要照搬 PER 的全局优先级,因为 HER 里“原始失败样本”与“重标记成功样本”天然存在难度差异,如果不做分类,容易被某一类样本霸占采样机会。我的做法是:按样本类型分组,组内分别用 PER 权重采样,组间按 K 比例混合。这在小规模实验里比普通 HER 快了近一倍。
6.2 自动课程学习:让 HER 自己决定学哪些后见目标
future 策略在简单任务里够用,但它其实是静态的。能不能让智能体根据自己的能力动态调整重标记目标的难度?可以。
做法是引入一个“目标难度网络”或者简单的启发式规则:当智能体对某个目标类型成功率超过阈值时,就减少对该类型重标记样本的采样频率,把更多比例让给当前成功率低的目标。我在一篇文章里看到过一种更轻量化的变体:用当前策略对重标记目标(新目标)做 rollout,选那些成功率介于 20%~80% 的重标记目标作为训练主力。这套逻辑基本相当于在目标空间做课程学习,效果在桌面推箱子环境里比静态 HER 的最终性能高了 15% 左右。
6.3 离线强化学习场景下的 HER 变体
如果你有大量的离线数据,但没有在线交互环境,HER 依然能做很多事情。关键在重标记的时机:离线数据里已经有完整轨迹,你完全可以在训练开始前一次性完成所有重标记,把所有“后缀状态作为目标”的成功样本都提前算好。
这种做法的价值在于,它解决了离线 RL 中数据覆盖不足的问题。比如一条轨迹本来没能完成原目标,但重标记后它变成了一条完成度为 100% 的成功轨迹,相当于凭空扩充了稀缺的“成功区域”数据。我在离线机械臂数据里跑过一个对比实验,使用离线重标记后的数据训练,比直接拿原始离线数据训练的策略成功率提高了近三倍。
6.4 从控制到语言与多模态:hindsight 思想的外溢
HER 这套“把未达成的目标重定义成已达成的目标、并据此学习”的思想,比“经验回放”本身更普适。在语言任务里,类似的做法叫“对比学习中的重标注”;在对话系统里,当模型没能回答用户的问题,但歪打正着回答了另一个问题时,也可以用“后见之明”形式构造训练数据。
我在一个客服意图分类项目中试过简化版思路:用户问了 A,模型答了 B,但 B 恰好匹配了另一个用户常见问题。这就相当于一条附带重标记目标的样本。把这类样本加入训练集后,模型对相似意图的泛化能力明显增强。所以,hindsight 不只是 RL 仓库里的一个工具,它其实是一种“从失败中提炼成功子目标”的通用学习策略。
7. 写在最后的调试心得与个人经验
这篇文章从原理到实现基本把 hindsight 这个项目的核心脉络过了一遍。最后分享两个我压箱底的小经验,都是熬过夜才换来的。
第一个是关于网络结构的。HER 对 actor 和 critic 的目标输入处理方式很敏感。我之前在 DDPG 里直接把 goal 拼到状态向量后面,实验效果尚可但也只是尚可。后来把 goal 单独过一个 64 维的编码网络,再与状态特征拼接,训练速度提升了近 30%,而且训练曲线平滑非常多。如果你的任务一直处于“勉强收敛但不稳定”的状态,值得试试这个改动。
第二个是回放池的均匀性问题。HER 重标记会让回放池里“人造目标”的分布与实际目标分布偏离。如果训练后期你发现策略在新目标上泛化能力差,大概率是重标记目标分布太集中在已完成轨迹附近。解决办法是在重标记目标选择时,以 30% 概率额外随机抽取一个“非本轨迹但属于目标空间”的状态作为目标。也就是在 final/future 和 random 之间做插值,而不是只用单一策略。这个小技巧在模拟仿真里对泛化指标有显著帮助,唯一代价是训练前期会慢一点。
hindsight 是我在强化学习路上遇到的第一个让人拍案叫绝的想法。它让我重新理解了一件事:很多问题我们没有解决,不是因为没有能力,而是因为缺少一种把“未达成”转化为“已学会”的视角。如果你现在正被某个稀疏奖励任务折磨,不妨先别急着调奖励函数,试试这个后见之明的思路。