强化学习稀疏奖励破解:HER事后经验回放原理与实战解析

发布时间:2026/10/4 5:22:23
强化学习稀疏奖励破解:HER事后经验回放原理与实战解析
说到强化学习里的稀疏奖励问题很多人第一个想到的招数就是reward shaping——想方设法给模型设计一个“中间奖励”让它一步步爬到目标。但真正在机器人操控、导航这类多目标任务里踩过坑的人都知道手工设计奖励函数有多痛苦要么给得太密策略直接耍赖绕路刷分要么给得不准训练半天模型学到的全是你的主观偏好。而hindsight这个思想恰恰提供了一条完全不同的路——不改变奖励只改变“目标本身”。我第一次接触Hindsight Experience Replay这个词是在OpenAI那篇《Hindsight Experience Replay》论文里。当时正被一个机械臂抓取任务折磨得不行稀疏奖励下策略几乎零梯度模型完全不动。但“事后视角”这个idea一下点醒了我如果这次没抓到红球但抓到了旁边的蓝球那为什么不让模型把这次经验当作“成功抓到蓝球”来学顺着一路实践下来我发现这套思想不光是HER这个算法的名字它几乎贯穿了多目标强化学习、逆强化学习乃至数据回放逻辑的设计。这篇东西就把我自己的理解和实操经验写下来适合正在搞Robot Learning、Goal-conditioned RL、或者被稀疏奖励折磨的朋友参考。1. 为什么稀疏奖励会让策略学习“卡死”1.1 一个二分类问题被做成了回归问题先捋一下问题本质。稀疏奖励的设定下智能体大部分时间拿到的奖励都是0只有真正完成任务那一步才拿到正反馈。比如抓取任务里整个episode可能几百步大部分transition的reward都是0只有成功那一瞬间reward1。这本质上是把一个需要连续决策的问题硬生生变成了一个“要么全对、要么全错”的二分类问题——还是那种正样本极其稀少的二分类。标准策略梯度方法在这种设定下有多惨呢我们算一下。策略梯度中某个transition的更新幅度和它的奖励直接挂钩。奖励全是0的时候梯度被压得几乎看不见策略更新相当于在随机游走。哪怕偶尔碰对一个成功轨迹如果这个成功轨迹不是策略当前分布下的典型输出这份梯度又会被后续的大量失败经验稀释掉。这就是我常说的“稀疏奖励下的信度分配危机”模型不知道是哪一步做对了也不知道做错的步骤离成功有多远。1.2 事后诸葛亮的直觉价值人类在复盘的时候有个天然优势做完一件事回头看我们总能找到一些“如果当时换个目标这次动作其实挺成功”的时刻。比如投篮没进但球的弧线和力度其实很接近标准教练会告诉你“按这个手型再调一点点就好”。HER的核心直觉正是这个——把每个失败轨迹经验性地视作另一个目标下的成功轨迹然后让模型从这份“成功经验”里学。这在数学上等价于什么呢一句话在目标条件策略goal-conditioned policy的框架下我们不只使用轨迹原本设定的goal还额外设定一些“事后goal”让同一条轨迹在多个目标下变成正样本。这样一来稀疏奖励导致的正样本缺失问题从数据侧就被缓解了——每个episode不再只贡献一份“成功与否”二值标签而是可能贡献多份“换了个目标之后成功”的经验。1.3 前提条件必须的Goal-conditioned设定必须要强调HER不是什么任务都能直接套。它要求你的环境天然带有一个goal接口并且奖励函数可以基于goal来计算比如$r_t g(s_{t1}, g_{desired})$。像机械臂抓取指定位置、机器人走向指定坐标、倒水到指定高度这类任务都很适合但像Atari游戏这种没有明确goal封装的任务HER并不能直接硬套得先自己设计goal的表征方式。这也是不少朋友跑HER没效果的第一大原因——环境根本不满足“目标可枚举、可重标记”这个前提。2. HER算法核心机制拆解2.1 四条采样策略是怎么定出来的HER在原论文里给了四种事后目标的采样策略final、future、episode、random。我逐个说下它们的行为和适用场景。final拿episode最后一步的state作为事后目标。适用于那种最后状态就是理想结果的任务比如抓取成功时物体就在夹爪里。future从当前时间步往后的状态里随机挑一个作为事后目标。覆盖面更广适合任务中间状态也有意义的情况。episode从整个episode的状态里随机挑一个作为事后目标。随机性最大。random完全从state空间随机采样目标跟轨迹本身无关相当于加探索噪声。实际用下来future策略是默认首选而且原论文实验也证明它综合效果最好。原因是future目标具备天然的时间一致性——目标的state和当前轨迹有一定相关性学到的策略容易泛化相比之下random目标容易和环境物理约束脱节学出来纯粹是噪声拟合。我自己做抓取任务future的比例占到80%以上剩下20%留给episode效果比纯future还稳一点。2.2 目标重标记的完整流程来走一遍HER的完整数据流。假设一个episode结束总共T步目标是$g$轨迹为$[s_0, a_0, s_1, a_1, ..., s_{T-1}, a_{T-1}, s_T]$初始奖励函数判定这个轨迹失败比如最终没抓到物体。常规回放缓冲区每存入一条transition都会记录$(s_t, a_t, r_t, s_{t1}, g)$。HER额外做一步从这个轨迹里重采样出若干个事后目标$g$针对每个$g$重算整条轨迹上每一步的奖励然后把重标记后的transition也存入缓冲区。关键点来了当某条transition被采样出来用于训练时它的goal字段可能是原始的$g$也可能是重标记的$g$。网络需要同时接受状态和目标的拼接作为输入这就是goal-conditioned策略和普通策略在输入结构上的差异。我最早实现的时候经常忘记把goal拼进输入结果训练出来的策略对goal毫无反应——后来检查了半天才发现输入层维度和loss对不上。2.3 重标记时的一个隐藏天坑重标记目标这件事看起来简单实际操作里有个隐蔽的雷不能用重标记后的transition去更新Q函数中的bootstrapping目标。这句话值得划重点。原因在TD误差公式里$$Q(s_t, a_t) \leftarrow r_t \gamma \max_{a} Q(s_{t1}, a, g)$$如果r_t和g是重标记后的数据而$Q(s_{t1}, a, g)$用的也是同一个重标记goal那么这条transition描述的是“在goal $g$ 下从 $s_t$ 走到 $s_{t1}$ 获得了正奖励”理论上是自洽的。但问题在于后续状态 $s_{t1}$ 并不一定真的是“在策略下朝着 $g$ 探索得到的下一步”它是朝着原始goal $g$ 走出来的。如果这条transition被反复用于更新Q函数会逐渐高估那些“表面成功”的状态价值最终策略震荡甚至崩溃。我的规避方式比较朴素但有效在重标记transition存入缓冲区的时候把原始goal也作为一条独立transition存一份。模型训练时均匀从原始轨迹和重标记轨迹里采样这样Q值更新既有“真实目标下的失败经验”做锚点又有“事后目标下的成功经验”做引导两套数据相互制衡训练稳定很多。原论文里其实也提到了类似思路但他们管这个叫“混合经验”建议比例可以调节我习惯是1:1。3. 实操从零搭建HER训练流程3.1 环境选型与Goal设计我强烈建议第一次跑HER的朋友不要直接上真实机器人或者复杂环境先用OpenAI的FetchReach-v1或者FetchPickAndPlace-v1这类现成环境跑通整个流程。这些环境同时满足两个关键条件state空间里有比较明确的goal分量奖励函数是稀疏但定义清晰的距离小于阈值就为1。下面这段是我常用的一套目标表征代码直接拿来做环境封装import gym import numpy as np class HERGoalWrapper(gym.Wrapper): def __init__(self, env, goal_thresh0.05): super().__init__(env) self.goal_thresh goal_thresh def step(self, action): obs, reward, done, info self.env.step(action) achieved_goal obs[achieved_goal] desired_goal obs[desired_goal] # 稀疏奖励距离小于阈值才算成功 dist np.linalg.norm(achieved_goal - desired_goal) sparse_reward 1.0 if dist self.goal_thresh else 0.0 # 同时把稠密距离也存起来训练时可以用来debug info[achieved_dist] dist return obs, sparse_reward, done, info注意这里我保留了环境自带的稠密距离到info里但传给算法的奖励是稀疏的。这么做的好处是训练过程中可以随时翻开日志看距离曲线判断策略是否在朝目标靠近——奖励是0或者1看不出变化趋势但distance曲线能让你早发现策略是否在乱蹭。3.2 核心实现HER回放缓冲区的构造HER的灵魂在数据回放。直接贴一段我自己的Buffer实现片段基于Python和numpy兼容stable-baselines3的接口思路class HERReplayBuffer: def __init__(self, buffer_size, her_strategyfuture, n_sampled_goal4): self.buffer_size buffer_size self.her_strategy her_strategy self.n_sampled_goal n_sampled_goal self.episodes [] # 暂存每个episode的全部transitions def add_episode(self, episode): # episode: list of (obs, action, reward, next_obs, done) # obs中需要包含desired_goal字典访问 self.episodes.append(episode) def sample_batch(self, batch_size): batch [] for _ in range(batch_size): ep_id np.random.choice(len(self.episodes)) ep self.episodes[ep_id] t np.random.randint(len(ep)) obs_t, act_t, rew_t, obs_next_t, done_t ep[t] # 以1/2概率保留原目标1/2概率重标记 if np.random.rand() 0.5: batch.append((obs_t, act_t, rew_t, obs_next_t, done_t)) else: # 重标记目标 new_goal self._recompute_goal(ep, t) new_obs obs_t.copy() new_obs[desired_goal] new_goal new_obs_next obs_next_t.copy() new_obs_next[desired_goal] new_goal new_rew self._compute_reward(new_obs_next, new_goal) batch.append((new_obs, act_t, new_rew, new_obs_next, done_t)) return batch这段代码里_recompute_goal根据her_strategy从future或者episode状态里选目标。实现的时候有个细节复制obs时一定要深拷贝不然直接改dict引用会污染原始数据。我之前就因为浅拷贝导致同一份缓冲区里的旧轨迹目标也被覆盖训练直接发散找了半下午的bug。3.3 用stable-baselines3快速验证的完整流程如果你不想手写bufferstable-baselines3提供了开箱即用的HER支持。官方文档里推荐的配置方式是配合OffPolicy算法SAC/DDPG/TD3。我一般这么写import gym from stable_baselines3 import SAC from stable_baselines3.common.buffers import HerReplayBuffer from stable_baselines3.common.goal_envs import GoalEnv from stable_baselines3.common.vec_env import DummyVecEnv env gym.make(FetchReach-v1) env DummyVecEnv([lambda: env]) model SAC( policyMultiInputPolicy, envenv, replay_buffer_classHerReplayBuffer, replay_buffer_kwargsdict(n_sampled_goal4, goal_selection_strategyfuture), policy_kwargsdict(net_arch[256, 256, 256]), learning_rate1e-3, buffer_sizeint(1e6), learning_starts1000, train_freq50, gradient_steps100, verbose1, ) model.learn(total_timesteps1_000_000)简单解释下这些参数的作用。n_sampled_goal4表示每条episode额外重标记4个事后目标越大数据利用率越高但缓冲区里重标记数据占比也会变高我建议4到8之间即可太高会让原始目标经验比例过低。train_freq50配合gradient_steps100意味着每50步环境交互就做100步梯度更新off-policy算法的数据效率就体现在这里。learning_starts1000是预热步数确保缓冲区里有足够多transition才开始更新防止初期随机策略样本把Q网络带偏。跑通之后我自己做的第一件事是把成功率和目标距离画在一张图里。你会发现诅咒成功率前期几乎一直是0但距离曲线是在下降的。这说明模型在“靠近目标”但离“成功”还很远。千万别因为成功率是0就以为模型没在学这个阶段是最容易误杀训练的时期。4. 训练实战中的常见问题与排查心得4.1 模型练了很久成功率仍然为0这应该是HER实践者最常撞的墙。排查顺序我给出一个标准流程看距离曲线是否下降。如果距离曲线在震荡而不是下降八成是目标重标记逻辑写错了或者奖励函数定义有bug。确认环境是否真的有意义的多目标空间。比如FetchReach里如果goal永远不变HER等价于稀疏奖励下的普通SAC效果会大打折扣。检查缓冲区内原目标transition和重标记transition的比例。如果重标记比例过高模型会学到“总是把当前状态当目标”的偷懒策略表现为距离卡在阈值附近但永远成不了功——因为成功本身被重标记稀释了。4.2 训练出现“奖励不降反升”的二象性这里要聊一个HER特有的悖论策略越接近成功重标记出来的“成功经验”反而越没价值。因为当策略已经很擅长接近目标时原始goal下的成功率也在提高此时重标记目标的意义相对降低但如果重标记比例没变大量的重标记transition会让Q值在“扩大成功范围”的方向上继续膨胀导致策略开始出现诡异行为——比如故意把物体拨到偏离原goal的位置因为那些位置在重标记后变成了“另一个成功目标”。我在FetchPickAndPlace上就见过这种情况。策略会把物体从目标位置扒拉开但在重标记视角下它“成功”把物体推到了另一个位置。排查方法也很直观把重标记策略关闭跑一个对比实验如果策略行为明显不同说明重标记比例需要下调。我现在倾向于动态调整训练后期把n_sampled_goal从4降到2效果比固定参数稳定。4.3 目标维度爆炸导致训练不稳HER对goal维度的增加非常敏感。比如FetchReach的goal是3维训练效果很稳定但一旦换成过程更复杂的任务goal是7维甚至更高缓冲区里重标记的有效目标覆盖率会急剧下降。这时两个思路一是做goal的下采样或者自动encoder二是把goal空间结构化例如拆成“物体位置”和“夹爪开合”两个子目标分别学习。另有一个工程上的trick在采样重标记目标前先过滤掉那些和原goal距离太近或太远的状态。距离太近的重标记没有信息量相当于拿一个本就可能成功的轨迹换个说法距离太远的又容易导致策略大幅震荡。我通常用一个简单的距离比例过滤重标记目标距离当前状态为0.10.5个原目标距离范围兼顾信息量和稳定性。4.4 实操问题速查表现象可能原因排查动作成功率长期为0且距离不降goal重标记未生效/奖励函数出错打印每条transition的重标记前后reward对比距离下降但成功率上不去重标记比例过高/阈值过紧调低n_sampled_goal放宽成功阈值策略故意偏离原goalQ值因重标记过渡高估增加原目标transition占比调低重标记比例训练初期loss爆掉缓冲区样本不均衡加大learning_starts让缓冲区先攒点数据复杂度高的任务学不会goal维度太高/采样策略不匹配降维或改目标采样策略为futureepisode混合5. hindsight的扩展思路与个人经验总结5.1 从HER到更广的“后见之明”思想HER的火爆不仅因为它自己效果亮眼更因为它点出了一个通用方法论用结果反推过程的价值而不是只用预设目标衡量成败。这套思想在后来的许多方向里都有影子。在多目标逆强化学习里研究者用事后状态分布来替代专家示范降低了示范数据量的需求。在离线强化学习里hindsight的思想体现在“用多个后续状态作为虚拟目标”来扩充数据集。还有不少结合gym Robotics环境做对比学习的工作本质上也是在做“hindsight embedding”——让状态的表征向着“可达到性”而不是“给定目标”对齐。我自己在实际项目中还试过一个变体把HER的目标重标记从“状态”扩展到“子任务序列”。比如一个倒水任务把“拿起杯子”“移到水槽”“倾斜杯身”三个子目标串成一个序列然后用类似HER的方式在序列层面做重标记。效果比在原始状态空间上直接重标记稳定很多因为子任务的分层结构天然限制了重标记的搜索空间。如果你做的任务天然可分阶段非常建议试试这个思路。5.2 训练节奏的掌控笔记跑HER这一年多我最大的体会是训练节奏比算法本身更影响最终效果。具体来说有三点。第一探索策略和HER是协同的不是替代的。HER能充分利用失败轨迹但前提是策略还在不断尝试新东西。如果actor过早收敛到一个局部行为模式重标记出来的目标也就困在那一小块状态空间里。所以我在实践中会把SAC自身的entropy系数调高一点让采样动作更分散。第二重标记数据的使用比例要随着训练阶段调整。类似于课程学习训练前期多依赖重标记经验快速建立“目标-行为”的粗映射训练中后期逐渐提高原始目标比例让模型精修“在真实目标下完成任务”的能力。我的做法是直接从n_sampled_goal这个超参数下手前期6后期2比固定参数能快出将近30%的收敛速度。第三每个episode结束之后把成功/失败的统计信息和重标记后的平均奖励都存一份。只靠总奖励曲线很难判断HER是否在正常工作但分开看这两条指标就清楚得多——重标记奖励上升说明模型在理解“目标置换”这件事原始成功率上升才说明真实任务在解决。两条曲线一起看你就能准确判断训练卡在哪一环。5.3 一个小小的心得如果你现在正卡在稀疏奖励的坑里我的建议是别急着堆reward shaping先看看环境能不能换成goal-conditioned的形式。很多时候把“目标”从隐含变量变成显式输入再配合HER这类后见之明式学习方法能绕开大量手工设计奖励的脏活累活。我在这条路上踩过的坑不算少但每次看到训练曲线从死气沉沉的0开始往上爬时都会觉得这套“事后”视角的思路确实值得。训练智能体有点像带新人光告诉他“你错了”没用得帮他把做对的部分拆出来哪怕方向不完全对也值得肯定和总结。HER做的正是这件事——它不说你失败了它说你在另一个目标下成功了然后顺着这条线索一点点逼近真正的目标。