事后经验回放HER:破解稀疏奖励难题的强化学习利器
发布时间:2026/10/2 19:01:37来源:尧图网络
我第一次在稀疏奖励任务上跑强化学习是给一条机械臂做抓取与推动。断断续续调试了一周模型在仿真环境里原地划水成功率稳稳地趴在地上偶尔出一次成功样本都能高兴半天。后来一位前辈丢给我一句话试试hindsight把失败也当成经验。我以为是随手调侃结果这一试直接把训练曲线从死亡心电图救活了。Hindsight Experience ReplayHER中文通常译作事后经验回放是2017年提出的强化学习训练技巧专门用来对付稀疏奖励sparse reward这种练了几百次投篮、一次都没进过于是什么都没学到的经典困境。这篇文章我会从问题本质、算法拆解、代码实现、调参踩坑到应用边界完整梳理一遍适合正在被机器人控制、多目标强化学习或者任何稀疏奖励问题折磨的算法工程师、研究生和强化学习爱好者参考。1. 从怎么教机器人抓东西说起稀疏奖励到底难在哪1.1 为什么二值奖励让传统强化学习几乎停摆先还原一下典型的场景。假设机械臂的任务是推动一个滑块到指定位置环境中没有中间奖励只有一步到位如果滑块最终到达目标区域奖励为0表示成功否则奖励为-1表示失败。整套动作序列可能有50步动作维度是4维连续量状态空间里光是物体位置、末端位置、目标位置就占了十几维。传统强化学习在这种设定下最痛苦的地方在于全程没有梯度信号。哪怕智能体已经非常接近目标比如最后一步只差1毫米它收到的反馈依然是-1和从零开始乱撞收到的-1没有任何区别。策略梯度类算法里这批样本的advantage全是负的梯度方向被平均掉参数就只能原地打转。Q-learning类算法稍微好一点但价值函数面对几乎所有状态动作对都是-1的监督信号也很难学出有区分度的Q值。更麻烦的是探索是随机的。你可以想象在十维空间里随机撒点想要恰好命中一个直径0.05的小球区域概率低到可以忽略不计。传统课程学习curriculum learning的思路是人为设计一串从易到难的目标但设计课程本身就需要大量的领域知识而且环境一变、任务一换课程又得重来。HER切入的角度完全不一样它不解决怎么找到成功样本而是解决怎么让不成功的样本也能产生学习价值。1.2 HER的核心思想让每个失败都自带一份成功HER最让我拍案叫绝的地方是它把哲学问题变成了数学操作。我们习惯上认为智能体的目标是环境给的、固定的但HER明确区分了两个概念desired goal期望目标和achieved goal实际达到的目标。在一个episode结束之后机器臂虽然没有完成指定任务但它一定到达了某个真实存在的状态——比如滑块滑到了某个位置。HER做的事情很简单把这条轨迹里每一步的期望目标全部替换成轨迹最后实际到达的那个位置然后重新计算奖励再把这些新transition塞进replay buffer。这么一改原本失败的轨迹就变成了成功的轨迹因为新的期望目标恰好等于智能体实际到达的位置代价函数判断为成功奖励从-1变成了0。于是同一段物理轨迹在buffer里同时拥有了两个身份一个是想推滑块到A点但失败了另一个是想推滑块到B点实际达到的位置并成功了。为什么这有价值因为智能体通过这种方式不断看到某个动作序列在某个目标下是成功的即便这个目标是被事后编造的Q函数依然能从中学到目标空间里的泛化规律。换句话说HER不改变环境的奖励结构它改变的是记忆的标注方式让智能体从失败经验中提取出如果我当时的目标是这个我其实做对了的成分。1.3 这个方案适合谁来用如果你是刚接触强化学习不久、正在跑经典控制任务如MountainCar、Fetch系列HER是一个性价比极高的进阶技巧代码量不大理解门槛也不算高如果你在负责真实机器人、仿真机械臂、自动驾驶决策等稀疏奖励场景HER几乎是必须优先尝试的baseline如果你是做研究的HER还打开了一条思路目标本身可以成为可操纵的学习信号后来的多目标强化学习、目标条件的RLGoal-Conditioned RL很多工作都能和它关联上。需要提前说明的是HER不是银弹。它要求你的任务必须能定义出实际达到的目标并且这些目标可以作为状态的一部分被重新代入奖励函数计算。像围棋这种无法从盘面倒推合法目标的任务HER就完全用不上。这一点后面在扩展边界部分会专门展开。2. 目标重标记HER的核心机制与四种策略2.1 Off-Policy是先决条件为什么HER不能搭PPO在讲重标记细节前先给一个很重要的定性结论HER必须配合离线off-policy算法使用比如DQN、DDPG、SAC、TD3这类带经验回放的方法。PPO、A3C这类on-policy算法直接套HER是不行的因为重标记后生成的新经验不再来自当前策略的真实交互分布这违背了on-policy算法对样本必须服从当前策略的假设。我常用一个类比来解释这件事HER就像学生考完试后对答案发现自己虽然没做对但知道标准答案后豁然开朗。这个豁然开朗的时刻是事后发生的不影响独立解题的能力。强化学习里Q-learning本质上是事后更新它根据下一状态的估计值来回溯更新当前值并不严格依赖本策略产出的轨迹分布所以事后重标目标完全可行。而策略梯度要求对自己采的样本求期望事后篡改目标会让这个期望不再可计算。2.2 数据结构设计obs里分三个字段要理解HER的实现首先要理解它的数据组织方式。在OpenAI的baselines实现里每个观测被拆成三部分observation也叫state包含完整环境状态比如机械臂关节角、物体位置等achieved_goal智能体当前实际达到的目标相关状态比如滑块当前位置desired_goal当前任务期望达到的目标状态比如滑块目标位置。这里有个设计哲学目标必须是状态空间的子集或变换。在FetchReach环境里achieved_goal就是机械臂末端的三维坐标desired_goal就是目标点的三维坐标。动作空间中不存在直接操纵目标的通道目标只作为条件输入给策略和价值函数。存入replay buffer的每条transition长这样transition { obs: { observation: state_t, achieved_goal: ag_t, desired_goal: dg_t, }, action: action_t, reward: reward_t, obs_next: { observation: state_{t1}, achieved_goal: ag_{t1}, desired_goal: dg_t, # 注意这里不变 }, done: done_t, }reward的计算方式通常是稀疏的当achieved_goal和desired_goal之间的距离小于等于某个阈值时给0否则给-1。代码上最常见的写法是def compute_reward(achieved_goal, desired_goal, info): distance np.linalg.norm(achieved_goal - desired_goal, axis-1) return -(distance goal_threshold).astype(np.float32)这段逻辑的关键在于只要你能把desired_goal换成任意一个状态reward函数就能立刻重算。HER正是利用这一点在存入buffer前对目标做手脚。2.3 事后重标的全过程一段轨迹如何变成两段经验假设一个episode长度为T每一步都有一个transition。HER在episode结束后遍历完整轨迹对每一步再做一次重新标定目标的操作。整个流程可以写成伪代码for t in range(T - 1): # 原始transition照常存入buffer replay_buffer.store(episode[t]) # 从episode中采样一个新的事后目标 new_goal sample_her_goal(episode, t, strategy) # 见下文 if new_goal is None: continue # 复制原始transition只替换目标字段并重算奖励 new_transition episode[t].copy() new_transition[obs][desired_goal] new_goal new_transition[obs_next][desired_goal] new_goal new_transition[reward] compute_reward( episode[t][obs_next][achieved_goal], new_goal ) replay_buffer.store(new_transition)adjacent字段里action不变状态转移不变只有desired_goal和reward发生变化。这看起来像伪造经验但伪造得很合理因为Q-learning更新时用的是状态-动作-奖励-下一状态四元组目标字段本质上是给状态和奖励做条件化标注。把我想去A却到了B重新标注成我想去B并到了B对价值函数来说是两条完全合法、一致的数据。2.4 四种目标策略为什么future是默认最优从论文和工程实践来看事后目标不能随便选一个状态就完事选择策略直接影响学习效率。主流的四种子策略如下策略事后目标采样方式特点与风险final取episode终止时的最后一个achieved_goal实现最简单但每个episode只给一个虚拟目标信息量少长期轨迹里早期transition被标成去终点前后状态差异过大random从整个episode里随机取一个achieved_goal比final丰富但可能取到当前时间步之前的状态导致过去的目标和当前状态之间存在时间倒置不利于学习episode从同一episode里均匀随机取一个achieved_goal覆盖整条轨迹但依然可能取到较早的状态目标和当前状态的因果性稍弱future从当前时间步之后的某个时间步取achieved_goal最符合物理因果目标发生在动作之后且天然构成一条渐近逼近的路径我用future策略时还遇到过一个小细节采样时步范围要包含t本身也就是在[t, T-1]内均匀随机而不是[t1, T-1]。因为当采到t时当前状态的achieved_goal就是目标本身这一步的reward重算后会直接变成0相当于给了模型大量已经在目标处的正样本有助于价值函数在关键边界附近收敛。别小看这个细节我最初按[t1, T-1]采样训练收敛明显变慢。论文里的babysitting实验也验证了future策略在所有测试环境FetchReach、FetchPush、FetchPickAndPlace、FetchSlide上综合表现最好之后的openai/baselines实现默认就是用future。所以实操上我建议直接无脑用future不必纠结。3. 手写一个HER从Replay Buffer到DDPG接入3.1 完整可运行的简化实现这一节给出一份可以直接跑通的PyTorch风格HER核心逻辑。为了可读性我把buffer和重标目标分开写实际项目中你可以把它们揉进一个类。先定义HER的buffer核心结构import numpy as np from collections import deque class HerReplayBuffer: def __init__(self, capacity, replay_k4, strategyfuture): self.buffer deque(maxlencapacity) self.replay_k replay_k # 每条原始轨迹额外生成的重标轨迹系数 self.strategy strategy # final / future / episode / random def store_episode(self, episode): episode: list of transition dicts 每条transition形如: {obs: {observation: s, achieved_goal: ag, desired_goal: dg}, action: a, reward: r, obs_next: {...}} for t, trans in enumerate(episode): # 原始transition self.buffer.append(trans) # 额外生成replay_k条重标transition for _ in range(self.replay_k): new_trans self._relabel_transition(episode, t) if new_trans is not None: self.buffer.append(new_trans) def _relabel_transition(self, episode, t): new_goal self._sample_her_goal(episode, t) if new_goal is None: return None trans episode[t].copy() trans[obs] trans[obs].copy() trans[obs_next] trans[obs_next].copy() trans[obs][desired_goal] new_goal trans[obs_next][desired_goal] new_goal # 重算奖励用新目标 vs 下一时刻的achieved_goal trans[reward] compute_reward( trans[obs_next][achieved_goal], new_goal, None ) return trans def _sample_her_goal(self, episode, t): if self.strategy final: return episode[-1][obs_next][achieved_goal] elif self.strategy future: future_idx np.random.randint(t, len(episode) - 1) return episode[future_idx][obs_next][achieved_goal] elif self.strategy episode: random_idx np.random.randint(0, len(episode) - 1) return episode[random_idx][obs_next][achieved_goal] elif self.strategy random: future_t np.random.randint(t, len(episode) - 1) return episode[future_t][obs_next][achieved_goal] return None注意我在future和random上的处理有一点点差别严格说论文里的random策略是从整条轨迹任意时间步随机采样但很多工程实现为了省事会直接复用future的步长区间。两者差别在实际任务中不大我保持最常见写法。compute_reward函数在前面已经给出按环境里的goal_threshold计算即可。Fetch系列环境里这个阈值通常是0.05。如果你想用密集奖励也可以把reward定义成目标距离的负值HER同样适用只是重标后要重新算一下距离。3.2 与DDPG训练主循环的对接HER本身不修改算法主体它只负责给replay buffer喂数据。以DDPG为例训练主循环里唯一的变化是环境返回的transition不要立刻塞进buffer而是攒成一个episode在episode结束时整体交给store_episode处理。for epoch in range(n_epochs): episode [] obs env.reset() for t in range(episode_len): obs_tensor torch.tensor(concat_goal(obs), dtypetorch.float32) action actor(obs_tensor).detach().numpy() noise() next_obs, reward, done, info env.step(action) episode.append(build_transition(obs, action, reward, next_obs)) obs next_obs if done: break # 一个episode结束统一交给HER buffer her_buffer.store_episode(episode) # 然后照常从buffer里采样batch更新actor和critic batch her_buffer.sample(batch_size) update_actor_critic(batch)在对接时最容易忽略的是concat_goal这一步。内置目标条件的网络输入通常是把observation和desired_goal拼接成一个向量再喂给actor和critic。如果你的环境里observation里已经包含achieved_goal要和desired_goal分开处理否则目标替换时改不动observation字段会白白多学一个没有任何变化的目标通道。3.3 网络结构与超参数选择的实际建议目标条件DDPG的actor和critic网络和普通DDPG基本相同唯一区别就是输入维度里多了目标部分的维度。我在Fetch环境上常用的配置actor两层MLP隐藏层256和256ReLU激活输出层用tanh缩放到动作范围critic将状态含目标和动作拼接后过两层MLP隐藏层256和256目标网络软更新系数tau0.05Fetch任务可以稍微调大actor学习率1e-3critic学习率1e-3buffer容量1e6batch_size 256replay_k 4每条原始轨迹新生成4条重标轨迹优化器Adam。有一个关键点replay_k 4意味着buffer里重标样本和原始样本的比例是4:1。这意味着实际训练的batch里绝大多数样本都是事后目标样本。这没有问题因为原始样本仍然存在且会参与训练价值函数依然能学到真实奖励分布的边界。但是如果replay_k太大比如超过50buffer会被虚拟目标淹没真实目标分布学不扎实收敛反而变慢。另一个参数是噪音。DDPG一般用Ornstein-Uhlenbeck噪声或者高斯噪声。我的经验是Fetch类任务用方差0.2的高斯噪声就行OU噪声参数难调容易被它带偏。HER的探索压力其实不大因为虚拟目标已经提供了大量正面信号关键是保证环境提供的原始状态覆盖足够多样这依赖初始状态重设和噪声扰动。3.4 一次完整的训练评估流程训练结束后评估时不能再把目标替换掉必须用真实任务原始目标。标准的做法是关闭探索噪声让actor吃真实desired_goal跑100个episode统计成功率。我习惯同时记录两个指标成功率achieved_goal与desired_goal距离小于阈值的比例平均最终距离每个episode终止时achieved_goal与desired_goal的平均距离。平均最终距离比成功率更细腻它能反映出模型在差一点点和完全跑偏之间的区别。有些失败episode在成绩单上是0但最后距离从1.0缩短到了0.1这个趋势对判断训练是否卡住很有用。我见过不少训练曲线成功率一直是0但平均距离在稳步下降其实是阈值定义问题导致差0.06就是不算成功这种情况你放宽阈值就有信号了。4. 调试与踩坑哪些细节才是决定成败的关键4.1 目标阈值宁松勿紧Fetch系列环境里goal_threshold0.05是官方默认值但不同任务差异很大。FetchReach的末端位置控制0.05很好达到FetchPickAndPlace的目标点涉及三维空间定位0.05要难一些如果是你自己定义的任务阈值尤其要小心。我踩过最典型的坑是环境给了稀疏奖励但阈值设得比任务本身精度要求还要小比如目标是一个区域你却把阈值设成区域半径的十分之一。这时候明明智能体已经肉眼可见地到达目标区域奖励却依然是-1。训练的Q值永远学不到0的附近曲线自然上不去。建议先用人工策略采样一批终点距离粗略看下距离分布再按距离分布的10%~20%设定阈值。如果任务本身要求高精度那就用curriculum的方式从粗到细逐步收紧阈值而不是一上来就上高精度。4.2 重标样本占比失衡导致的价值函数漂移前面提过replay_k不宜过大这里展开说一下原因。replay buffer里的重标样本越多价值函数学到的分布就越偏离真实任务的目标分布。这会导致一个现象训练时损失降得很低但一到评估所有目标都是真实目标就拉胯因为Q函数对真实目标区域的价值估计不准确。解决思路有两个。一是控制replay_k让原始和重标样本比例保持在1:4左右二是在采样时显式分层每次batch里至少保留20%~30%的原始目标样本。我在baselines实现里看到它其实是纯随机混合的但在实际任务中我会手动加一个sample_real_ratio参数保证每次batch的真实样本比例不低于20%。实测在FetchPush这类目标靠近初始分布的任务上这个改动让成功率提升了5到10个百分点。4.3 探索噪声太大重标轨迹全是僵尸数据HER的重标轨迹质量取决于状态多样性。如果探索噪声很大动作序列飘忽不定虽然achieved_goal的覆盖范围很广但每条轨迹内部的状态跳变太大重标出的目标-状态对没有平滑的递进关系价值函数很难从中提取规律。反过来如果噪声太小agent每次都在初始位置附近打转achieved_goal覆盖不够重标样本也会单调。我的习惯是初始噪声大一些比如0.3训练中期成功率开始破零之后削减到0.1或0.05。如果噪声衰减条件不好判断可以直接用固定0.2大多数Fetch任务都能收敛不用过度调参。4.4 常见故障速查表现象可能原因排查与处理训练几千episode成功率一直为0平均距离也不降actor输入里没有正确拼接desired_goal或者reward计算用了原始目标而非obs_next中的achieved_goal打印一条重标transition人工核对reward是否与新目标匹配训练早期loss很小但评估成功率低重标样本占比过高真实目标分布被稀释降低replay_kbatch里强制加入真实样本future策略训练比final还差目标采样区间包含太多同episode早期的伪成功检查采样区间是否误用了[0, t]改用np.random.randint(t, T-1)buffer被塞满但样本重复率极高episode长度短replay_k过大减小replay_k或buffer容量增大成功率曲线震荡剧烈噪声方差过大学习率偏高降噪把critic学习率降到3e-4actor降到1e-3折腾很久还是学不会环境目标定义不符合achieved_goal可观测且可作为条件输入的前提回头重新审视任务建模考虑是否换用更稀疏友好的算法比如SACHER搭配4.5 一个容易忽略的坑achieved_goal必须是可观测状态这句话听起来像废话实际上非常容易踩。假设你的任务是把积木推到蓝色区域如果你只把机械臂末端位置定义为achieved_goal却把积木位置也塞进observation里期望模型学会推积木那么HER重标后的目标无法表达积木应该去的位置重标再多也没用。正确的做法是把achieved_goal定义成任务真正关心的、且可以从obs中提取出的状态片段在这个例子里应该是积木的二维位置而不是机械臂末端位置。换句话说HER的泛化靠的是目标空间和状态空间的重合。如果目标只是状态空间的一个不可见隐变量或者目标本身是离散的、无法从连续状态倒推的HER就没有用武之地。5. 实战效果与边界HER能做什么不能做什么5.1 在Fetch系列基准上的表现OpenAI论文里报告的数据非常震撼在FetchPush任务上DDPG不加HER训练200个epoch之后成功率依然接近0加上HER之后大约在50个epoch左右成功率就能突破80%。我在自己复现时也观察到了类似现象不过收敛速度略慢大概在60到80个epoch之间冲上80%。FetchPickAndPlace更难一些因为同时涉及夹取和移动成功率大概能到70%~80%之间。FetchSlide因为它本身是滑动摩擦环境对物理参数敏感成功率会低到40%~50%但依然比不带HER的完全学不动强太多。这里要给出一个重要提醒不要只盯着成功率这一个指标。HER的价值函数在目标空间里是连续的所以训练末期你会看到虽然成功率只有80%但剩下20%的失败轨迹平均最终距离已经和成功阈值只差一个微小量。如果业务需求允许你可以靠后处理比如再走一小段修复动作把成功率顶上去而不需要重新训练。5.2 什么场景下HER会失效首先是目标不可反推的任务。比如图像生成任务里目标是一张好看的图好看与否无法从生成结果中提取一个可计算的向量距离HER无从重写目标。其次是目标与状态不同构的任务比如目标是回答正确而状态是对话文本你没法把对话文本直接当作目标向量输入给网络。再就是on-policy为主的算法框架除非你愿意额外维护一个重放缓冲区并冒着违背策略分布的偏置否则别硬套。我自己还踩过一个相对隐蔽的失效模式当环境目标不是单点而是一段连续轨迹比如按顺序通过三个检查点时简单地把最后到达的检查点当作目标会丢失路径顺序信息。这种场景需要用sequence-level的HER变体或者把目标空间重定义为每个检查点的独立子目标再分别应用HER。如果你碰到这类任务我的建议是先把目标拆碎让每个子目标都满足可观测且可替换的条件再用HER的框架一个个解决。5.3 从HER延伸出去多目标RL与课程学习HER引了一个很好的思路目标不应被看作是固定的而应被视为可以与轨迹搭配的变量。沿着这个方向很多工作做了延展。比如多目标强化学习里不再为每个目标单独训练一个策略而是训练一个以目标为条件的通用策略HER天然适合为这种策略提供训练数据。又比如自动课程学习Automatic Curriculum Learning它不是在环境侧改难度而是在目标空间里按智能体的当前水平动态采样目标和HER的事后标定在哲学上是互补的。如果你在做一个新的机器人任务遇到稀疏奖励我建议的优先尝试顺序是先确认能否定义achieved_goal然后上DDPG或TD3配合HER如果还不够再考虑加课程学习、加探索奖励等更复杂的机制。不要一上来就堆叠模型架构和奖励工程HER用最朴素的逻辑解决了最要命的问题足够你撑起80%的进度。最后再分享一个小技巧HER训练时把每条episode的achieved_goal轨迹存下来训练结束后你会发现它本身就构成了一张从失败通往成功的路径图。我经常拿它做可视化观察agent从完全乱撞到逐渐靠近目标的轨迹形状变化比盯着一堆数字直观得多。这个习惯帮我发现过好几次目标定义不合理的低级错误——有一次机械臂末端位置的坐标系单位没对齐轨迹图一眼就看出了异常。好的工具不仅解决问题还能让你看清问题HER就是这样一种工具。
网站建设高端定制企业官网