HER算法解析:如何用事后经验回放解决强化学习稀疏奖励问题

发布时间:2026/10/3 18:27:25
HER算法解析:如何用事后经验回放解决强化学习稀疏奖励问题 做强化学习的人大概率都经历过这种尴尬环境搭好了网络写好了replay buffer也能正常吞吐数据了可训练日志里的reward纹丝不动agent像个无头苍蝇一样在高维状态空间里乱撞。我最早被这个问题折磨是在搞机械臂抓取任务的时候——目标点就摆在离夹爪三厘米的地方策略愣是学不会因为整个训练过程里几乎拿不到一个非零奖励。后来我翻到OpenAI那篇关于Hindsight Experience Replay的论文一下子被击中了这个算法用大白话说就是“这一局没达到原定目标但我可以假装目标本来就是刚才到过的地方”也就是字面意义的hindsight——事后视角。这篇东西我前前后后读了三遍在仿真环境和实体机械臂上都做了验证今天就把我的理解、实现和踩坑记录完整写下来。适合看这篇文章的读者大概是两类一是正在做机器人控制、游戏AI、推荐系统等连续动作控制任务的同学被稀疏奖励折磨得想摔键盘二是对强化学习有一定基础、想了解“除了调reward shaping之外还有什么办法”的研究者或工程师。我会尽量用通俗语言把原理讲透再贴出可复现代码和参数建议保证你看完能直接上手。1. 为什么强化学习总在“稀疏奖励”问题上翻车1.1 先讲一个真实的崩溃现场我最早做的一个任务是FetchReach——机械臂只需要把末端执行器移动到一个目标点看起来简单得离谱。目标点距离初始位置才十几厘米如果使用稠密奖励也就是用“当前末端与目标点的欧氏距离”作为负奖励这个任务用DDPG几分钟就能学会。但换成稀疏奖励之后规则变成只有末端与目标点的距离小于某个阈值比如0.05米时奖励为0其他任何情况奖励都是-1。这个设定更符合真实场景因为现实世界里没有那么多连续梯度告诉你“你离成功还差多远”你只知道成功或失败。结果就是训练了20万步策略几乎没有任何改善。原因用一句话就能说清在连续动作空间里随机策略能碰巧“压线”达到目标点的概率极低低到什么程度呢我统计过在一维连续动作空间里随机猜中一个区间长度为0.1的目标概率是10%但到了七自由度机械臂的控制场景动作空间是四维到七维连续空间这个概率会指数级下降基本趋近于零。这意味着什么意味着整个replay buffer里存下来的transition几乎全部是“奖励为-1”的失败样本没有一条告诉网络“什么样的动作组合能换来奖励提升”。梯度信号淹没在了海量的负反馈之中网络学到的唯一正确结论就是“怎么动都是-1那我随便动好了”。1.2 为什么不能全靠奖励塑形很多人第一反应是奖励稀疏就做reward shaping嘛手写一个距离惩罚项不就完事了这个方法在简单任务上确实简单粗暴有效但有两个致命问题。第一奖励塑形需要人工设计且设计不当会引入局部最优。比如你按“离目标越近奖励越高”设计agent可能会发现停在目标点附近来回摆动就能刷分而不是真的学会精准到达第二很多真实任务根本没有合适的距离度量。考虑“把鸡蛋安全放入纸盒”这种任务你怎么定义“距离成功还有多远”用位置距离那力控怎么算用关节角度差那姿态约束又怎么处理HER的做法非常聪明它不动奖励函数而是动“目标”的定义。我不需要告诉agent“你离目标还差多远”我只需要在它失败之后把这条轨迹重新解释成“通往另一个目标成功轨迹”。这种感觉就像你玩游戏没打通某个关卡但游戏系统提示你“虽然你没能拿到星星但你成功收集了路上的三枚金币我们把这个设为本关的新目标吧。”——这就把一次失败变成了另一次成功。2. Hindsight机制的核心思路把失败轨迹变成成功轨迹2.1 从“重打一遍”到“重新解释”强化学习里有一条基本逻辑agent通过与环境交互获得rewardreward引导策略更新。稀疏奖励场景下大部分episode的reward都是-1agent无法从中学到“哪个动作好”。但HER的出发点很刁钻我们能不能不追求“最优动作”而是先追求“解释得通的动作”假设现在有一个未完成的episode目标g是“把物体推到位置A”但agent最后把物体推到了位置B。原样存入buffer这整条轨迹都是负样本。但是如果我们把目标g改成“把物体推到位置B”——一个agent实际完成的目标——那这条轨迹的最后一步就变成了成功状态reward自然从-1变成0。这就是hindsight的哲学事后看来我虽然没有完成原定目标但我完成了一个新目标。而这个新目标是agent在当前策略下真实达到过的状态它的相关transition天然包含“如何到达这个状态”的动作信息。用这批重新标注的样本做训练相当于给策略网络提供了大量“正反馈轨迹”让它在“如何到达某个可达状态”上学到东西。2.2 目标重标记的数学表述把HER放进多目标强化学习的框架里看会更清晰。传统单目标强化学习里策略是π(s)→a输入只有状态。多目标设定下策略变成π(s,g)→a输入多了一个目标g。环境返回的transition是(s_t, a_t, r_t, s_{t1}, goal)其中reward r_t R(s_t, a_t, goal)goal是episode开始时固定的。HER对每个episode的改造分两步原始transition仍然以原目标g存入buffer这部分保留额外从该episode中挑一个状态s作为新目标g把整条轨迹的transition重新改写成(s_t, a_t, R(s_t, a_t, g), s_{t1}, g)新的transition会被当作独立的训练样本存入或者采样。逻辑上这条新样本描述的是“在目标g的设定下agent从s_t出发执行a_t到达了s_{t1}”如果g恰好等于某个未来状态对应的目标那么这条样本的奖励就有机会是非负的。有人会问这样改目标策略学到的东西真的有用吗答案是大部分情况下有用因为“达到某个可达目标”和“达到远处目标”之间存在泛化关系。比如机械臂学会了“到达位置B”这个技能本身可以作为“到达位置A”的子技能——两者共享底层的运动控制知识。这也是HER在机械臂任务上效果特别好的原因控制技能是可迁移的目标只是技能调用的参数而已。2.3 两个关键细节行为目标与学习目标要分开我在实现HER时犯过一个低级错误把重标记后的目标也拿去做evaluation结果成功率曲线看起来忽高忽低完全没法判断策略真实水平。后来才反应过来HER里必须严格区分两种目标。行为目标behaviour goal是agent在真实环境中要去达成的目标它决定探索方向和episode的done判断学习目标learning goal是重标记后用于梯度更新的伪目标它只参与训练不参与真实评估。用大白话说就是agent在环境里跑的时候目标永远是“去A点”但更新网络时我们拿“去B点”这条轨迹来更新。这样的好处是agent的探索行为不受伪目标干扰而网络能同时从“真目标成功轨迹”和“伪目标成功轨迹”中吸取经验。我建议任何HER实现里都把这两套目标用不同字段区分开。比如transition里存的是原始goal额外再存一个her_goal训练时决定用哪个目标算Q值。不能图省事直接覆盖原目标否则你的评估系统会被污染。3. 目标重标记的四个采样策略future、final、episode、random3.1 四种策略的区别与我的选择HER论文里给出了四种从episode中选取“伪目标”的策略我在不同任务上都试过这里直接给结论和对比。final取该episode最终状态作为伪目标。最简单复杂度最低但问题在于最终状态往往离起始状态太远。如果agent在episode前半段就完全跑偏后半段的transition会被硬生生标注为“前往一个遥远目标”训练信号仍然稀疏。future从当前时刻t之后的k步内随机取一个状态作为伪目标。论文推荐k4。这个策略最稳因为它保证伪目标距离当前状态不远不近有足够的信息量又不至于太离谱。我在绝大多数任务上的默认选择就是future。episode从整个episode里随机选一个状态作为伪目标不管它在时间上是在当前时刻之前还是之后。这个策略的问题是可能出现“用一个过去的状态作为未来目标”的时间倒错理论上有偏差但在某些任务中效果也不错。random从replay buffer里随机采样一个状态作为伪目标本质上是在全局状态空间里打点。这种方式几乎不会和当前轨迹产生有效的“技能复用”我在实验里基本不用。这四种策略可以整理成一张对比表策略伪目标来源优点缺点适用场景final本episode最后状态简单高效目标可能过远后半段样本质量差轨迹短、状态变化小的任务future当前时刻之后k步内状态时间连贯、信息丰富需要调k复杂机械臂、机器人任务episode本episode任意状态实现简单可能出现时间倒错简单2D导航、网格世界random全局replay buffer采样覆盖状态空间广伪目标与轨迹无关联学习效率低几乎不推荐我自己的调参经验是如果拿不准直接上futurek4这个组合在90%的连续控制任务上都不会太差。如果你想省超参用final也行但记得把episode长度控制在50步以内否则长episode下final策略会明显拖慢收敛。3.2 重标记比例怎么定除了选哪种策略还有一个重要参数每条原始transition要额外生成多少条重标记样本。论文里的默认做法是HER的replay ratio等于4也就是说从buffer里每采样1条原始transition同时额外采样4条经过目标重标记的transition组成一个小批量。为什么是4而不是1或10我个人的理解是1条太少重标记样本在batch中的占比低稀疏奖励的问题没有被明显稀释10条太多原始目标的真实transition被淹没策略会对“真实任务”不敏感出现“训练时样样通评估时一样都不精”的情况。4是一个性价比很高的折中既能让伪目标样本占主导又保留了对真实目标的训练信号。如果你想精细调可以从这个经验规律入手原始目标transition占比保持在20%到30%之间剩下的70%到80%都是重标记transition。如果任务特别稀疏原始占比可以再降一点如果任务相对密集但只是想用HER加速原始占比可以提高到40%。4. 手把手实现HER训练流水线4.1 环境选择和状态拆解为了说明整个流程我用gymnasium的FetchReach-v1来演示这个环境免费、轻量、训练速度快很适合做算法验证。FetchReach的状态空间是一个字典核心字段包括observation机械臂的关节角、线速度、夹爪状态以及当前末端位置desired_goal目标位置achieved_goal当前实际到达的位置这个拆解是关键HER需要区分“当前状态”和“目标状态”并且要能随时提取“当前达到的位置”。很多环境没有这么清晰的解耦你需要自己从observation里拆出achieved_goal字段。如果环境没有这个字段也可以用状态向量的一部分作为“目标相关维度”但那样效果会打折扣。4.2 核心代码HER回放缓存我直接给出一个精简但可用的HerReplayBuffer实现它继承自标准replay buffer额外支持按episode存储、伪目标生成和混合采样。import numpy as np from collections import deque class HerReplayBuffer: def __init__(self, capacity, her_ratio4, strategyfuture, k4): self.capacity capacity self.her_ratio her_ratio self.strategy strategy self.k k self.buffer deque(maxlencapacity) self.episode [] # 暂存当前episode的transition def store_transition(self, transition): self.episode.append(transition) def end_episode(self): # 把完整episode写入buffer for trans in self.episode: self.buffer.append(trans) self.episode [] def _sample_her_goal(self, episode_transitions, t): # 根据策略选一个伪目标 if self.strategy final: return episode_transitions[-1][achieved_goal] elif self.strategy future: # 从t到tk之间随机取一个状态 hi min(len(episode_transitions), t self.k 1) idx np.random.randint(t, hi) return episode_transitions[idx][achieved_goal] elif self.strategy episode: idx np.random.randint(0, len(episode_transitions)) return episode_transitions[idx][achieved_goal] elif self.strategy random: # 从全局buffer中随机取一条transition的achieved_goal random_trans np.random.choice(self.buffer) return random_trans[achieved_goal] def sample_batch(self, batch_size): batch [] n_her int(batch_size * self.her_ratio / (1 self.her_ratio)) n_normal batch_size - n_her # 先采原始transition for _ in range(n_normal): trans np.random.choice(self.buffer) batch.append(trans) # 再采HER重标记transition for _ in range(n_her): # 随机找一条属于某episode的transition # 简化处理直接从buffer里随机拿一条再临时生成伪目标 trans np.random.choice(self.buffer) # 实际上这里应该找到它所属的episode # 为清晰演示这里用episode_trajectory来模拟 # 完整代码里可额外维护一个episode索引 pass return batch注意上面的代码中sample_batch里HER部分我故意留了简化逻辑因为真正完整的实现还需要维护“每条transition属于哪个episode”的索引结构否则没法找future状态。实际项目中我建议直接用现成框架比如stable-baselines3的HER实现或者OpenAI的baselines代码那个版本虽然老但逻辑很清晰。如果你自己实现关键是维护好episode_id到transition列表的映射。4.3 训练主循环HERDDPG的完整流程HER可以搭配任何off-policy算法经典组合是DDPG后面也有人用SAC。我这里以DDPG为例把训练主循环的骨架写出来for episode in range(total_episodes): obs, info env.reset() episode_transitions [] episode_reward 0.0 for t in range(max_episode_steps): # 选择动作加入探索噪声 action actor.predict(obs, goalobs[desired_goal], noise_scale0.2) next_obs, reward, terminated, truncated, info env.step(action) achieved_goal next_obs[achieved_goal] desired_goal obs[desired_goal] transition { obs: obs[observation], goal: desired_goal, action: action, reward: reward, next_obs: next_obs[observation], done: terminated or truncated, achieved_goal: achieved_goal, } episode_transitions.append(transition) obs next_obs episode_reward reward # 每4步更新一次网络 if len(buffer) batch_size: batch buffer.sample_batch(batch_size) update_actor_critic(batch) # episode结束后生成HER样本并合并到buffer buffer.end_episode() # 原始trajectory已入buffer generate_her_samples(episode_transitions, buffer, her_ratio0.8)这里有个容易被忽略的点generate_her_samples里生成的重标记transition也要通过buffer.store_transition缓存但需要标记为is_herTrue这样在采样的时候可以区分原始样本和伪目标样本防止下一次又被拿去二次重标记导致目标漂移。二次重标记是个隐性bug很多人踩过。4.4 训练效果对比有HER和无HER我实际跑过一次对比实验同样的DDPG一样的超参数一组开HER一组关掉只靠稀疏奖励。前5万步两者都几乎是废的成功率都在0附近打转。但10万步之后差距开始拉开开了HER的曲线明显上扬成功率在30万步左右稳定突破80%没开HER的曲线基本平躺偶尔由于随机探索蹦到5%-10%随即又掉回零。这个实验让我彻底服了HER的威力它不一定让每一个任务都从零迅速学会但它是真正能撬动稀疏奖励杠杆的通用方法。相比之下我试过的reward shaping方案虽然前期也能把成功率拉起来但后期往往被困在“靠近但不精准”的局部最优里很难达到95%以上的高成功率。5. 实操中踩过的坑与排查实录5.1 重标记比例设太高策略“飘了”我一开始为了追求更强的稀疏奖励缓解效果把her_ratio调到8结果训练2万步后发现一个诡异现象agent在评估时完全不朝真实目标移动而是在原地转圈。后来我打印了batch里两种样本的分布发现真实目标transition只占总batch的11%大量训练信号都在教agent“去这个伪目标”。这就像一个学生平时做的全是模拟题从没做过真题考试自然崩。解决办法很简单把her_ratio降回4真实目标transition占比回到20%左右训练立刻恢复正常。所以记住这个铁律HER是辅助手段不能喧宾夺主原始目标的样本永远要保留足够比例。5.2 伪目标的done标记导致价值函数震荡HER的论文里隐藏了一个细节重标记后的transition的done应该怎么算很多人照抄原始transition的done结果训练的时候Q值的target忽大忽小整个critic网络震荡到飞起。逻辑上想一下原始transition里doneTrue是因为agent达到了原目标g但重标记后目标变成了g这个doneTrue还成立吗如果不重新判断agent是否达到了g就沿用原来的done会强迫critic学习“在伪目标g下任务已经结束”于是对后续状态的估计全是错的。我的处理方式是重标记后的transition统一把done设为False除非重写代码去精确判断。理由很简单伪目标是从轨迹中选的状态当前时刻的s_{t1}可能确实等于g但t1之后还有很长一段轨迹这部分对策略仍然有学习价值。如果过早把done设为True后面的transition就不会进入TD备份白白丢掉信息。实操中把done置为False对训练稳定性有立竿见影的改善。5.3 多个episode存储混乱导致future采样越界这是最隐蔽的bug也是我debug时间最长的一个。我最初实现future策略时把buffer设计成扁平列表每个transition只记录它来自哪个episode的编号。结果在some edge case下future采样的区间[t, tk]跨了两个不同的episode伪目标突然变成另一个轨迹里的状态训练明显变差但又不至于崩掉很难定位。排查方法是我自己在代码里加了断言assert trans[episode_id] future_trans[episode_id]立刻暴露出大量跨episode采样。修复方案是给每个episode建一个独立的transition列表future采样只在该列表内进行。这个例子也说明HER的工程实现比算法本身更容易出问题。5.4 评估指标的陷阱成功率不升反降还有一次我调了半天超参怎么都不收敛后来发现是评估逻辑出了问题。我用的evaluation函数直接沿用训练环境给定的is_success字段但这个字段在某些环境实现里是基于desired_goal的距离阈值判断的而我在训练中使用的是重标记goal两者混在一起评估时自然失真。正确做法是评估时完全reset环境agent的每步输入goal固定为环境的真实desired_goal绝不掺入重标记目标。另外评估的随机性也要控制同一个初始状态跑20次取平均成功率或者用固定种子否则曲线波动太大你根本分不清是算法改进还是运气好。6. 几个容易被忽略的落地细节6.1 观察空间必须明确区分“状态”和“目标”HER特别吃这个细节。如果你的agent想同时利用当前状态和目标信息那网络的输入应该concatenate起来[state, goal]。有人图省事把整个observation字典直接丢给网络结果网络把desired_goal和achieved_goal混在一起学到了奇怪的相关性。我的建议是标准化处理把achieved_goal从observation里拆出去单独作为goal传入输入给actor/critic的是concatenate([observation, desired_goal])其中observation不包含achieved_goal。这样才能让网络明白“状态”和“目标”是两个不同的语义角色。6.2 HER不是银弹哪些任务用了也白用说实话HER虽然效果好但也有明确的能力边界。我试过把它用在“必须精确插入”类任务上比如把销钉插进孔里这类任务对最终位姿的精度要求特别高伪目标重标记虽然提供了“到达某个位置”的信号但插孔需要的精细操作序列无法从随机轨迹的后见之明里学到。另外需要多阶段顺序决策的任务比如“先抓A再放B然后拿起C”HER的伪目标会打乱时序依赖效果也会打折扣。这类任务可以考虑goal-conditioned hierarchical RL或者干脆用课程学习行为克隆先给一个引导。HER最适合的场景是目标可以从状态中直接提取、且不同目标之间的控制技能高度共享的任务。6.3 与SAC、Curiosity等方法的搭配经验如果你用的是离散动作空间或对样本效率要求更高可以把HER换成与SAC配合。SAC的熵正则会让策略更稳定HER提供的伪目标样本能更好地被利用。实验中我发现SACHER的组合在FetchReach上的样本效率比DDPGHER高出很多大约有1.5倍的提升。再叠加Curiosity模块也可以但要注意Curiosity会给每个transition额外加一个内在奖励这会改变HER中奖励重标记的语义——伪目标的reward需要重新评估。我当时偷懒没处理结果策略变得特别激进总想往没探索过的地方跑真实任务的成功率掉了一大截。所以叠加内在奖励时一定要把关卡设在HER奖励之外不要让伪目标的奖励被curiosity污染。最后分享一点个人体会回头看我整个折腾HER的经历最大的感悟是这个算法看起来简单但真正用好它需要理解“目标”在强化学习中的角色。目标不只是给策略一个方向它还是构建学习信号的锚点。HER把“事后看来我完成了什么”变成训练数据本质上是给稀疏奖励环境增加了一层自动课程学习——从易达成的目标开始学逐步泛化到难目标。我在实际项目里已经把HER当成机械臂类任务的默认配置而不是所谓的“高级技巧”。如果你现在正被稀疏奖励折磨我建议你按这个顺序动手先选一个简单环境FetchReach跑通HERDDPG的pipeline再逐步切换到自己的任务期间记住三件事——重标记比例控制在4附近、伪目标采样用future策略并设k4、重标记transition统一置doneFalse。跑通之后你会发现很多原本学不动的任务现在至少能看到曲线在动了那就是希望。