稀疏奖励下的强化学习救星:HER后见之明经验回放原理与实战 我第一次跑稀疏奖励的机器人控制任务时两千个episode跑完成功率还是0。不是网络结构有问题也不是学习率没调好——agent完全不知道自己在干嘛因为整个环境里能拿到非零奖励的次数一只手数得过来。后来我把目光转向一个名字非常诚实的算法hindsight中文可以叫“后见之明”对应的论文全称是Hindsight Experience ReplayHER。这个算法本质就一句话既然这次没达到原始目标那就把这段经历重新解释成“已经到达过某个地方”的经验照样拿来学习。如果你正在做机械臂抓取、机器人导航、游戏AI这类连续控制任务又恰好被稀疏奖励折磨得怀疑人生这篇文章就是写给你的。我会从问题背景讲起把HER的原理、四种目标采样策略、代码实现和调参经验全部拆开最后还会分享一些我实际踩过的坑保证读完能直接上手复现。1. 稀疏奖励为什么能把强化学习逼到墙角1.1 用一个丢球例子理解稀疏奖励想象你蒙着眼睛站在操场上手里拿一个网球目标是把球丢进三米外的一个小桶里。你的动作是随机的右手角度、力度、出手时机差了分毫球就飞偏了。请问你丢多少次能进一次运气好的话几百次运气差可能一整天都不进。强化学习里的agent面临的是同样的问题只不过它比人更“笨”它没有任何先验知识所有动作都是从一个随机初始化策略里采样出来的。在稀疏奖励任务里环境只在“完成任务”时给1奖励其他所有时刻都是0。比如Fetch机器人推箱子目标是把箱子推到指定位置网络输出的是一串7维连续动作。如果随机探索一万个episode可能只有几次能碰巧靠近目标绝大多数轨迹的奖励序列长得一模一样全是0。这意味着什么意味着反向传播的时候梯度信号几乎是空的那个唯一的1奖励就像大海里的一根针对网络参数更新的贡献微乎其微。我用一段公式来说明一条轨迹长度为T奖励是r_t 1(s_{t1} g)。在T50、动作空间7维的情况下随机策略命中目标的概率如果只有千分之一那平均需要几百条轨迹才能看到一次非零奖励。更致命的是就算撞上了这一次由于奖励稀疏agent也搞不清楚到底是哪一步动作起了作用。这就是稀疏奖励问题的核心不是不能学而是学习信号太稀根本传不到决策层。1.2 为什么不能全靠dense reward可能有同学会问那你干脆设计一个稠密奖励函数不就行了比如离目标越近奖励越大。这个问题我当年也困惑过实际做下来才发现稠密奖励在很多场景下压根设计不出来或者设计出来会“带偏”。举一个具体的例子。假设机械臂要抓一个杯子你用“末端到杯子的距离”做负奖励agent很快就学会了伸过去、碰到杯子但就是不去抓——因为碰到杯子的距离已经很小再往下可能就是0奖励甚至负奖励它停在“接近”这一步就能刷高分。如果你给“抓取成功”单独加一个非常大的奖励又等于重新引入了稀疏性。所以reward shaping本质上是一门玄学需要你对任务的每一步都有精确判断这在复杂场景下几乎不可能完成。还有一个更隐蔽的问题稠密奖励会让agent学会“刷分”。我见过一个导航实验agent为了拿到每步的“前进奖励”在原地转圈因为转圈不会导致死亡还能一直获取小数值奖励。设计者只看到曲线往上走完全没发现行为已经完全畸形。这也是为什么很多研究者宁愿用稀疏奖励——它虽然难学但至少奖励信号是诚实的不会诱导agent走歪路。1.3 从“事后视角”重新定义目标既然稀疏奖励难以直接学习硬啃不行那就换个思路能不能把“失败”变成“成功”人类在这方面的能力很强。你考试没考好但复盘的时候会说“虽然总分不高但我把第一道大题做出来了第二道题前半部分思路也对”。这些局部的“成功”虽然不是原始目标但确实积累了经验下一次就能用上。hindsight想法的本质就是让agent也学会这种“事后总结”把没达到目标的失败轨迹重新解释成到达了其他状态的成功轨迹。HER的具体做法非常巧妙一条轨迹结束后反正你已经知道agent走到了哪些状态那就挑其中一个状态作为“替代目标”然后把原本全是0的奖励重新计算一遍。这样一条原本毫无学习价值的稀疏轨迹瞬间变成了很多条有稠密奖励信号的学习样本。agent在这个被“改写”的轨迹里学到的不是“如何到达原始目标g”而是“如何从当前状态到达替代目标g”。别小看这个改变它把整个学习难度从“一步到位解决问题”降到了“逐步学会状态空间的路径”而后者才是agent真正需要掌握的东西。2. HER的核心机制目标重标注的四种策略2.1 目标重标注到底改了什么先看原始数据。假设agent在目标g下跑了一条轨迹其中在时间步t有一条转移数据(s_t, a_t, r_t, s_{t1}, g)如果任务没完成那么r_t0。HER做了一件非常简单的事在这条轨迹中再选一个状态把它当作新的目标g然后重新计算奖励(s_t, a_t, r_t, s_{t1}, g)其中r_t reward(s_{t1}, g)。如果s_{t1}恰好就是g那么r_t就是正奖励。这样一来原来那些奖励全0的数据现在变成了围绕g的稠密信号。比如Fetch机器人推箱子原本目标位置在坐标(1.2, 0.6, 0.3)实际轨迹最后停在(0.8, 0.5, 0.2)那HER就把(0.8, 0.5, 0.2)定为g重算这条轨迹中每一时刻到g的距离奖励整个样本池瞬间活了过来。注意一个关键点HER不是把原始经验替换掉而是在原始经验之外额外生成一批新的transition。原始目标g的经验仍然保留因为agent还需要学会区分“任务真正要什么”和“替代目标是什么”。两者必须共存缺一不可。我在早期实现时就犯过错误为了省空间直接把原始数据覆盖掉结果agent学会了满地图跑但对指定目标毫无概念。还有一个容易忽略的点HER必须配合off-policy算法使用。原因很好理解——它改写的是已经发生过的经验而在线策略算法一般不用经验回放数据用完就扔根本没机会重新标注。所以HER通常搭配DDPG、TD3、SAC这类actor-critic结构它们天然依赖replay buffer加一层目标重标注只是水到渠成。2.2 四种采样策略对比目标重标注最关键的问题来了替代目标g从哪来论文里系统比较了四种策略final、future、episode和random。final策略最朴素把一条轨迹的最终状态作为g。比如机械臂推箱子轨迹结束时的箱子位置就是替代目标。这个策略没啥成本一个轨迹一份数据但问题也很明显如果整条轨迹都离目标远远的那最终状态作为目标也没什么意义而且中途那些“短暂接近某个位置”的宝贵经验全都扔了。future策略是论文里的默认选择也是我实际用下来效果最好的方案。它的规则是对于时间步t的transition从同一轨迹的时间步t1到T之间随机取K个状态作为替代目标。为什么这样做有效因为t1时刻的状态和t时刻的动作直接相关你在t时刻做的动作影响了后续到达的位置这个时间相关性让“从当前状态到未来状态”的监督信号非常自然。agent很容易从这种数据里学到动作和状态转移之间的因果关系毕竟因果关系的另一头就是它自己造成的。episode策略和random策略相对粗糙。episode策略是从整条轨迹的任意状态里随机取目标不考虑时间先后random策略是从全局replay buffer里随机取一个见过的状态作为目标。两者都能提供多样性但学习效率明显不如future。尤其random策略目标可能跟当前轨迹毫无关系agent学到的是“从A状态到B状态”的随机映射噪声太大。我把这四种策略的对比整理成了一张表策略目标来源优点缺点推荐程度final轨迹最终状态简单直接无需额外采样忽略中途接近目标的好经验一般future当前时刻之后的轨迹状态时间相关性强学习效率最高实现稍复杂需要整条轨迹强烈推荐episode整条轨迹任意状态多样性好部分样本时间相关性弱较推荐random全局buffer任意状态覆盖范围广噪声大学习效率低不推荐单独用2.3 HER比例与奖励再计算实操选型目标采样策略确定之后还有一个比例问题一批训练数据里原始目标和HER替代目标各占多少论文默认做法是80%的数据用HER重标注20%保留原始目标。这个比例我建议不要轻易改。HER比例太低稀疏奖励的学习信号还是不够比例太高agent会沉迷于“到达随便一个地方”的目标丢失任务的方向感。我自己做过一组对比实验HER比例从1.0降到0.5时成功率掉了将近20个百分点原因就是原始样本太少agent对真正目标的位置产生了遗忘。奖励函数的再计算是另一个需要小心的点。最常用的做法是用负距离reward -||achieved_goal - desired_goal||2。比如机械臂末端位置是achieved_goal替代目标是desired_goal两者距离越近奖励越高。这里有个尺度问题如果环境里成功判据是距离0.05那负距离奖励的最大值也就是-0.0几数值非常小网络梯度会很弱。我自己习惯把奖励乘一个系数比如10倍或100倍让信号在一个合理的量级。具体系数跟环境状态范围有关要观察s{t1}和g之间的距离分布再定不能拍脑袋。如果任务是离散目标比如“打开红灯”和“打开绿灯”距离函数就不太适用了可以改成匹配则0、不匹配则-1。这种情况下HER的收益会比连续控制小一些因为离散目标空间本身有限重标注的多样性也有限。但如果动作空间还是连续的HER依然能发挥不错的效果。3. 手写一个HER训练流程PyTorch风格3.1 一个能存轨迹的ReplayBuffer怎么设计HER实现中最大的变化在replay buffer普通buffer存的是单条transitionHER需要临时保存整条episode等episode结束再做目标重标注。所以buffer内部至少要维护两块数据一块是暂存当前episode的临时数组另一块是存储最终transition的大缓冲池。我给出的实现经验是一个HindsightReplayBuffer类它的接口有三个push_transition把单步数据推进来end_episode在episode结束时做重标注并写入主缓冲池sample做随机采样。这里最容易被忽略的是push_transition阶段不仅要存obs、action、reward、next_obs还要存当前episode的goal因为重标注的时候需要知道原始goal和替代goal才能算新奖励。还有一个容量问题HER会同时写入原始和替代transition数据量是普通buffer的1HER比例倍。如果单条transition占0.5KB100万条就是500MB级别内存压力和磁盘都要考虑。我的做法是只保留最近N条transition并且定期检查buffer里原始样本的比例避免替代目标样本过度占据空间。3.2 目标重标注与训练循环的核心实现直接上一个我日常使用的最小实现基于Python和NumPy训练框架用PyTorch核心算法用TD3或SAC都行import numpy as np import random class HindsightReplayBuffer: def __init__(self, capacity, her_ratio0.8, future_k4): self.capacity capacity self.her_ratio her_ratio self.future_k future_k self.buffer [] self.episode_transitions [] staticmethod def compute_reward(achieved_goal, desired_goal): return -np.linalg.norm(achieved_goal - desired_goal, axis-1) def push_transition(self, transition): self.episode_transitions.append(transition) def end_episode(self): episode self.episode_transitions self.episode_transitions [] horizon len(episode) for t, (obs, act, _, next_obs, goal) in enumerate(episode): ach_goal next_obs.copy() # 保留原始transition reward self.compute_reward(ach_goal, goal) self._add(obs, act, reward, next_obs, goal) # HER重标注部分 if random.random() self.her_ratio: future_idx np.arange(t 1, horizon) if len(future_idx) 0: continue sampled_idx np.random.choice( future_idx, sizemin(self.future_k, len(future_idx)), replaceFalse, ) for g_idx in sampled_idx: her_goal episode[g_idx][3].copy() her_reward self.compute_reward(ach_goal, her_goal) self._add(obs, act, her_reward, next_obs, her_goal) def _add(self, obs, act, reward, next_obs, goal): if len(self.buffer) self.capacity: self.buffer.pop(0) self.buffer.append((obs, act, reward, next_obs, goal)) def sample(self, batch_size): batch random.sample(self.buffer, batch_size) obs np.stack([x[0] for x in batch]) act np.stack([x[1] for x in batch]) rew np.stack([x[2] for x in batch]) next_obs np.stack([x[3] for x in batch]) goal np.stack([x[4] for x in batch]) return obs, act, rew, next_obs, goal训练循环的思路很清晰先跑一个episode把所有transition临时存起来episode结束后调用end_episode做重标注然后从中采样更新策略网络buffer HindsightReplayBuffer(capacity500000) for episode in range(max_episodes): obs env.reset() goal env.goal done False while not done: action policy.select_action(obs, goal) # 加噪声探索 next_obs, reward, done, info env.step(action) buffer.push_transition((obs, action, reward, next_obs, goal)) obs next_obs buffer.end_episode() for _ in range(update_rounds): batch buffer.sample(batch_size) td3.update(batch) # 或者其他off-policy算法这个实现里有两个细节值得注意。第一episode里的reward在push_transition阶段并没有真正使用真正用于学习的reward是在end_episode里重新计算的。不要担心这样会丢失原始奖励信息因为原始目标那条transition会单独保留原始奖励也一起存了进去。第二future采样时时间步t1到T之间如果没有可选状态比如t是最后一步直接跳过HER即可这一条transition不至于把整个训练拖垮。3.3 实测表现以FetchReach为例我在本机用Gymnasium Robotics套件的FetchReach-v1做过一组测试。这个任务的目标是让7自由度机械臂末端到达一个随机给定的三维坐标点状态空间里包含末端位置和目标位置。算法用TD3网络结构是两个三层MLP隐藏层256HER比例0.8future的K取4每收集一条episode更新40个batch。实验结果显示前500个episode成功率几乎在0附近徘徊这是正常的因为replay buffer里还都是随机探索数据目标重标注虽然有效但网络还在初步拟合。到了1000到2000个episode成功率开始明显抬头能到30%左右。4000到5000个episode之后基本稳定在85%到95%之间偶尔会掉下来几个百分点但整体曲线是向上的。如果把HER关掉同一套超参跑到5000个episode成功率还不到5%。差距就是这么直接。我还做过一组对比把future换成final策略同等条件下大概2500个episode才开始抬头最终稳定成功率在70%左右。这说明future的“多目标、强时间相关”确实带来了额外收益。另一个尝试是把HER比例从0.8改成0.5成功率从90%掉到了70%。所以在条件允许的情况下直接用论文的默认配置是比较稳妥的。4. 常见问题与排障经验实录4.1 训练不收敛的三种典型症状我周围朋友用HER的时候最常碰到的问题就是训练曲线全程贴地。这时候第一反应不是调学习率而是先确认HER到底生效没有。我见过一个案例代码里忘了在episode结束调用end_episode结果buffer里全是原始稀疏transitionagent跟没加HER一模一样。检查方法很简单在end_episode里打印一下buffer里的数据量如果每个episode过后buffer只增加了horizon条说明重标注没跑起来正常应该增加(1her_ratio*k)乘以horizon左右的数据量。第二种症状是成功率曲线忽上忽下波动幅度特别大。这个问题大概率出在奖励scale上。前面说过负距离奖励的数值很小如果网络没有配合好尺度比如输出层是tanh且范围是[-1,1]而奖励数量级是0.01那么critic网络很难区分不同动作的好坏。我的建议是先做一次奖励值分布的统计跑50个随机episode把所有负距离奖励收集起来看中位数和方差然后乘一个系数把均值拉到0.1到1之间。没有这一步后面调什么都会很费劲。第三种症状是前期有起色但中期戛然而止。这种情况常见于buffer容量太小导致旧经验被冲掉或者future的K值太小导致目标多样性不足。HER的核心在于“不断产生替代目标”如果buffer只存最近几万条那些早期学到的目标分布很快就被新数据覆盖agent就会表现出阶段性的遗忘。把buffer容量调到50万到100万K值调到4通常能缓解。4.2 目标采样和奖励设计的隐性坑未来状态采样的一个容易写错的点是索引。如果你在实现里用了replaceTrue可能重复采样到同一个目标导致K个目标里出现大量重复多样性下降。另一个细节future采样必须严格从t1开始如果把当前状态采样进来那目标就是当前状态本身奖励一定是正最大agent学到的全是“保持不动”短期看曲线很漂亮长期看策略彻底废掉。还有goal表示的问题。同样一个目标用绝对坐标还是相对坐标效果差很多。我的经验是优先用相对坐标把目标表示成“相对于agent当前状态的位置差”。原因是不同episode的绝对位置差异可能很大网络要花更多容量去拟合位置分布相对坐标天然与动作输出对齐学习更容易。但要注意环境返回的goal往往是一个绝对值数组需要自己在代码里做一次坐标系转换而不是直接塞给网络。还有reward函数和成功判据的匹配问题。很多环境里的success条件是距离小于某个阈值但HER里用的是负距离。如果你对阈值没有概念可能出现一种情况负距离奖励已经很接近0了但还没达到阈值网络已经满足了agent就停在目标附近不再动作。这种问题在训练曲线上很难看成功率曲线涨到某个平台就上不去。解决办法是在奖励里加一个稀疏成功奖励项比如距离小于阈值给0否则给负距离这样既保持稠密又有明确成功信号。4.3 调参决策速查表我把实际排障过程中总结的检查项整理成一张表按照优先级从上往下查大部分问题都能在一两个小时内定位症状可能原因检查方式解决方案成功率始终为0重标注未生效打印buffer数据量是否成倍增长检查end_episode是否被调用成功率始终为0原始目标比例过低检查采样中原始transition比例恢复HER比例到0.8左右曲线波动剧烈奖励尺度不合适统计负距离奖励分布乘系数把均值拉到0.1以上中后期停滞future采样K太小打印采样目标去重率把K设为4到8中后期停滞buffer容量不足观察旧transition是否被频繁覆盖扩大buffer到50万以上行为异常采到了当前状态做目标检查future索引范围确保从t1开始采样收敛但成功率不高绝对坐标未转相对坐标查看goal与obs的分布范围转为相对坐标表示5. 把hindsight思想带走5.1 从HER到后续算法的思路延伸HER本身已经能解决不少稀疏奖励问题但研究者并没有停留在原地。后来的CHERCurriculum-guided HER就是进一步探索“替代目标的选择顺序”它会优先选择那些当前策略能力范围内能到达的目标然后逐步提高难度相当于给HER加了一个课程表。GoalGAN则是学习一个生成网络专门产生难易适中、有学习价值的替代目标。这些工作本质上都在回答同一个问题什么样的“后见之明”对学习帮助最大如果你现在正踩在稀疏奖励的坑里我的建议是先跑通标准HER把成功率和训练稳定性做上去再去尝试这些变体。标准HER的代码量不大但调试过程中的那些小坑会让你对on-policy和off-policy的理解深很多这也是一种实在的收获。5.2 用“结果重标注”做项目复盘我有时候会觉得HER这个思想不只在强化学习里有价值。做项目的时候你设了一个季度目标但最终的结果可能完全偏离了预期。大多数人会把它当成一次失败写一页复盘然后继续焦虑。但如果把HER的视角搬过来这次“偏离”其实是一次真实发生过的轨迹你说不定在过程中学到了新的技术、积累了人脉、试出了某个方向不可行。把这些实际结果当作新的目标重新评估这段时间的价值你会发现自己并没有白干而是收获了一系列可复用的“子目标经验”。具体操作方法也很简单跟算法一一对应。先写清楚原始目标然后列出实际发生的所有结果再把这些结果当作替代目标逐个问自己“为什么能到达这里”、“中间哪一步起了作用”。最后挑出两三个可以迁移到下一个项目的技能点把它们变成下一步的具体任务。这个方法我从HER论文里受到启发已经用了好几年每次做技术汇报准备材料时都会翻一翻当时的记录。我个人在实际跑HER时体会最深的一件事是不要一上来就怀疑网络结构或算法实现先确认你的replay buffer是否真的产生了足够多高质量的目标重标注样本。数据质量永远排在模型复杂度前面。另外再分享一个小技巧每次调参前固定随机种子看前500个episode的success rate趋势。如果它完全没有向上走的迹象别急着加大网络先用一个最简单的二维连续控制环境把HER流程跑通确认buffer、奖励、采样三块逻辑都没有问题再回过头来面对复杂环境。算法领域里永远不缺新技巧但把最基础的数据流捋清楚才是解决一切问题的前提。