ARTICLE DETAIL

资讯详情

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

稀疏奖励如何破局?Hindsight Experience Replay实战解析

稀疏奖励如何破局?Hindsight Experience Replay实战解析 训练机械臂抓东西反馈一路全是0你就能体会到什么才是真正的hindsight——事后聪明。我去年在仿真环境里跑一个七轴机械臂抓取任务连续三个通宵奖励曲线纹丝不动一次正反馈都没出现过。后来把思路换成后见之明既然当前目标够不到那就把我们实际到达过的状态重新当成“目标”再学一遍那些看似废掉的失败轨迹突然就有了价值。这套东西在强化学习里叫Hindsight Experience ReplayHER专门用来砸稀疏奖励的场子。这篇文章是我实际项目里的完整记录适合正在做稀疏奖励任务、被goal-conditioned策略逼到怀疑人生的同行也适合刚接触HER、想搞懂它到底在做什么的新手。1. hindsight是个什么思路失败经验也是黄金1.1 稀疏奖励问题的困境先复现一下最让人抓狂的场面。你设计了一个目标条件强化学习任务机械臂需要把方块推到指定位置。环境只给一种奖励方块和目标位置的距离小于某个阈值奖励1否则奖励0。这类奖励不是没有信息量而是信息量实在太稀疏。一百万步里智能体可能一次成功都没碰到过那么整个轨迹的经验都是“这不合适”没有任何“这么做更好”的梯度策略就会一直原地打转。这个问题和考生蒙大题的逻辑是一个路数。如果平时只做满分或零分两种批改并且满分标准是“在月球上写对答案”那考十辈子也得不到一次正向反馈自然谈不上改进。稀疏奖励的难点不是模型收敛慢而是探索完全失去了方向。很多任务看似简单实际上动作维度高、初始状态随机用随机策略偶尔命中目标的概率低到可以忽略。传统的破解方式无非是奖励塑形reward shaping人为加一些中间指标比如“离目标越近奖励越高”。但奖励塑形需要大量领域知识而且规则设计不好会诱导智能体钻空子做出一些在指标上好看、实际却不可用的诡异行为。我见过不少人把精力耗在调reward shape上最后发现环境稍微改一下就要从头再来这根本不具备泛化能力。1.2 事后聪明为什么能改变学习信号HER的想法非常反直觉既然没抓到目标那就把“当前实际抓到的东西”当目标重新学一遍。比如机械臂想把方块推到A点结果一路猛推把方块停在了B点。这个过程中机械臂其实完成了一次“把方块推到B点”的任务。如果我们把这次轨迹的目标改写成B点这次失败的轨迹就变成了一条成功的演示。这就是后见之明站在事后看到了这条轨迹真正的价值。放到学习信号上说原来的轨迹在goal A下全是负样本reward0重标成goal B后轨迹内至少有一部分状态已经满足goal B的条件于是这些状态会得到正向奖励策略开始知道在“目标是B”的情况下做出什么动作是有用的。等积累了足够多“各种实际位置当目标”的经验智能体对目标空间的覆盖会越来越密当它再次面对真正的目标A时即使在执行过程中没有立刻到达A也能利用大量过去“接近并到达某一点”的经验逐渐调整策略。这种做法的本质是换了个角度看数据而不是换个奖励函数。它没有人为编造与任务无关的中间奖励所有奖励依然来自环境真实反馈只是我们把目标字段做了重新标记。所以在很多稀疏奖励benchmark上HER比精心设计的reward shaping更稳因为它不需要你有任务先验几乎可以插到任何目标条件算法里当外挂。2. Hindsight Experience Replay 的核心设计与细节2.1 从“目标达成”到“目标接近”的转换要把HER落实到代码里第一件事是把问题定义成多目标强化学习的标准形式。我们有一个状态s一个动作a一个目标g策略是π(a|s,g)价值函数是Q(s,a,g)。环境的每个transition包含五样东西当前状态s_t、动作a_t、奖励r_t、下一状态s_{t1}、目标g。同时为了重标注我们最好还要从状态里提取一个“已达成目标”achieved goal。比如抓取任务里achieved goal就是当前机械臂末端位置或者说方块位置。目标g是一个和achieved goal同维度的向量。原始的稀疏奖励可以写成def compute_reward(achieved_goal, desired_goal, threshold0.05): distance np.linalg.norm(achieved_goal - desired_goal, axis-1) return (distance threshold).astype(np.float32)HER的套路是在真实目标g之外另找一个新目标g用这个g重新计算这条transition的奖励然后把重标后的transition存进经验池。关键点在于state和action不变变的只是“目标”这个输入字段和对应的奖励。这相当于告诉模型在这样一条状态下你要达成的是这个新目标。这里有一个很多新手容易绕晕的地方HER不是把reward变得更稠密而是把“成功”的机会变多。因为目标换成实际到达过的状态后轨迹中更有可能出现“当前状态达到目标”的时刻正样本的比例因此提高。如果之前一万步都见不到一个正反馈重标后可能一百步就能见到好几个学习的效率自然上去了。2.2 为什么future策略是首选k个额外目标的采样逻辑重标目标的来源有好几种策略原论文里对比过final、episode、random和future。核心问题是给定一条轨迹我们要从哪些状态中挑选新目标final只用轨迹最终状态作为新目标。episode用轨迹中所有状态作为候选随机选一个。random从整个状态空间随机采样一个目标。future从当前时间步之后的某个状态中采样一个目标。实际跑下来future策略是最稳、最常见的。原因在于它不存在“时间穿越”的问题。如果选一个t1时刻之前的状态当目标那t1时刻还没到达过这个目标这个“成功”就是不真实的容易给策略传递错误信号。future策略选的是未来才出现的状态意味着轨迹确实走到了那个状态而在此之前的所有transition都是在朝这个目标靠近的路上训练出来的变化过程是符合因果关系的。具体操作里有一个超参数k作用是为一个transition额外生成多少个重标样本。通常取k4。也就是说轨迹里的每一步都会额外采样4个来自未来的状态作为候选目标加上原来的原始目标这条transition会被写进buffer最多5次。k不是越大越好。k太大经验池里全是“事后聪明”的数据原始目标被淹没k太小时正样本增幅又不够明显。论文和一些复现实验里k4是性价比很高的点但如果你环境的状态维度特别高或者未来轨迹特别长可以尝试k2或k6这一步值得做一次线性搜索。我在自己项目里经常遇到一个问题一条episode可能很短比如只有二三十步。这样future候选集不够大采样会有偏差。解决办法是把k设置成min(k, len(future_indices))至少保证不因越界报错另外不要replaceFalse导致采样数量不够处理这个边界逻辑是很多隐藏bug的来源。2.3 目标重标注的完整流程把HER放进agent里的流程可以拆成五步智能体在环境中跑一个episode记录每一步的s_t、a_t、achieved_goal_t、goal_t。episode结束后对每一步t先保留原始transition (s_t, a_t, r_t, done, goal_t) 进buffer。找到t之后的所有未来时间步索引即[t1, T]。从这个候选集合中随机挑min(k, len(candidate))个索引。对每个索引f取出achieved_goal_t作为新目标g用compute_reward(achieved_goal_t, g)计算新奖励生成一条新transition (s_t, a_t, r_t, done, g)同样进buffer。注意第五步里我们重新计算的奖励针对的是当前状态s_t和当前动作a_t下的achieved goal不是future状态自身。很多人会在这里写错把future状态的achieved goal当成当前transition的奖励目标去算导致正样本满天飞策略基本被污染。整个流程看起来简单我第一次实现时却犯了几个低级错误。最典型的是在采样future索引时没有排除当前步导致把当前状态当成未来目标重复添加另一个是把done原样复制到重标样本里导致target网络在计算TD时就出错了。这些细节点不写进paper但会直接影响你的训练曲线长成什么样。3. 实操在PyTorch里把HER接到DDPG上3.1 环境、状态与目标定义我用的是类似OpenAI Fetch的仿真环境机械臂末端需要把立方体推到目标位置。状态s包含七个部分机械臂关节角度、关节角速度、末端位置、末端线速度、方块位置、方块线速度、还有相对位置。目标goal一般只需要方块目标位置三维向量。achieved goal也取方块当前三维位置。输入到actor和critic的观测通常做法是直接把state和goal拼成一个向量。对于图像输入的任务处理方式会不一样需要把目标编码成额外通道但核心逻辑不变。网络结构没有花活三层MLP每层256个神经元中间用ReLUactor输出层用了tanh限制动作范围。一个我特别想强调的坑state和goal归一化。Fetch环境里位置在0到1左右速度却可能到几直接拼接会让critic在计算时对位置维度的敏感度下降。我第一次直接在原始尺度上跑效果很差。后面把state和goal都做了clip和scale让每个维度基本落在[-1, 1]区间学习效率一下子提了上来。3.2 HindsightReplayBuffer代码实现下面给一个我实际用过的简化版缓冲类可以直接抄进项目里调整。import numpy as np from collections import deque class HindsightReplayBuffer: def __init__(self, capacity, k4, threshold0.05): self.capacity capacity self.k k self.threshold threshold self.buffer deque(maxlencapacity) def compute_reward(self, achieved_goal, desired_goal): distance np.linalg.norm(achieved_goal - desired_goal) return 1.0 if distance self.threshold else 0.0 def add_episode(self, episode): # episode: list of dict, key include # state, action, reward, done, achieved_goal, goal length len(episode) for t, trans in enumerate(episode): # 原始样本一定要保留 self.buffer.append({ state: trans[state], action: trans[action], reward: trans[reward], done: trans[done], goal: trans[goal] }) future_indices list(range(t 1, length)) if not future_indices: continue sample_num min(self.k, len(future_indices)) sampled_ids np.random.choice(future_indices, sizesample_num, replaceFalse) for fidx in sampled_ids: # 新目标来自未来某个时刻的achieved_goal g_prime episode[fidx][achieved_goal] # 重算奖励用当前时刻的achieved_goal new_reward self.compute_reward(trans[achieved_goal], g_prime) self.buffer.append({ state: trans[state], action: trans[action], reward: new_reward, done: trans[done], goal: g_prime })这个缓冲区有两个地方还能继续优化。一是episode本身如果很长可以不用全部存下来直接用滑动窗口保留最近若干条轨迹二是为了减少数据相关性最好在add_episode之前对轨迹做一下随机裁剪或者截断。不过这些都不是核心先把基础逻辑跑对再说。3.3 训练循环与超参选择训练循环和普通DDPG基本一样只是采样时buffer里每条数据带有不同的goal。actor输入是拼接后的state-goal向量critic输入是state-goal-action向量。每次从buffer采样一批数据计算TD误差q_target reward gamma * (1 - done) * target_critic(next_state, target_actor(next_state, next_goal), next_goal)这个公式看起来简单但有地方需要单独说next_goal必须和当前样本里的goal一致不能随意换。我当时写代码图方便从buffer里取batch后直接把每个样本的goal都替换成了“该样本自己的未来目标”结果导致transition与goal完全不匹配模型越训越飘。正确做法是严格保持同一个sample内部goal的一致性。训练里另一件事是探索噪声。DDPG对动作加OU噪声效果一般后来我用高斯噪声加衰减效果更稳定。初始噪声标准差设为0.3每隔一定episode乘0.98让策略前期充分探索后期逐渐收敛。超参方面我把我常用的配置列在下面参数数值备注actor学习率1e-4太低收敛慢太高不稳定critic学习率1e-3相对可以大一点gamma0.98任务步数短衰减不用太强tau0.05软更新稍快适合仿真buffer容量1e6注意内存占用k4每个transition额外生成4个重标样本batch size256目标条件任务建议大batch奖励阈值0.05需要根据任务尺度调整这组参数不一定在所有环境都最优但作为起点已经足够。跑的时候记得先每个epoch采样一条episode做一次HER重标再更新4到8次网络这个比例对稳定训练很重要。4. 跑实验踩过的五个坑与排查实录4.1 稀疏奖励阈值没调HER也白搭阈值太大会让“成功”变得太廉价智能体只要稍微靠近一点就算达到目标那它学到的东西就很粗糙阈值太小会让正样本依然稀缺HER的优势被削弱。我一开始在抓取任务里把阈值设成0.02方块初始离目标有0.5几乎没有一个重标样本能落到阈值范围内曲线照样是平的。后来改成0.05正样本比例明显上升训练曲线才开始有反应。调整阈值没有万能公式我个人的习惯是先统计一下HER重标后正样本所占比例最好维持在5%到30%之间。如果低于1%说明太严格如果高于50%说明太宽松策略没必要精细逼近目标。4.2 状态和目标没做归一化critic学不动这个问题我在3.1里提过但它是真的值得单独拿出来再骂一遍的坑。critic接收state-goal-action作为输入如果state里的速度和位置数值相差两个数量级那网络前向传播时某些维度会天然拥有更大影响梯度更新就会失衡。再加上HER里不同重标目标的数值范围变化很大归一化不好很容易让critic对目标的泛化能力变得极差。解决也不复杂状态和目标统一做clip再除以预设范围。我甚至见过有人直接用整个buffer的均值和方差做running normalisation效果也不错。关键是要把归一化参数稳定下来别每一条episode都动态重算。4.3 重标样本比例过高原始目标被淹没HER的核心是“额外”提供hindsight样本不是替代原始经验。如果你为了增加正样本把k调得过大或者end episode后反复把同一条轨迹重标几十次buffer里绝大多数样本都是事后聪明。这时候策略对“如何达成真实目标”反而不敏感因为真实目标下的正样本占比太低它学成了“只管走向去过的地方”。我做过一个对比实验k从0到16递增在同一个任务上k4效果最好k8和k16反而回退。原因是纯重标样本太多了。如果你发现训练后期策略在测试时老往轨迹曾经走过的位置跑而不去够真正的新目标大概率就是重标比例出了问题。建议至少保留原始transition作为20%以上的buffer成分。4.4 和SAC搭配时熵系数不稳定HER此类off-policy算法组合有一个隐性矛盾。HER的数据分布不是平稳的重标操作让buffer里大量样本的目标分布和真实初始目标分布相差很大SAC的熵系数自动调整在这类数据下极其容易波动。我一开始直接用SAC默认的auto alpha结果训练到中期alpha掉到接近零策略退化成和DDPG差不多的确定性策略丢掉SAC本来该有的探索能力。后面我的处理是给alpha设置一个下限比如0.01同时把学习率调低。如果你没有特殊需求直接用DDPG或者TD3加HER会更省心SAC加HER需要多花时间盯熵系数收敛情况。4.5 原目标一定要保留否则策略走偏这一点和前面对应但还有更具体的表现。某些实现里新手会把原transition直接丢弃只保存重标目标下的transition理由是“反正在训练中也要采样”。这是非常危险的做法。原目标才是最终评测目标如果训练数据全部是各种各样的hindsight目标策略会彻底从“解决原任务”偏向“探索状态下曾经出现的各种局面”。测试时一评测成功率几乎为零。所以add_episode里必须把原始transition也写进buffer。我有时候会额外做一个小动作对原始目标样本给予更高的采样权重这样即使k设得稍大策略也不会忽视真实目标。虽然这会引入一点采样偏差但实测对最终成功率有正面帮助。问题表现排查思路解决办法奖励阈值过小正样本率极低打印HER重标后reward1的占比放大阈值观察5%到30%区间特征量纲不平衡训练曲线大震荡查看state各维度标准差统一归一化到[-1,1]重标样本过多测试时总走老路统计buffer中原样本占比k降到2-4或加原始样本采样权重alpha不稳定SAC逐步不探索打印alpha曲线设alpha下限或改用DDPG/TD3原目标被丢弃训练loss正常但测试为0检查buffer中goal的分布原transition必须保留5. 从仿真到实机以及什么情况下别用HER5.1 仿真训练完实机上“Hindsight目标”要重新审HER在仿真里表现得越激进迁移到实机时越要小心。原因是HER会在目标空间中凭空生成很多极端情况比如从未来某个方块位置学到的轨迹可能要求机械臂突然加速或经过一个在物理世界里不现实的路径。仿真里这些轨迹可能允许但实机会因为动力学限制完全执行不了。所以我的习惯是仿真里训练完成后做一次目标空间的可视化。把所有HER重标样本出现过的目标在空间里画出来看看是否和真实任务的目标分布有明显偏差。如果有就在实机微调时只保留原始目标的真实轨迹重标样本全部丢弃再跑几十个epoch的RL微调。这个策略虽然浪费了一部分仿真成果但能避免实机上的安全隐患。5.2 结合域随机化和更多hindsight变体HER并不孤立存在它和domain randomization几乎是天作之合。域随机化改变物体的摩擦系数、方块质量、目标位置范围而HER让这些随机环境中的失败轨迹都变成可用经验。两者一叠训练出的策略在实机上的成功率会比单独使用其中一个高很多。也有后续工作扩展了hindsight思路比如hindsight planning它在模型预测控制里用后见之明动态规划目标还有自动在目标空间进行采样的变体。如果你已经掌握了基础HER再去接触这些延伸不会陌生因为它们共享同一条核心思想现实世界里没有完全废掉的轨迹只有还没被你重新定义的目标。5.3 判断该不该上HER的三个条件不是所有项目都需要HER。我建议你先问自己三个问题第一个任务是不是目标条件的也就是“有没有一个显式的goal字段”。如果没有HER根本没有可以替换的对象。第二个奖励是不是稀疏的。如果环境本身已经有稠密距离奖励而且效果还不错那HER带来的提升可能很有限反而增加代码复杂度。第三个任务是不是存在多种可能的达成状态。如果任务只有唯一解比如棋类游戏只有一种合法目标那后见之明几乎没有发挥空间因为失败状态里根本不包含“替代目标”。HER最适合的场景就是机器人操作、物体搬运、路径规划这类“目标是一个连续空间中的位置或状态”的问题。在这些场景里失败轨迹往往已经包含了大量“接近某个目标点”的信息重标价值很高。反之如果你的任务里目标是非空间化的离散属性比如图像分类标签那这套方法基本派不上用场。从吃到第一口HER的红利到现在我最大的感受是在强化学习项目里遇到稀疏奖励千万别急着加reward shaping先试试hindsight。它可能不是最强的算法但绝对是最能改变你对失败经验认知的思路。每次看到训练曲线从一条直线变成一条带坡度的斜线我都觉得那次“失败到怀疑人生”的经历没白熬。现在再遇到难搞的任务我脑子里第一反应不是“怎么设计奖励”而是“换个目标看这些数据”这个习惯就是HER留给我的礼物。
返回列表