拓冰建站拓冰建站
首页 / 资讯中心 / 正文

强化学习中的HER:用后见之明经验回放破解稀疏奖励问题

这个项目叫hindsight在强化学习领域这个名字已经和一个算法深度绑定——Hindsight Experience ReplayHER后见之明经验回放。我第一次在论文里看到这个思路时说实话是被惊艳到的它把一堆“失败”的轨迹重新解释成了“成功”的训练样本让智能体在没有密集奖励的情况下也能稳定学起来。这篇文章适合正在做机器人控制、操作任务、导航或任何带稀疏奖励RL任务的人也适合刚入门强化学习、想搞懂目标条件策略的读者。我会把HER的原理拆开讲清楚再给出可以直接照着写的实现代码以及论文里不会写、只有实际训练才踩得到的那些坑。1. 先搞清楚HER到底解决了什么问题1.1 稀疏奖励强化学习最常见的“劝退”现场很多RL任务写成MDP之后奖励函数是“成功给1失败给0”或者更严格一点“成功给0失败给-1”。听起来很公平但真正拿算法去跑的时候问题就大了。比如机械臂推箱子到指定位置箱子的初始位置随机目标是桌面某个点。智能体刚开始完全随机探索大概率是怎么推都推不到目标点的。于是整个经验回放缓冲区里装满了reward -1的样本没有一条正样本。Critic网络在这种数据上学出来的Q值对任何(s, a)都差不多都是负数都是“没戏”。Actor根据Q值做梯度上升得到的梯度方向基本是噪声学不到任何有意义的动作。这个现象在机器人操作、迷宫导航、棋类游戏等场景里特别常见。我之前调过一个机械臂任务纯用DDPG跑了40万步成功率一直是零学习曲线就是一条水平线。不是代码bug不是超参问题就是稀疏奖励下的典型失学症状。生活里类比一下你想训练一个人投篮但规则是“只有投进三分线外的篮筐才算得分”其他所有出手都不给任何反馈。这个人随机扔了几百次一次没进那他根本不知道“我离篮筐近了一点”是好是坏最后只能在原地乱扔。这和稀疏奖励下的RL是同一个困境。1.2 传统解法为什么差一口气面对稀疏奖励业界最常见的应对手段大概是这么几类手动设计密集奖励Reward Shaping比如“离目标越近奖励越高”。这个方法直接但工程量大而且很容易引入次优策略。你鼓励它靠近目标它可能学会先撞过去再弹回来你鼓励它避免碰撞它可能干脆原地不动。课程学习Curriculum Learning从简单目标开始逐步提高难度。这条路线在仿真里有效但要额外设计课程策略任务一复杂就说不清楚了。内在奖励Intrinsic Motivation用好奇心或状态新颖度作为额外奖励。思路不错但在高维连续控制任务里很容易被“环境里的随机噪声”吸引注意力导致学到一堆与任务无关的行为。注意到没有这些方法本质上都是在改“奖励信号”。没有人想过能不能不改奖励而是改“已经发生的经验”HER就是从这个角度切入的。它不动奖励函数不引入额外模块只对经验回放缓冲区里的数据做“重新解释”让失败轨迹的一部分变成正样本。1.3 “后见之明”的直觉失败经验也可以当正样本“Hindsight”这个词的直觉其实特别简单。回到投篮的例子一个人练习投篮虽然没投进三分线但他投出去的动作让篮球划出了一条漂亮的弧线并且落到了篮板附近。这件事真的毫无价值吗换个角度看如果目标不是“投进三分线”而是“把篮球抛出这个弧线、落到篮板附近”那这个动作就是完美的。也就是说一个动作没有达成“你预设的目标”但它一定达成了“某个实际发生的结果”。这个实际结果在事后看来完全可以当作一个正样本来学习。HER的核心操作就是把这句话落到了数学上一条经验原本是(s, a, r, s, g)其中g是智能体没有达成的“期望目标”HER拿着实际到的状态重新构造一个伪目标g achieved_goal再把奖励按新目标重算一遍得到(s, a, r, s, g)。因为这个伪目标确实达到了所以新奖励r是成功奖励这条经验就从负样本变成了正样本。我当时看到这个思路的第一反应是这也太简单了能有用吗结果我自己实现了一遍在Fetch环境上一试效果是真的立竿见影。2. HER的核心机制经验是怎么被“改写”的2.1 一条transition的四个角度在具体写代码之前先把HER处理的“对象”搞清楚。一条transition原本长这样(s, a, r, s_next, goal)s当前状态比如机械臂关节角度、夹爪位置、箱子位置。a当前动作。r在目标goal下执行动作a之后获得的奖励。s_next执行动作后的下一状态。goal当前episode的期望目标比如“箱子最终位置”。但在HER实现中我们通常把环境返回的observation拆成三部分而不是一个扁平向量observation - state # 机器臂自身状态比如关节角、线速度 - achieved_goal # 实际达到的目标比如夹爪当前坐标、箱子当前坐标 - desired_goal # 用户给定的目标比如夹爪期望坐标这个拆分很重要。因为HER重标注的时候需要把desired_goal替换成某个achieved_goal再重新算奖励。如果observation是一团混在一起的向量你根本不知道哪几个维度是“目标空间”也就没法做替换。HER改写经验时具体操作是这样原始描述transition { state: s, action: a, reward: sparse_reward(s_next, desired_goal), next_state: s_next, goal: desired_goal, }HER改写后transition { state: s, action: a, reward: sparse_reward(s_next, pseudo_goal), # 这个通常是成功奖励0 next_state: s_next, goal: pseudo_goal, # 从同一episode未来某时刻的真实achieved_goal里采 }注意state和next_state没动动作没动真正改的只有两处目标值和对应的奖励。这个看起来很小的改动把“失败”数据的标签翻了个面。2.2 为什么这种改写不是自欺欺人第一次接触HER的人都会有个疑问把目标换掉这不就是自己骗自己吗智能体明明没完成用户给定的任务你却告诉它“你成功了”这难道不会学歪这里面的关键不在于“骗”而在于HER训练的是一个目标条件策略。目标条件策略的形式是π(a|s, g)意思是给定当前状态s和一个目标g策略输出动作a。它学的不是“从s出发到达某个固定目标”而是“从s出发到达任意指定目标的能力”。你想想一个真正的目标条件策略如果它能“推箱子到桌面上任何一个可达的位置”那你给它当前桌面上任意一个点作为目标它都应该能推。用户指定的目标“把箱子推到A点”本质上也是这个目标空间里的一个点。所以用伪目标训练出来的行为并不是无用功——它在教会策略“完成一个目标”的通用能力。当真实目标是A点的时候策略调用的是同一套底层技能只不过输入的目标向量不同而已。还有一点很多人忽略HER之所以能从伪目标中得到有效的学习信号是因为伪目标不是凭空产生的它来自这个episode中某个未来时刻真实到达的achieved_goal。这说明从当前时刻的状态出发这个伪目标在物理上是可达的并且轨迹里确实存在“通过某种动作到达它”的证据。策略学到的是“在这个状态下执行这个动作能靠近这个目标”这完全是一条真实有效的信息和自欺欺人沾不上边。用一段话归纳HER不改变环境动力学不改变真实目标上的评估逻辑它只是换了一个角度把“没达成预期目标”的经验转化为“达成某个实际目标”的经验让本来就存在的因果信息不被稀疏奖励浪费掉。2.3 伪目标采样策略final、future、episode怎么选HER论文里给了好几种伪目标采样策略代码实现时通常用一个参数切换final只用episode结束时的最终状态作为伪目标。最朴素但问题也明显——最终状态离得很远中间很多动作和这个伪目标的因果关联特别弱。random从状态空间里随机抽一个状态当伪目标。这个最不推荐因为随机目标大概率物理上不可达你等于在教策略做一件根本做不到的事。episode从当前episode里随机抽一个状态作为伪目标。比random好一些但同样存在“和目标因果无关”的问题。future从当前时间步之后的若干时间步里挑一个状态的achieved_goal作为伪目标。这是实践证明最稳的选择。为什么future最好因为伪目标是“当前状态之后某个时刻真实到达过的位置”这意味着从当前状态出发这个位置在时间上是可达的轨迹段和目标天然存在因果链条。比如机械臂第10步在A点第35步到了B点把B点当作第10步的伪目标那条轨迹就是“从A点出发逼近B点”的有效证据。用final也能做到这一点但final只能提供episode末端一个目标样本多样性太差future可以提供每个时间步之后的一批候选状态数据量丰富得多。实际操作中我一般固定用future策略超参数future_k取4意思是在“未来时间步”里均匀抽4个不同的状态作为伪目标。这个值和强化学习里的batch size一样不需要特别精细地调4到8之间都行。3. 手写一个HER环境、算法与代码落地3.1 环境选型与算法搭配想快速复现HER效果推荐用这几个环境从简单到复杂环境特点难度Bit Flippingn位二进制翻转目标是把当前比特串翻转为目标比特串入门FetchReach控制机械臂末端移动到目标点简单FetchPush用机械臂把箱子推到目标位置中等FetchPickAndPlace夹取物体并放到目标位置困难这些环境在Gym/Gymnasium的Robotics系列里都有reward的默认设置恰好是稀疏模式到达目标给0没到达给-1非常适合演示HER。算法选择上HER本身不是策略优化算法它只是一个经验重标注技巧。它必须搭配一个off-policy、带经验回放的算法才能发挥作用。最经典的搭配是DDPG但我个人更推荐直接用TD3或者SAC尤其是连续控制任务。原因很直接DDPG对超参数敏感Q值过估计问题在稀疏奖励下会被放大TD3在DDPG基础上加了裁剪Q值和延迟更新稳定性好很多训练省心。为什么必须是off-policy因为HER要让“重标注后的经验”进入回放缓冲区然后在更新时被反复采样。如果像PPO那样每次更新用完后丢掉数据重标注的价值就大打折扣。所以你会看到凡是和HER配合的算法基本都是off-policy的actor-critic架构。3.2 重标注模块的代码实现下面这段代码是我在项目里的核心逻辑去掉了一些工程细节保留主干import numpy as np from collections import deque def sparse_reward(state, goal, tolerance0.05): # state是实际状态/位置goal是目标状态/位置 # 这里用的是欧氏距离判据 distance np.linalg.norm(state - goal) return 0.0 if distance tolerance else -1.0 def her_relabel(episode_trajectory, her_ratio0.8, future_k4): episode_trajectory: list of dict 每个dict包含 obs, action, reward, obs_next, achieved_goal, desired_goal 返回: 一组可以加入replay buffer的transition relabeled_transitions [] for t, trans in enumerate(episode_trajectory): obs trans[obs] action trans[action] obs_next trans[obs_next] achieved_goal trans[achieved_goal] desired_goal trans[desired_goal] # 以一定概率保留原始目标 if np.random.rand() her_ratio: relabeled_transitions.append({ obs: obs, action: action, obs_next: obs_next, goal: desired_goal, reward: sparse_reward(obs_next, desired_goal), }) continue # 从当前时间步之后的未来状态里均匀采样future_k个伪目标 future_steps np.arange(t 1, len(episode_trajectory)) if len(future_steps) 0: # 最后一帧只能保留原目标 relabeled_transitions.append({ obs: obs, action: action, obs_next: obs_next, goal: desired_goal, reward: sparse_reward(obs_next, desired_goal), }) continue sampled_indices np.random.choice(future_steps, sizemin(future_k, len(future_steps)), replaceFalse) for idx in sampled_indices: pseudo_goal episode_trajectory[idx][achieved_goal] relabeled_transitions.append({ obs: obs, action: action, obs_next: obs_next, goal: pseudo_goal, reward: sparse_reward(obs_next, pseudo_goal), }) return relabeled_transitions有几个细节值得说。第一obs_next和achieved_goal一定要对齐。很多环境里obs_next里就带着achieved_goal_next你可以直接从obs_next里拆出来不要自己去状态向量里硬算位置容易对错索引。第二future_steps采样时要注意t1可能越界。上面的代码用len(future_steps)0做了保护实际项目里最后一帧的状态基本已经到episode末尾不值得再重标注。第三重标注入库时state和next_state保持不变变的只是goal和reward。这个逻辑一定要写正确如果手滑把state也替换掉训练就会彻底乱掉。3.3 训练循环中的参数与流程设计把HER塞进训练循环整体流程大概是这样一个节奏replay_buffer ReplayBuffer(capacity1_000_000) for episode in range(max_episodes): obs, info env.reset() trajectory [] for step in range(max_steps): action actor.select_action(obs, noise_scale0.1) obs_next, reward, done, truncated, info env.step(action) trajectory.append({ obs: obs.copy(), action: action.copy(), obs_next: obs_next.copy(), achieved_goal: obs_next[achieved_goal].copy(), desired_goal: obs_next[desired_goal].copy(), }) obs obs_next if done or truncated: break # 关键先重标注再入库 transitions her_relabel(trajectory, her_ratio0.8, future_k4) for trans in transitions: replay_buffer.add(trans) # 常规off-policy更新 for _ in range(num_updates): batch replay_buffer.sample(batch_size256) loss_critic, loss_actor td3_update(batch)训练过程中有一个参数值得反复强调her_ratio。它控制“多少比例的经验会被重标注”。我实测下来0.8左右效果比较好。如果设成1.0所有经验都被改写成伪目标缓冲区里几乎没有“用户真实目标”的样本训练后期反而容易让策略淡忘真实目标分布如果设成0.5以下正样本比例太低HER的优势体现不出来。0.8的意思是80%的经验用伪目标学习20%保留原目标这样真实目标分布一直被保留在样本里。future_k4的意思是对每条原始transition额外生成4条重标注版本。这会显著增加回放缓冲区里“成功”样本的比例但同样要注意future_k太大会导致缓冲区里同一(s, a)对应一堆不同目标经验冗余度变高内存和算力都会浪费我一般不超过8。为了便于实验对比我建议在训练时记录三个指标reward_before_her原始reward的均值。success_rate_pseudo重标注后伪目标的“成功率”。success_rate_true用真实目标评估策略的真实成功率。前两个用来观察训练动态最后一个是唯一可信的评估指标。原因后面会专门讲。4. 训练实测与调试记录4.1 从Bit Flipping到FetchPush效果差异我自己在四个环境上都跑过对比实验结论和论文里的趋势基本一致。Bit Flipping这个环境最简单状态是20位二进制串动作就是翻转某一位。不用HERDDPG几乎学不到任何东西因为随机翻转20位恰好命中目标的概率低到可以忽略用了HER之后策略在1万步内就能学会“朝目标位翻转”的技能成功率稳定在95%以上。这个环境特别适合新手验证代码是否正确。FetchReach比Bit Flipping复杂一点但动作空间是4维连续目标只控制末端位置。有趣的是不用HER只要多加探索噪声DDPG偶尔也能摸到目标但成功率波动特别大而上了HER之后成功率曲线平滑往上走明显稳很多。最典型的是FetchPush。这个环境需要机械臂把桌面上的箱子推到目标点普通DDPG在2万多个episode里成功率一直是零不管怎么调探索噪声都没用。同样配置只加HER大概8000个episode左右成功率就开始抬头15000个episode能到70%以上。我自己跑出来的曲线形状和论文里的趋势是一致的前期会有一段“平台期”看起来没什么进步实际上critic正在缓慢积累目标条件价值信息熬过这段之后成功率会突然起来。这也是一个很重要的经验HER训练初期的学习曲线往往不是线性上升而是先平后涨。如果前期看到曲线不动就急着停掉很可能会错过后面的大幅提升。4.2 成功率评估的正确姿势HER一个很容易踩的坑是用“重标注后的奖励”来评估策略。有些人训练时看到回放缓冲区里奖励慢慢从-1变成0就以为策略在进步。这其实是个错觉。重标注后的奖励高只能说明“伪目标被达到了”不能说明“用户设定的真实目标被达到了”。比如机械臂每次都推到A点但用户目标是B点那么所有以A点作为伪目标的经验都会显示成功可真实成功率依然是零。正确的评估做法是训练时定期冻结当前策略在若干种随机初始条件下让策略以“真实desired goal”作为输入跑完整条轨迹看看最终状态和真实目标的距离是否小于阈值统计成功率。注意评估时不要使用重标注不要加探索噪声动作直接用actor输出的确定性动作。我在代码里就是这么处理的def evaluate(env, actor, episodes10): success_count 0 for _ in range(episodes): obs, _ env.reset() done False while not done: action actor.select_action(dict_to_obs(obs), deterministicTrue) obs, _, terminated, truncated, _ env.step(action) done terminated or truncated achieved_goal obs[achieved_goal] desired_goal obs[desired_goal] if np.linalg.norm(achieved_goal - desired_goal) 0.05: success_count 1 return success_count / episodes这个成功率才是项目汇报时能拿出手的指标。4.3 训练不收敛的排查清单HER虽然好用但绝不是加了就能稳定收敛。实际调试中我遇到最多的训练异常和对应排查思路如下症状排查方向学习曲线一直水平不动先确认经验回放里是否出现了伪目标的0奖励样本再确认actor输入里是否真的拼接了goal前期Q值特别大/震荡检查reward是否写反检查state与goal是否归一化真实成功率上去后又崩掉大概率是经验回放里真实目标样本太少试试降低her_ratio伪目标成功率很高但真实成功率很低说明策略过度适配伪目标分布试试改future策略或降低future_k训练时好时坏一段稳定一段掉探索噪声可能过大建议把噪声std从0.1调低或加衰减排查有一个顺序技巧先看reward是否变化再看Q值是否合理最后才调HER相关的超参数。很多时候问题根本不在HER上而是底层的TD3/DDPG训练不稳定比如critic和actor学习率失衡、目标网络更新节奏不对。HER只是经验层面的增强不能替代算法本身的稳定性。5. 使用HER的避坑清单与经验总结5.1 伪目标不是随便抽的这一点值得反复强调。伪目标必须来自“当前episode未来某时刻真实到达过的状态”不要用random策略随便抽。你可能觉得random策略抽出来的目标更随机、更能覆盖状态空间但训练时会让actor去逼近一堆物理上根本到不了的目标Q函数在这种数据上会学到一堆标准差极大的错误预测整个训练就崩了。我测试过一次在FetchPush上用random替代future成功率直接掉到个位数。所以这条经验记住伪目标要“从轨迹里来到轨迹里去”保持因果上的可到达性。还有一个小技巧如果future_k比较大或者her_ratio偏高同一批(s, a)会在缓冲区里出现很多份只是goal不同。这是正常的也不是问题但对应的训练步数要同步加大让critic有机会把“不同目标下的价值差异”学出来。5.2 状态与目标一定要归一化HER训练里最常见的一个隐形杀手是量纲差异。比如机械臂关节角度是0到1之间的数盒子位置是0.1到2.0之间的数物体朝向可能在-π到π之间。这些数值直接拼进state向量Q函数的目标值会受个别大维度主导小维度上的梯度几乎被淹没。我的处理方式是把observation拆开后分别做归一化# 统计初始状态里的均值和方差 obs_mean, obs_std compute_running_stats(env) # 对state、achieved_goal、desired_goal各做一次归一化 norm_state (state - obs_mean[state]) / obs_std[state] norm_achieved (achieved_goal - obs_mean[achieved_goal]) / obs_std[achieved_goal] norm_desired (desired_goal - obs_mean[desired_goal]) / obs_std[desired_goal]注意achieved_goal和desired_goal属于同一个坐标空间应该用同一组均值方差归一化否则等价于做了一个线性变换麻烦且没必要。我在写代码时一开始犯过这个错把两个goal分开归一化结果Q值怎么训都稳不下来。5.3 别被回放数据的“假成功率”骗了这条是给所有做RL人的忠告训练日志里的mean reward不是用来判断成功率的指标尤其在HER这种会对奖励做重写的算法里。我在一个项目里看到过一张“漂亮”的曲线回放奖励从-1一路涨到0demo视频里机械臂却根本推不到目标。原因就是eval脚本用的还是伪目标策略被成功“骗”了。所以日志里至少要同时打印这三个值replay_reward_mean回放缓冲区平均奖励反映训练动态不反映任务完成度。pseudo_success_rate重标注伪目标上的成功率只在模型分析时有参考价值。true_success_rate真实目标上的成功率唯一的任务评估指标。如果线上/线下汇报只用true_success_rate说话。5.4 HER不是银弹边界要清楚HER有效是有前提的。第一个前提是任务必须能定义“achieved goal”也就是说从状态里能提取出“实际达到的目标”。比如机械臂任务夹爪位置、箱子位置天然就是achieved goal图像生成任务目标是一张图你很难从每次生成的图像里抽出一个“实际达到的目标”再重写奖励。第二个前提是目标空间不能太怪异最好是具备连续性和可达性的空间像推箱子这种目标在桌面上连续分布就非常适合HER如果目标是“在三维空间里排序一串复杂物体”HER还能用但效果会大打折扣。另外HER解决的是“稀疏奖励下样本利用率低”的问题不是“探索不够”的问题。如果任务本身探索空间极大伪目标再怎么重写也覆盖不了状态空间的重要区域这时候该上的是更好的探索策略、课程学习或层次化方法而不是死磕HER。我个人的体会是HER是一个非常优雅的“观点转换”它把失败重新定义成另一种形式的成功让每个episode里的数据都有了自己的用途。你可以把HER看成RL框架里的一个插件而不是一个完整的算法。它和DDPG、TD3、SAC都能搭配使用但对底层算法的稳定性有一定要求——底层算法稳住HER才能发挥出最大的价值。最后再分享一个小经验训练后期如果发现真实成功率在高位反复震荡、掉点可以尝试把her_ratio从0.8逐步降到0.5甚至0.3。因为训练后期策略已经掌握基本技能这时需要更多“真实目标”的样本来精修目标条件策略的泛化边界减少伪目标对最终目标分布的干扰。在FetchPush上我用这个“衰减HER比例”的技巧把最终成功率从68%提升到了81%左右算是性价比很高的一步调参。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门