我第一次跑稀疏奖励的机器人控制任务时,两千个episode跑完,成功率还是0。不是网络结构有问题,也不是学习率没调好——agent完全不知道自己在干嘛,因为整个环境里能拿到非零奖励的次数一只手数得过来。后来我把目光转向一个名字非常诚实的算法:hindsight,中文可以叫“后见之明”,对应的论文全称是Hindsight Experience Replay(HER)。这个算法本质就一句话:既然这次没达到原始目标,那就把这段经历重新解释成“已经到达过某个地方”的经验,照样拿来学习。
如果你正在做机械臂抓取、机器人导航、游戏AI这类连续控制任务,又恰好被稀疏奖励折磨得怀疑人生,这篇文章就是写给你的。我会从问题背景讲起,把HER的原理、四种目标采样策略、代码实现和调参经验全部拆开,最后还会分享一些我实际踩过的坑,保证读完能直接上手复现。
1. 稀疏奖励为什么能把强化学习逼到墙角
1.1 用一个丢球例子理解稀疏奖励
想象你蒙着眼睛站在操场上,手里拿一个网球,目标是把球丢进三米外的一个小桶里。你的动作是随机的,右手角度、力度、出手时机差了分毫,球就飞偏了。请问你丢多少次能进一次?运气好的话几百次,运气差可能一整天都不进。强化学习里的agent面临的是同样的问题,只不过它比人更“笨”:它没有任何先验知识,所有动作都是从一个随机初始化策略里采样出来的。
在稀疏奖励任务里,环境只在“完成任务”时给+1奖励,其他所有时刻都是0。比如Fetch机器人推箱子,目标是把箱子推到指定位置,网络输出的是一串7维连续动作。如果随机探索一万个episode,可能只有几次能碰巧靠近目标,绝大多数轨迹的奖励序列长得一模一样:全是0。这意味着什么?意味着反向传播的时候,梯度信号几乎是空的,那个唯一的+1奖励就像大海里的一根针,对网络参数更新的贡献微乎其微。
我用一段公式来说明:一条轨迹长度为T,奖励是r_t = 1(s_{t+1} == g)。在T=50、动作空间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_{t+1}, g)
如果任务没完成,那么r_t=0。HER做了一件非常简单的事:在这条轨迹中再选一个状态,把它当作新的目标g',然后重新计算奖励:
(s_t, a_t, r_t', s_{t+1}, g')
其中r_t' = reward(s_{t+1}, g')。如果s_{t+1}恰好就是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,从同一轨迹的时间步t+1到T之间随机取K个状态作为替代目标。为什么这样做有效?因为t+1时刻的状态和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{t+1}和g'之间的距离分布再定,不能拍脑袋。
如果任务是离散目标,比如“打开红灯”和“打开绿灯”,距离函数就不太适用了,可以改成匹配则0、不匹配则-1。这种情况下HER的收益会比连续控制小一些,因为离散目标空间本身有限,重标注的多样性也有限。但如果动作空间还是连续的,HER依然能发挥不错的效果。
3. 手写一个HER训练流程(PyTorch风格)
3.1 一个能存轨迹的ReplayBuffer怎么设计
HER实现中最大的变化在replay buffer:普通buffer存的是单条transition,HER需要临时保存整条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的1+HER比例倍。如果单条transition占0.5KB,100万条就是500MB级别,内存压力和磁盘都要考虑。我的做法是只保留最近N条transition,并且定期检查buffer里原始样本的比例,避免替代目标样本过度占据空间。
3.2 目标重标注与训练循环的核心实现
直接上一个我日常使用的最小实现,基于Python和NumPy,训练框架用PyTorch,核心算法用TD3或SAC都行:
import numpy as np import random class HindsightReplayBuffer: def __init__(self, capacity, her_ratio=0.8, future_k=4): 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, size=min(self.future_k, len(future_idx)), replace=False, ) 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(capacity=500000) 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采样时时间步t+1到T之间如果没有可选状态,比如t是最后一步,直接跳过HER即可,这一条transition不至于把整个训练拖垮。
3.3 实测表现:以FetchReach为例
我在本机用Gymnasium Robotics套件的FetchReach-v1做过一组测试。这个任务的目标是让7自由度机械臂末端到达一个随机给定的三维坐标点,状态空间里包含末端位置和目标位置。算法用TD3,网络结构是两个三层MLP,隐藏层256,HER比例0.8,future的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里全是原始稀疏transition,agent跟没加HER一模一样。检查方法很简单:在end_episode里打印一下buffer里的数据量,如果每个episode过后buffer只增加了horizon条,说明重标注没跑起来;正常应该增加(1+her_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 目标采样和奖励设计的隐性坑
未来状态采样的一个容易写错的点是索引。如果你在实现里用了replace=True,可能重复采样到同一个目标,导致K个目标里出现大量重复,多样性下降。另一个细节:future采样必须严格从t+1开始,如果把当前状态采样进来,那目标就是当前状态本身,奖励一定是正最大,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索引范围 | 确保从t+1开始采样 |
| 收敛但成功率不高 | 绝对坐标未转相对坐标 | 查看goal与obs的分布范围 | 转为相对坐标表示 |
5. 把hindsight思想带走
5.1 从HER到后续算法的思路延伸
HER本身已经能解决不少稀疏奖励问题,但研究者并没有停留在原地。后来的CHER(Curriculum-guided HER)就是进一步探索“替代目标的选择顺序”,它会优先选择那些当前策略能力范围内能到达的目标,然后逐步提高难度,相当于给HER加了一个课程表。GoalGAN则是学习一个生成网络,专门产生难易适中、有学习价值的替代目标。这些工作本质上都在回答同一个问题:什么样的“后见之明”对学习帮助最大?
如果你现在正踩在稀疏奖励的坑里,我的建议是先跑通标准HER,把成功率和训练稳定性做上去,再去尝试这些变体。标准HER的代码量不大,但调试过程中的那些小坑会让你对on-policy和off-policy的理解深很多,这也是一种实在的收获。
5.2 用“结果重标注”做项目复盘
我有时候会觉得,HER这个思想不只在强化学习里有价值。做项目的时候,你设了一个季度目标,但最终的结果可能完全偏离了预期。大多数人会把它当成一次失败,写一页复盘然后继续焦虑。但如果把HER的视角搬过来,这次“偏离”其实是一次真实发生过的轨迹,你说不定在过程中学到了新的技术、积累了人脉、试出了某个方向不可行。把这些实际结果当作新的目标,重新评估这段时间的价值,你会发现自己并没有白干,而是收获了一系列可复用的“子目标经验”。
具体操作方法也很简单,跟算法一一对应。先写清楚原始目标,然后列出实际发生的所有结果,再把这些结果当作替代目标,逐个问自己“为什么能到达这里”、“中间哪一步起了作用”。最后挑出两三个可以迁移到下一个项目的技能点,把它们变成下一步的具体任务。这个方法我从HER论文里受到启发,已经用了好几年,每次做技术汇报准备材料时都会翻一翻当时的记录。
我个人在实际跑HER时体会最深的一件事是:不要一上来就怀疑网络结构或算法实现,先确认你的replay buffer是否真的产生了足够多高质量的目标重标注样本。数据质量永远排在模型复杂度前面。另外再分享一个小技巧:每次调参前固定随机种子,看前500个episode的success rate趋势。如果它完全没有向上走的迹象,别急着加大网络,先用一个最简单的二维连续控制环境把HER流程跑通,确认buffer、奖励、采样三块逻辑都没有问题,再回过头来面对复杂环境。算法领域里永远不缺新技巧,但把最基础的数据流捋清楚,才是解决一切问题的前提。