尧图精选

稀疏奖励困境下的破局利器:HER事后经验回放原理与工程实践

🕒 发布时间:2026/10/2 14:50:37 📁 来源:尧图网络
1. 从一个让人抓狂的问题说起稀疏奖励在强化学习里折腾过的朋友大概率都遇到过这种场景训练一个机械臂去抓取物体策略网络在环境里随机探索了几十万步reward曲线纹丝不动像一张死人的心电图。动作空间稍微大一点或者目标状态稍微复杂一点随机探索撞上成功状态的概率低得令人绝望。我见过不少同学在这里卡了两三周最后忍不住开始手写密集奖励函数给智能体设计一堆朝向目标加分、靠近目标加分、碰到物体加分的规则然后陷入无尽的调参地狱。这就是所谓稀疏奖励问题sparse reward problem。它背后有一个很反直觉的事实在真实机器人任务里大部分环境的奖励信号并不是连续且有梯度感的而是你成功了给个1否则一律0。智能体在这种情况下学不到任何梯度方向纯靠随机策略去碰运气成功率低到约等于零。举个例子一个4自由度机械臂去抓一个放在桌面上的方块如果策略是随机初始化的连续的关节力矩组合空间维数极高随机动作恰好把末端执行器送到方块位置的几率几乎可以忽略。就算偶尔碰到了也会因为缺少接近目标的中间信号无法把这次成功泛化成可复用的策略。传统的解决方案是奖励塑形reward shaping也就是手工设计一个密集奖励函数去引导智能体。做过的人都知道这是个泥潭你得不断加权重、调距离函数、防伪最优解稍不小心智能体就会学出各种钻空子行为——比如用机械臂把方块推到墙角但不抓起来因为这样距离目标更近。真正让我眼前一亮的是Hindsight Experience ReplayHER事后经验回放这个方法。它的出发点跟奖励塑形完全不同不去修改环境奖励而是修改经验本身。核心思想一句话就能说清——当一个轨迹没有达到原定目标时不把它当作彻底失败的样本丢掉而是把轨迹里实际到达的状态重标定为一个新的目标让智能体从这次失败里学到东西。这个方法来自Plappert等人在2018年发表的论文配合DDPG、SAC等off-policy算法使用直接把Fetch系列机械臂操作任务的样本效率提升了几个数量级。这篇就围绕HER展开我会讲清楚它为什么有效、有哪些最容易被忽略的工程细节、以及我用它训练操作类任务时踩过的坑和排查经验。适合正在做机器人操作、自动驾驶决策、或者任何碰到稀疏奖励任务的强化学习研究者参考。2. Hindsight的核心思路事后诸葛亮也是好老师2.1 思想源头人类从失败中学习的方式事后诸葛亮这个译名很传神因为它说的确实就是这么回事。我们回忆一下自己学投篮的经历前几十次出手球根本没碰到篮筐但你不会把这几十次练习当作毫无价值的随机错误。每一次投偏之后你脑子里都会形成一个校准动作的机会——如果刚才的手腕角度调整成这样球就能更接近篮筐。放到强化学习语境里HER做的是同一件事它把一次失败的探索轨迹转化为一个有价值的训练样本。具体机制是把目标这一项重新赋值。在goal-conditioned RL任务里一个完整经验长这样( (s_t, a_t, r_{t1}, s_{t1}, g) )其中g是智能体被要求达到的目标。标准经验回放库里存的就是这些原始经验HER在存储时多留了一个字段——achieved goal也就是这一时刻智能体真实到达的状态。等到从经验池采样训练时出其不意的地方来了除了保留一部分原始目标g的经验还会额外拿一部分经验把原来的目标g替换成这条轨迹里实际到达过的某个状态g然后重新计算奖励。替换之后这条经验就变成以g为目标、且恰好达到了目标的正样本。相当于每一次探索无论它多么失败都额外变成了若干条对智能体有正反馈的训练数据。2.2 目标重标注的完整过程用一个我实际跑过的机械臂推积木任务来拆解。假设目标是把积木推到桌面坐标 (0.5, 0.2) 处某次探索中机械臂从初始位置出发推积木一通乱怼最后积木停在 (0.3, -0.4)任务失败整个轨迹所有step的奖励都是0。HER在这个轨迹上做的事情是把这条轨迹里每一个step的目标字段从(0.5, 0.2)替换为这条轨迹结束时积木实际到达的位置 (0.3, -0.4)并重新计算每一步的奖励。重算之后的轨迹就变成了一条成功轨迹——因为目标就是积木最后到达的位置所以轨迹最后几步的奖励是1或者0取决于你自己的sparse reward定义。这里有个细节很多人第一次没转过弯来不是只把最后一步的奖励改成1而是整条轨迹每个step的奖励都要按新目标重新计算。前几步积木还没到终点所以奖励依然是0但随着轨迹推进积木逐渐接近最终位置奖励会按你的奖励函数自然变化。这样一条原来毫无信号可言的轨迹瞬间变成了逐渐逼近目标的正向示范。用伪代码描述回放采样时的重标注过程# replay时对每个采样batch做如下处理 for transition in batch: # 保留原始经验以一定的概率通常是1/K if random.random() 1.0 / K: g_new transition[original_goal] else: # 从轨迹未来时刻中采样一个新目标 g_new transition[achieved_goal_future] # 用新目标重新计算奖励和done标志 r_new compute_reward(transition[next_state], g_new, env_info) done_new is_goal_reached(transition[next_state], g_new) transition[goal] g_new transition[reward] r_new transition[done] done_new2.3 为什么这个取巧的方法真的有效第一次听说HER的人通常会怀疑把失败样本强行说成成功样本这不等于给智能体的错误行为贴正确标签吗会不会反而教坏它这里其实有个微妙的逻辑。标准强化学习里我们要求智能体学习的是从状态s出发执行动作a能得到高收益。HER重标定的样本说从状态s_t出发执行动作a_t最终到达了状态g所以在以g为目标的任务下这个动作是对的。这句话并没有逻辑错误它是对动作-状态转移这段因果关系的真实描述。关键点在于目标g是轨迹真实到达的状态不是凭空编造的。这条经验反映了真实环境动力学——从s_t走a_t这条路确实能通向g。因此策略网络学到的是这些动作序列能够通往这些状态而这种状态转移的知识恰恰是稀疏奖励环境下最稀缺的信号。当任务目标是固定的、真实的g时智能体虽然没法直接从当前探索中学到精确的goal-conditioned策略但通过大量失败的轨迹重标定的目标它积累了大量关于什么动作通向什么状态的经验然后逐渐泛化到新目标上。这本质上是一种课程学习的变体目标从容易到达的状态逐步迁移到困难的原目标。3. 从零开始实现一个HER代码与实操要点3.1 环境与问题设定为了把HER讲透彻我选一个最简单的goal-conditioned环境二维平面上的点目标到达任务point navigation。环境是一个无边界的二维平面智能体是一个粒子动作是二维速度指令每一步执行后更新位置。初始位置固定为(0, 0)每次episode随机生成一个目标位置比如范围[-2, 2]内的随机点当智能体与目标的欧氏距离小于0.05时判定成功。这个环境虽然简单但完美复现了稀疏奖励的核心痛点如果纯随机探索智能体很容易在平面里乱转撞上目标区域一个半径为0.05的小圆的概率很低尤其是目标范围较大的时候。我故意把目标范围设得比较大让随机探索的成功率降到千分之一以下逼出HER的价值。实际工作中这个环境相当于Fetch类任务的最小可复现版本。我先在这个环境里验证算法实现确认没问题再迁移到更复杂的MuJoCo机器人环境效率会高很多。3.2 算法基座的选择为什么用DDPG而不是DQNHER是经验回放层面的改造不能独立使用必须搭配一个off-policy的强化学习算法。最常用的搭配是DDPG、TD3和SAC。我最初用的是DDPG原因有三个DDPG结构简单actor-critic分离清晰排查bug时心智负担小连续动作空间原生支持机械臂操作类任务基本都是连续控制HER官方论文里DDPG是主力benchmark算法很多超参可以直接抄作业后来我换到TD3性能提升明显主要因为TD3对过估计问题做了双Q网络和延迟更新处理在机械臂任务上稳定性更好。如果你的任务接触强烈比如抓取、推拉我建议直接上TD3。表里是两套基座算法的核心差异项目DDPGTD3Q网络数量1个2个取最小值策略更新频率每个step都更新延迟更新通常每2步更新1次目标策略平滑无加噪声平滑适用场景简单连续控制高维、接触密集任务对超参敏感度较敏感相对不敏感3.3 核心代码模块下面给出一个可运行的Python实现骨架。虽然平台无关但我用PyTorch写的这也是目前学术界和工业界最常用的组合。先看经验池部分这是HER的关键所在——我额外存了achieved_goal并且根据HER的future策略在采样时动态重标注import numpy as np import torch import random class HindsightReplayBuffer: def __init__(self, capacity, k_future4, future_strategyfinal): self.capacity capacity self.k_future k_future # 每个样本额外重标注的次数 self.future_strategy future_strategy self.buffer [] self.pos 0 self.episode_transitions [] # 暂存当前episode的所有transition def add_transition(self, transition): 临时保存当前episode的transition self.episode_transitions.append(transition) def end_episode(self): episode结束时统一清入经验池并额外生成HER样本 episode self.episode_transitions for i, trans in enumerate(episode): # 原始经验入池 self._store(trans) # 生成k_future条重标定经验 for _ in range(self.k_future): # 从当前时刻之后的transition中选择一个achieved goal future_idx random.randint(i, len(episode) - 1) future_goal episode[future_idx][achieved_goal] new_trans trans.copy() new_trans[goal] future_goal new_trans[reward] self.compute_reward( new_trans[next_obs], future_goal ) new_trans[done] self.check_success( new_trans[next_obs], future_goal ) self._store(new_trans) self.episode_transitions.clear() def _store(self, transition): if len(self.buffer) self.capacity: self.buffer.append(transition) else: self.buffer[self.pos] transition self.pos (self.pos 1) % self.capacity def sample(self, batch_size): 采样时不对目标做二次重标注重标注已经在写入时完成 return random.sample(self.buffer, batch_size)这段代码里有个设计决策值得解释常用的HER实现有两种时机做重标注一种是写入时重标注如上另一种是采样时重标注。写入时重标注会牺牲一点灵活性但计算量集中容易控制比例采样时重标注更灵活但每次采样都要重复计算奖励高频率采样下开销明显。我在实践中发现写入时重标注更适合后续扩展多个future strategy所以选择了这种方案。future策略这里我用了最简单的final策略——从当前时刻之后的transition中随机采一个作为新目标。这个策略来自于Andrychowicz等人论文实测效果往往优于从全部未来状态中均匀采样因为它保证了目标轨迹的时序一致性。稍微进阶一点的策略是按指数衰减权重从未来状态中采样早期的HER实现里也有用这个的但在我的任务上收益不大反而多了一个超参要调。3.4 主训练循环与关键参数主循环看起来跟普通DDPG差别不大真正的区别只在经验池写入方式。这里给出训练循环的伪代码env PointNavEnv() agent TD3Agent(obs_dimenv.obs_dim, act_dimenv.action_dim) buffer HindsightReplayBuffer(capacity1_000_000, k_future4) for episode in range(num_episodes): obs env.reset() goal env.sample_goal() done False step 0 while not done and step max_steps: # 训练早期加探索噪声后期衰减 action agent.select_action(obs, goal, noise_std0.2) next_obs, reward, done, info env.step(action) transition { obs: obs, action: action, reward: reward, next_obs: next_obs, goal: goal, achieved_goal: next_obs[achieved_goal], } buffer.add_transition(transition) obs next_obs step 1 # episode结束后统一入池并生成HER样本 buffer.end_episode() # 每收集一定数量的真实样本后训练若干轮 if len(buffer.buffer) warmup_steps: for _ in range(train_iters): batch buffer.sample(batch_size) td3_update(agent, batch)参数方面我实际用下来几个值是性价比最高的起点k_futureK值 4每个原始样本额外生成4条重标定样本。论文里的标准选择5条总体经验中原始目标占1条、重标定占4条。低于2效果差高于8收益递减且增大经验池压力batch_size 256比起默认的128大batch对HER这类高样本效率算法更友好Q函数估计更稳定探索噪声 0.2刚开始可以加到0.3后期逐步衰减到0.05。噪声太小会失去探索性太大又会让重标定的轨迹过于随机没有一个目标值得学warmup_steps 1000经验池攒够一定量再开始更新避免起手就学的样本全是胡闹轨迹网络结构 3层MLP ReLU LayerNorm隐藏层256维。关键技巧是给obs和goal分别做编码再拼接起来进Q网络比直接拼接效果稳定很多提示HER的K值不是越高越好。K值高意味着经验池里重标定样本比例大原始目标样本被稀释。如果原始目标样本太少策略会对原目标本身产生遗忘导致最终评估时泛化能力下降。建议K4并在K2和K8之间各试一组画学习曲线对比再定。4. 实操过程中最容易翻车的几个细节4.1 achieved_goal到底该取什么状态这个细节看起来基础但我见过太多人在这里犯错。achieved_goal必须取环境真实反馈的状态而不是智能体内部预测或估计的状态。拿机械臂抓取来说achieved_goal应该取物体被夹爪真实推动后的位置而不是夹爪中心位置或者视觉模型估计的位置。一个典型错误抓取任务中智能体的观测包括手臂关节角度、夹爪状态和物体位置。有人偷懒直接拿物体位置观测值当作achieved_goal这在仿真里没问题但一旦物体被夹爪遮挡、或者视觉观测有噪声重标定的目标就会带误差而误差会被HER放大——因为重标定的新目标本身就来自观测观测不准等于目标不准经验池里塞满了一批假成功样本。实际项目里如果观测噪声不可避免建议把achieved_goal设计成后验融合状态比如用卡尔曼滤波或滑动平均平滑物体位置再做归一化。这一步能在不改变算法的情况下显著提升真实环境上的成功率。4.2 奖励函数怎么写才不影响HERHER虽然能缓解稀疏奖励但不代表你可以完全忽略奖励设计。用二值化稀疏奖励是最标准的做法达到目标给0没达到给-1或者反过来达到给1没达到给0。这里有个容易踩的坑有些同学为了让奖励更有信息量给距离目标远近加了个连续惩罚项。结果是HER重标定目标时连续惩罚项会引入大量非零梯度而这些梯度信号和重标定的目标状态纠缠在一起反而干扰策略学习。我自己做过对比实验同一套TD3HER二值奖励版本比连续距离奖励版本收敛更快、最终成功率更高。原因在于HER已经通过目标重标定提供了足够的渐进学习信号此时奖励函数反而应该保持干净让目标变化作为唯一的语义改变因素。记住一句话HER是让稀疏奖励变得可学不是让你省掉奖励函数的设计。4.3 归一化和目标空间设计HER对目标空间表示极度敏感。我踩过最大的坑就是目标空间各维度量纲不一致。比如机械臂任务里目标由3D坐标单位米范围0~0.5加旋转角度单位弧度范围0~2π组成。如果不做处理直接拼接欧氏距离计算会被弧度维度主导重标定时选出的future目标几乎总是在旋转维度上变化而位置维度几乎不变。这样的重标定样本对学习位置技能毫无帮助。正确的做法是对目标空间的每个维度独立做归一化缩放到[-1, 1]或者[0, 1]。如果各个维度重要性不同甚至可以给每个维度一个权重让距离函数变成加权欧氏距离。经验是目标空间的语义越清晰HER越好学。如果你发现训练中期策略一直在某一个目标维度上停滞优先检查这个维度的量纲和归一化。4.4 训练初期探索策略的补充技巧HER重标定建立在探索轨迹的基础上——如果轨迹本身过于单一重标定目标也就单一。因此探索策略的质量直接影响HER的上限。我早期直接用高斯噪声叠加在动作上效果一般后来做了两个改动样本效率明显提升第一动作噪声的方差做自适应衰减。具体做法是每隔一定episode比较成功率如果连续N个episode成功率没有提升就临时增大噪声激励探索如果成功率在快速上升期则减小噪声避免破坏已有策略。这本质上是一种自动化的exploration-exploitation平衡。第二在episode开头增加随机目标注入。有些环境下初始状态固定智能体每次都从同一个位置出发轨迹的分布很窄。我会以30%的概率在episode中途随机重置一个子目标让智能体先学会走向这个临时目标。这不算标准HER的范畴更像课程学习但配合HER重标定效果出奇地好。如果你发现训练后期策略探索范围停滞可以试试这个技巧。5. 常见问题与排查技巧实录5.1 训练不收敛怎么定位问题HER训练不收敛时最先要判断的是问题出在基座算法还是HER实现。我有一套固定的排查顺序先跑一个密集奖励版本的相同任务。如果密集奖励下DDPG/TD3都不能收敛说明问题在基座算法而不是HER先修基座去掉HER用普通回放跑稀疏奖励任务确认任务确实学不动——这一步是为了验证问题场景确实存在避免你在一个其实不稀疏的任务上白折腾加回HER但把K值设为1future_strategy用final看有没有任何学习迹象。如果K1即额外的重标定样本只有1条都不收敛检查经验池写入逻辑有没有bug如果以上都通过再逐步调回K4。这个顺序可以帮你把算法实现bug和任务本身难度分开。很多同学一上来就调超参调了一周发现是经验池里achieved_goal字段根本没正确填充白费功夫。5.2 经验池里的脏数据问题HER的经验池比普通回放更容易混入脏数据最典型的有两类。一类是episode没结束就被截断。比如机械臂任务里如果episode因为超时中断最后一条transition的done标志是False或者被强行标记为TrueHER重标定时会从这条轨迹的未来状态里采样目标。如果未来状态里有机械臂中途脱手、物体飞到台面外之类的异常状态这些目标就成了无意义甚至有害的标签。我建议在处理done标志时区分自然结束和超时截断超时截断的轨迹不参与future目标采样。另一类是目标空间越界。有些achieved_goal状态超出了任务合法范围比如物体被推出桌面重标定出的目标本身不可达。这会导致策略学到去碰撞边界也能成功的伪规律。排查方式是定期打印经验池里goals的分布直方图如果发现目标分布和真实环境的目标分布差异很大多半是重标定目标包含了越界状态需要加一步目标合法性过滤。5.3 调参过程中的几个观察与心得我把调参过程中几个比较反直觉的观察列出来这些在论文和标准教程里很少提到K值增加带来的收益并不单调。K从1提到4效果提升明显但从4提到8不仅没有继续提升反而在某些任务上成功率下降了。原因我前面提到过是原始目标样本被稀释。如果你用K8建议同时把batch_size提高到512以上补偿原始样本的不足HER对actor与critic更新频率的比例很敏感。在TD3默认的critic每步更新、actor每2步更新基础上如果HER经验池里重标定样本比例较高可考虑把actor更新频率降为每3步甚至每5步一次。原因是重标定样本的reward是人为构造的过多的快速策略更新可能导致策略追逐这些人为信号的方向过拟合achieved_goal维度越高收敛速度越慢。这是一个很朴素但容易被忽视的规律。在目标维度超过10维时HER的效果会明显打折扣。此时可以考虑对目标空间做降维比如用自编码器压缩目标表征而不是全量保留原始空间5.4 一个经典的崩溃案例复盘最后分享一个印象深刻的bug排查过程。某次训练机械臂推方块任务reward曲线前期正常上升到大约3万episode时突然剧烈震荡随后成功率从80%掉到15%再也回不去。排查过程持续了两天。第一反应是怀疑网络结构或者学习率衰减出了问题但回滚到之前能收敛的版本依然复现。后来我在经验池里随机抽了几条transition可视化发现了一个可疑现象那段时间episode里机械臂偶尔会把方块推到一个边缘区域在这个区域里方块离目标虽然远但机械臂的end-effector正好处于一个特定位置导致环境自带的距离判定函数出现了一个假性成功。而HER重标定把这种假性成功当作正样本喂给了策略策略很快学会了推方块到边缘区域这个投机行为而不是真正的到达目标。这个案例说明HER有个隐藏假设环境的success判定必须是稳定、一致、无歧义的。如果环境里存在单词的成功判定噪声HER会把这种噪声放大成策略的劣质泛化。如果你遇到类似的骤降型崩溃先别急着调算法去检查环境的成功判定逻辑和观测的稳定性。真实机器人在这一点上尤其脆弱传感器误差和打滑都会造成类似问题。6. 后续可以扩展的三个方向如果在标准HER上跑通了想继续深挖我建议关注这三个方向。第一个是Chosen Goals优先采样来自Kim等人2019年的工作核心思路是重标定时优先选择那些原始目标预测误差大的状态作为新目标相当于更主动地挑选智能体最困难的失败案例。这个思路在稀疏奖励比较极端、探索初始成功率极低时能比随机future采样更稳。第二个方向是与课程学习结合。HER的目标重标定本质上是隐式的课程但从多个项目的经验看显式的课程策略比如先让智能体在近距离范围内采样目标成功率高后再扩大目标分布范围和HER叠加后训练速度往往还有一次明显提升。原因是显式课程能更充分地利用策略早期的学习能力减少智能体在完全不可达目标上浪费的步数。第三个方向是目标空间表征学习。当目标不是低维的坐标向量而是图像或传感器读数时HER的距离函数不再好定义。此时可以用一个目标编码器把高维目标压缩成低维向量再在低维空间做重标定和距离计算。我在仿真环境里试过用自编码器压缩机械臂的图像目标虽然增加了训练复杂度但对视觉目标的可泛化性提升非常直观。以上这些方向目前都有不少开源代码可以参考但建议先把基础版HER吃透、调稳确认自己对重标定机制的理解没有偏差再去做扩展。这个基础版本我在二维点导航任务上从写到收敛大约需要一两天时间拿到机械臂任务后真正花时间的反而是那些环境细节和调试习惯。希望我的这些经验能帮你少走一些弯路。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →