“hindsight”这个词,在强化学习圈子里可不是“事后诸葛亮”的贬义说法。它背后是一个相当经典的算法——Hindsight Experience Replay(事后经验回放,习惯简称HER)。我第一次听说这个概念的时候,心里想的是:这不就是把“失败的经验”硬掰成“成功的经验”来用吗?听起来有点像自欺欺人,但实际跑过实验之后,我发现这玩意在解决稀疏奖励问题上,确实是目前最实用的思路之一。
这几个月我一直在折腾一个机械臂抓取的任务,用的是稀疏奖励环境,目标是把方块放到指定位置,只有放准了才有奖励,其他时候全是0。刚开始用普通的DDPG,训练了上百万步,一点起色都没有,agent完全是个呆子。后来我把HER加上去,同样的算法、同样的超参数,几十万步就开始出现明显的抓取动作了。这篇文章我想把我对HER的理解、从论文到代码的落地过程、还有我在实验里踩过的坑,完整地记录下来,希望能给正在跟稀疏奖励死磕的朋友一些参考。
1. 为什么偏偏是HER:先从稀疏奖励这个老大难说起
1.1 你给不了密集奖励,机器就不知道该学什么
先说说我为什么会接触这个方向。我做的是一个机械臂仿真抓取项目,任务本身不复杂:机械臂从初始位置出发,去抓取桌面上的一个方块,然后放到目标位置。问题在于,我给环境设计奖励函数时,用的是稀疏奖励——成功得1分,失败得0分,中间没有任何过渡信号。这样的环境对强化学习算法来说极其不友好,因为在训练初期,agent随机探索时几乎不可能一次就成功,所以它收到的一直是0、0、0……没有任何梯度信息,算法根本无从更新策略。
我试过换成密集奖励,比如根据机械臂末端和目标的距离给一个负的距离惩罚,这样确实能引导agent“动起来”,但随之而来的是一堆工程问题:奖励形状怎么设计?权重怎么调?距离用什么度量?过于密集的奖励还会诱导agent“钻空子”,比如机械臂停在某个中间位置,虽然距离没变近,但因为动作幅度小,惩罚也小,它就赖在那儿不动了。换句话说,密集奖励让问题变得“手调味道”太重,而且泛化性很差。
正是这种体验,让我意识到为什么HER在2020年前后会被那么多人关注。OpenAI那篇《Hindsight Experience Replay》论文提出的思路特别朴素:既然agent在探索过程中大概率会失败,那我们能不能别把失败的轨迹直接丢掉?失败轨迹里,机械臂虽然没有把方块放到指定的目标位置,但它确实把方块“放到”了另外一个位置。那好,我们就把那个实际到达的位置,重新当成一个“目标”,让agent去学着完成这个“新目标”。这样一来,一条失败的轨迹就变成了一条“成功轨迹”——因为对于重新定义的目标来说,agent确实是完成了的。这就是HER的“事后聪明”:用实际发生的结果,去重新解释过去的经验。
1.2 为什么强化学习常规套路在这里会失效
要真正理解HER的价值,得先搞清楚标准强化学习在稀疏奖励下为什么会崩。以我当时用的DDPG为例,它属于基于价值的方法,核心是用critic网络去拟合Q函数,也就是估计“在状态s采取动作a之后,未来能获得多少回报”。在稀疏奖励环境下,大部分状态动作对都没有收到过正向反馈,Q函数的输出在所有地方都差不多——都接近0。critic给不出有区分度的价值信号,actor的网络更新就失去方向,策略梯度几乎就是噪声,训练自然就停摆。
这时候有人会说:“那我就加大探索呗,让agent多碰几次运气,总有一次成功吧?”问题在于,高维度连续动作空间里,运气碰出成功轨迹的概率低得可怜。拿我的机械臂任务来说,动作空间是7维的(关节角度变化),每个维度稍微偏一点点,末端执行器的位置就会差出好几厘米。在这么大的连续空间里随机探索,撞到“安全区”的概率可能连百分之一都不到。哪怕算法真的偶然成功一次,这一条轨迹放进replay buffer里,也很快就被海量失败样本淹没,训练根本稳住不住。
另外还有一个容易被忽略的点:在goal-conditioned的设定下,失败并不是完全没价值。一个轨迹虽然没能达成目标g,但如果它达成了另一个状态g',那么这条轨迹对于目标g'来说,就是一条标准的成功轨迹。标准强化学习算法不会去挖掘这一层信息——它看到的只是“对目标g失败了”,然后就把轨迹丢掉了。HER做的事情,本质上就是把这种“隐性成功样本”显式地挖掘出来,重新注入训练。
1.3 HER解决的是哪一类问题
这里必须说清楚,HER不是一个通用的强化学习算法,而是一种适用于特定任务族的思想。它适用的是goal-conditioned任务,也就是任务可以被描述为“从任意初始状态出发,达成某个目标状态”。典型例子包括:机械臂抓取并放到指定位置、机器人导航到指定地点、魔方复原(虽然动作空间离散,但思路同样适用)等。
这类任务都有一个共性:目标状态可以明确描述,并且我们可以方便地拿到“轨迹实际结束时的状态”。HER的核心操作就是对这个实际结束状态做文章。如果任务不具备这种条件,比如目标是“让机器人保持平衡不倒”,没有明确的目标状态可以重标注,那HER就不太合适。再比如像打网球这种有大量博弈成分的任务,目标不是某个静态状态,那也需要额外改造才能套用HER。
所以,当你决定要用HER时,先确认三件事:第一,任务是不是goal-conditioned?第二,目标状态能不能在轨迹中自然地获取(比如直接读取仿真环境中的物体坐标)?第三,你手里是否有一个现成的off-policy强化学习算法(比如DQN、DDPG、SAC)可以承接HER的经验回放机制?三个条件都满足,HER才有用武之地。
2. HER的核心机制拆解:目标重标注是怎么运作的
2.1 “新目标”从哪里来:状态回放和目标回放的差异
先看公式化的表述。假设我们定义了一个goal-conditioned MDP,状态记为s,动作记为a,目标记为g,奖励函数是r(s, a, g)。在稀疏奖励下,r在绝大多数情况下都是0,只有当s(或状态的一部分)和目标g匹配时才给正值。
标准的强化学习流程是采一条轨迹τ = (s₀, a₀, s₁, a₁, …, sT),在这条轨迹中,目标g始终固定不变。轨迹放进replay buffer时,存的是(s, a, r, s', g)这样的四元组。这个流程在密集奖励下没问题,但在稀疏奖励下,大多数四元组的r=0,critic学不到梯度。
HER的做法是在存入buffer之前,对目标做一次“替换”。同样一条轨迹,除了原始目标g外,我们再额外生成K个替代目标g'(عادة取轨迹中后面某个时刻的真实状态)。对于每个替代目标,我们重新计算奖励r' = r(s, a, g')。由于替代目标就是轨迹里实际到达过的状态,这条轨迹的最后一个状态sT和目标g'是匹配的,所以最后的奖励r' = 1,整条轨迹在替代目标下就变成了一条成功轨迹。
这里有个关键细节:我们在重标注时,并不是对轨迹里所有time step都无脑给新奖励。还是得按照原始奖励函数的定义来算。HER只是把目标从g换成g',奖励计算还是要走一遍正常的奖励逻辑。也就是说,“某个状态是否达成目标”还是由原始奖励函数判定,只不过目标本身变了。这也是HER能够完全复用原始奖励函数、不需要额外设计密集奖励的根本原因。
2.2 替代目标怎么选:future策略和final策略
论文里介绍了多种替代目标的采样方式,我实际用下来,最常用且效果最稳的是两种:final策略和future策略。
final策略最简单:对于每条轨迹,只取轨迹的最终状态sT作为替代目标,额外生成1个新样本。这种做法实现几乎零成本,但缺点是信息量有限,尤其当轨迹很长时,最终状态与轨迹中很多早期状态差异很大,agent需要在“离目标很远”的状态下学习怎么向目标靠近,信号太稀疏。
future策略更丰富一些:对于轨迹中的每个时间步t,随机从轨迹后续时间步t' ∈ [t+1, T]中采样一个状态,作为该时间步的替代目标。这样做的好处是,替代目标与当前状态的距离不会太远(因为t' > t,目标在时间上处于当前状态之后),agent学习的是一条渐进的路径,而不是一次性学会大跨度移动。我在实验中用的就是future策略,每个原始样本额外生成K=4个替代目标样本。
还有一个值得提的采样trick:不要完全均匀地从未来的时间步里选,而是可以加重靠近轨迹末尾的时间步的采样权重。因为轨迹末尾的状态通常更接近“真正成功的结果”,而早期状态和末尾状态之间的差距,恰好能提供更有价值的监督信号。论文里用的是一种基于“时间距离”的采样方式,大概意思就是越靠近末尾的时间步,被选中的概率越高。我在实操中直接用了一个指数衰减的权重,效果比均匀采样好一些。
2.3 Replay Buffer的容量和组成:别让重标注样本淹没原始样本
HER的replay buffer里,同一物理轨迹既会以原始目标g的形式存储,也会以替代目标g'的形式存储。这里就有一个很实际的工程问题:这两种样本的比例怎么控制?
我最初踩过一个坑,直接把替代目标样本全部塞进buffer,结果原始成功样本被稀释得厉害。agent虽然用重标注出来的“伪成功”样本学到了不少东西,但真正对原始任务有用的“真成功”样本反而占比太低,策略在评估时很难稳定复现成功动作。
后来我看了一些开源的复现实现,比较合理的方式是:每条轨迹在存入buffer时,原始目标样本保留1份,替代目标样本生成k个不等。一般k取1到4之间,取决于你的buffer总量和任务复杂度。如果任务动作空间大、状态维度高,可以适当把k调大,用更多“伪成功”样本补偿探索不足的问题。但如果任务本身有比较简单的成功条件,比如目标点很小、成功判定严格,那k不用太大,2左右就够了。
另外,buffer总容量也要注意。HER本质上是在做目标的重新标注,这种方法会提升样本的利用率,但代价是buffer里的样本之间存在很强的相关性(同一轨迹的不同目标版本)。buffer太小时,这种相关性会让训练不稳定;buffer太大时,样本利用率又上不去。我用的DDPG+HER,buffer容量在10^6量级,效果还不错。如果你的算力紧张,至少也要保证buffer能覆盖几十万条完整轨迹,否则重标注的优势发挥不出来。
3. 手把手落地:从论文到可以跑的实验
3.1 环境搭建和基线选择
我选的是MuJoCo环境,机械臂用的是FetchReach和FetchPickAndPlace这两种典型任务。FetchReach是让机械臂末端到达某个目标点,相对简单,适合快速验证HER代码是否写得对;FetchPickAndPlace涉及抓取和移动,复杂度高一个量级,适合用来验证方法真正的大幅提升。
算法层面,我推荐用DDPG作为基线上的实现。原因很简单:DDPG是off-policy连续控制方法,天然的用replay buffer,而且结构简单,HER和buffer的融合改起来最直观。SAC也可以,代码结构上稍微复杂一点,但效果通常更好。我自己是从DDPG开始改的——因为入门成本低,跑通了再迁移到SAC上也不难。
如果你不想从零手写,OpenAI的baselines库里有个her的文件夹,里面有完整的DQN和DDPG实现。我建议先把它下载下来跑一遍,确认环境能出结果,然后再对照着理解每个模块怎么工作。我第一次就是急着手搓,结果在标准库和第三方库之间来回踩坑,浪费了不少时间。
3.2 核心代码流程:HER应该插在哪一步
关键是理解HER在训练循环中的位置。伪代码如下:
# 伪代码:标准DDPG + HER训练循环 for episode in range(total_episodes): # 采样一个目标g,重置环境 goal = sample_initial_goal() obs = env.reset(goal=goal) trajectory = [] # 用来存(s, a, r, s')序列 for t in range(max_episode_steps): action = agent.select_action(obs, goal, noise=True) next_obs, reward, done, info = env.step(action) trajectory.append((obs, action, reward, next_obs, goal)) obs = next_obs if done: break # HER的核心操作:从trajectory中生成重标注样本 for t, (obs, action, reward, next_obs, g) in enumerate(trajectory): # 原始目标样本直接存 replay_buffer.add(obs, action, reward, next_obs, g) # 额外生成K个替代目标样本 for _ in range(K): # 从future策略采样目标:从t后面任意时间步里随机选一个状态 if stride == 'future': future_t = np.random.randint(t+1, len(trajectory)) new_goal = trajectory[future_t][3] # next_obs作为新目标 new_reward = compute_reward(next_obs, new_goal) replay_buffer.add(obs, action, new_reward, next_obs, new_goal) # 用replay_buffer的标准流程更新DDPG for _ in range(num_updates): batch = replay_buffer.sample(batch_size) agent.update(batch)注意几个关键点:
第一,compute_reward这个函数必须和原始环境的奖励逻辑保持一致。在FetchReach这类环境里,奖励函数通常是判断某个状态(比如机械臂末端)和目标之间的欧氏距离是否小于阈值,如果小于就为1,否则为0。重标注时直接调用这个奖励函数即可。千万不要自己再写一套奖励,否则整个算法的语义就乱了。
第二,替代目标采样时,选的是next_obs(也就是状态转移后的观测),不是当前obs。因为Future策略的语义是“从当前状态之后能达到的状态”,不能在t时刻就把当前的观测当作目标,否则目标永远就在眼前,学不到东西。
第三,当轨迹结束时,最后一个时间步的替代目标可以选终态sT本身,这样整条轨迹的最后一步就是一个完美的成功样本。对所有早于T的时间步,从后续状态里采样。
3.3 超参数应该怎么调:我的五次实验记录
我在这部分花了最多时间,因为HER虽然效果好,但超参数不像常规强化学习那么直观。下面是我调的几组主要参数和对应效果,供大家参考:
| 参数 | 经验值 | 说明 |
|---|---|---|
| 每条轨迹额外生成样本数K | 4 | K太小信息利用不足,K太大buffer重复率过高 |
| Buffer容量 | 10^6 | 大buffer对HER更重要,因为重标注增大了样本量 |
| 替代目标采样方式 | future(指数加权) | 优于final和随机采样 |
| 训练更新频率 | 每5条轨迹更新一次 | 更新太频繁容易过拟合当前buffer中的轨迹 |
| 奖励函数阈值 | 0.05(FetchReach) | 阈值越小任务越难,建议先跑通大阈值再收紧 |
第一次实验:我用的final策略,K=1,结果在FetchReach这种简单任务上效果还可以,但在FetchPickAndPlace上训练曲线几乎不动。分析原因是任务复杂,只靠终态一个替代目标提供的监督信号不够,agent不知道中间过程该怎么走。
第二次实验:改成future策略,K=4,FetchPickAndPlace的成功率开始稳步上升,差不多训练到40万步时能看到明显效果。这里我体会到future策略的“渐进性”非常重要——它是在每一个时间步上都在给agent提供前进方向的信号,等于变相提供了一种密集奖励的感觉。
第三次实验:我把buffer容量从10^5调到10^6,发现训练后期稳定性明显提升。原因是容量小的时候,早期的“伪成功”样本会被新的失败样本大量冲掉,导致agent后半天性对成功的记忆丢失。
第四次实验:我在替代目标采样时加了指数权重,让更靠近末尾的状态更容易被选中。效果是训练曲线的方差变小了,最终收敛的成功率比均匀采样高了大概10%。
第五次实验:我尝试了用SAC替换DDPG,发现同样的HER代码在SAC上效果只会更好,但SAC调参复杂度高一些,建议先把DDPG版本跑通再迁移。
3.4 效果评估:别只盯着最终成功率
评估HER效果时,我吃过一个亏:只看最终成功率曲线,导致我一度以为算法没效果。原因是我的评估逻辑是“跑一次完整的rollout,看最后有没有达成目标”,但HER训练出来的agent在前期目标是偏向“探索”,成功率上升很慢。后来我改成每1000步做一次评估,每次评估跑20个episode,统计成功比例,并且把“部分成功”(比如方块被抓起来了但没放到位)也单独统计,才看到了趋势。
这里建议评估时关注两个指标:一是最终成功率,二是“目标状态的有效覆盖率”,即agent在路径上是否能够在一定容差内接近目标。覆盖率提升说明agent学到了趋向目标的能力,最终成功率提升说明趋近能力已经细化到可以精确完成任务。两者结合,才能正确判断HER是否在发挥作用。
4. 实操中踩过的坑和排查实录
4.1 重标注目标算错坐标空间,训练完全不动
我在FetchReach里遇过一个大坑:替代目标采样时,我把机械臂末端的坐标直接当成了“全局目标位置”,但实际上环境里目标点有一个固定的坐标系,而且目标点本身有随机偏移。我采样得到的状态是一个相对坐标,直接用绝对值去算奖励,结果奖励函数永远是0。这个bug花了整整两天才排查出来。
排查方法很笨但有效:去训练环境里随机采样几千条轨迹,直接手动检查“重标注后”的奖励值是不是包含1。如果所有重标注样本的奖励都是0,那说明目标采样的坐标空间有问题,或者是奖励函数里目标的定义跟状态不匹配。这个测试异常重要,可以说是HER代码正确性的金标准。
4.2 训练后期成功率涨不上去:目标分布太单一
另一个常见问题是训练早中期效果不错,但到后期成功率卡在30%-50%就不再涨了。我排查后发现,原因是我在重标注时,用得太集中于轨迹末尾那些“成功状态”,导致agent对“真正需要精确达成的目标”产生的成功样本越来越少。换句话说,重标注样本的分布和原任务的真实目标分布不匹配。
解决办法是:在重标注时,不仅采样轨迹内部的状态,还可以适当混入一些“探索失败状态”,但用原始目标g来算奖励——也就是保留一部分失败的原始样本。这样可以让agent继续感知“对原始目标失败”的样子,不至于过于乐观。具体做法是每条轨迹存数份原始目标样本,且这部分样本比例不低于整体buffer的30%。我在实操中就是这么调的,成功率从35%提到了55%左右。
4.3 HER替代不了探索,更不是万能解药
最后必须说一点:HER牛逼,但它不是替你做探索的。它的作用是让你“已经做过的探索”变得更有价值,而不是帮你去新的区域探索。在FetchReach这种简单任务里,随机探索很容易覆盖到各种位置,HER的威力非常明显;但在高难度任务中,如果agent的探索始终只停留在初始位置附近,那重标注能提供的目标也都在附近,它始终学不会“走向远方”的动作。
我做了一个验证实验:把机械臂的初始位置集中在桌面左下角,目标在右上角,HER在训练中后期一直无法突破初始区域的限制。后来我在动作空间上加了更大的噪声、让agent有一定概率执行随机重置策略(把机械臂移到随机位置),探索范围变大之后,HER的作用才彻底发挥出来。所以,HER和探索强度的关系不是替代,而是配合——探索到哪,HER就能学到哪。
这块的实际经验可以总结成一句话:用HER的项目,一定要在训练初期把探索强度拉高一点(比如动作噪声系数调大),等看到覆盖率达到一定水准后再逐步降噪。如果一上来就用小的探索噪声,HER的信息量会被源头枯竭卡死。
5. 别只背算法:HER在不同任务里的适用边界
5.1 从逐步目标到长时序任务:HER仍然是底层思想
随着训练任务的复杂度提高,HER本身也在不断演进。比如DIAYN、DCD等算法在做面向目标的无监督探索时,常常会借鉴HER的目标重标注思想;在分层强化学习中,底层策略的目标往往也是由上层用“事后重标注”的方式生成的。这种思想贯穿了整个goal-conditioned RL的发展脉络。
我自己的体会是,HER最核心的价值不是某个具体代码实现,而是提供了一种建模思路:不要把经验看作“对与错”,而要看它“对什么是对的”。这种思路在做任意稀疏奖励RL项目时都值得保持。
5.2 什么时候别用HER:离散奖励和状态不可观察时
虽然HER很强,但也有明确的反例。如果你的任务不是goal-conditioned,比如奖励来自一些复杂的外部逻辑(例如“是否超过某速度阈值”),目标状态很难从轨迹中抽取出来,那HER就没有用武之地。另外,如果环境状态只有部分可观察(比如只有图像,但不能直接读取到物体坐标),你需要先做一个状态表征提取,再把HER叠加在上面,否则直接对原始图像做目标重标注,计算量大且容易过拟合到像素级别。
还有一个隐蔽问题是,HER对“目标”的定义必须连续且平滑。在离散控制任务中,比如“按某个按钮”这样的任务,目标状态是一个离散的标签,HER的重标注操作意义有限。这种任务应该考虑使用别的探索策略,比如参数噪声、状态计数探索等。
写在最后的一个建议
HER跑了这么久,给我的最大感受是:它把一个“失败堆积如山”的问题,变成了“失败里找成功”的问题。如果你手头刚好有个稀疏奖励的goal-conditioned强化学习任务,我强烈建议你先别急着设计复杂的dense reward或奖励塑形,花两天时间把HER加到现有的off-policy算法里。实测下来,代码量不大(核心改动集中在replay buffer和目标采样上),但带来的训练信号增益是非常显著的。跑通之后,再去考虑更复杂的变体也不迟。