ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

HER算法解析:用事后经验回放解决稀疏奖励难题

HER算法解析:用事后经验回放解决稀疏奖励难题 为什么“hindsight”值得你重新认识如果你稍微接触过强化学习尤其是机器人控制、游戏AI这类方向大概率会在各种代码仓库里撞见“hindsight”这个名字。它直译是“后见之明”但在强化学习里对应的是Hindsight Experience Replay事后经验回放简称HER算法。它的出现针对的是强化学习里最让人头疼的稀疏奖励问题——智能体在绝大多数时间步拿不到任何正向反馈训练半天依然像个无头苍蝇。这篇文章就以“hindsight”为线索把HER的算法原理、实现细节、训练陷阱和实际应用一次讲透适合正在复现机器人抓取任务、研究样本效率或者单纯被稀疏奖励折磨过的RL学习者。我会先解释HER为什么有效再给出可运行的代码骨架和超参数配置最后分享几个我在实际项目里被坑过恍然大悟的排查经验。你可以把这篇文章当作一份来自实战现场的HER落地笔记。1. HER的核心思想如何把“失败”变成“训练信号”1.1 从“事后聪明”到目标重标记HER算法由OpenAI团队在2017年提出原文标题就叫Hindsight Experience Replay。它解决的是样本效率问题在稀疏奖励环境下传统的experience replay buffer里存了大量奖励为0的样本价值网络根本学不到有效梯度整个训练过程基本停滞。想想看一个机械臂连续试了几千次直到最后一次才偶然碰到目标位置如果只奖励这最后一次前面的几千次试错都是无效数据简直是极大的信息浪费。人类的做法和这完全不同。回想你第一次组装宜家家具装到一半发现结构装反了虽然整个柜子没成功但你至少学会了“这个螺丝不应该先拧紧”这个教训。下次再遇到类似情况你会直接避开这个错误。这就是“事后聪明”目标没达成但失败过程中的一些步骤本身是有参考价值的值得被记住和复用。HER把这个直觉搬进了算法。每当一条轨迹episode结束传统的做法是把这条轨迹里的所有transition状态转移样本连同原始目标一起存入buffer。HER则会额外做一件事从这条轨迹中挑出一些实际达到过的状态作为“新目标”再生成几份改写过的样本存入buffer。这样一来原本朝着原始目标失败了的样本被“改写”成了朝另一个实际到达的状态成功了的样本。对智能体来说它本来学不到“如何到达目标g”但可以学到“如何从当前状态到达目标g”——而g恰好是这条轨迹里真实出现过的状态这条样本就不再是死样本了。1.2 HER在数学上到底做了什么先看一条普通transition的组成(s, a, r, s, g)分别代表当前状态、动作、奖励、下一状态、目标。在HER里保存原始样本之外还要额外构造一条改写样本(s, a, r, s, g)其中g是从这条轨迹后续状态里采样出来的新目标r则是根据g重新计算得到的奖励。关键点就在这里r必须重算不能直接复制原来的r。如果原始目标g下s离目标很远、奖励是-1但换一个目标g后s很可能恰好到达了g奖励立刻变成0或正值。看起来只是把一个参数换了换但这背后是学习信号的彻底重造。原本一条“做了个动作但失败了”的样本被解释为“做了个动作恰好达到了另一个目标”的成功样本。智能体可以从中学到正向的动作-状态关联而不是一次次的挫败感。这里还要注意一个细节HER是不改变奖励函数的。它没有人为设计中间奖励或势函数来引导智能体而是从数据本身挖掘可学习信号。相比手搓奖励塑形函数HER的自动化和泛化能力都更高尤其是当任务的目标是可描述的状态空间时几乎是一键的。1.3 HER只能配DDPG吗很多早期的开源代码里HER和DDPG绑定出现导致不少人误以为HER是DDPG的专属配件。其实HER是典型的model-free、off-policy算法增强模块只要算法本身从replay buffer里采样样本更新策略就可以套HER。DQN、TD3、SAC、DDPG都可以区别只是样本的表示方式要适配离散或连续动作空间。不过HER确实依赖off-policy特性因为on-policy算法如PPO、A3C会及时丢弃旧样本没有replay buffer这个环节HER无从介入。这是我最初选型时忽略的点后来在项目文档里反复看到“HER is agnostic but requires replay buffer”这句话才彻底明白。2. 目标重标记的四种策略与关键参数2.1 final、future、episode、random的取舍逻辑HER的实现优劣很大程度上取决于“如何选择新的目标g”。原论文对比了四种策略final直接用整条episode的最终状态作为新目标。理解简单实现最省事适合目标状态本身就是轨迹终点可达状态的任务比如机械臂末端到达任务。future从当前transition后续的时间步里随机采样一个状态作为新目标。这是最常用的策略因为它在“状态在时间上可达到”这一前提下增加了样本多样性训练效果最稳。episode从整条episode中随机抽一个状态作为新目标。随机性最大样本多样性高但容易混入与当前状态因果关系太弱的目标造成训练信号混乱。random从所有已见过的状态里随机采样。实践中几乎没人用因为采样空间过大新目标和当前状态往往毫无干系对学习毫无帮助。在实际项目中我用得最多的是future其次是final。future策略在保证目标可达的同时让样本之间形成“时间上的因果推进感”让智能体学到“从当前步到后续步之间动作是如何影响状态演化的”。final策略适合目标状态容易抵达的场景比如FetchReach但如果是长时episode且轨迹中充满偶然因素final策略会把“偶然到达的状态”当作目标造成误导。2.2 future策略里的k值和采样窗口用future策略时有个参数k表示每条原始transition额外生成几条重标记样本。论文推荐的k4也就是说每条transition会存成1条原始样本加4条重标记样本总共5条进buffer。k1时重标记信号太稀薄训练速度提升有限k8时buffer占用和训练开销都大幅上升虽然样本效率更高但对算力和显存的要求也随之水涨船高。我个人的经验是先跑k4如果发现buffer容量吃紧再把k降到2而不是一开始就开k8否则很容易在实验早期就被内存瓶颈卡住。还有一个经常被忽视的隐性参数future采样窗口。比如在transition(s_t, a_t, r_t, s_{t1})之后是从[t1, T]整个区间采样还是只从[t1, t10]这个局部窗口采样后者在很多控制任务里更稳定因为遥远目标与当前状态差异过大critic在拟合时会面临更大的方差。对局部运动控制任务我建议先用较短的窗口等训练到稳定阶段再逐步放宽。2.3 新目标可达性的判定逻辑不论使用哪种重标记策略都会预设一个前提新目标g是可达的且和这条轨迹最终状态足够接近。如果g远离最终状态这条样本即使被改写智能体也无法学习到有效映射。通常的做法有两个一是当目标空间是欧氏空间时直接用归一化距离阈值判断比如目标到达距离小于0.05米就算成功二是复用环境自带的目标判定接口比如gymnasium的Fetch环境里有is_success标志直接把它当作成功判据即可。这个阈值不仅是环境reward判断的标准也是HER重标记时r和done计算的依据。很多复现项目训练曲线迟迟不涨根源就在这reward计算用了0.05的阈值成功判定却用了0.01导致重标记后的样本明明“成功”了但奖励还是负的。这种不自洽会让智能体学到的内容极度扭曲。3. 实操过程搭一个HER训练流水线3.1 环境和算法选型为了直观演示HER的效果我用gymnasium的robotics套件里的FetchReach来跑实验。这个环境是机械臂末端点到达任务动作空间是4维连续控制目标是3维坐标。它的奖励设置非常稀疏到达目标给0否则给-1。临界目标距离是0.05米。这样的环境非常适合做HER验证因为它的成功判定足够清晰训练曲线在加HER后能明显爬升。算法上我用TD3配HER。TD3是DDPG的改进版通过双critic和延迟更新抑制过估计和HER配合效果非常稳定。如果你想用SAC也行但要额外关注熵系数对探索的影响。3.2 重放缓冲区的核心实现下面是我在实际项目里用过的简化版HER Replay Buffer代码。重点是展示目标重标记的实现逻辑而不是完整训练框架所以省略了网络更新部分。import numpy as np from collections import deque import random class HindsightReplayBuffer: def __init__(self, capacity200000, k4, strategyfuture): self.capacity capacity self.k k self.strategy strategy self.buffer deque(maxlencapacity) self.episode_transitions [] def add(self, transition): # transition: (s, a, r, s_next, done, goal) self.episode_transitions.append(transition) def end_episode(self): ep_len len(self.episode_transitions) for i, (s, a, r, s_next, done, goal) in enumerate(self.episode_transitions): # 存原始样本 self.buffer.append((s, a, r, s_next, done, goal)) # 生成k条重标记样本 for _ in range(self.k): new_goal self._sample_new_goal(i, ep_len) new_r, new_done self._recompute_reward(s_next, new_goal) self.buffer.append((s, a, new_r, s_next, new_done, new_goal)) self.episode_transitions.clear() def _sample_new_goal(self, idx, ep_len): if self.strategy final: return self.episode_transitions[-1][3] elif self.strategy future: future_idx random.randint(idx 1, ep_len - 1) return self.episode_transitions[future_idx][3] elif self.strategy episode: rand_idx random.randint(0, ep_len - 1) return self.episode_transitions[rand_idx][3] else: raise ValueError(unknown strategy) def _recompute_reward(self, s_next, new_goal): # Fetch类环境距离 0.05 视为成功奖励0否则-1 dist np.linalg.norm(s_next[achieved_goal] - new_goal) if dist 0.05: return 0.0, True else: return -1.0, False def sample_batch(self, batch_size256): batch random.sample(self.buffer, batch_size) return map(list, zip(*batch))这块代码有几个值得讲清楚的设计决策。第一缓冲区容量至少是“纯训练步数”的两倍以上。因为每步数据被扩大成1k份容量小了会导致原始样本被重标记样本挤出导致训练时原始目标反而成了少数派。第二end_episode在episode结束才把数据批量写入这是因为future策略依赖“未来状态”来采样新目标必须在整条轨迹完整后执行。第三_recompute_reward里用的还是环境原始的距离阈值0.05这一点要和环境的success判定完全对齐否则会出现上面提到的自洽性错误。3.3 训练循环的组织顺序HER的完整训练流程比普通DQN多了一个“episode暂存”的过程伪代码大概是这个节奏env gym.make(FetchReach-v2) buffer HindsightReplayBuffer(capacity200000, k4, strategyfuture) agent TD3Agent(...) for episode in range(max_episodes): obs env.reset() goal obs[desired_goal] episode_reward 0 done False while not done: action agent.select_action(obs[observation], noise_scale0.2) next_obs, reward, done, info env.step(action) transition (obs[observation], action, reward, next_obs, done, goal) buffer.add(transition) obs next_obs episode_reward reward if buffer.size() batch_size and step % 4 0: batch buffer.sample_batch(batch_size) agent.update(batch) buffer.end_episode()我在实际项目里习惯每收集4步更新一次策略而不是每一步都更新。这样buffer里供采样用的样本多样性更好不会刚收集到的样本把全局经验冲掉。而且一旦用了HER样本的类型多了很多单步更新的性价比反而下降了。3.4 超参数配置参考这里给出一组我在FetchReach上实际跑通的参数作为新手起步的基线。你可以按需调整但至少要保证k和buffer容量不要同时缩水。参数建议值说明k4每条transition生成的重标记样本数strategyfuture最稳定的重标记策略buffer容量200000至少为训练总步数的两倍batch size256重标记样本占比高batch要够大学习率3e-4TD3常规配置exploration noise0.2高斯噪声标准差target noise0.2TD3用于critic更新的平滑噪声gamma0.98Fetch任务通常短episodegamma略低可减少引导负担这组配置不是黄金标准但它能保证你在合理时间内看到训练曲线上升。如果你发现曲线不涨先不要急着调超参数大概率是某些实现细节出了问题按第4节的内容排查会更快。4. 常见问题与排查技巧实录4.1 训练很久奖励曲线仍然停在最低点遇到这种情况我的第一反应不是去调学习率而是先确认“重标记样本是否真的进了buffer并且真的生成了正向奖励”。检查方法很简单训练过程中每5000步打印一次buffer里doneTrue样本的比例。如果这个比例几乎为0说明重标记基本失效。常见原因有三个一是距离阈值设得太严格导致重标记后的新目标几乎不可能碰巧在0.05以内二是环境里的achieved_goal字段传递有误导致目标距离计算永远偏大三是k值设成1导致重标记信号密度不足。另外一个容易被忽略的原因是探索噪声过小。HER擅于把失败的样本变成有用的训练数据但它依然需要探索。如果初始策略太保守动作长期集中在一个很小的邻域内智能体连“多样的失败”都产生不了自然更学不到“多样的成功”。把exploration noise从0.1调整到0.2或者0.3之后训练曲线往往会有肉眼可见的改善。4.2 收敛后波动过大评估成绩忽高忽低这种阶段通常发生在训练中后期智能体已经初步掌握了任务但策略稳定性不够。问题往往出在future窗口过长目标空间里的有效区域太大导致critic在拟合时一直面对剧烈的分布变化。你可以把future采样范围从[idx1, ep_len-1]改成[idx1, min(idx10, ep_len-1)]让重标记目标更贴近当前步的可达范围。这样critic的压力会小很多价值估计的方差也会下降。另一个思路是降低k至2减少低质量重标记样本对buffer的污染。请注意这里强调的是“低质量”不是所有重标记样本都好用。那些因果关联太弱的重标记样本虽然形式上是成功的但智能体很难真正学习到动作和目标之间的映射它们占着buffer空间还会干扰价值拟合。4.3 重标记目标太“假”怎么办“假”目标指的是一种现象新目标g是轨迹中真实出现过的状态但它与当前动作a的因果关系很弱。比如在future采样时选到了一个非常遥远的目标这条样本的语义就变成了“你现在随便做个动作就能在未来某个时刻到达一个遥远的地方”这样的样本虽然数学形式上合法但对学习毫无帮助。经验法则是重标记质量大于数量。与其盲目开k8不如把k控制在4左右并限制future采样窗口只用那些“确实在后续几步里能到达的”状态作为新目标。如果是episode策略则需要谨慎使用因为它的随机采样机制对因果完全不敏感很容易产生大量“假”样本。4.4 从仿真迁移到真机时的额外挑战如果你在仿真里用HER训练好了策略直接部署到真实机械臂上大概率效果会大打折扣。HER在仿真中的重标记策略高度依赖“状态可达”这个精准假设但真机的执行噪声和观测延迟会破坏这一点。真机上的s_next和仿真里的预测误差可能相差很大原本“成功”的重标记目标在真机上根本够不着。我建议在仿真阶段就给观测增加高斯噪声给动作增加随机执行误差或者用domain randomization随机化目标的初始位置范围。这一步改动的是环境接口不是HER本身但对后续真机迁移非常有帮助。4.5 别忘了done标志的重算这个坑我在第三节的代码注释里提了一嘴但值得单独拎出来强调因为太容易犯。很多人在HER复现时只重算了r却忘了重算done或者只重算了done却忘了同步目标距离阈值。假设某条原始transition本来未达成目标doneFalse。重标记成新目标g后s_next恰好在g的0.05米范围内此时这条样本应该标记为doneTrue。如果这里不改正critic会一直认为这条样本是“未结束”的并把未来回报错误地累加下去导致价值估计严重偏高或偏低。最后训练出来的策略可能在评估时能成功但在训练曲线里却表现诡异很难定位根因。我的经验是在_recompute_reward里同时返回r和done并且让r和done共用同一个距离阈值。这样至少能保证“奖励为正时done必为True”避免自相矛盾。5. 给后来者的一些个人经验5.1 HER最适合的任务类型整理过几个项目的经验后我给HER画了一个比较清晰的边界它最适合“单目标、连续状态空间、稀疏奖励但目标可描述清晰”的任务。典型如机械臂末端到达、推动物体、放置在指定区域等。这些任务里目标状态本身就是一个点重标记目标g可以用一个向量直接表示非常简洁。如果是多目标组合任务比如“先抓A再放B再抓C”成功与否往往依赖一整串子目标的完成单个状态没法完整描述目标HER的重标记就会很吃力。更实际的做法是把大目标拆成多个子目标每个子目标单独用HER训练再用上层调度逻辑串联起来。这也是复现Meta-World这类多任务基准时大家常见的做法。5.2 final策略在长episode任务里要慎用final策略实现简单让人忍不住想用但在长时间episode里它有副作用。如果episode特别长机械臂可能经历多次“撞墙-反弹-偶然到达角落”的过程那个最终状态虽然可达但作为目标是误导性的——它会让智能体误以为“撞墙撞到角落里就是好事”。future策略之所以普遍更稳是因为它只管“下一时刻能到达的某处”带有时间上的因果推进感而不是把结局当作唯一目标。5.3 HER与奖励塑形不是二选一我在做过一个按压铆钉的仿真任务后对HER和reward shaping的关系有了更清醒的认识。当时只靠HER时前期训练进展缓慢因为重标记虽然能提供方向性信号但缺少渐进式的步进奖励智能体在很长的探索阶段几乎就是在瞎试。后来我在奖励里加了一项简单的距离惩罚项前期收敛速度提升将近一倍HER保留的中后期样本多样性优势依然在。注意一旦用了reward shaping重标记样本的r也要用新目标同步计算对应的塑形奖励绝不能让两个奖励体系互相对不上否则智能体学到的东西一定会很奇怪。5.4 复现HER最快的路径如果现在让我给一个刚开始接触HER的人一个最务实的建议那就是不要直接从原论文的数学推导开始啃先跑通一个k4的future策略在FetchReach这类小环境上看到奖励曲线爬升再逐步加复杂度。曲线涨了说明核心实现是对的曲线不涨多半是done和r的计算不自洽或者buffer容量太小。搞定了这些HER的威力才能真正体现出来。我在多个项目里来回折腾后最大的体会是HER看起来只是一个小小的“样本改写”动作但它对强化学习样本效率的提升是跨越式的。当你第一次看到原本死气沉沉的奖励曲线因为晚上这个模块而开始爬升时那种感觉是很不一样的。希望这篇基于hindsight展开的分享能帮你少走一点弯路把更多精力放在真正的问题上。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进