前阵子调一个机械臂抓取任务,真是被稀疏奖励折磨到怀疑人生。三维空间里夹爪死活碰不到目标物,试了拆解动作、调奖励权重、换网络结构,训练曲线就像心电图,偶尔跳一下然后继续躺平。后来把 hancer 这个词对应的那套经典思路——Hindsight Experience Replay(HER,后见经验回放)捡起来用上,折腾了两天,曲线终于能看了。这篇文章就是想把我在实际项目里对 hindsight 这个方向的理解、落地细节和踩过的坑完整写出来。适合正在跟稀疏奖励搏斗的强化学习玩家,尤其是做机器人控制、多目标任务、或者用 off-policy 算法做自定义环境的同学。
1. 从“事后诸葛亮”到稀疏奖励救星:hindsight 到底是什么
1.1 一个让我失眠的机械臂抓取任务
事情是这样:我用一个七自由度机械臂模型做目标抓取,状态包含夹爪位置、姿态、目标物位置,动作是连续的位置增量。奖励函数本来写得挺“标准”:每个 step 给一点点距离惩罚,抓到了给 +100。结果训练完全跑不动。后来我把连续距离惩罚去掉,改成“只有抓住才算成功、成功给 1 否则给 0”的二值奖励,这问题就彻底炸了。随机策略在 50 个 episode 里可能一次成功都碰不到,整个 replay buffer 里全是零奖励的失败数据,价值网络学到的几乎全是“Q(s,a)=0”,策略梯度再强也没用。
这就是典型的稀疏奖励问题。强化学习的核心是拿奖励信号做信用分配,可当绝大多数交互样本的奖励都是同一个常数时,网络根本分不清哪个动作更有可能接近目标。很多人第一反应是设计稠密奖励,比如距离的负值、相对位置差、速度惩罚,但这些奖励函数特别容易被“钻空子”:机械臂学会了把目标物推远来减少相对距离,或者干脆原地抖动骗距离值。那几天我就是反复在“稀疏学不动”和“稠密学歪了”之间来回横跳。
后来让我真正定下心的,是 hindsight 这个想法本身。它不跟你玩奖励塑形,而是直接改变一个核心假设:如果这次没达到原来设定的目标,那能不能把“这次实际到达的状态”当成另一个目标,去重新解读这次失败?这个思路在处理机械臂这类多目标任务上,比我想象中管用得多。
1.2 HER 的核心思想:把没达成的目标“平移”到已达成处
HER 的正式名字是 Hindsight Experience Replay,最早是 OpenAI 在 2018 年左右那篇《Hindsight Experience Replay》里提出来的,作者里有如今大家熟悉的 Wojciech Zaremba 那批人。核心操作听起来像在做“数据造假”,但里面的逻辑非常自洽。
正常强化学习里,一条 transition 长这样:(s_t, a_t, r_t, s_{t+1}, done_t)。r_t 是策略在“原有目标 g”下的奖励,机械臂没抓到就永远是 0。HER 做的事情是:等这个 episode 结束后,我们知道整条轨迹的后续状态变化,于是从这条轨迹中重新挑一个“替代目标 g'”,然后对每一条 transition 重新计算奖励 r't = reward_function(s{t+1}, g')。g' 往往取的就是轨迹中某个后续时刻的真实状态,比如“球在 0.3 秒后飞到了坐标 X”或“夹爪在最后一刻其实碰到了物体边缘”。这样一来,原本一堆零奖励的失败样本,就有相当一部分变成正奖励样本。
为什么这有用?可以这样理解:假设你的目标是练投篮命中,hindsight 的视角是——这次投歪了,球落在左上角,但你不能只学“怎么投中正中央”这一条路;你可以先学习“怎么把球稳稳送到左上角”这个中间技能,而这次失败的轨迹恰恰展示了这个中间技能的可行动作序列。当任务空间里有大量不同的目标坐标时,学会“把球送到某些可达位置”比“只学会送到一个点”更有泛化价值,而 HER 相当于免费扩充了训练样本中的目标分布。
有一点需要强调,HER 并没有改变探索策略,它改变的是数据的使用效率。它不会让你的机械臂“突然知道去哪里找目标”,而是让网络从已经走过的轨迹里学到更多可用的因果关系。这也是后来我反复跟同事强调的:HER 本质是对已有的 experience 做目标空间的数据增强,不是探索机制。
2. 目标重标记的四个采样策略,到底选哪个
2.1 final / future / episode / random 逐个拆
HER 从一条轨迹里采替代目标 g' 的方式,论文里给了典型的四种策略,我在工程上都是按这四种来做的,实际效果差异极大。
先说final 策略。这个最朴素:整个 episode 结束后,直接把最后一个状态当成替代目标。也就是说,机械臂一条轨迹只增加一种 hindsight 视角。它的优点是实现简单到极致,任何环境都能用,但缺点也很明显——一条轨迹只提供一个额外目标,而且这个目标往往是轨迹末尾的“残骸状态”,如果任务本身轨迹很长、中间状态起伏大,你会浪费掉大量中间状态里蕴含的因果信息。
future 策略是我最常用的。具体做法是,从轨迹的第 t 个时间步之后,随机挑一个时间步 k 的观测状态 s_{t+k} 作为替代目标,为每条 transition 配上这个目标。注意,这个 k 是在轨迹长度范围内均匀随机选的,实际操作里往往把未来状态集合缩到“距离 t 至少 1 步之后”的范围内。为什么是“之后”而不是“之前”?因为从时间因果上讲,当前动作确实影响了未来状态,拿未来状态当替代目标是符合“这个动作最终把系统带到了这里”的逻辑。如果随便拿轨迹里任意位置的状态当目标,包括过去的、甚至重复的,虽然也能用,但因果关系会变弱。
episode 策略是整条轨迹里完全均匀随机挑一个状态作为替代目标,不要求它一定在当前时间步之后。优点是对状态空间的覆盖更均匀,缺点是不是每个替代目标都跟当前动作有因果关系。我在二维到达任务里试过这个策略,曲线也能收敛,但收敛速度比 future 慢一些,可能是因为很多替代目标和当前动作之间的关联度太弱。
random 策略则是从整个 replay buffer 的所有状态里随机抽一个当替代目标,完全不管轨迹来源。这个策略方差太大,我基本不用。除非你的环境状态分布本身非常集中,否则 random 策略会产生大量“要求当前动作去达到一个根本没经历过的状态”的伪标签,网络会被带偏。
| 策略 | 采样范围 | 因果性 | 数据多样性 | 我的推荐 |
|---|---|---|---|---|
| final | 轨迹末状态 | 强 | 低 | 简单任务可用 |
| future | 当前步之后的状态 | 强 | 高 | 首选 |
| episode | 整条轨迹任意状态 | 弱 | 中 | 简单覆盖 |
| random | 全 buffer 任意状态 | 无 | 高 | 不推荐 |
2.2 K 值和重标记比例的工程经验
大家读 HER 论文时一定会看到一个超参 K,意思是对每条原始 transition,额外生成 K 条经过目标重标记的 transition。如果 K=8,那么原本一条 transition 会变成 9 条进 replay buffer:1 条原始 + 8 条 HER 增强。这 8 条增强样本都共享同一个(s_t, a_t, s_{t+1}),只是替代目标 g' 不同,所以 r'_t 也有 K 种不同结果。
实际工程里我的建议是:优先用 future 策略,K 取 4~8 起步,然后逐个试。我之前在 FetchPush 类似的任务上做过对比,K=1 时效果很差,因为一个失败样本只新增了一种目标解读,对平衡正负样本帮助不大;K=8 时学习曲线明显更平稳;K=16 时在部分环境里收益反而下降,因为重复样本太多,每个 batch 里出现同一条轨迹的概率变大,容易出现相关性过强导致的过拟合,训练抖动也更明显。
另一个容易被忽略的比例问题:到底该对多少条 transition 做 HER?论文里通常是“对每个 episode 的每条 transition 都做”,但实际内存和训练效率要考虑。我的做法不是全量做,而是每个 episode 里随机抽取一部分 transition 做 HER,比如抽取 50%~80%。这样既保留了原始轨迹的分布结构,又给 buffer 带来了足够多的增强样本。全量做最大的问题不是训练效果,而是 replay buffer 膨胀太快,尤其长轨迹任务,一个 200 步的 episode 直接变成 1800 条数据,后期内存压力不小。
2.3 适用边界:不是所有稀疏任务都适合 HER
很多人看到 HER 就兴奋,以为任何稀疏奖励都能救。这是最大的认知误区。HER 只在“目标空间足够丰富”的任务中发挥价值。举个例子:多目标抓取任务里,目标物体可以在桌面上随机放置,HER 可以拿轨迹中某个实际到达的位置当替代目标,这些替代目标都是有物理意义的可达状态,于是网络学到的是“如何到达这个可达的位置”,这对后续达到真正目标有直接帮助。
但如果是“走迷宫到出口”这种固定唯一目标的任务,HER 就基本失效。因为轨迹中的任何中间位置都不是出口,把它们当替代目标,学到的是“如何到达迷宫某角落”的经验,和“到达出口”可能完全不相关,甚至会干扰策略学习。有人会反驳说中间位置对“探索出口位置”有帮助,但对离散迷宫类任务,这种帮助非常间接,远不如加 curiosity、加课程学习来得直接。所以先判断任务是否属于多目标、可达到目标集合大、且二值奖励的任务。满足这三点,HER 的上限很可观;只满足一两点,就要掂量掂量。
3. 手写一个 HER 训练管线:从经验池到损失更新
3.1 环境与数据格式怎么设计
先明确一下我通常跑的典型环境假设。目标是一个二维到达任务,状态空间里既包含机械臂末端位置,也包含目标位置,还要把“期望目标”和“实际到达位置”区分开。绝大多数 RL 环境里,状态是全局的一维向量,但在 HER 里,你最好把它显式拆成两块:一块是跟目标无关的固有状态(比如关节角、速度),另一块是当前已到达的目标相关状态(比如末端坐标)。因为替代目标 g' 本质上是从“已到达状态”里采样出来的局部坐标,如果这个坐标和原始状态耦合太深,重标记时就不好切。
我在代码里一般这样定义:
- observation:
obs,包含机械臂自身状态 + 当前目标坐标; - achieved_goal:
achieved_goal,只包含当前实际到达的坐标; - desired_goal:一个 episode 开始时固定的目标坐标;
- action:连续动作,比如末端位置的增量。
奖励函数在设计上要能让“目标坐标”和“已到达坐标”直接比较,比如判断两者的欧氏距离是否小于某个阈值。这也是 HER 能重标记奖励的前提:只要给一个新的目标坐标new_goal,就能立刻根据当前achieved_goal算出新奖励,完全不需要重新跑模拟器。
3.2 核心实现:经验池、目标重标记与网络更新
下面这段我直接给出一个简化但能跑通思路的 PyTorch 风格伪代码,省略了 DDPG 的完整网络层,重点放在 HER 的数据处理部分。
import numpy as np import random from collections import deque class HERBuffer: def __init__(self, capacity=100000, k_future=8, her_ratio=0.8): self.buffer = deque(maxlen=capacity) self.k_future = k_future self.her_ratio = her_ratio def push_episode(self, episode_transitions): # episode_transitions: list of (obs, achieved, action, reward, next_obs, next_achieved, done) # 原始样本全部入库 for t in episode_transitions: self.buffer.append(t) # 对每个时间步,按比例决定是否做 HER 重标记 for t_idx, trans in enumerate(episode_transitions): if random.random() < self.her_ratio: # 从未来状态中随机选 k 个替代目标,生成 k 条新样本 future_achieves = [item[5] for item in episode_transitions[t_idx+1:]] if len(future_achieves) == 0: continue sample_count = min(self.k_future, len(future_achieves)) new_goals = random.sample(future_achieves, sample_count) for new_goal in new_goals: # 重新计算奖励,这里假设奖励函数是判断距离是否小于阈值 rew = 1.0 if np.linalg.norm(trans[5] - new_goal) < 0.05 else 0.0 done = 1.0 if rew == 1.0 else 0.0 new_trans = (trans[0], trans[1], trans[2], rew, trans[4], trans[5], done, new_goal) self.buffer.append(new_trans) def sample(self, batch_size): batch = random.sample(self.buffer, batch_size) # 把每个样本拆成模型需要的张量格式 return (np.array([b[0] for b in batch]), np.array([b[1] for b in batch]), np.array([b[2] for b in batch]), np.array([b[3] for b in batch]).reshape(-1, 1), np.array([b[4] for b in batch]), np.array([b[5] for b in batch]), np.array([b[6] for b in batch]).reshape(-1, 1), np.array([b[7] for b in batch]))仔细看这段代码,你会发现 HER 的关键其实就是两件事:第一,新样本里next_achieved = trans[5]就是替代目标的来源对象,我一直强调要保留这个字段,因为奖励的重新计算完全依赖它;第二,新样本的done必须跟新奖励同步修改,很多实现的坑就在这里,后面说。训练 DDPG 时,网络输入要把观测和目标任务拼起来,例如 critic 输入是(obs, goal, action),actor 输入是(obs, goal),因此采样后每组数据都要把new_goal拼到状态里。这样网络才能学到“给定任意目标,我该往哪里走”,而不是死背原始轨迹。
3.3 训练效果与参数解读
我在同样二维到达任务上对比过“无 HER”和“future 策略 K=8”两组。无 HER 时训练 300 个 episode,成功率一直在 0% 到 2% 之间抖动,完全没希望。加 HER 之后,150~200 个 episode 就开始出现跳跃式提升,最后可以达到 80% 以上的成功率。这里说的成功率是“评估时用原始目标,不用 HER 重标记目标”的成功率,这一点特别重要——HER 只是训练辅助手段,推理阶段还是要按真实目标动作。早期我犯过糊涂,评估阶段误用了训练目标的混合数据,得到一个虚高结论,后来被狠狠教育了一次。
训练过程里我发现一个现象:HER 让 critic 的 Q 值经常偏高。因为它在数据里注入了大量“按替代目标计算出来的成功样本”,这些样本其实是“伪成功”,虽然对策略学习有帮助,但会让 Q 网络误以为“在这些状态动作下会有高回报”。如果后续不加减小 target Q 的偏置处理,整体训练会不稳定。实际操作中我把 DDPG 的 target smoothing 系数保持默认,再把 reward 阈值设定严格一些(比如 0.05 改成 0.03),让伪成功样本的“成色”更真实,效果会好一些。
4. 实战中踩过的坑与能直接抄的排查清单
4.1 奖励改了但 done 没改,曲线直接崩
这个坑我印象最深。HER 重标记后,一条原本失败的 transition 可能因为替代目标离得近而变成成功,奖励变成了 1。但如果此时的done还沿用原轨迹的false(表示没结束),会出现什么情况?Critic 看到(s, a, r=1, s', done=false),它会学到“这个状态动作组合有高回报,并且任务还没结束,后面还可以继续获取高回报”。这在多步序列里会形成一种偏差,网络会认为只要往某些方向走就能无限刷奖励,于是策略扭曲到不停地往替代目标附近撞,哪怕评估时真实目标根本不是那个位置。
反过来,如果把原本成功的 transition 因为替代目标偏移较大而重标记为失败,却还在done=true,CRITic 也会学到错误的终止条件。所以我的铁律是:每次根据new_goal重新计算奖励后,done也必须是这个 new_goal 下是否完成任务的结果。在一定要给终止条件设置done=false的环境里,可以设计成done = True if reward == 1 else False,再根据实际任务可能需要截断,截断位置单独处理。
4.2 为啥 HER 和 PPO 八字不合
有段时间我尝试在 PPO 上套 HER,结果惨不忍睹。原因很简单,PPO 是 on-policy 算法,它的每一步更新都必须来自当前策略采样出的轨迹,任何对这些轨迹做目标重标记再重新采样的行为,都会破坏 on-policy 的分布假设。HER 本质上是在 replay buffer 里做数据增强,天然要求算法从历史 buffer 里反复采样,这正好是 off-policy 的 DQN、DDPG、TD3、SAC 的强项。所以如果你非要在 PPO 上做稀疏奖励,建议优先考虑加 curiosity、加课程学习,而不是硬套 HER。如果想用 HER,先把算法切到 off-policy 家族。
我自己在机器人类任务上最常用的搭配是:SAC 或 DDPG 作为底层算法,加上 HER 的 future 策略重标记,再配合一点目标课程。这个组合在多个模拟环境里都表现稳定,值得从零起步。
4.3 HER 参数速查与诊断表
我整理了一个速查表,方便排查时快速定位问题:
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 训练初期 Q 值虚高,后期崩塌 | HER 伪成功样本太多,target Q 偏置 | 调严格奖励阈值、降低 K、减少 her_ratio |
| 成功率中期停滞在 50% 左右 | future 采样范围过大或 K 过小 | 缩小未来采样窗口、K 调大到 8 或 12 |
| 动作输出抖动,越训越差 | reward 阈值过松,伪成功太多 | 把 reward 判定阈值从 0.1 收紧到 0.03 |
| 评估成功率远低于训练时 | 评估阶段混入 HER 目标 | 确认评估只用真实目标 |
| buffer 抽样时 obs 和 goal 未拼接 | 网络输入不包含目标,学不到目标条件策略 | 检查状态拼接逻辑,goal 必须并进去 |
如果你看到训练曲线一直平着走,先别急着加代码,可以打印一下 HER 重标记后的 reward 均值。如果均值长期接近 0,说明替代目标采样方式有问题,大概率是 future 窗口太小,或者环境里真的不具备“通过多样化目标生成有效训练信号”的条件。
5. 我的一些工程体会与后续玩法
5.1 HER 不是银弹,它解决的是“拿到失败数据后的学习效率”
跑了一堆任务之后,我越来越觉得 HER 更像一个“数据解释器”,而不是万能解药。它能在不修改探索策略的前提下,把失败的轨迹重新塑造成有学习价值的样本。但它的前提是:这些失败轨迹里必须存在“与某个可达目标状态接近”的时刻。如果任务本身状态空间单调、目标唯一、或者动作空间离散到快速搜索也能完成,那 HER 的收益就会变得很有限,甚至是一种负担。
在我自己的项目里,我会先用一个更“暴力”的方式验证任务是否值得上 HER:随机初始化 200 个 episode,统计 achieved_goal 的分布多样性。如果分布很窄(比如机械臂永远只在一个小区域内活动),那 HER 能给的信息也不多,因为替代目标的集合本身就太少,此时优先要把“探索”问题解决掉,比如在初始位置、目标采样、动作噪声上下功夫。只有当随机探索已经能让系统访问到足够广阔的状态空间,HER 的多目标数据增强价值才会被充分释放。
给新手的建议是先跑 FetchReach、FetchPush 这一系列环境,它们干净、目标明确、HER 的收益一开机就能看见,能帮你快速建立“什么样的经验数据可以重标记”的直觉。再回来处理自己的自定义环境,你会少走很多弯路。
5.2 几个我觉得值得尝试的变体和兼容技巧
HER 之后的这些年,社区做了不少有趣改进。比如在采样时不止用未来状态,还会结合“能量函数”挑选更有学习难度的替代目标,这能避免所有替代目标都太简单、太局部化。还有人把 HER 和 curriculum learning 结合,先用 HER 训练一个“初级策略”,再用真实目标分布继续微调,两阶段模式在部分任务上效果更稳。
另外,我在实际工程里会用一些不起眼但很好用的兼容技巧:一是把 HER 的增强样本单独放在一块内存区,而不是跟原始样本混在一起,这样可以控制抽样的比例,避免增强数据淹没原始分布;二是每个 epoch 对 HER 目标做一次“目标去重”或相似目标过滤,防止 buffer 里囤积大量几乎相同的伪目标;三是如果环境允许,尽量让动作噪声在 episode 中后期衰减,让 HER 生成的伪成功样本更贴近可重复执行的策略行为。
总之,hindsight 这个思路给我最大的启发,其实是“失败经验也是一种经验,关键在于你怎么定义它”。在强化学习的实际项目里,真正的瓶颈往往不是网络结构不够深、算力不够大,而是我们太执着于原来的赛道,不愿意把一个失败定义切换成另一个成功。HER 教会我的,就是用更务实的方式重新审视数据,而不是天真地寄希望于模型自己从零探索出奇迹。