
如果让我在强化学习的实操里挑一个“止损神器”我的答案大概率是HER——Hindsight Experience Replay后见经验回放。名字直译过来就是“事后诸葛亮”机器人明明没抓住杯子但轨迹里某个瞬间它的爪子确实离杯子近了几厘米那就把那个瞬间的状态当成目标重新学一遍动作。靠着这个朴素的“复盘”思路一大堆在稀疏奖励下死活学不出来的目标导向任务被盘活了。这篇内容我打算从HER的核心动机讲起把目标重标注的代码逐段拆开再把调参时踩过的坑全倒出来。适合正在做强化学习、机器人操作、多目标任务或者被“奖励全是-1”折磨到怀疑人生的朋友。不需要你有很深的理论功底只要你写过一点DQN或DDPG并且愿意跟着思路手动推一遍就能把HER装进自己的训练流程里。1. 项目概述从“事后复盘”到目标重标注1.1 稀疏奖励下的目标导向任务到底难在哪先明确一下HER瞄准的问题。它解决的是“目标导向强化学习”也就是给智能体一个明确的goal让它学会“从当前状态出发到达目标状态”。典型场景包括机械臂抓取、推箱子、到达指定坐标、翻转一组比特位等。这类任务在MDP上有个共同写法状态s目标g动作a奖励r。如果下一时刻的状态s达到了目标g奖励是0否则是-1。也就是说在这个环境里你几乎拿不到任何“中间分数”只有成功和失败两个结局。真实的机器人操作尤其如此你不可能给机械臂说“手腕角度偏了3度奖励0.1”——那是密集奖励的shaping思路手工设计起来费劲还容易把策略带偏。问题在于一旦奖励变成“非黑即白”探索就成了纯碰运气。假设机器人要连续做20个正确动作才能成功一次而每个动作选对的概率是五分之一整体成功率大约是20的20次方分之一。这个数字小到策略网络根本收不到正向梯度。随机rollout一百万个episode一条成功轨迹都未必撞出来价值网络看着满屏的-1学来学去只能输出“不管做什么都一样差”策略自然原地踏步。这正是我当年第一次跑稀疏奖励任务的体验loss一直在降eval成功率始终是0。后来才发现不是网络不学习而是环境中根本没有可以让它学习的“成功样本”。1.2 一句话理解HER把靶心挪到箭落的位置HER这个名字很有迷惑性我最初以为它是什么高深的重参数技巧其实核心思想一句话就能说清楚当一条轨迹没有达到原始目标时不要扔掉它而是给它重新标一个“事后目标”。举个例子你射箭瞄准十环结果箭射到了七环的位置。教练走过来把靶心挪到你箭落的地方说“现在目标就是这里你刚才那一箭正好命中。”于是你就有了一条“成功经验”在当前姿势下发力箭会落到目标点。下一次再面对真正靶心的时候身体至少知道“往哪个方向偏一点”了。HER就是把这个“挪靶心”的过程自动化拿一条失败轨迹里的某个后续状态当新目标把原来的动作序列标记成“成功达成这个新目标”的示范。所以它的全称是Hindsight Experience Replay关键词其实是“Experience Replay”的改造——同一个经验换一个目标再看价值完全不同。1.3 谁适合读以及你能拿到什么如果你是这几类人这篇内容应该对得上胃口正在做目标条件强化学习Goal-Conditioned RL发现网络不收敛想找一个简单有效的方案。在做机器人仿真任务比如Gym里的Fetch系列被稀疏奖励卡住。已经了解DDPG、TD3、SAC这些off-policy算法但还没有把HER集成进训练管线。甚至只是单纯好奇“经验回放还能这么玩”的读者跟着思路推一遍也不亏。读完你应该能回答三个问题HER为什么有效目标重标注应该从哪些状态里采样实际训练时K值、归一化、噪声这些参数怎么配不翻车。2. 核心思路拆解为什么“重标目标”能救命2.1 信用分配困境没有中间信号策略就没有方向HER之所以有效首先得从“稀疏奖励为什么学不动”说起。强化学习里的信用分配问题指的是智能体很难判断“过去哪一步动作对最终结果负责”。密集奖励下每一步都有一个数值梯度误差信号逐层回传网络能逐步调整策略。稀疏奖励下信号只有末端一个中间动作全是“黑箱”。更麻烦的是在二值奖励的设定下价值函数几乎没有任何可学习的梯度。你要算Q(s,a)的好坏得靠蒙特卡洛估计整个episode的累计回报。而当真实成功率趋近于零时所有(s,a)的回报估计都是-1Q函数退化成常数。这就是我常说的“网络还在梯度死了”。HER从另一个角度切入**它不修改奖励函数本身而是修改这个轨迹被赋予的目标语义。**一次失败尝试里一定存在某个中间状态和起点差异很大而能走到那个状态本身就说明这些动作在“通往那个状态”上是有价值的。把这个状态定义为目标原来那些动作就从“无意义”变成了“正确示范”。学习信号立刻从稀疏变密集。2.2 三个必要组件真实轨迹、虚拟目标、重算奖励HER要做好至少要处理三块东西。第一是真实轨迹。你在环境里正常跑一个episode收集到一系列transition每条包含状态、动作、即时奖励、下一状态。但别忘了这条轨迹是为了原始目标g而跑的所以状态本身要和g拼接后才能喂给网络。第二是虚拟目标。针对轨迹中的某一条或某几条transition额外选出一个新目标g。这个g不是环境随机给的而是从轨迹后续状态中采出来的。因为后续状态是当前策略实际到达过的状态把它们当目标物理上完全合理。第三是重算奖励。原始transition的奖励是针对g算的换成g后奖励必须重新计算如果下一时刻的实际状态达成了g奖励就是0否则还是-1。伪代码长这样# episode 结束后对每条 transition 做一次 HER 扩充 for t in range(len(episode)): # 原始 transition 照常存入 buffer store(obs[t] || goal, action[t], reward[t], obs[t1] || goal) # 额外生成 k 个虚拟目标 transition for _ in range(k_future): # 从未来状态里采一个实际到达过的状态作为新目标 future_idx random.randint(t, len(episode) - 1) new_goal achieved_goal[future_idx] new_reward compute_reward(achieved_goal[t1], new_goal) store(obs[t] || new_goal, action[t], new_reward, obs[t1] || new_goal)注意一个细节新目标在拼接时不仅状态要换下一状态也要用同一个new_goal拼接。因为后续Q值的计算依赖于“下一个状态下要完成什么目标”如果这里只换s不换s网络学到的目标语义就错位了。这个坑我在代码里踩过eval时才发现Q值一路混乱。2.3 四种目标采样策略future为什么是首选HER论文里给出了四种从轨迹中选额外目标的策略我直接把这几个策略的差异整理成表格方便对照策略选目标的位置优点缺点final轨迹最后一个状态最简单一条轨迹只生成一个额外目标目标多样性差长轨迹可能离当前状态太远episode整个轨迹里随机抽一个状态目标覆盖全轨迹多样性比final好可能抽到很早的状态导致目标太容易达成学习收益低random从经验池里随机抽一个状态覆盖全局状态分布目标可能和当前轨迹完全无关噪声大future从当前时刻之后的轨迹状态里抽一个目标可达成性强和当前轨迹上下文一致代码稍多一点需要保留轨迹的achieved_goal序列OpenAI论文里的实验结论很明确**future策略效果最好。**我当时不理解后来想明白了——样本的效率取决于“这个目标对当前策略来说是否可达、是否有意义”。如果抽一个完全无关的random状态相当于拿一条射箭轨迹把靶心挪到月球上这个经验基本没有学习价值。从future状态里选意味着“智能体随后真的达到了这个状态”动作和目标之间的因果关系是成立的。2.4 HER只改了一行代码普通Replay Buffer和它的区别很多教程讲HER会放到DDPG框架里说显得很复杂。但本质上普通经验回放存的是五元组(s, a, r, s, done)HER额外存了goal并且存储时对一半以上的transition做了目标重标注。区别就这两点。普通Replay Bufferbuffer.store(s, a, r, s_, done)HERReplay Bufferbuffer.store(s || g, a, r, s_ || g, done) # 额外存 k 份用虚拟目标 g 重标过的样本 buffer.store(s || g, a, r, s_ || g, done)看到没原有的DDPG、TD3、SAC框架完全不用动。你只需要替换经验回放模块让它在保存episode时顺便生成“虚拟目标经验”再让奖励函数支持“给定目标重算奖励”。所以HER的工程侵入性极低这也是它能广泛应用的原因之一。后面我给的参考实现就是围绕这个区别来写的。3. 从零搭建HER一个可落地的参考实现3.1 建议先从bit-flip环境跑通直接上机械臂任务容易让人沮丧因为环境维度高、训练慢出了问题也不好定位。我的建议是先用bit-flip这个玩具环境把HER的流程跑通。任务很简单状态是一串n位二进制数动作是翻转其中一位目标是让整个串变成预先指定的目标串比如全0。你从某个初始串出发每一步只能翻一个位直到全部匹配。这里面有两个概念正好对应HER的输入goal就是目标串achieved_goal就是当前串。它们都是离散的、可比较的奖励函数一句话def compute_reward(achieved_goal, goal): return 0.0 if achieved_goal goal else -1.0当n20时随机翻位的成功率低得可怜但HER只需要短短几万步就能解出来。跑通这个环境的过程能把“目标重标注”的感觉建立起来你会在日志里看到真实的success rate还非常低但buffer里的虚拟成功样本已经在推着Q值走了。这种“分离感”非常重要不亲眼看一次很容易在复杂任务里误判训练状态。3.2 四个必须自己动手的模块我梳理了一下实现HER最少需要四个模块缺一个都会出问题。第一个环境封装。环境step返回的obs要与achieved_goal区分开。OpenAI Gym的Fetch系列里info字典会返回两项achieved_goal和goal。如果你的环境没有这个接口得像下面这样手动补。def unpack_obs(obs, info): state obs[observation] achieved_goal obs[achieved_goal] desired_goal obs[desired_goal] return state, achieved_goal, desired_goal, info.get(is_success, 0)第二个HERBuffer。代码核心如下我注释得很细class HERBuffer: def __init__(self, capacity, k_future4, strategyfuture, compute_rewardNone): self.buffer deque(maxlencapacity) self.k_future k_future self.strategy strategy self.compute_reward compute_reward def store_episode(self, episode): # episode 是 dict包含该轮轨迹所有时间步的数据 for t in range(len(episode[obs])): # 原始 transition self.push( torch.cat([episode[obs][t], episode[goal][t]]), episode[action][t], episode[reward][t], torch.cat([episode[next_obs][t], episode[goal][t]]), 0.0 # 这里省略了 done 细节 ) # 生成 k_future 个 HER transition for _ in range(self.k_future): if self.strategy future: future_idx random.randint(t, len(episode[obs]) - 1) new_goal episode[achieved_goal][future_idx] elif self.strategy final: new_goal episode[achieved_goal][-1] elif self.strategy episode: new_goal random.choice(episode[achieved_goal]) else: # random 策略需要从全局状态池里采这行只作示意 new_goal random.choice(self.all_achieved_goals) new_reward self.compute_reward( episode[achieved_goal][t 1], new_goal) self.push( torch.cat([episode[obs][t], new_goal]), episode[action][t], new_reward, torch.cat([episode[next_obs][t], new_goal]), 0.0 ) def sample(self, batch_size): return random.sample(self.buffer, batch_size)第三个Actor-Critic网络。以DDPG为例actor和critic的输入都要拼接上goal。状态和goal的拼接维度要提前算清楚漏了这一步网络就会把“当前状态”和“目标任务”当成同一个向量里的独立特征学不出目标条件语义。第四个训练循环。每收集完一个完整episode先调用store_episode把原始经验连同HER经验一起灌进buffer再从buffer里sample batch走DDPG/TD3的更新流程。这一步和标准off-policy训练几乎一样不再赘述。3.3 为什么状态要和目标拼接一个容易被忽略的细节很多人问为什么不能把goal单独作为一个网络输入而是要把obs和goal拼接成一个向量原因很简单目标条件下的策略本质是一个以(状态, 目标)为输入的映射。同样的状态s如果目标不同最优动作可能完全相反。比如机械臂当前在杯子的左边如果目标是“靠近杯子”它要向右如果目标是“远离杯子”它要向左。如果不把goal拼进去网络根本没有办法区分这两类情况。实际操作中我习惯先把obs和goal各自归一化再concat。连续控制环境里obs维度可能差异很大比如关节角在-3到3物体位置在-1到1直接拼接会让网络对数量级大的维度更敏感。归一化到[-1, 1]之后训练稳定性会明显提升。3.4 用FetchReach和FetchPickAndPlace验证参数迁移跑通bit-flip后可以挑战机械臂系列。OpenAI Gym里几个环境难度递增FetchReach末端移动到目标点、FetchPush推动物体到目标点、FetchPickAndPlace抓取并放置。我的经验参数表如下可以直接抄作业参数bit-flip (n20)FetchReachFetchPickAndPlace算法TD3 HERDDPG HERDDPG HER或TD3k_future448策略网络学习率1e-31e-31e-3Q网络学习率1e-31e-31e-3gamma0.980.950.95batch_size256256256探索噪声0.30.20.2训练步数2万10万~20万50万~80万成功率参考95%90%70%特别强调一下gamma。FetchReach里episode最长50步gamma0.95足够如果任务轨迹更长比如机械臂需要连续操作100步以上gamma我会调到0.98甚至0.99否则远端状态的未来回报会被过早截断。这个细节我见过不少人忽略导致长horizon任务怎么调都上不去。4. 实操过程中的5个坑与排查技巧4.1 Q值震荡与过高估计先检查TD3和target网络HER和DDPG搭配时最容易翻车的现象是训练到一半eval成功率突然往下掉Q值曲线大幅波动。这通常不是HER的锅而是DDPG本身的过高估计问题。稀疏奖励下Q值对某个(s,g)组合很容易被推到虚高policy顺着虚高的Q方向更新然后越走越偏。我的排查顺序很固定先确认target网络有没有soft updatepolyak系数建议0.95~0.995再检查critic是否用了double Q结构如果还是DDPG建议直接换TD3。在FetchPush上我用TD3替代DDPG之后成功率大约提升了15个百分点而且训练曲线平稳很多。4.2 训练前期reward全-1是正常的吗看achieved_goal分布第一次跑HER的朋友很容易被日志吓到“跑了5万步真实奖励还全是-1这算法到底行不行”答案是正常。HER的价值在于构造虚拟成功样本并不是让真实成功率立刻变高。这时候你应该看另一个指标achieved_goal的分布。如果智能体只在初始状态附近打转说明探索不足如果achieved_goal覆盖了目标空间的大部分区域说明策略已经开始“动作多样化”了离真实成功不远。我在bit-flip上训练时最开始几千步的真实success rate确实是0但buffer里的虚拟成功样本占比已经超过一半了。所以排查时不要盯着reward要看动作空间有没有被充分探索。4.3 HER比例和K值怎么定这个参数比学习率还关键k_future代表每条原始transition额外生成多少条HER样本。这个参数直接影响学习信号的构成。我试过k2、4、8、16在机械臂任务上的规律很清晰k太小时虚拟目标覆盖不充分训练慢k太大时真正的多目标语义被冲淡策略偏向“到达中间状态”但对原始目标不敏感。经验值是简单任务k4机械臂操作类任务k8。另外不要把所有transition都用HER重标一遍给原始经验留一定的比例否则网络对“真实目标”的记忆会被淹没。我在代码里通常让HER样本占总样本的60%到80%效果比较稳。4.4 目标空间不归一化距离阈值就是玄学连续目标下奖励函数往往不再是比较是否相等而是判断欧氏距离是否小于某个阈值。比如FetchReach里判定成功的条件是末端执行器距目标点小于5厘米。问题来了如果goal向量的三个分量尺度不一致比如x、y在[-1, 1]而z轴在[0, 0.3]那么直接用欧氏距离计算就会让z轴被“淹没”阈值怎么调都不对。我踩过一次机械臂明明已经到目标点了成功率还是0最后发现是距离计算时没做归一化某个维度贡献的距离被放大了好几倍。解决办法很简单把achieved_goal和goal各自减去均值除以标准差或者缩放到[0, 1]再做距离判断。4.5 哪些任务不适合HER提前识别边界能省几天时间HER不是万能药。我把它适用边界划得清楚一点免得大家在一个不合适的任务上死磕。不适合的情况有三类。第一目标无法用状态表示。比如“穿过迷宫出口”这类任务你没法把出口挪到某个墙内状态上重新标因为迷宫通关不存在“中间状态即目标”的语义。第二目标是唯一且不可改变的。严格来说HER看重标目标的可达成性如果任务定义死了某个必须精确触达的终点重标目标会改变任务本质训练出来的策略对真实目标没有泛化能力。第三奖励依赖整条轨迹历史而不是当前状态距离目标的差距比如“必须先按开关再抓杯子”这种时序约束没法靠目标重标注表达。如果发现自己的任务属于以上情况就别强上HER改用手工奖励塑形加课程学习会更靠谱。5. 写在最后一点个人体会我常说HER是强化学习里的“后悔药”它让算法能在失败里翻出金子。但后悔药不能解决所有问题它在目标可重标的任务里才发光。从bit-flip到FetchPickAndPlace我亲手跑过不下二十个变体最大的体会是HER真正改变的不是算法结构而是看待样本的心态。最后分享一个小技巧训练过程中评估模型时一定要用“真实目标”去测不要用重标注后的虚拟目标评估。HER会让策略在某些中间状态上表现得异常好但这不能代表它在原始任务上的能力。我习惯把评估脚本单独写出来每个环境固定随机种子每10万步跑50个episode只统计真实成功率。这样曲线虽然看起来涨得慢但每一分提升都真实可靠。如果你正被稀疏奖励卡到头秃不妨先拿bit-flip热身再上机械臂相信HER会是你工具箱里“返场率”最高的那个组件。