ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

稀疏奖励强化学习实战:HER事后经验回放原理与PyTorch实现

稀疏奖励强化学习实战:HER事后经验回放原理与PyTorch实现 “hindsight”这个词在算法领域有一个被低估得厉害的含义事后复盘。这恰好是稀疏奖励强化学习里最值钱的一招。把失败经验重新标记成有用样本让智能体从“没做到”里学到“能做到什么”这是DeepMind提出的HER算法Hindsight Experience Replay事后经验回放最核心的出发点。这篇博文就把这套方法从原理到PyTorch实现完整拆开给那些被稀疏奖励卡到怀疑人生的朋友一份可直接上手的实操笔记。我还记得第一次跑机器人抓取仿真时的窘境reward函数写得很“标准”抓到目标给1没抓到给0结果训练了上万步Q值纹丝不动连个残影都学不到。后来换成HER同样的网络、同样的环境几千个episode之后策略就开始像样了。差别不在于网络或超参而是思路换了一个维度别让智能体只从成功中学也要让它从失败中带点东西走。这套方法对做机器人操控、导航、推荐、游戏AI的开发者都非常值得参考。无论是你在用DQN、DDPG还是SACHER都可以作为一套改造成本极低但收益极高的复盘机制叠加进去。下面我从四个角度把这个“事后复盘”讲透整体思路、核心细节、实操实现、以及我踩过的坑。1. 项目概述HER在解决什么1.1 稀疏奖励问题是怎么卡住强化学习的先还原一下场景。你让一个机械臂学习抓取桌上某个固定位置的红色方块奖励设计很直白末端执行器到达目标位置附近给reward1否则reward0。这种奖励设计在强化学习里被叫“稀疏奖励”。它看起来简单干净但它致命的地方在于随机探索几乎不可能碰到成功状态。如果目标位置在三维空间里机械臂末端动作空间是连续的随机碰到的概率趋近于零那么智能体收集到的每一条经验都是0回报没有正样本没有梯度信号训练就像对着墙推车——车不动你也不知道该往哪使劲。在真实任务里情况更复杂。比如开门、叠衣服、插线缆这类任务的目标状态本身都很窄——要么门开了要么没开要么插孔对齐了要么没对齐。稀疏奖励之下agent早期学不到任何“好”与“坏”的相对概念它的策略分布基本就是随机扰动任何一个方向的尝试都没有被鼓励或惩罚于是陷入了“死循环式探索”。稀疏奖励问题的本质并不是奖励函数设计得不好而是目标在状态空间中过于“孤独”。成功样本太少样本效率就无限趋近于零。很多人第一反应是给奖励塑形——中间每一步都加点引导信号。这当然有效果但奖励塑形也有自己的麻烦你要设计很多中间态、猜测什么样的引导是合理的、还要防着agent薅羊毛比如为了靠近目标乱转甚至抖动得奖励。HER走的是另一条路不去手动造中间奖励而是“事后再造目标”。1.2 HER的“事后诸葛亮”核心思路HER的核心想法用一个类比最容易说清你本想去公司附近一家餐馆吃饭结果走错了到了一家看起来也不错的面馆。虽然和原计划不符但你记住“这条路能走到一家不错的面馆”下次想吃面的时候就知道往这走。放到强化学习里就是一个episode虽然没达到预设目标但它一定“达到了某个实际状态”。HER做的就是把这个实际状态当作目标把整条轨迹重新标记成一次成功经验。假设机械臂想抓A点却没抓到最后停在了B点旁边。对任务“抓A点”来说这一整条轨迹全是失败回报全0。但如果把目标改成“抓B点”那这还是一条失败轨迹吗不是。它是一次完美成功——agent确实到达了B点。于是我们就可以给这条轨迹复制一份把目标从A改成B把最后一步的reward改成1存进经验池。这个操作的妙处在于它用一次“失败的探索”凭空制造出好几条“成功的经验”。这些经验虽然对原始任务没有帮助但它们教给agent一个更基础、更通用的知识在某个状态下采取某类动作可以把环境带到一个特定的状态。当所有这类知识积累得足够多agent实际上就学会了一张“环境动态图”再去完成原始目标它只需要知道怎么从这张图里找路径就行。这也是为什么HER特别适合多目标、goal-conditioned的任务。因为它附加的正是“目标”这一维度经验不再只对一个goal有效而是对一条轨迹结束时的真实状态有效。后面所有细节都在围绕这个思想展开。2. 算法细节拆解与实现要点2.1 目标重标记的四类策略怎么选HER论文里给了四种重标记策略final、future、episode和random。它们的区别在于“选轨迹里的哪个状态当作新目标”的方式不同。这里逐个过一遍顺便说说实际用下来的感受。final把轨迹最后一个状态作为新目标。简单粗暴适合最终状态有固定意义的任务比如机械臂末端最终停在哪就追哪。缺点是如果轨迹很长前面一大半状态离目标距离很远这条经验对策略早期的指导意义有限。future从当前时刻往后随机采样k个状态之一作为新目标。这是论文里实测最推荐的方案。原因很自然目标不能离当前状态太远。如果拿最后的成功状态去重标记轨迹开头的transition相当于要求agent一步就从老远跳到终点这太难学了。future策略保证目标总是出现在当前状态之后的某个时间点从时间顺序上更合理学习曲线也更平滑。episode从整条轨迹里随机选一个状态作为目标不受时间先后限制。实现比future简单但因为可能选出“过去”的状态当作目标会造成部分transition目标太远或逻辑混乱。random完全随机从状态空间里采样一个状态基本不推荐因为随机目标命中“有意义区域”的概率太低。我实际做实验时future k4是性价比最高的组合。每一条真实轨迹额外复制4份每份用未来某一步的状态替换原目标。这样经验池里“接近成功”的样本密度一下子高了非常多。有一点值得注意k不能开太大。因为这个现在的4份经验里有一部分目标离当前状态还挺远已经对agent形成挑战了。你复制太多经验池里大部分是噪声很大的“远程目标”反而稀释了高质量近端样本。k选4还是8要看环境步长episode比较长的任务可以适当增大但不要超过一个上限。2.2 网络结构把goal接进value functionHER不是单独的一种actor-critic架构它更多是一种“经验组织方式”。但要把这套机制跑起来网络的输入必须同时包含state和goal。因为重标记之后同一份transition会在经验池里对应两个不同goal的副本原始goal和重标记goal网络必须能区分“我是在追求哪个目标”。最省事的做法是直接把goal拼到state末端形成一个高维向量然后正常输出Q值或策略。这在简单任务里能跑但有一个隐患goal和state的数值尺度可能完全不同。比如state是机械臂关节角度范围[-3.14, 3.14]goal是物体位置坐标范围可能是[0, 5]这种量纲不匹配很容易让网络训练初期重点跑偏。实践中我会分别对state和goal做归一化再拼接到一起会稳很多。更规范一点的做法是采用**UVFAUniversal Value Function Approximator**的思路网络不直接输出单个Q值而是输入state-goal组合输出对“所有goal”的估计——或者说把goal当成输入条件传给网络。在DeepMind的原始实现里网络结构大概是state经过几层全连接得到特征goal经过几层全连接得到目标特征然后两者融合拼接或相加过一个输出层预测Q值。这个结构的好处是让网络学会“基于目标来理解状态价值”而不是简单把两个向量当成一个大向量处理。我在实际项目里两种都试过差异在任务复杂度上去之后才显现简单摆块无所谓复杂操作UVFA训练明显稳。如果你用的是SAC这类连续动作算法actor和critic都需要输入goal条件一般做法是把goal拼进observation后给actorcritic里再用UVFA结构。这里有个工程细节actor和critic里的goal编码层可以共享权重但实践下来各用各的反而更稳定因为两者的loss不同共享权重会互相干扰。2.3 核心代码实现Goal-conditioned DQN HER直接上一个简化版的PyTorch实现框架。这里我用最经典的DQN举例因为结构最清楚换成SAC/DDPG只是把目标拼接和重标记的代码原封不动搬过去。import torch import torch.nn as nn import numpy as np import random from collections import deque class GoalConditionedQNetwork(nn.Module): def __init__(self, state_dim, goal_dim, hidden_dim256): super().__init__() self.state_encoder nn.Sequential( nn.Linear(state_dim, hidden_dim), nn.ReLU(), ) self.goal_encoder nn.Sequential( nn.Linear(goal_dim, hidden_dim), nn.ReLU(), ) self.fuse nn.Sequential( nn.Linear(hidden_dim * 2, hidden_dim), nn.ReLU(), nn.Linear(hidden_dim, 1), ) def forward(self, state, goal): s self.state_encoder(state) g self.goal_encoder(goal) return self.fuse(torch.cat([s, g], dim-1))训练主循环里的经验存储和目标重标记是整篇文章最值得盯紧的部分。大致流程先按普通逻辑跑一个episode把原始轨迹存进一个临时列表。episode结束后遍历里面的每一条transition做重标记然后一次性写入回放缓冲区。def hindsight_replay(episode_transitions, achieved_states, k4): 对一条完整episode做HER重标记返回扩展后的transition列表。 episode_transitions: [(obs, goal, action, reward, next_obs, done)] achieved_states: 每个时间步的真实到达状态 her_transitions [] for i, (obs, goal, action, reward, next_obs, done) in enumerate(episode_transitions): # 原始过渡保留 her_transitions.append((obs, goal, action, reward, next_obs, done)) # future策略从当前时刻之后的轨迹中随机挑k个状态作为新目标 future_indices np.random.randint(i 1, len(achieved_states), sizek) future_indices future_indices[future_indices len(achieved_states)] for idx in future_indices: new_goal achieved_states[idx] # 以“是否到达新目标”为标准重新计算reward这里用简易判定 new_reward 1.0 if np.linalg.norm(next_obs - new_goal) 0.1 else 0.0 her_transitions.append((obs, new_goal, action, new_reward, next_obs, done)) return her_transitions这里有几个容易被代码被忽略但致命的点。第一个是future_indices必须过滤掉越界的索引或者直接限制范围否则重标记目标用的是不存在的状态。第二个是reward重新计算的判定阈值不要拍脑袋定最好用环境里真实目标判定函数否则会出现“目标明明没到但给了1分”的污染经验。第三个是done的重新判断重标记后如果下一状态已经“到达新目标”那么done应该设为True而不是沿用原trajectory的False。如果目标就是“到达某状态”到达了就该终止这条经验才对Q学习没有误导。完整的训练循环里目标网络、经验回放采样这些都是DQN的常规操作。HER真正改动的地方只有两块一是存经验时多存一份“真实到达状态”序列方便episode结束后重标记二是采样训练时把state和goal共同作为网络输入。这两块加进去DQN立刻就有了“事后复盘”的能力。3. 实操过程与参数经验3.1 训练流程与经验回放设计HER训练一个完整流程的推荐顺序是这样的初始化Q网络和目标Q网络经验池。用epsilon-greedy策略跑一个完整episode记录每一步的state、action、reward、goal、next_state。额外记录每一步的“achieved goal”——也就是环境实际转移到的状态中与goal同维度的部分。这里概念很容易混如果goal是“物体位置”那achieved goal就是“物体实际位置”如果goal是“机械臂末端位置”achieved goal就是“末端实际位置”。它们的维度必须完全一致否则重标记没法做。episode结束后调用hindsight_replay把原始transition和重标记transition一起塞进经验池。从经验池里随机采样一个batch计算Q目标reward gamma * max(Q_target(next_state, goal))。注意这里的目标值里也带着goal因为网络是goal-conditioned的。正常梯度更新周期性同步目标网络。经验池的设计有个讲究。HER会产生大量额外经验一个episode存4份重标记副本整个buffer里重标记经验的比例就是80%。这时候如果全部混合采样原始目标的经验会被淹没。但实际里这通常不是问题因为原始目标经验难学重标记经验提供基础动力学知识两者互补。如果你想更精细可以像优先经验回放那样给原始经验稍微加一点采样权重。我试过把原始经验权重设为1.2倍训练速度确实有小幅提升但提升幅度不大不值得为这个引入复杂度。回放缓冲区的容量也要根据经验数量调整。HER agent一个epoch产生的经验是普通经验的k1倍假设每episode 100步1万episode就是100万条普通的经验加上重标记就是500万条。如果buffer设太小比如10万大量旧经验被新经验挤出去训练的稳定性会下降。建议buffer容量至少能覆盖最近5000个episode产生的所有经验。3.2 超参选择k值、buffer比例、目标取样区间HER涉及几个关键超参这里把经验值直接摆出来方便大家参照调参。k每步重标记数我推荐4。k2时重标记样本太少样本效率提升不明显k8时经验池膨胀太快而且很多重标记目标离当前状态太远学起来开销大。k4是论文里测试过的值也是我实测下来的甜点。目标取样区间future horizonfuture策略里新目标从“当前时刻之后的所有状态”里选。如果你的episode特别长比如200步老远的未来状态和当前状态差太多一条transition里“起始状态和目标”的距离跨度太大。建议限制只从当前时刻到最后时刻的前80%区间里采样或者干脆从当前时刻往后固定N步比如N30的范围内采样。这个改动在长episode任务里提升明显。HER经验的比例如果你想单独调节可以在写入buffer时以一定概率丢弃重标记经验。实践中我一般保持全部写入因为Q学习本身会泛化不需要手动控比例。reward阈值判断“是否到达目标”的阈值非常影响效果。定得太大虚假成功太多定得太小成功样本依然稀少。建议至少参考环境本身的误差容忍度——比如物体抓取位置精度是多少阈值就设在那个量级附近。调参的时候有个共性建议先不改任何网络结构只调k和reward阈值观察Q值曲线和成功率的对应关系。如果成功率上来了但Q值还在震荡通常是threshold导致的reward偏差如果Q值很稳定但成功率低通常是目标分布太广、网络没辨清。3.3 计算代价与工程优化一个说烂但还是要强调的点HER不增加任何策略网络的计算量代价全在“经验重标记和存储”这一层。以k4为例训练一条episode后你写入buffer的经验数量是原来的5倍。这带来两个问题一个是内存翻倍一个是采样时的IO压力。工程上我有三个实用优化方案。第一个方案是把重标记结果用numpy结构化数组预存不要用Python的list of tuple否则在大buffer下采样速度会变得不可接受。第二个方案是不对每一条transition都做重标记而是每隔几步才做一次。比如每5步生成一份重标记目标经验数量从5倍压缩到2倍多一点训练效果损失很小。第三个方案是懒重标记先只存原始经验训练时随机选一部分轨迹用HER逻辑在线重标记。这个对代码侵入更大但在经验池极大时能省大量内存我在实际工程里用过唯一要注意的是每次采样时多传一个“goal序列”代码复杂度上升不少。训练速度和算法选择上HER配合DQN在离散动作任务里很稳但连续控制任务我还是推荐SACHER。SAC自带熵正则探索性更强配上HER的经验复用能处理更复杂的操作任务。区别主要在actor和critic的输入拼接方式上前面2.2节提到的UVFA结构可以直接套过去。4. 常见问题与避坑心得4.1 训练不收敛排查速查表HER做起来不算复杂但问题也不少。这里整理了我实际跑课时遇到的高频问题直接以速查表形式给出。问题现象大概率原因处理方法训练前期Q值完全不动goal和state拼接后量纲差异过大分开归一化state和goal再拼接成功率上去了但Q值不收敛reward阈值过大虚假成功太多调小阈值或换成环境真实判定函数重标记样本没起什么作用new_goal维度与achieved state不一致检查goal维度与状态维度是否严格对齐训练中段突然崩溃经验池过大采到大量尺度异常样本减小buffer容量或对经验做标准化重标记目标太远学不动future采样范围太大限制目标只从未来N步内采样效果比不用HER还差done标志没重设重标记后到达新目标应把done置True这表里最容易被忽略的是最后一行。很多人改了reward漏该done结果agent明明已到达新目标却因为它“还在旧任务语境里”继续当作未完成状态Q值更新逻辑就乱了。这个bug在调试时很难找因为它不影响loss数值但影响整个训练的方向。我建议排查时直接打印几条重标记transition肉眼核对reward和done是否一致。另一个高频问题出在多目标泛化能力上。HER本质是教一个goal-conditioned策略所以最终效果好坏取决于网络区分不同目标的能力。如果所有goal在表征空间里差异很小重标记经验就是纯噪声。遇到这种情况我会先检查即将送入网络的目标维度是否包含了足够多的区分信息。比如目标是“位置坐标”只有x和y两个数在平面简单任务里没问题但在复杂环境里建议把物体相对位置、速度等关键信息都写进goal表征否则agent会迷失在高相似度目标里。4.2 容易被忽略的几个细节关于“利用未来信息”的适用性。有一条值得单独拿出来讲HER重标记用到了“未来的状态”当目标很多初学者担心这会不会造成信息泄漏。这个顾虑本身合理但在ER算法背景下是没问题的。因为回放经验已经是一次完整实现之后的离线样本我们用“事后知道的结果”来重新组织目标恰好是HER的核心目的而在policy evaluation时网络接收的是“实时状态当时的目标”并没有偷看未来。理解这一点能避免很多人把自己绕晕。关于归一化的时间点。state和goal的归一化必须在replay buffer层面做而不是在环境层面直接改数值。因为重标记产生的goal来自经验池内部必须和网络训练时的取值范围保持一致。如果你在环境里就缩放好那没问题但如果你把归一化放在网络输入层那重标记的goal也要经过同一套归一化否则分布不一致网络泛化能力会很差。关于真实机械臂迁移。如果要部署到真实机器人HER仿真训练出来的策略直接迁移通常会摔跟头。一个可行路线是在sim里调通k值和reward阈值然后做domain randomization把物体位置、摩擦系数、末端执行器的初始偏差都加随机扰动。HER的goal-conditioned特性决定了它对“同一目标从不同初始状态出发”的泛化能力比较强这正好适合应对sim-to-real的分布偏移。但这个话题展开又能写一整篇只说一个关键经验迁移时不要重训HER只用它学到的goal-conditioned策略再叠加一个低层的位置控制器去做插值平滑成功率会比直接用原始策略高很多。关于训练时的目标采样。训练时每次从buffer里采样batch里头的goals来自两部分原始目标和重标记目标。它们的reward分布差异很大原始目标几乎全是0重标记目标则有一小部分为1。这个不平衡是正常的不需要额外处理。真正要注意的是Q update里对重标记经验计算目标价值时用目标网络输出时要按“该条经验对应的目标”来取max。如果每条经验的goal不同必须向量化地分别取不要混用同一个goals张量否则你会在训练中不断累积错误偏差。写在最后的一点体会从第一次听说HER到真正用它跑通机械臂任务我最深的感触是强化学习里很多看起来是“算法能力”的问题本质上是“经验结构”的问题。稀疏奖励也好、探索效率也好很多方法在往“怎么更聪明地探索”方向使劲而HER换了一个成本低得多的思路——不改变探索策略把已经探索过的经验重新挖一遍价值。这种“事后复盘”的逻辑放到真实工作里其实也说得通多复盘失败路径从中找到可复用的规律往往比闷头尝试下一次成功更高效。如果你正被某个稀疏奖励任务卡住不妨先别急着写更复杂的奖励函数先让agent学会利用已有的失败样本。用我项目里的经验来说这一个改动常常比换任何高级算法都更快见到效果。
返回列表