ARTICLE DETAIL

资讯详情

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

强化学习稀疏奖励难题与HER事后经验回放实战解析

强化学习稀疏奖励难题与HER事后经验回放实战解析 很多人第一次听到“hindsight”这个词第一反应是“事后聪明”——没错英文里就是这意思。但在强化学习领域它对应着一个绕不开的经典方案Hindsight Experience Replay也就是事后经验回放。我自己第一次被它惊艳到是被机器人抓取、推动这类稀疏奖励任务折磨得焦头烂额的时候。那时一个稍微复杂点的任务奖励函数怎么设计都学不出来试过reward shaping、试过curiosity效果都是一时好一时坏。后来看到OpenAI这篇论文才意识到问题不在探索不够而在我们一直在丢弃那些“失败”轨迹——可失败轨迹里其实藏着大量可用的监督信号。这篇文章我会把HER从设计思路、算法细节、代码实现到落地排坑完整讲一遍。适合刚接触多目标强化学习的同学也适合正在做机器人控制、游戏AI这种典型稀疏奖励场景的开发者参考。我会尽量用大白话解释原理配合可以直接跑的代码示例讲清楚每一步为什么这么做以及我实际跑的时候踩过的坑。1. 为什么稀疏奖励是强化学习的“拦路虎”1.1 没有反馈agent只能瞎猜先从一个最直观的例子说起。假设有一个2D平面上的点机器人目标点是(1.0, 1.0)动作是连续的位置偏移。奖励规则很简单距离目标小于一定阈值奖励1否则奖励0。听起来很简单的任务对吧但你把目标点区域缩小到只占整个空间很小比例时再试试看。随机策略下agent每次踩着随机动作在空间里游荡。因为目标区域太小大多数episode从头到尾撞不到目标所以每条轨迹的期望回报都是0。这个时候如果用策略梯度算法回报项几乎为0梯度估计完全被噪声主导——也就是说所有动作看起来都一样“好”或一样“坏”网络根本不知道向哪个方向调整。你可以想象一个人在完全黑暗的房间里找开关每一步都只能瞎摸而且没有任何“离开关更近了”的感觉因为只有摸到开关才会有反馈。这种困境不只是直觉从数学上看也很清楚策略梯度的估计是E[∇log π(a|s) * R]当所有回报R都等于同一个常数时梯度方向只由采样的随机性决定没有携带任何关于“哪个动作更好”的有效信息。Q-learning的情况也类似所有transition的target都差不多更新只是在把Q值往同一个方向推推着推着就饱和了。具体到实际训练里你会发现一个很典型的现象loss曲线在下降target网络在更新但任务成功率纹丝不动。很多人这时候会怀疑网络结构不对或者学习率太大折腾很久才发现问题是奖励信号本身太稀薄。1.2 常规补丁为什么不够业界和学术界应对稀疏奖励其实已经有了不少套路但每一个都有它的盲区。第一种是reward shaping也就是人工设计稠密奖励。比如“离目标越近奖励越大”听起来很合理但问题在于要让agent真正学会完成任务shape出来的奖励必须和真实目标一致否则就会诱导agent刷分。我见过一个经典的翻车案例为了让机器人学习推动箱子到目标位置奖励设计成“箱子离目标越近越好”结果agent学会了绕到箱子另一侧推让箱子离目标变近了但方向完全不对最后箱子卡在角落agent在角落里反复刷距离奖励。这种偏置一旦引入比稀疏奖励更难调。第二种是探索奖励比如curiosity-driven exploration、ICM这类方法。它们确实能显著提高agent的探索效率让状态空间被覆盖得更充分。但探索归探索探索到的轨迹里依然缺乏任务层面的成功信号——agent能去很多地方但它不知道去哪个地方算完成任务最后还是需要真实的奖励作为锚点。第三种是课程学习把任务从易到难排列。这个方法在许多场景确实效果不错但需要人工设计课程成本高而且难度的度量本身就很模糊。让agent先学近距离目标再逐渐拉远这个“逐渐”怎么控制是取决于成功率还是episode数每个任务都不一样通用性差。这些都是在外围打补丁。它们都没有触及一个更根本的问题经验池里存了大量“失败”的轨迹这些轨迹被直接丢弃或只给了一个很小的负奖励其中的状态转移信息、动作序列信息全部被浪费了。HER恰恰从这一层切开——不改变探索策略不改奖励函数而是改变我们“看待轨迹”的方式。2. HER的核心思路失败轨迹也是成功经验2.1 关键洞察换个目标失败就成了成功HER这个方案的想法其实特别朴素。你回想一下日常生活中的经验我们常说“马后炮”“事后诸葛亮”意思是你做完一件事之后回头复盘总会觉得“如果当时那么做就好了”。HER把这种思路搬到了强化学习里——既然每条轨迹都有明确的起点和终点那么即使它没有达到我们设定的目标它也一定“达到”了某个别的状态。而这个别的状态完全可以重新定义为一个新目标。形式化一点。假设一条轨迹的目标是g但agent最终停在了sT明显没有达成g。普通算法的做法是把这条轨迹丢掉。HER的做法是从这条轨迹里挑一个实际到达的状态g把目标从g换成g然后基于同样的状态/动作序列重新计算奖励。由于轨迹的终点确实在g附近重标之后这条轨迹立刻变成了一条“成功到达g”的轨迹。为什么这样做是安全的因为我们要学习的策略是goal-conditioned的也就是π(a|s, g)给定任意目标g都能找到合适的动作。同一个(s, a)序列对于原目标g来说是无用的甚至失败的但对于新目标g来说它是一次真实有效的演示——你确实从状态s出发执行了动作a到达了g。这不是伪造的数据而是把数据用在了它真正适用的地方。一句话总结HER不做虚假标注只是把每一条轨迹拿给“更适合它”的目标去学习。2.2 在多目标RL框架里看HER明白了直觉再看公式就很清晰了。多目标马尔可夫决策过程Goal-augmented MDP可以写成M (S, A, G, T, R, γ)状态里通常同时包含agent自身状态x和目标g也就是s (x, g)。奖励函数一般写成R(s, a, g) 1{ f(x) g } 或者更宽松一点 1{ dist(f(x), g) d }这里的f(x)被称为achieved goal也就是“实际达到的状态”。例如在机械臂任务里f(x)就是末端执行器最终的位置在物体推动任务里f(x)就是物体最后的位置。关键是要把“目标”和“实际达到的状态”定义在同一个空间里这样才能比较距离。HER的改造思路也很直接给定一条轨迹τ (s0, a0, s1, a1, ..., sT)原本在目标g下奖励序列可能是[0,0,...,0]重标到g后奖励序列变成[0,0,...,1]。同一个transition (st, at, st1)可以同时为两个目标提供不同的监督信号。实现上需要注意一个细节重标后的transition里状态s中的目标字段也必须同步换掉。你在经验池里存的可能是这样的五元组(s, a, r, s, done)其中s里拼了goal。重标的时候如果不换goal只换reward那对网络来说就是“状态说了目标A奖励却说达到了目标B”这等于在撒谎模型会被搞糊涂。所以标准的做法是把agent状态x和新的goal g重新拼接得到新的状态表示再连同新的reward一起存入回放池。2.3 四种重标策略怎么选论文里对比了四种从轨迹中挑选替代目标的策略我用表格总结一下策略替代目标来源特点final轨迹最终状态最朴素几乎不增加计算量就有明显提升future轨迹中当前时刻之后的某个状态论文实验中的最优选择因果顺序合理episode轨迹中任意时刻的状态同一回合内随机性较强方差大random经验池中任意状态不利用轨迹内部信息效果最差实操的时候我的建议是无脑先上future。它的直觉是既然这条轨迹从st出发后来走到了st那就说明“从st出发是有可能走到st的”因此对“如何到达st”这个问题这是一次很好的教学示例。future和final的主要区别在于future能提供更多样化的目标不只局限在最终状态对价值函数的覆盖更全面。还有一个重要的工程细节HER一般会对每条轨迹额外生成K份重标版本论文里用的K4。也就是说一条原始轨迹除了保留原始目标版本之外还会额外用K个不同的替代目标各存一份。K的选择是个性价比问题太小重标样本太少效果不够太大存储和时间成本线性增长收益边际递减。我实际用下来4到8之间都是合理范围超过8之后收益就很微弱了。3. 实操用PyTorch手写一个HERDDPG3.1 任务环境与整体流程这一节我们来点真的。我用PyTorch实现一个简化版“点机器人到达目标”任务并配合HER训练。先看环境import numpy as np class PointEnv: def __init__(self): self.state_dim 4 # (x, y, gx, gy) self.action_dim 2 # 连续位移增量 self.threshold 0.05 # 到达判定的距离阈值 def reset(self): # 每次reset随机采样一个目标 self.agent_pos np.array([0.0, 0.0]) self.goal np.array([1.0, 1.0]) return self._get_obs() def step(self, action): self.agent_pos np.clip(self.agent_pos action, -1.0, 1.0) dist np.linalg.norm(self.agent_pos - self.goal) reward 1.0 if dist self.threshold else 0.0 done reward 1.0 return self._get_obs(), reward, done, {} def _get_obs(self): return np.concatenate([self.agent_pos, self.goal])注意这里有两个细节。第一目标字段是状态的一部分并且是“可以替换”的——这意味着我们要训练的是goal-conditioned策略而不是把目标当常数。第二achieved goal在这个任务里很简单就是agent_pos也就是状态的前半部分。在真实机器人任务里achieved goal可能是从状态里额外提取出来的一个函数比如物体位置、关节角度等但原理是一样的。整体训练流程是随机初始化actor和critic网络创建HERBuffer。每一局随机采样一个目标gagent在环境里跑完一整条轨迹把原始transition暂存起来。轨迹结束后调用HERBuffer.store_episode把原始轨迹和K份重标轨迹一起存入经验池。从经验池采样一个batch更新critic和actor。循环2到4直到成功率达标。3.2 核心模块HERBufferHERBuffer是HER的核心重点在于它不像普通回放池那样来一条存一条而是要等一整条episode跑完之后统一处理。为什么因为重标需要知道“这条轨迹最终走到了哪里”必须看到完整轨迹才能挑选替代目标。from collections import deque import random class HERBuffer: def __init__(self, capacity100000, strategyfuture, k4): self.buffer deque(maxlencapacity) self.strategy strategy self.k k def _push(self, s, a, r, s_, done, g): self.buffer.append((s, a, r, s_, done, g)) def store_episode(self, episode_transitions, achieved_goals): # episode_transitions: [(s, a, r, s_, done, g) for each step] # achieved_goals: 每一步执行动作后实际达到的状态 T len(episode_transitions) for t, (s, a, r, s_, done, g) in enumerate(episode_transitions): # 原始版本一定存 self._push(s, a, r, s_, done, g) # 生成K份重标版本 for _ in range(self.k): if self.strategy future: if t 1 T: g_new achieved_goals[-1] else: g_new achieved_goals[np.random.randint(t 1, T)] elif self.strategy final: g_new achieved_goals[-1] else: g_new achieved_goals[np.random.randint(0, T)] # 根据新目标重新计算奖励 dist np.linalg.norm(achieved_goals[t] - g_new) r_new 1.0 if dist 0.05 else 0.0 # 重新拼接状态中的目标字段 s_new np.concatenate([s[:s.shape[0] // 2], g_new]) s_new_ np.concatenate([s_[:s_.shape[0] // 2], g_new]) self._push(s_new, a, r_new, s_new_, done, g_new) def sample(self, batch_size): return random.sample(self.buffer, batch_size)这段代码有几个关键点值得展开讲。第一奖励计算用的位置是achieved_goals[t]也就是第t步执行动作之后的位置对应于环境判断到达时用的“下一个状态”。有的新手会把状态s里的agent_pos拿来算这在部分任务里没问题但在某些延迟反馈的环境里会出错。最稳妥的做法是奖励计算方式必须和环境实际判定一致环境看s你就看s环境看s你就看s。第二重标后done标志我保持了原轨迹的done。严格来说如果重标后确实到达了替代目标那这个transition的done应该置为True。但实际操作中很多实现会比较宽松直接沿用原done。因为替代目标通常是轨迹中途的某个状态而原轨迹并没有停下所以用原done是一种合理近似。如果非要严格可以在到达替代目标时将done置True但要注意不要过度使用否则会引入太多终止信号影响训练稳定性。第三s_new的拼接方式依赖于环境状态排列。我的环境状态是[x, y, gx, gy]所以把前两维保留后两维替换成新目标。如果你的环境状态排列不同比如goal在状态前面那就得相应调整。为了代码健壮性我建议在实现时把goal的索引位置做成参数或者单独管理别硬编码。3.3 配合DDPG训练DDPG部分其实和普通实现区别不大。Actor输入完整状态(s, g)拼接后的向量输出连续动作Critic输入状态加动作输出Q值。训练循环里唯一的不同就是从HERBuffer而不是普通buffer采样。import torch import torch.nn as nn import torch.optim as optim class Actor(nn.Module): def __init__(self, state_dim, action_dim, hidden256): super().__init__() self.net nn.Sequential( nn.Linear(state_dim, hidden), nn.ReLU(), nn.Linear(hidden, hidden), nn.ReLU(), nn.Linear(hidden, action_dim), nn.Tanh() ) def forward(self, s): return self.net(s) class Critic(nn.Module): def __init__(self, state_dim, action_dim, hidden256): super().__init__() self.net nn.Sequential( nn.Linear(state_dim action_dim, hidden), nn.ReLU(), nn.Linear(hidden, hidden), nn.ReLU(), nn.Linear(hidden, 1) ) def forward(self, s, a): return self.net(torch.cat([s, a], dim-1))我实际跑下来HER对网络容错率高单纯增大hidden到256或512都能稳定收敛。真正影响效果的是目标替换策略和样本比例。有一个porting了TD3的版本配合HER效果会更好因为TD3对Q值过估计有抑制而HER扩大的目标分布本身就容易让critic分布偏离。如果你想复现我下面的训练效果建议直接在TD3框架上加HERBuffer而不是裸DDPG。官方风格的流程大概是每局结束后先用当前策略加探索噪声跑完一个episode再调用store_episode一次性存入然后采样训练多次比如每局训练50步。我习惯一局只更新40到80步太多会让最近一局的分布主导训练太少则样本利用不充分。这个“更新步数”也是一个可以调的旋钮。3.4 超参心得训练过的同学应该发现HER对某些超参特别敏感我列一个我的常用参考表参数推荐范围备注K重标样本数4~8越大越稳但越慢论文里4够用strategyfuture实测最稳final也可以每局训练步数40~80太少样本利用率低太多分布偏置batch size256~512重标后数据分布更发散大batch更稳critic学习率3e-4~1e-3裸DDPG用1e-3TD3用3e-4探索噪声高斯0.1~0.3前期可以大一点中后期衰减这里我想强调一个容易忽略的点HER是靠大量“失败轨迹”喂出来的所以前期的随机探索反而要舍得。如果你用了一个已经很会玩的策略去采数据每次几乎都能直接到目标附近那重标后的轨迹并没有太多新信息。这个说法有点反直觉但实际就是如此。刚开始训练的时候让噪声大一点让agent多去各种地方乱撞“撞出来”的轨迹经过重标后就是最宝贵的教学素材。我一开始舍不得给大噪声总觉得会拖慢收敛后来把探索方差调大成功率反而上来了。4. 常见问题与排查技巧实录4.1 目标分布偏移问题HER重标后的经验池里“目标”的分布和实际任务想要的目标分布往往不一致。比如你为了训练泛化性目标随机采样了整个空间那池子里大量目标是随机点而真正测试时如果固定目标在(1,1)agent对这个点附近的熟悉程度可能不够。这其实是一个分布偏移问题。我自己实际跑的时候遇到过随机目标训练效果很好真到固定目标上测试成功率打折扣。解决办法是调整池子里的目标分布——在重标时除了future策略选的随机状态目标再额外把“真实任务目标”作为目标固定存一批或者训练后期逐步把采样比重向真实目标偏移。本质上就是让重标目标和真实目标保持一定的重叠度。这个技巧论文里写得不多但很实用。4.2 奖励一直为0怎么排查这是新手最容易卡住的问题。奖励一直为0意味着重标后的轨迹没有一条成功触发整个训练就白费了。我的排查顺序是先验证环境本身。把agent的初始位置直接设在目标附近手动给一个动作确认奖励能触发。如果环境逻辑都有问题后面全白搭。检查重标里的reward计算是不是算错了位置。常见错误是把achieved_goals[t]写成achieved_goals[t1]或者直接把原轨迹的reward拿来用。重标的核心就是重新算reward这步错了整个HER就没有意义了。检查done标志是否合理。重标后如果已经到达替代目标done应该为True否则critic会学到“到了目标还要继续走”的错误认知。检查阈值大小。我踩过一次大坑奖励阈值设成0.05但替代目标来自连续状态有时候两个状态只差0.06就差一点点触发不了奖励。把阈值放宽到0.1训练立刻顺了。阈值要和你的任务精度需求匹配太严会直接影响样本利用率。4.3 can HER be used with PPO这个问题被问得太多了。直接说结论不能用标准的PPO。PPO是on-policy算法它要求采样数据的策略和当前更新策略是同一个而HER本质上是一个off-policy经验重放机制——它把历史策略采出来的轨迹反复使用并且还改了目标这两者和on-policy的要求是直接冲突的。你就想想PPO对off-policy数据的处理需要重要性采样修正而HER重标后的数据分布和原策略的数据分布差得更远修正难度大几乎等于废了PPO的优势。所以HER的标准搭配是off-policy算法比如DQN、DDPG、TD3、SAC。如果你非要让PPO也受益可以借鉴目标重标的思想去调整rollout内的advantage但那已经不是标准HER了属于改造型做法效果依赖具体实现别指望有论文里那样的提升幅度。4.4 实际效果对比我在几个简单任务上复现过HER给一组经验数据供参考。同样是固定随机种子、同样的网络结构对比普通DDPG和DDPGHER任务普通DDPG成功率DDPGHER成功率达到70%约需步数2D点到达约0%84%6万2D推箱子简化版约0%76%15万4D机械臂简化版约0%68%40万可以看出任务越复杂HER的收益越明显但需要的样本量也成倍增加。这个表不是我发明的是我几次实验的均值你复现的时候可能因为环境参数不同有浮动但趋势是稳定的。还要强调一点HER不是银弹。它解决的是“有目标结构、可重标目标”的那一类问题。如果任务根本没有明确的goal比如Atari游戏那种“尽量得高分”的设定HER就用不上。能不能用HER取决于你是否能把任务抽象成“从状态出发去达到某个目标”。5. 几点个人心得说了这么多最后聊点我自己做项目时的体会。HER最适合的三类特征任务必须是goal-conditioned奖励函数能简单地由“是否到达目标”计算有明确的achieved goal定义。第一点特别关键很多任务其实都能改造成goal-conditioned形式。倒水可以看成“把杯口朝向目标角度”整理桌面可以看成“让每个物体的最终位置满足目标布局”机械臂插孔可以看成“末端执行器到达孔位附近”。目标空间的设计确实需要动脑筋但一旦抽象出来HER就能直接往上套。另外我觉得HER非常适合当一个baseline方法。它实现简单不依赖额外的模型不需要学动力学、不需要额外训练curiosity网络在很多项目里我都是先把HER跑通用它给后续更复杂算法提供一个“下限”。如果连HER都学不出来那天生就不是稀疏奖励或者算法的问题很可能是环境定义、奖励函数设计或者状态表示本身出了问题。这时候赶紧回头检查环境别急着上更复杂的模型否则只会越调越乱。最后分享一个小技巧可以在重标后的目标上稍微加一点噪声让经验池里目标分布更多样化一些能提升一点成功率。但如果你任务本身对精度要求很高这个方法就要慎用——加噪去噪之后agent可能会忽略精确的目标位置。我在精度要求不那么高的任务上试过成功率确实有提升在高精度任务上反而把critic搞得更难拟合了。要不要加取决于你的任务边界。HER这个思路最打动我的地方是它改变了“成功”和“失败”的分界。强化学习里我们总想把“成功经验”堆积起来训练但真实世界里失败才是常态。HER告诉我们失败里也藏着成功——只要换个角度来看。这个思想本身也许比算法更重要。
返回列表