ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

HER算法实战:从稀疏奖励困境到事后经验重放

HER算法实战:从稀疏奖励困境到事后经验重放 hindsight这个词英文直译是“事后聪明”中文常被翻译成“后见之明”或者更直白一点——“事后诸葛亮”。我第一次认识它不是因为在英语课上背单词而是在强化学习的论文里OpenAI 2017年发的那篇《Hindsight Experience Replay》标题直接用了这个词。当时我正被一个机械臂推箱子任务折磨得死去活来奖励函数怎么写都不收敛看到这篇论文的标题好奇心一下子就上来了。这篇博文想聊的就是这个算法以及我在实际项目里用它踩过的坑、调过的参、总结出来的经验。her要解决的是一个非常经典的强化学习难题稀疏奖励。说人话就是——智能体在环境里折腾半天大多数时候拿不到任何奖励反馈直到最后碰巧达成了目标才收到一个“1”。这种情况下普通强化学习算法基本是废的因为随机探索要碰巧撞到目标概率低到可以忽略。HER的思路非常聪明既然当前目标的成功概率太低那就换个目标看待已有轨迹把“失败”变成“另一种成功”的经验。这篇文章适合正在做机器人控制、操作类任务或者刚接触多目标强化学习、被稀疏奖励问题劝退的读者读完你不仅能理解原理还能直接照着代码把HER跑起来。1. hindsight到底解决了什么先说说稀疏奖励这个“老大难”1.1 没有奖励信号的时候智能体有多抓狂先说一个最典型、最折磨人的场景机械臂推箱子。目标是把桌上的箱子推到某个指定的位置。假设机械臂有7个自由度每一帧的动作是一个7维连续向量。如果箱子初始位置和目标位置相距比如20厘米机械臂每次动作的推进效果大约1厘米左右那至少需要连贯地执行20步“方向基本正确”的动作箱子才能到达目标附近。问题在于稀疏奖励设定下只有当箱子最终到达目标位置时环境才返回reward 1其他所有情况下都是reward 0。我们可以粗略算一下如果每一步动作要“方向正确”的概率只有20%那连续20步都正确的概率是0.2的20次方大约是10的负14次方。这意味着让智能体随机瞎动动到宇宙毁灭也撞不上一次成功。你可能会想那用策略梯度直接优化呢问题是策略梯度依赖奖励信号做出梯度更新全程零奖励意味着没有任何梯度信息策略在参数空间里原地打转什么都学不到。我当时测过一个基线直接用DDPG在FetchPush环境上跑不给任何额外奖励设计跑了50万步成功率一直是0。不是收敛慢是真的连一步都迈不出去。环境的reward曲线从头到尾就是一条平得不能再平的直线。这不是算法本身的问题而是稀疏奖励环境里“探索信号完全缺失”的必然结果。1.2 作弊式奖励函数治标不治本面对稀疏奖励很多人的第一反应是手动设计奖励函数。最常见的是shaping奖励奖励箱子与目标之间的距离减少量。比如每一步结束后计算新的距离比上一步更近就给一个小的正奖励。这听起来合理实际操作起来全是坑。第一个坑是奖励尺度非常难调。距离奖励给多了智能体会学会“故意绕远路再回来”来刷奖励给少了又和稀疏奖励一样没信号。第二个坑是reward hacking这是绕不过去的问题。我见过一个经典案例有人在机械臂任务里给“靠近目标”加了奖励智能体最后学会的诡计是——把手伸到目标附近但不完成任务因为这样既能持续获得距离缩小的奖励又不会因为“提前完成”而失去练习机会成功率反而比精心设计时更低。第三个坑更隐蔽你设计的潜在函数可能和真正的最优策略相冲突导致智能体收敛到一个局部最优的“别扭姿态”。所以奖励工程做多了你会明白一件事与其想方设法往每个状态塞人为奖励不如换个思路——让算法自己从失败轨迹中挖掘学习信号。HER就是这个思路的代表作。1.3 hindsight视角的登场给轨迹换一个目标重新解读HER的核心思想用一句话说就是每个失败的轨迹都不是失败只是“换一个目标”之后的成功。举个生活中的例子。你想去某家网红餐厅结果导航把你带偏了最后误打误撞进了一家更好吃的苍蝇馆子。事后你回头看会觉得这次出行的目标“到达那家餐厅”虽然没达成但“吃到一顿好饭”的目标其实达成了车也没白开。HER把这种“事后诸葛亮”的视角搬进了强化学习既然当前目标你没达成但轨迹里确实到达过某个状态S那我就把S当作这次轨迹的“事后目标”然后重新标注这条轨迹的奖励——在S这个目标下你的确成功了奖励为1。这样得到的好处非常直接原本一整条reward全为0的轨迹经过重标记后会分裂出好几条“部分成功”的轨迹里面有正奖励样本了。智能体就能从中学到“在某个状态下哪条动作序列通往了一个可以达到的目标”。只要这个逻辑每回合不断重复哪怕智能体从来没有真正达成过原始目标它也能逐渐学会“把东西推向一个可达的位置”这个泛化技能最终被用在真正的原始目标上。2. 核心原理拆解HER的重标记魔法2.1 给强化学习加一个“目标维度”要理解HER为什么能起效得先弄明白它改写了强化学习的问题定义。传统的MDP是四元组S, A, R, P状态、动作、奖励、转移概率。HER引入了一个额外的东西目标goals。此时策略不再是π(a|s)而是π(a|s, g)——在状态下根据当前目标g来决定动作。注意这个g不是马尔可夫决策过程里的“长期目标”而是每条轨迹开始时采样出来的一个具体指令比如“把箱子推到坐标(1.2, 0.5)”整条轨迹的奖励公式统一为在最终状态s_T判断是否满足g满足则奖励为1否则为0。这个“多目标”结构是HER能成立的前提。因为每条轨迹都携带一个目标我们才能在事后“篡改”这个目标在重放时把g替换成另一个g而不影响轨迹本身的物理合理性——状态、动作、转移都是真实发生的只是我们换了一个评判视角。如果环境任务是单一目标的、不存在可以替换的目标维度HER就没有用武之地。HER论文里用的是Universal Value Function ApproximatorsUVFA的思路也就是让价值函数Q(s, a, g)同时接收目标g作为输入。网络结构上只需要把状态向量和目标向量拼在一起作为输入即可不需要特殊的复杂设计。实操时很多人会问我的状态是图片怎么办HER在图片输入上也能用但需要额外处理要么用比较小的图片分辨率要么先接一个编码器把高维状态压缩成向量再把目标向量拼进去。2.2 final和future两种重标记策略HER论文给出了四种重标记策略名字有趣final、future、episode、random。用大白话说就是final每次重标记时直接用本条轨迹最终到达的状态s_T作为新的目标g。future从当前时刻t之后的状态{s_t1, s_t2, ..., s_T}里随机抽取一个作为目标g。episode从整条轨迹的所有状态里随机抽一个作为目标。random从整个训练集的历史状态里随机抽一个完全不考虑时间顺序。实测下来future是效果最好的也是社区最常用的。原因很直观future只在“当前时刻之后的未来状态”里面选目标这些状态在物理上是可以从当前状态s_t通过后续几步动作够到的。拿它当目标相当于要求智能体“继续完成本来就已经在做的事情”这条经验对学习正向转移是有帮助的。final从轨迹终点抽取相当于要求智能体“复现整条轨迹的全过程”在长轨迹上偏难。episode和random有时候会抽到一个“从当前位置根本过不去”的瞎目标等于给训练注入噪声。没关系我们实际用future并且配合一个参数k意思是每一条真实轨迹会额外生成k条重标记样本。论文里推荐k取4我用下来也是这个值最稳太小了正样本太少太大了重复样本太多导致多样性下降。2.3 HER“为什么有效”的三个直观解释有人会问把失败重标记成“另一种成功”这不会把模型带偏吗短期看策略确实在追求那些“事后目标”但这些目标并不是随便找的而是从真实轨迹中来的可达状态。这三个机制决定了它有效第一目标具备可达性。因为新目标是从轨迹里实际到达过的状态采样来的所以它总是处于当前策略的可达范围内。更直白地说重标记后的“成功经验”都是“实际上发生过的事”这比凭空设计出来的伪目标可靠得多。第二梯度信号密度提高了。原始轨迹中可能一条正样本都没有重标记后每个时间步至少有一条“正确动作”样本策略梯度总算有地方使劲了。第三多目标泛化。随着重标记的积累价值函数Q(s, a, g)见过越来越多的“目标-达成”组合它会逐渐学会一个通用的模式只要最终状态满足目标就有价值。这个泛化能力让原始目标虽然从未被达成网络也能推断出“朝某个方向推应该值钱”。这三个点叠加起来就解释了那个反直觉的现象为什么HER可以在从来没成功过一次的任务里让算法最终学会成功。3. 实操落地把HER跑起来3.1 环境选型用bitstring系列的三个经典任务学习HER首选环境是OpenAI Gym里基于MuJoCo的bitstring系列一共三个任务FetchReach机械臂末端到达目标点、FetchPush推箱子到目标点、FetchSlide把箱子滑到目标点。这三个任务难度依次递增Reach最简单Push适中Slide需要让箱子在光滑桌面滑动存在大量“错过目标点”的失败轨迹非常适合观察HER的价值。这三个环境都有标准的稀疏奖励模式只要目标和达成状态的距离小于设定阈值通常0.05就给1否则给0完全不需要额外奖励工程。安装依赖不算复杂核心是Python环境加上gymnasium和mujoco。MuJoCo现在免费了不再需要老的mujoco_py那套授权机制用pip install mujoco gymnasium就行。NVIDIA显卡在这类任务上完全用不上CPU训练就已经足够快——FetchPush用双层MLP单进程训练一般几个小时就能看到明显收敛迹象。但我建议还是准备好一个可以长期挂机跑实验的环境因为HER训练曲线波动很大同一套配置跑三次结果可能差不少。我用的是python 3.10、gymnasium 0.28、mujoco 2.3.7这套组合兼容性比较好。早期那些直接clone的baselines仓库版本太老动不动就是gym版本不兼容的问题不建议新手折腾。3.2 HER重放缓冲区的核心代码实现HER的工程实现核心不在网络结构而在重放缓冲区的逻辑。这里我给一个可以直接用的简化版实现核心逻辑都保留了import numpy as np from collections import deque def compute_reward(achieved_goal, desired_goal, threshold0.05): # 目标达成判定距离小于阈值 distance np.linalg.norm(achieved_goal - desired_goal) return 1.0 if distance threshold else 0.0 class HerReplayBuffer: def __init__(self, capacity100000, replay_k4, strategyfuture): self.buffer deque(maxlencapacity) self.replay_k replay_k self.strategy strategy def add_episode(self, episode): # episode: list of dicts, 每个dict包含: # observation(不含goal), achieved_goal, desired_goal, # action, reward, next_observation(不含goal), next_achieved_goal, done self.buffer.append(episode) # 以轨迹为单位存储重标记时整体处理 def sample(self, batch_size): transitions [] while len(transitions) batch_size: episode np.random.choice(self.buffer) t np.random.randint(len(episode) - 1) # 原始transition transition { obs: episode[t][observation], achieved_goal: episode[t][achieved_goal], action: episode[t][action], obs_next: episode[t 1][next_observation], achieved_goal_next: episode[t 1][next_achieved_goal], done: episode[t 1][done] } # 以原始目标入库 transition[desired_goal] episode[t][desired_goal] transition[reward] compute_reward( transition[achieved_goal_next], transition[desired_goal]) transitions.append(transition) # HER重标记根据策略生成新目标 if self.strategy future: future_idx np.random.randint(t 1, len(episode)) new_goal episode[future_idx][achieved_goal] elif self.strategy final: new_goal episode[-1][achieved_goal] else: raise ValueError(unsupported strategy) transition_her dict(transition) transition_her[desired_goal] new_goal transition_her[reward] compute_reward( transition_her[achieved_goal_next], new_goal) transitions.append(transition_her) return transitions[:batch_size]你会发现关键点就两个第一缓冲区以整条episode为单位存储而不是存单条transition因为重标记需要整条轨迹的完整信息第二每条transition都会额外生成一份“重标记版本”。实际训练时每个batch里一半是原始经验一半是重标记经验比例可以用参数调。这里要特别提醒一个初学者常犯的错误重标记后的transitionobs和action保持不变但desired_goal是新的所以计算reward时拿的是obs_next里包含的achieved_goal和新的desired_goal来算。你要是拿错的state去算距离重标记就完全错乱了。3.3 超参数怎么定直接抄我这份配置我把在FetchPush上调试多轮后稳定可用的配置直接列出来供你作为一个起点。不用逐字照搬但参考价值是有的参数项推荐值备注replay_strategyfuture社区最常用效果最稳replay_k4每条轨迹额外生成4份重标记样本buffer_size100000条轨迹如果按transition存至少存100万条batch_size256太大太小都会影响稳定性网络结构MLP: 256-256两层ReLU输入为obs和goal拼接学习率1e-3用Adam不需要额外衰减gamma0.98比常见0.99略小任务总步数短探索噪声Ornstein-Uhlenbecksigma0.2机械臂连续控制任务的标准选择训练总步数50万到100万FetchPush大约30万步可见效果目标拼接方式Fetch类环境的observation默认是一个字典包含observation字段机械臂的关节角度等和desired_goal字段。送入网络前要把observation和desired_goal拼成一个向量维度大概是25维。这步做错会导致维度不匹配报错或者更隐蔽的——网络压根没接收到目标信息训练曲线也会是平的。底层算法我用的DDPG。你也可以直接用TD3或者SACHER不依赖具体算法只要这个算法能利用replay buffer就能套。甚至PPO也行但要注意PPO本身不是off-policy的和HER搭配需要额外修改importance weight不推荐新手首次尝试就上PPO。4. 踩坑实录这些细节不到位模型就是不动4.1 常见问题速查表我在实际操作中反复踩过一些坑这里统一做成速查表方便你排查问题现象可能原因解决办法reward曲线一直是0成功率毫无起色重标记没用上buffer里没有正样本确认每个batch确实混入了一半以上的HER样本训练到中途reward突然掉到0探索噪声过大导致策略退化调小OU噪声的sigma或者改用epsilon贪心退火loss很小但成功率上不去奖励计算用了错误的distance检查目标阈值确认achieved_goal和desired_goal的坐标系是否一致运行报维度不匹配obs和goal拼接方式错误打印obs和goal的shape确认维度后再拼接buffer里全是同一类经验多样性差replay_k太大调小replay_k或者增大buffer_size三个seed跑出的结果差异巨大HER本身方差大不是bug跑多个seed求均值或者固定seed观察单次趋势4.2 极易翻车的一个点done标志的处理HER的调参里done标志是我认为最容易出错、影响也最大的细节之一。在多目标设定下“done”有两层含义一是episode是否因为到达最大步数而终止二是是否因为目标达成而提前终止。如果你的环境的done字段是“是否成功”那么重标记后同样需要根据新的desired_goal重新计算done。HER重标记后的done大多数情况下应该为False因为新目标恰好被达成在当前步的概率很低除非你恰好采到s_t本身作为目标这种情况要特别注意。如果这里处理错误最典型的症状是训练初期策略被“成功样本”猛烈拉扯然后瞬间崩溃成功率暴涨到60%以上再跌回0。原因就是重标记样本的done被错误标成True导致GAE或n-step return的引导错误价值函数被污染。我的建议是在HER环境里不要直接用环境返回的done而是自己写一个is_success判断逻辑基于最终状态和目标距离计算再在边界处显式处理。重标记后重新计算reward和done而不是沿用原样本的值。4.3 怎么看训练曲线什么情况算正常收敛HER训练曲线的形状和传统奖励工程很不一样。第一次跑通时你可能以为哪里写错了——成功率在早期很长一段都是0然后可能在某个节点突然跳起来。这不是玄学而是“阈值”效应价值函数对目标空间的泛化达到临界点后策略开始“顿悟”成功率从个位数跳到百分之一二十然后稳步上升。以FetchPush为例我用上面配置跑的经验曲线是这样的前10万步成功率长期在0到2%之间徘徊20万步左右出现第一次跳升可能突然到20%30万到40万步爬到50%再往后缓慢逼近80%到90%。这期间reward曲线的绝对值不重要可以一直很低因为你奖励本来就稀疏。关键指标是成功率也就是“原始目标达成比例”不是总奖励。如果你的曲线在10万步内就迅速爬到很高别高兴太早大概率是训练信息泄漏了——检查一下是否把未来信息塞进了observation或者验证集和训练集没分开。如果50万步还是0优先检查缓冲区里HER样本的reward是否存在打印一下sample出来的batch里reward1的比例如果为0重标记逻辑一定有问题。4.4 一点个人心得HER不是万金油最后说点经验性的判断。HER在“目标可描述、可判定、且轨迹中大概率能碰巧到达某个有价值状态”的任务上非常强大。但如果你做的任务没有明确goal比如对话生成、短视频推荐那“事后目标”无从定义HER自然没用。另外有些任务的目标空间极大单个episode长度也长重标记的样本几乎都是“远距离目标”同样收效甚微。这种情况下我会更倾向于用分层RL或者其他基于模型的探索方法而不是硬套HER。我在实际项目里用过HER做过一次六自由度机械臂的插孔装配尝试环境奖励极其稀疏只有插进去那一刻给奖励。HER确实让成功率从0提升到了可以训练的水平但最终稳定插孔率也就60%出头离实用还有距离。它的价值更多体现在“把打不开局面的稀疏奖励问题变成可以下手的稠密信号问题”至于后续怎么把成功率推高还需要配合课程学习、示教引导这些手段。如果你手头正好有一个“奖励稀疏到根本学不动”的任务我的建议是别急着调网络结构也别急着加手工奖励先试试HER。可能只需要改一下replay buffer的逻辑之前怎么调都不动的曲线就开始自己往上涨了。
RELATED READING

延伸阅读

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