HER算法实战:稀疏奖励下DDPG目标重标记与Fetch环境实现
做机器人控制、自动驾驶或者游戏AI的朋友大概率都撞上过同一个坎稀疏奖励。环境给的反馈要么是0要么就是终点处那一个1中间漫长的探索过程完全靠瞎猜。传统强化学习在这种场景下基本是废的而Hindsight Experience Replay后见经验回放简称HER就是专门来啃这块硬骨头的算法。它解决的核心问题是当智能体无论如何都到不了目标时如何让失败的轨迹也变成有价值的学习信号。这篇文章基于我实际复现HER并在Fetch系列机器人环境中跑通的经验把算法原理、目标重标记的细节、DDPGHER的完整实现以及调试中踩过的坑一次性讲清楚适合正在做稀疏奖励RL项目或者准备复现经典论文的朋友参考。1. 稀疏奖励为什么难HER到底要解决什么1.1 从“蒙眼找钥匙”说起想象你被蒙上眼睛扔进一个大房间任务是找到一把钥匙摸到钥匙才给1分其他任何操作都不给反馈。你只能在房间里瞎转转到天荒地老也不知道自己走得对不对。普通强化学习算法遇到的就是这个局面一步奖励为0两步奖励为0一百步还是0Q值的更新里目标项根本得不到有效信号策略梯度算出来也接近无效。很多新手第一次跑稀疏奖励环境时都以为是自己代码写错了其实算法压根没有可学的梯度这是结构性问题不是调参能解决的。传统RL算法能work靠的是密集奖励——每一步都告诉你“好”还是“坏”。比如让机械臂走向目标每靠近一厘米给一个小奖励算法就能通过梯度逐步逼近最优策略。但现实任务里奖励往往不是现成的或者设计起来极其困难于是就有了稀疏奖励设定只有成功那一瞬间给1其他时刻全部是0。这种设定更接近真实世界但学习难度直线上升。1.2 传统经验回放为什么救不了稀疏奖励经验回放Experience Replay本身是把智能体走过的轨迹存进一个buffer训练时随机抽小批量更新网络。它解决的是“样本相关性”和“灾难性遗忘”的问题能让off-policy算法稳定训练。但在稀疏奖励场景里replay buffer里存下来的transition绝大部分reward都是0少数成功的transition可能十万个里才有一个抽样根本抽不到Q网络学到的全是“做什么都没回报”。这就是为什么DDPG、DQN这类算法在稀疏奖励下成功率常年为零。有人可能会说那我多跑一些episode总会撞到一两次成功吧确实可能但概率低得离谱。机械臂推箱子箱子和目标都在一个2D平面上动作又是连续四维的随机探索能恰好把箱子怼进目标区域5cm半径内的概率小到忽略不计。哪怕跑一万个episode成功样本依然是凤毛麟角训练本质上还是在原地打转。1.3 HER的核心洞察让失败经验变废为宝HER的关键想法来自OpenAI在NeurIPS 2017发表的论文《Hindsight Experience Replay》作者Andrychowicz等人注意到一个特别朴素的事实人类是会“事后诸葛亮”的。你投篮没投进球落到了篮筐右侧你不会觉得这一投毫无收获——你会知道“往左边偏一点能进”。也就是说失败本身包含了关于如何接近目标的宝贵信息只是我们一直用固定的目标去评价这次失败所以信息被浪费了。HER的做法非常直白这次没到目标A但我实际到达了B。那好我就把这条轨迹的目标改成B重新放回经验池。对于“到达B”这个目标来说这条轨迹就是一条完美的成功示范。虽然智能体学到的不是如何到达A但它学到了状态转移、动作与目标之间的因果关系这些经验在不同目标之间是共享的。积累了大量这样的经验后智能体对“怎么控制物体到某个位置”就有了基本认知再迁移到原始目标A上就容易多了。2. HER算法原理与目标重标记机制2.1 先给任务建个数学模型Goal-Conditioned MDPHER处理的任务有明确的目标条件学术上叫Goal-Conditioned MDP。它在标准MDP的元组(S, A, R, P, γ)里额外引入了一个目标空间G每个episode会采样一个目标g∈G策略π(a|s, g)在给定当前状态和目标时输出动作。奖励函数写成R(s, a, g)或R(s, a, g, s)典型形式是如果φ(s)与目标的距离小于某个阈值ε奖励为0或1否则为-1或者0。这里的φ(s)叫“已实现目标”achieved goal比如机械臂场景中物体当前的实际位置。这种形式化很关键因为HER的目标重标记逻辑完全建立在“任务可以用一个可量化的目标状态来判断成败”之上。如果任务没法写出明确的achieved goal比如“写一段优美的散文”那HER也用不上。所以拿到一个任务时我第一件事就是问自己这个任务的目标状态能不能用向量表示、能不能算距离能才有HER发挥的空间。2.2 目标重标记的完整流程HER的算法流程可以拆成四步照常跑一个episode记录整条轨迹包括每个时间步的状态obs、动作action、奖励reward、下一状态next_obs以及这个episode实际达到的achieved_goal序列。把这条轨迹以原始目标g存入replay buffer奖励用原始奖励通常全是-1或0。额外做K次目标重标记从这条轨迹中挑一个新的目标g用环境自带的奖励函数重算每个transition的reward得到一组“以g为目标”的新transition也塞进buffer。训练时从buffer里均匀采样DDPG或类似算法正常更新。重点说第三步里的“新目标从哪来”。论文给出了四种策略final取整个episode最终状态的achieved_goal作为新目标future对轨迹中第t个transition从t1及之后的某个状态里随机取一个achieved_goal作为新目标episode从同一个episode里随机取一个状态的achieved_goalrandom从整个replay buffer里随机取一个状态的achieved_goal。我实测下来future策略综合效果最好论文里也推荐它。原因后面第5节单独讲。2.3 future、final、episode……重标记策略怎么选先解释一下为什么“随机选目标”这种粗暴方案也能work。因为无论选什么目标只要这个目标确实是智能体曾经达到过的状态那么对应轨迹就是一条真实可达的样本动力学的因果关系是成立的。区别在于不同策略选出来的目标和当前状态之间的距离分布不一样直接影响TD学习的难度。final策略的问题在于只取终点一个状态。机械臂推箱子100步都没推到位最后的箱子位置可能离起始点很远用这个位置当目标去回放早期几个transitionTD误差会非常大Q网络要跨越很长的时间跨度去学习收敛自然慢。episode策略稍微好一点但依然可能选到离当前状态很远的中间状态。future策略聪明在它选的目标一定是当前状态之后“几步之内”真实到达的状态从当前状态到目标之间的实际路径很短奖励信号和状态转移之间的关联很紧密Q函数学起来轻松得多。实际操作里还有一个细节每个transition除了存原始样本我一般额外生成K4个future重标记样本。这个K不是拍脑袋定的论文消融实验显示K4性价比很高。K太大比如8或16虽然正样本更多但buffer里充斥着“成功”样本反而稀释了真实的失败信息critic可能变得过度乐观。2.4 HER为什么不能直接配PPO这类on-policy算法不少朋友会问既然HER这么好用能不能直接把PPO、TRPO加上HER一起上答案是不建议。HER会在episode结束后重写历史轨迹的目标和奖励这意味着存进buffer的数据已经不属于当前策略的真实分布了。PPO这类on-policy算法依赖“用当前策略采样的数据来估计策略梯度”数据被篡改后重要性采样估计也会失真训练基本就崩了。相反DDPG、SAC、TD3这些off-policy算法天然适合HER。反正它们本来就是从replay buffer里抽历史数据学buffer里的数据是否来自当前策略影响不大只要足够覆盖状态空间就行。这也是为什么最经典的组合是DDPGHER论文里也是这么配的。如果你非要用SACHER也没有问题只要保证SAC的entropy相关loss不要被重标记录的数据搞乱就行我后面会提一下怎么处理。3. 验证环境与实验设计3.1 用Fetch系列机器人环境当试验场OpenAI Gym里有一套Fetch系列环境是验证HER最常用的基准FetchReach机械臂末端去碰目标点、FetchPush把物体推到目标位置、FetchPickAndPlace抓取物体并放到目标位置、FetchSlide击打物体让它滑到目标位置。这些环境自带稀疏奖励动作空间是四维连续量末端三轴位移加夹爪开合observation是字典格式包含三块关键信息observation机械臂关节角、末端位置、夹爪状态、物体位置等achieved_goal当前“已完成目标”的3维坐标比如物体的当前位置desired_goal本episode要完成的3维目标坐标。FetchReach是入门首选因为它没有物体操作就是单纯的末端点到目标点状态维度低、动力学简单用很浅的网络就能跑通。FetchPush难度高一个档次需要学“推”这个接触操作物体一开始可能在目标的另一侧策略必须学会绕过去推而不是把物体推离目标更远。FetchPickAndPlace又更难接触、抓取、搬运三个技能都要学。3.2 奖励函数与done标志的设置陷阱Fetch环境的奖励函数在源码里是compute_reward(achieved_goal, desired_goal, info)内部计算两点欧氏距离小于5cm判为成功。CRUCIALLY重标记后的reward不能自己手写一个简化版我见过有人图省事用-np.linalg.norm(g - g_new)当奖励结果数值分布和原环境对不上success判据也偏了。正确的做法是调用env.compute_reward保证重标记样本的奖励定义和真实环境完全一致。另一个极其隐蔽的坑是done标志。默认的Fetch环境在成功或超时100步后终止。如果你在rollout时发现“物体没推到位但步数到了”就提前截断episode那这条轨迹就不完整了。HER的future策略需要在整条轨迹的后半段里采样新目标如果轨迹被截断成半个可用的future状态就没几个重标记的效果大打折扣。所以跑HER时我建议rollout固定跑满max_steps不看done只看步数。换句话说episode的“成功”与“终止”要解耦成功了也要继续跑完不成功了可以提前停但失败了不要提前停。实际上更稳妥的做法是除非达到成功条件否则一律跑满步数。3.3 网络结构与输入标准化的实际考量DDPGHER的网络输入有一个容易被新手忽略的组装问题。Fetch环境返回的是dict你不能直接把整个dict塞进网络。我采用的组法是状态向量s concatenate(observation, achieved_goal)目标向量g desired_goal。这里achieved_goal必须拼进状态因为Q函数需要知道“物体当前实际在哪里”才能判断“离目标还有多远”而desired_goal作为目标输入重标记时只需要替换goal向量和重置reward状态向量本身完全不用动。网络规模方面FetchReach用两个256维全连接层足够了。FetchPush和FetchPickAndPlace建议用256x256x256三层宽度加大能明显提升表达力。激活函数ReLU即可动作输出层用tanh把动作限制在[-1,1]。我额外加了LayerNorm在Q网络输入层后面稳定性会好一些尤其是输入尺度不统一的时候。关于标准化obs里关节角度是角度制数值物体坐标是米制小数尺度差了好几个量级。我维护了一个running mean/std统计器对obs、achieved_goal、desired_goal、action分别做标准化后再喂进网络。这一步不做训练前期Q值会剧烈震荡严重时直接NaN。baselines仓库的her里也是这么干的可以说是标配。4. DDPG HER完整实现PyTorch实战4.1 项目代码结构按下面的目录组织代码逻辑最清晰her_ddpg/ ├── env_wrapper.py # 封装gym环境把dict转成flat数组 ├── her_buffer.py # 经验回放 重标记核心实现 ├── networks.py # Actor/Critic网络定义 ├── ddpg.py # DDPG训练器 ├── train.py # 训练主循环 ├── evaluate.py # 成功率评估脚本 └── config.py # 超参数集中管理env_wrapper的核心工作就是构造transition。每步从环境拿到的dict里我提取obs、achieved_goal、desired_goal加上action和reward拼成一条transition存起来。一个episode结束时把整条轨迹传给her_buffer做重标记。4.2 经验回放里重标记的具体实现HER的灵魂在her_buffer.py里。我写一个简化版future策略重标记函数def add_episode(self, episode, k4): # episode: list of dict, 每个dict包含obs, achieved_goal, action, reward, next_obs, next_achieved_goal for t, trans in enumerate(episode): # 原始目标样本 self.add(obstrans[obs], actiontrans[action], rewardtrans[reward], next_obstrans[next_obs], goaltrans[desired_goal]) # future策略重标记 future_goals [e[achieved_goal] for e in episode[t 1:]] if len(future_goals) 0: continue for new_goal in random.choices(future_goals, kk): new_reward self.env.compute_reward( trans[next_achieved_goal], new_goal, None) self.add(obstrans[obs], actiontrans[action], rewardnew_reward, next_obstrans[next_obs], goalnew_goal)这段代码有几个细节决定成败。第一new_goal一定是从t1之后的achieved_goal里选绝不能从包括当前步在内的全部轨迹里选否则会出现“目标就是当前状态、奖励恒为0”的无效样本。第二计算new_reward时用的是next_achieved_goal也就是执行动作后到达的位置而不是当前obs里的achieved_goal。后者经常会忘记加next一错整个奖励信号全乱。第三原始目标那条transition无论如何都要保留哪怕它的reward全是-1因为真实目标的信号不能丢。4.3 DDPG核心更新流程DDPG部分没有什么魔法核心是四套网络actor、critic、actor_target、critic_target。更新时def update(self, batch): obs, goals, actions, rewards, next_obs batch with torch.no_grad(): next_actions self.actor_target(next_obs, goals) target_q self.critic_target(next_obs, next_actions, goals) target_y rewards self.gamma * target_q current_q self.critic(obs, actions, goals) critic_loss F.mse_loss(current_q, target_y) self.critic_opt.zero_grad(); critic_loss.backward(); self.critic_opt.step() actor_loss -self.critic(obs, self.actor(obs, goals), goals).mean() self.actor_opt.zero_grad(); actor_loss.backward(); self.actor_opt.step() # 软更新目标网络 for tp, p in zip(self.actor_target.parameters(), self.actor.parameters()): tp.data.copy_(self.tau * p.data (1 - self.tau) * tp.data) for tp, p in zip(self.critic_target.parameters(), self.critic.parameters()): tp.data.copy_(self.tau * p.data (1 - self.tau) * tp.data)注意actor loss里的负号最大化Q值就是最小化负Q。目标网络用软更新τ取0.05比常见的0.005稍大一些因为HER任务episode短、环境相对简单快一点更新能加速收敛。4.4 超参数表与训练主循环设置我把一套在FetchPush上实测可用的超参数列出来参数取值说明回放buffer容量1,000,000越大越稳但要控制内存batch_size128太小方差大太大更新慢actor学习率1e-3Adam默认参数即可critic学习率1e-3与actor保持一致γ0.98比默认0.99更稳任务短τ0.05目标网络软更新系数每episode最大步数100Fetch系列默认重标记K4future策略的额外样本数动作噪声OU噪声 高斯噪声前期探索后期衰减训练主循环按epoch组织。每个epoch跑50个episode每步采集的transition先暂存在一个episode临时buffer里一个episode结束后调用add_episode做重标记再整体加入replay buffer。每采集完一个episode的数据做40步DDPG更新。每5个epoch评估一次固定随机种子让actor用确定性动作连续跑30个episode统计成功率。4.5 调参与基线对比的实操笔记FetchReach上用这套配置大约10个epoch500个episode后成功率就能到95%以上。FetchPush难度大一些我实测无HER版本跑了2万episode成功率仍然是0而加上HER大概2500个episode就突破50%4000个episode左右能到80%。噪声策略也很关键。前期探索阶段我用Ornstein-Uhlenbeck噪声加高斯噪声混合OU噪声带时间相关性能让机械臂朝一个方向持续探索一段时间避免原地抖动。到训练后期把噪声幅度衰减到0.05以下评估时直接把噪声关掉。我踩过的一个坑是高斯噪声标准差设太大比如0.5导致动作频繁打满边界策略受噪声驱动比受策略驱动更多训练奇慢。5. 训练结果与收敛性分析5.1 有HER vs 无HER成功率的差距有多大以FetchPush为例同一个DDPG实现只差加不加HER差距是断崖式的。无HER时即使把学习率调低、加大噪声、增加buffer容量连续跑100个epoch成功率始终是0。因为经验池里根本没有正奖励样本critic的Q值对所有(state, action)组合都趋近同一个负数policy gradient退化成噪声更新。有HER后训练曲线就健康很多。前500个episode是“打地基”阶段成功率在0附近徘徊但Q值开始有区分度了——它会学到“把推力用在物体靠近目标的方向上Q值更高”。1000个episode之后成功率开始抬头1500个episode能到50%之后的提升速度取决于策略能不能把“推到位”这个动作精确化。fetch_pick_and_place需要更久大概6000到8000个episode才能到70%以上。5.2 训练曲线怎么读Q值虚高、成功率上升速度读训练曲线时我提醒一句别盯着Q值绝对值看。HER重标记会产生大量“成功”样本会让critic在早期就对很多状态动作对给出偏高的Q值看起来像已经收敛了实际成功率还是零。这是HER特性的副作用不是bug。评估时以真实环境的成功率为准Q值只关注它的相对变化趋势——Q值开始分化说明学习有信号了。一个我在第一版实现里遇到的迷惑现象成功率早期会突然跳到20%然后掉回0。原因是future重标记让策略学会了一些局部技能比如“稍微拨动一下箱子”偶尔能把箱子怼进目标区。但这种成功依赖运气和初始条件的巧合并不稳定多跑几个episode就会露馅。这是正常的学习过程不必慌。5.3 为什么future策略实测最强论文里的消融实验也验证了future策略优于final和episode策略。我实测的结论和论文一致。从学习难度上解释future选的新目标和当前状态的时间间隔短通常只有几步远Q函数的Bootstrapping误差小学习曲线平滑。final策略选的目标是整个episode的终点状态离得远前期回归难度大。从信息量上解释future策略每条轨迹能产生多个不同的“近期可达目标”数据多样性高等于给智能体提供了更多“在这个状态下做这个动作就能接近那个目标”的正样本。多样性越高泛化越好。我在FetchReach上还试了把K从4调到1效果明显变差成功率过50%的epoch数翻了一倍。K调到16成功率上限并没有提升反而训练前期Q值异常偏高。所以K4确实是个性价比甜点。6. 常见问题与调试技巧实录6.1 代码级坑achieved_goal取错、done标志设错第一个高频bug是把achieved_goal和desired_goal搞混。achieved_goal是环境返回的“已完成目标”在FetchPush里就是箱子当前位置desired_goal才是本episode要求的目标位置。重标记时只能用achieved_goal当新目标用desired_goal当新目标等于没改reward算出来全是0白做。第二个高频bug是reward计算的接口传参顺序。Fetch环境要求compute_reward(achieved_goal, desired_goal, info)两个参数顺序不能反。我一度以为两个都是3维向量顺序无所谓结果所有奖励都是-1训练直接废掉。Debug方法很简单跑一个已知成功的状态打印compute_reward返回值如果成功状态拿到的不是0那就是传参顺序错了。第三个坑是done标志处理。前面说过一定要跑满episode步数再结束除非环境确认成功。如果你在rollout中遇到doneTrue就break并丢弃后半段轨迹那future采样的候选状态就缺失了后半段重标记效果大打折扣。6.2 效果不达预期的排查清单如果你的HER实现跑了一万个episode成功率还是0按这个顺序查查奖励分布从buffer里随机抽1000条样本统计reward的分布。理想情况下应该有正有负或者至少有相当比例的0和-1混合。如果全是-1重标记根本没有生效。查目标分布看看buffer里存的goal向量是不是五花八门。如果所有goal都一样说明重标记代码没被调用或者add_episode传错了。查Q值打印critic在固定状态上对不同action的Q值。如果Q对action的变化没有梯度actor也学不出来。查归一化确认obs和goal的归一化统计量在更新且没有用全局统计替代running统计。查探索噪声如果前期噪声太小智能体永远在起点附近打转靠运气撞目标是不可能的。前期噪声一定要大最好动作范围的正负边界都能频繁触达。6.3 训练速度优化建议HERFetch环境训练通常要跑2万到10万个episode单机CPU也能在几个小时到一天内跑完但优化一下能省不少时间。我用过两招效果明显。第一招是向量化环境。用gym.vector.make同时开8个或者16个环境并行采集每个episode的采集时间直接除以并行数训练循环里buffer的填充速度大幅提升。注意重标记仍然按每个episode独立做互不干扰。第二招是减少评估频率。评估30个episode看起来很便宜但每5个epoch评估一次训练到一半时累计评估成本可能比训练本身还高。我一般改为每20个epoch评估一次训练早期根本不评估。另外评估时把actor切成eval模式关闭所有噪声否则评估结果会被噪声污染。6.4 复现论文必须注意的三个细节第一个细节是归一化的作用域。baselines里的her用了obs、goal、action三路running normalizer如果你漏了action标准化actor输出的尺度会和环境动作尺度不匹配可能直接导致动作在边界来回震荡。第二个细节是future采样时的时间边界。论文伪代码里是“从当前时刻之后的状态里随机选”很多第三方实现为了省事直接“从整条轨迹里随机选”这在训练效果上确实也能跑通因为它本质上增加了数据多样性但我不推荐因为它引入了“未来信息泄露”理论上不干净。第三个细节是原始目标样本不能丢。有些简化实现为了省代码只存重标记样本结果buffer里没有真实目标的训练数据智能体能学会“推箱子”但学不会“推到我要求的那个位置”。保留原始transition是保证策略最终对齐真实目标的最低要求。最后再分享一个实用性极高的经验在正式跑FetchPush这种难度较高的环境之前先花半小时写个玩具版1D Push环境——一条直线上一个点动作是左右移动目标在右边某处。在这个极简环境里验证HER的重标记逻辑是否正确观察一条失败轨迹是否正确生成了K条“伪成功”样本。这一步能挡掉几乎全部实现bug。等玩具版能跑到100%成功率再把代码切换到Fetch环境你会发现调试成本直接砍掉一大半。我在实际项目中踩过不少坑但每次进入新环境前用玩具任务回归一遍HER逻辑基本都能一次跑通。这套东西后续还能往自动选择重标记策略、多任务共享目标空间的方向扩展底层逻辑不变收益会更大。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →