
hindsight experience replay我是在复现机械臂抓取任务时第一次认真啃下来的。这个名字起得很有意思hindsight 直译是“后见之明”翻译成大白话就是“回头看才知道当时该怎么改”。当时我的智能体跑在稀疏奖励环境里几十万步下来成功率始终是零训练曲线和心电图里的直线没什么区别我已经开始怀疑是环境写错了还是算法选错了。后来把目标重标注接进去曲线才真正被拉起来。这篇不打算把论文公式原封不动抄一遍而是按我自己调试验证时的顺序把设计思路、四种目标采样方式、复现代码和踩过的坑都讲清楚。想直接跑实验的重点看第三节想判断你的任务适不适合上 HER先看第一节和第四节。1. 先看懂“Hindsight”在强化学习里到底指什么1.1 稀疏奖励环境里智能体到底会遇到什么要理解 hindsight必须先理解“目标条件强化学习”里的一个典型困局。比如机械臂抓取、机械手推开物体、机器人导航到指定点这类任务都有一个明确目标最终状态要落在某个目标范围内。奖励往往是一刀切的——达到了给 1没达到给 0中间没有任何渐进信号。这就叫稀疏奖励。问题在于离策略学习依赖样本里的正反馈可在一个空旷且任务难度高的状态空间里随机策略“瞎猫碰上死耗子”的概率非常低。拿 FetchReach 这种相对简单的环境来说目标点若是随机撒在一个较大的工作空间里机械臂手指末端离目标 5 厘米以内才算成功随机探索的成功率可能只有百分之几。放到 FetchPickAndPlace 这种需要抓取、搬运、释放连续动作的任务里随机探索几乎不可能撞到一个完整成功样本。没有正样本神经网络从“全部失败”里只能学到“什么都不对”梯度更新方向是混乱的损失可能降了成功率却纹丝不动。这里可以用一个生活类比。你去一片没有手机信号的沙漠找一口特定的井你每次出发都失败回来后不知道是方向错、距离错还是阻力错。如果你只知道“没找到”这个结果那么下一次寻找和瞎走没有本质区别。你真正需要的是某些足够接近井口的位置信息才能修正之前的路线。HER 的核心就是主动制造这种“接近成功状态”的样本而不是站着等自然成功出现。1.2 目标重标注的直觉把“未达成的目标”当成“已经达成的目标”HER 的处理方式非常反直觉。既然某条轨迹里智能体没有到达预设目标那么我们就“事后”修改这条轨迹的目标改成它实际到达过的某个位置然后重新计算奖励把这条轨迹当作一次成功示范喂给网络。举个例子。机械臂原本想抓取 A 点结果手指落在了 A 点附近的 B 点。按原始目标这是一个失败样本奖励是 0网络无法从中学到有价值的东西。但如果我们把目标临时改成 B 点那么“从当前状态出发最终停在 B 点”这件事就变成一个成功样本奖励变成 1。网络从中可以学到原来在这个初始状态、这一组动作序列之下是能够抵达 B 点的。下次面对“去 B 点”这个目标时它就有了可参考的经验。这听起来像是“自欺欺人”但强化学习并不在乎数据如何产生只在乎从数据里学到什么。HER 的本质是把失败的探索轨迹重新解释为成功轨迹从而把原本稀疏的正样本密度瞬间提高。你不需要永远不犯错的采样过程只需要记录每个状态下的实际结果然后把“实际结果”伪装成“期望目标”就能让每个失败片段都贡献出有监督信号。从理论角度说这种重标注有一个很重要的前提假设在目标条件任务中很多行为模式的底层结构是共享的。学会“去 B 点”和学会“去 A 点”的行为路径存在大量共性所以用 B 点重标失败轨迹训练出来的策略会网罗地提升真实目标 A 上的表现。HER 论文里把这个思想概括为“如果没达到目标那至少学会了达到某个别的状态”。这个逻辑听着简单放到策略梯度和 Q 学习的框架里效果却立竿见影。2. 核心细节解析四种目标采样策略与算法配合逻辑2.1 final、future、episode、random 四种策略怎么选HER 论文并不是简单地把目标改成“最后实际到达的位置”就完事它提出了四种从已采集轨迹里选取替代目标的方式。这四种策略的选择直接决定重标样本的质量很多人按论文公式敲完代码后效果不稳往往就是在这里设错了。采样策略替代目标来源特点适用场景final轨迹最后一个状态的 achieved_goal最简单、稳定但可能离其实状态太远语义偏差大轨迹较短智能体逐步接近目标的任务random从整条轨迹里随机挑一个 achieved_goal采样多样但可能挑中与动作关联弱的状态学习信号噪声大作为 baseline 对照episode从同一回合里随机挑一个状态作为目标比 random 稍好利用了时序上的连续性简单环境备用方案future从当前时刻之后的状态里挑一个 achieved_goal兼顾时序关系与样本多样性是实践中最常用的一种大多数连续控制任务实际使用中我默认先试 future 策略。原因很直接它对“向前看”这段子轨迹来说目标一定落在未来某个真实到达过的状态上这意味着网络学到的动作序列和最终状态之间存在真实的因果关系更新信号可靠度最高。相比之下final 策略把整个轨迹后半段的实际结果统一压缩成最后一个状态如果轨迹很长后半段的动作可能和最终状态没有明显关联容易学出误导性的 Q 值。paper 里的超参数也支持这个选择。future 策略会额外带一个采样轮数比如每个失败轨迹额外采样 4 次也就是从该轨迹的未来状态中分别挑出 4 个目标生成 4 条重标样本。这个倍率太小正样本不够倍率太大会把同一批真实过渡反复重放导致网络过拟合到少数几条轨迹上。我实测下来采样轮数 4 是一个性价比比较高的甜点值。2.2 为什么必须配合 off-policy 算法以及更新公式怎么变很多刚开始接触 HER 的人会问这件事听起来很通用能不能接到 PPO 或 A2C 这类 on-policy 算法上我在实验里试过结果是方案能跑但收益非常有限。原因在于目标重标注本质上是在重新解释旧数据它要求算法天生具备多次重复利用历史样本的能力。on-policy 算法默认数据只能被用一次重标后会继续用在当前策略更新上但策略早就变了样本分布严重错位。HER 的正确搭档是 DDPG、TD3、SAC 一类的 off-policy 算法。这类算法会把所有轨迹样本存进 replay buffer训练时反复从 buffer 里抽样更新。Hindsight 的自制兴奋剂就是通过这种“反复回放”发挥作用的。配合 off-policy 算法以后目标重标下的 Q 更新公式大体如下new_target r(s, a, g) gamma * max_a Q_target(s, a, g)这里 g 是重标注后的新目标。注意动作 a 不需要重新生成因为它已经在环境中真实执行过了。我们只是把“目标”从原目标替换成某个 achieved_goal同时用稀疏奖励函数重新计算 r。网络输入里多一路目标特征actor 和 critic 都必须把 g 拼进去。还有一点容易被忽略重标后的“成功奖励”数值设计要谨慎。如果用 shaped reward原样本和新目标之间的奖励尺度可能不一致训练波动会非常大。我推荐统一使用二值稀疏奖励比如“距离小于阈值给 1否则给 0”。这样既能维持 HER 的本意又不会引入额外的奖励噪声。即便后续要做奖励塑形也应该以稀疏二值奖励为基准再叠加距离惩罚项而不是完全依赖密集奖励。3. 实操环节复现 HER 的完整流程与关键实现3.1 环境选择、基线算法与参数配置我自己完整跑通的第一组实验选择的是 Gym 系里的 FetchReach 和 FetchPush 环境一个是为了验证 pipeline 是否通顺另一个是为了验证 HER 在稍复杂任务里的效果。FetchReach 的目标是让机械臂末端到达目标位置非常适合作为 first baselineFetchPush 需要把物体推到指定位置已经能体现“物体交互”带来的额外难度。算法基线上我犹豫过 DDPG 还是 TD3。最后选了 TD3因为 TD3 对动作噪声更稳Q 值过估计更小。即便和 DDPG 使用同一套重标逻辑TD3 在 Fetch 系列环境里通常更不容易发散。SAC 也可以但要额外调温度系数前期复杂度更高。以下是我实测比较稳的一套起始参数参数项参考值备注actor 学习率5e-4过高会导致目标更换不稳定critic 学习率5e-4与 actor 持平也可略低batch size2048大一点对 HER 重标样本友好replay buffer 容量1e6Fetch 系列必须给大HER 采样轮数4future 策略默认值动作噪声高斯噪声标准差 0.2训练阶段加评估阶段清零回合时长50 步FetchReach采样存储依赖回合边界训练步数100k ~ 300k 步FetchReach 快PickAndPlace 需要更多经验是先把 FetchReach 跑到成功率接近 100%再切 FetchPush。FetchPickAndPlace 我一度太乐观直接上手结果第一周全在调参循环里效率很低。先易后难不是偷懒而是为了尽早暴露代码里的低级 bug。3.2 目标重标注的实现细节与代码骨架HER 的代码并不复杂复杂的是数据结构的组织方式。每条经验不仅要存状态、动作、奖励、下一状态还要存两个关键字段desired_goal 和 achieved_goal。前者是智能体被要求达成的目标后者是智能体实际到达的状态。目标重标注做的事情就是把 desired_goal 替换成某个 achieved_goal再重新计算奖励。下面是我复现 HER 时使用的核心重标函数骨架按可运行逻辑精简过。import numpy as np def sparse_reward(achieved_goal, goal, threshold0.05): # 目标是二值稀疏奖励距离小于阈值则成功 distance np.linalg.norm(achieved_goal - goal, axis-1) return (distance threshold).astype(np.float32) def replay_episode_with_her(episode, her_ratio, threshold0.05): # episode 是当前回合每条 transition 的列表 # 1. 先把原始经验正常存入 buffer replayed [] for t, trans in enumerate(episode): replayed.append(trans) # 2. 对每一条没有成功的 transition 做重标 for t, trans in enumerate(episode): if trans[is_success]: continue for _ in range(her_ratio): # future 策略从当前时刻之后的 state 里随机选一个 achieved future_t np.random.randint(t 1, len(episode)) new_goal episode[future_t][achieved_goal].copy() # 生成新 transition原动作与状态不变只换目标 new_trans dict(trans) new_trans[desired_goal] new_goal new_trans[reward] sparse_reward( trans[next_achieved_goal], new_goal, threshold ) new_trans[is_success] new_trans[reward] 0 replayed.append(new_trans) return replayed这套代码里有三个容易出错的地方。第一个future_t 必须大于 t不能从整条轨迹随便挑否则目标和当前时刻的 action 没有因果联系学出来基本等于随机目标。第二个重标样本里的 reward 必须重新用新目标计算不能沿用原始 reward。第三个原始经验也要正常保留不要让重标样本把原样本完全替换掉否则智能体对真实目标的感知会被冲淡。我自己在实际复现中踩过比较深的一次坑是把 achieved_goal 直接从 observation 里取了但没注意 observation 里不同维度的拼接顺序。Gym Fetch 环境的 observation 里既有机械臂状态也有物体位置、目标位置不同版本拼接方式不一样。如果你把目标那一维度错当做物体位置重标出来的“成功目标”本身就是错的训练曲线看起来在涨实际 eval 成功率纹丝不动。遇到这种情况先单独打印几个 sample 的 obs、achieved_goal、desired_goal人工核对数值再继续调参。3.3 训练节奏、调参与指标观察HER 引入之后还有一个常常被忽略的问题是训练节奏。正确做法是先用某个探索策略采样一整轮 episode写入 buffer然后从 buffer 里抽 batch 做若干次梯度更新隔一定步数跑一次固定的评估记录 eval 成功率。训练和评估的比例也很关键。我踩过连轴转的坑为了节省时间让 agent 边采样边更新结果 HER 重标样本刚写进 buffer 就被同一轮更新抽出去训练样本分布高度相关训练曲线震荡得厉害。后来改成“先采样 8 个 episode再更新 20 次梯度”的节奏稳定性明显变好。评判标准除了最终成功率还要盯住两个指标重标样本占 buffer 的比例以及难以区分原始目标成功率和整体成功率。如果原始目标几乎从不成功说明探索策略还需要再多样化光靠 HER 重标无法弥补探索不足。对于 FetchReach我跑通流程大约用了 10 万步FetchPush 大约 20 万步到 30 万步能稳定到 90% 以上。FetchPickAndPlace 的情况更挑探索策略不确定性大有人能 60 万步解决有人要 100 万步。但不管哪种环境都有一个共性规律成功率不是一路平滑上涨的前几万个步数内会先有一段“沉睡期”因为 HER 里重标样本还没来得及积累足够多过了一个临界点后成功率才会加速拉升。看到前期平线不要急着放弃先确认 buffer 里失败轨迹的重标正样本数量是否在增长。4. 常见问题与排查技巧实录4.1 收敛慢、震荡、失效的排查思路我把复现 HER 过程中遇到的高频问题整理成一张排查表逐个对应处理过有些问题表面上像是参数不对实际原因是数据流里就埋了雷。现象可能原因排查方向训练 loss 在降eval 成功率一直为 0重标目标没有进入 critic 更新或 achieved_goal 取错打印重标样本的目标与奖励人工核对 distance 阈值成功率前期一直横盘探索噪声太小智能体每个 episode 都走相似轨迹加大动作噪声或提高 epsilon观察 trajectory 多样性成功率涨到某个值后突然崩掉replay buffer 里早期坏样本被反复重标或学习率过高增大 buffer 容量降低学习率限制 HER 采样轮数同一个目标反复执行同一动作策略输入里目标特征丢失网络忽略目标分支检查 actor 输入是否包含 desired_goal而非只拼状态换一个 seed 后结果差异巨大HER 对采样随机性敏感重标 future_t 选择不稳定做多 seed 实验避免只看单次结果并固定随机种子对照这里我要特别提一个问题很多人以为 HER 是银弹加上就能稳定收敛但实际它的效果高度依赖探索质量。HER 只是把已有失败轨迹重新解释出正信号如果失败轨迹本身千篇一律重标出来的目标也是千篇一律。所以探索噪声、动作噪声、episode 起始随机化这些基础设置比想象中更重要。我甚至在测试时发现把机械臂初始位置从固定改为随机采样对最终成功率的影响比调 HER 采样轮数还大。4.2 HER 与奖励塑形、课程学习的边界HER 的一个隐性适用边界是它对“目标条件型”任务有效但对非目标条件任务基本无用。比如某个任务只有一个固定终点无论初始状态如何都要求回到同一个位置那 achieved_goal 本身就是固定的重标毫无意义。反之像机械臂操作、自动驾驶变道、机器人抓取这类“不同状态下有不同可达终点”的任务HER 才真正发挥优势。另外很多人会陷入一种误区既然 HER 能让稀疏奖励任务学会那我叠加一段密集奖励脚本是不是学得更快我试过不止一次结论是HER 与强烈 shaping 的奖励会打架。HER 重标的本质是二值化地解释“是否到达某点”如果原任务奖励里已经给了密集距离惩罚重标后的 reward 和原始 reward 尺度不一致Q 值目标忽大忽小训练更容易震荡。奖励设计上我更推荐先用纯二值稀疏奖励配合 HER 跑通再考虑最小限度的人为引导比如“未成功时给一个小负惩罚”也可以但幅度要小别盖过二值信号。课程学习可以和 HER 搭配但不能混为一谈。课程学习通过把初始目标分布由易到难逐步推进把探索空间一步步缩小HER 则是在固定探索空间内把失败样本重新解释。两者结合在一起效果更好先用课程学习把目标和环境分布收敛到可解区间再用 HER 把区间内的失败样本变成有效学习数据。我在 FetchPickAndPlace 上的成功方案就是这两者结合的结果。4.3 复现 HER 时容易踩的坑清单最后整理一个避坑清单每一条背后都是一段真实调试记录。第一个坑是混淆 desired_goal 和 achieved_goal这是最基础但出现频率最高的错误。网络输入要同时包含两个目标信息一个是给智能体下达命令的目标一个是智能体当前实际位置如果只输入 desired_goal网络根本不知道“当前状态和目标”的相对关系HER 重标的威力就发挥不出来。第二个坑是忘掉对重标样本做目标归一化。Fetch 系列环境里物体位置和目标位置都是三维坐标但 observation 里目标被缩放到一定范围附近如果原实现已经做了归一化重标时也要做同样的归一化两边不做对齐距离计算就失真。第三个坑是 evel 时忘了把噪声关闭导致在线监控成功率永远低于真实水平。这个坑很隐蔽因为训练曲线看着不错一评估就不动容易让人白调半天参。我的办法是在代码里给评估函数强制传零噪声甚至写一个断言确保评估阶段动作方差为 0。第四个坑是 buffer 容量不够。HER 重标本质上是把一条轨迹复制成多条样本如果 buffer 很小重标样本很快就会把原始真实成功样本挤出去最后网络只会在“事后解释”的数据上反复打转。我在小型实验里用 5e5 buffer 的效果明显差于 1e6。第五个坑也是大家最容易忽视的不要一次性把所有失败轨迹全部重标。保留一部分原始 failure 轨迹非常重要它们能让网络知道“这个目标没有达到其实是失败的”从而建立目标成功与否的判别边界。如果 buffer 里几乎全是重标成功episode 反而变成“永远成功”的独角戏收敛效果会下跌。根据我个人的实验体会HER 是一个“轻实现、重调试”的技术。代码量不大但它对数据结构、目标维度、奖励函数和探索策略的组合极其敏感。复现它最好的方式是先把 FetchReach 跑通然后逐步替换环境、替换目标采样策略观察每一步的变化而不是一上来就堆复杂环境。当你真正理解“失败的轨迹可以被重新解释为成功”这一点后回头再看更复杂的目标条件任务思路会清晰很多。