它出现在论文里、代码库里、也出现在我对“怎么教机器人”这件事的长期困惑里。别急着关页面,这里说的不是时间管理,也不是复盘方法论,而是深度强化学习里的一个具体算法:Hindsight Experience Replay,后见经验回放,缩写叫HER。我第一次看到这个名字时,第一反应是“这不就是事后诸葛亮吗”,仔细读完才发现,这个“事后诸葛亮”恰恰是解决稀疏奖励问题最优雅的思路之一。如果你正卡在“奖励函数怎么写都学不会”的泥潭里,或者你在调试某个机械臂、自动驾驶、游戏AI,发现智能体总是傻站着不动,大概率你需要看一下这种“用失败当成功”的训练思路。
这篇文章会把这个算法的来龙去脉、它背后的原理、我在实操中踩过的坑,以及一份可以直接改成自己的环境的代码框架,全部铺开来讲。无论你是刚开始接触强化学习的学生,还是已经在工业界跑过几个环境的工程师,这篇都可以当一份有点“私人心得”味道的参考笔记。
1. 整体思路拆解:HER到底在解决什么问题
1.1 从“投不进就换目标”说起
Hindsight Experience Replay的核心思想,用生活化的话说就是:如果一个人投篮总是不进,那不妨换个标记,把篮筐的位置改到他实际投到的落点上,然后让他学着“如何在下次把球投到这个地方”。强化的不是“进球”这个结果,而是“投出手感”。这句话翻译成强化学习语言就是:在经验回放中,把一条没有达成原始目标的轨迹,重新标注成“达成了它最终实际到达的那个目标”的轨迹,再把重标后的数据放进回放缓冲区里。这样,原本稀疏的、几乎没有正反馈的样本,就被转化成了一条带正奖励的样本。
我来解释一下这个转变有多“反直觉”。通常我们训练一个策略网络,告诉它“把红色方块推到绿色位置”,它试了几万次,一次都没成功到达,那所有样本的奖励全是零或负的小数。神经网络从这些全零样本里学不到任何梯度方向。但是HER的思路是:你虽然没推到绿色位置,但你把方块推到了蓝色位置。那就把“蓝色位置”设置成这次episode的目标,重新算一次这条轨迹的奖励。你发现,这次“手指触碰方块并移动它”的动作,确实达成了“把方块推到蓝色位置”这个目标。一条原本毫无引导作用的失败轨迹,变成了一条“示范性成功轨迹”。这个训练信号密度一下子就上去了。
本质上,HER把单目标强化学习问题改造成多目标强化学习问题,多出来的目标就是“尝试过程中真实发生过的状态”。这个思想在目标条件化强化学习的框架下特别自然,因为策略网络本来就是输入一个目标再输出动作的,换个goal重新标一遍奖励,完全不改变样本的可利用性。这种妙处,我后面会展开。
1.2 稀疏奖励环境的数学本质
为什么稀疏奖励这么难解,要专门用“后见之明”来处理?把问题简化成一个公式:目标是初态、末态和一个奖励函数。如果奖励只在到达目标时变成1,在其他时候都是0,那这就是一个稀疏奖励问题。在这种设定下,随机探索找到成功样本的概率,随状态维度和目标距离呈指数级下降。一个机械臂要推动滑块到某个坐标,初始随机抖动基本不可能恰好推到位。而强化学习的梯度、价值函数的更新,全都依赖样本里的非零奖励传递。奖励极度稀疏时,训练过程很多个epoch都搜索不到有效的学习信号,策略就一直停留在随机初始化附近。
有人会说,那我用“奖励塑形”行不行,每一步减少一次“当前距离目标”的距离作为惩罚。这个方案在很多简单任务里确实有效,但它有两个我实际项目中很头疼的问题:第一,奖励塑形需要人工设计中间势能函数,这个函数本身就要针对环境仔细调,运动学一变就要重新设计;第二,塑形过密会让策略学会“作弊”,比如在目标点附近来回转圈刷距离惩罚,而不是真正完成高阶行为。HER走的是另一条路:不去设计人工奖励,而是通过切换目标来把一个稀疏信号变成“如果目标是状态S,那这个信号就密集”的条件性信号。每个过渡其实都可能是在“某个目标”上成功发生的动作,只是不巧那个目标不是我们自己想要的目标。把目标换成实际发生过的那个状态,稀疏问题就被结构性绕开了。
1.3 为什么选择HER而不是其他“事后补救”方案
我先把话放在这儿:HER并不是万能的,它有明确的适用边界。但在它的边界内,它比很多复杂度更高、花里胡哨的方法要稳健得多。对比一下几个思路:第一是基于逆态动力学的“目标重标注”方法(比如把当前状态倒推成之前的动作走向),实际上HER就是这个家族的成功代表,它的朴素版本直接在轨迹里采样新目标,不需要学习任何额外的模型。第二是分层强化学习,先把任务分解成子目标序列,再用子目标密集奖励训练底层策略,但分层本身对子目标划分的合理性要求很高,实际工程里拆分错了就很难救。第三是基于好奇心机制的内在奖励,让策略优先探索未见过的状态,这在无目标场景里可以做熵增探索,但是它对“完成任务”这个目标本身不敏感,容易让智能体变成到处乱逛的新状态收藏家。
相比之下,HER改动少、侵入性极低。如果环境本身就提供observation和achieved_goal结构化信息(很多gym环境,比如Fetch系列就是这么设计的),那只需要在样本收集循环里加几行重标代码,把replay buffer里的样本复制一份,把目标换成未来的某个真实状态,算法主体完全不动。这一点在实际工程里特别重要:因为强化学习项目里真正烧时间的,往往不是模型训练本身,而是反复调试环境、奖励函数、各种callback以及和模拟器的耦合。一个只需要在数据管线上加“目标替换”模块的方案,天生就比别人更“好落地”。
2. 核心细节解析与实操要点
2.1 四类目标采样策略:future、episode、random、final
HER的论文里其实提到了好几种从轨迹中选取“替代目标”的策略,实操时这绝对是最容易被忽略但实际效果差距很大的细节。我直接给你列一个表:
| 策略名称 | “替代目标”是怎么选的 | 我的使用环境和感受 |
|---|---|---|
| final | 只用轨迹最后一个状态当目标 | 简单但太粗暴,早期探索没到有价值的区域时,这个“后见之明”参考性有限 |
| random | 从整条轨迹中均匀随机抽一个状态当目标 | 训练稳定,但有时抽到太早的状态,导致目标难以达成 |
| episode | 从当前时间步之后的轨迹状态里抽一个 | 保证目标是“未来可能到达的位置”,比random更有方向性 |
| future | 从当前时间之后的某一段窗口里抽一个状态 | 多数论文和开源实现里最优首选,有足够探索性又不过分发散 |
我自己实测下来,future通常最稳。原因在于它保证了替代目标相对于当前状态来说,是一个“未来状态”,这意味着新目标在动力学上是可达的,策略不需要去学那些物理上根本到不了的幻想目标。random虽然简单,但它可能抽到一个已经在过去发生、此后环境状态不可逆变化的位置,导致伪成功轨迹里的Step逻辑混乱。
有个细节值得单独提一下:future策略通常取k个未来状态之一,k是一个超参。比如取未来状态集合里的第k个,k可以在3到10之间随机抽。这个“时间偏移”会让训练有更好的鲁棒性,因为目标不会过度集中在短期内可到达的位置。我在实验里常用的默认值是:从当前时间步后4步到后面20步中随机抽一个状态。这个值在机械臂和导航任务中表现都不错。
2.2 奖励稀疏值的选择:不是所有环境都设0/1
很多人以为HER就是“把所有奖励变成0和1”,其实不对。HER解决的思路保留原环境的奖励结构。如果原始奖励是一个0/1的判断函数,那重标后也还是0/1,只是判断的目标变了。如果原始奖励是带距离的衰减惩罚,比如每一步减去当前位置和目标位置的欧氏距离,那重标时只需要把距离计算换成实际到达状态与替代目标的距离即可。
实操里最重要的是“目标判断阈值”。很多gym环境,比如FetchReach,默认当欧氏距离小于0.05时判定为成功。这个阈值不是随便给的:太小,训练后期微调困难;太大,策略会“到附近就满足”,泛化后没法精确定位。我个人的经验法则是:阈值设定为任务可接受误差的2~3倍,训练中期观察一下“成功率”曲线的走势,再逐步收紧。不要一开始就设得极严格,那样等于把稀疏奖励问题又改回升级版。
2.3 HER在数据流层面动刀的5个关键点
这里我把“我们要对样本做什么”理成五个步骤,每个都是在实际写代码时容易含糊的地方。
第一,原始goal和achieved goal必须分离。观察向量里要有当前状态obs、期望目标goal(也叫desired_goal)、实际达成目标achieved_goal。在gym的Dict observation空间里一般就直接提供这三个字段,如果你的环境没有,那要自己定义“哪个量代表实际达成目标”。这一步没做对,后面所有重标都是空谈。
第二,每条transition要存两个版本。原始的存储在replay buffer里用于学习原始目标;另一个是重标后的版本,把desired_goal替换成轨迹里采样的替代目标。要注意替换的不只是状态里的dict那一个key,还要重算奖励和done标志。如果done标志在原始轨迹里是False,但重标后正好到达了替代目标,那么这条样本的done要置为True。
第三,重标应该在一个轨迹收集结束后,遍历整条轨迹动态执行。这样才能保证future策略的“未来采样”存在一个时间顺序索引。提前收集完再做重标是最简单的,我就用event buffer存储一整条transition轨迹,在episode结束后统一重标再入池。
第四,重标的比例不是1:1强制。有些实现会让每条transition对应一条原始样本加一条重标样本,也有些实现选择30%的样本做重标。我自己的建议是:初始阶段让重标样本比例高一点,比如每条原始样本都额外生成一条重标样本,等于buffer里每个样本存两份。这样可以快速建立“动作目标对应关系”的基础。如果任务是简单控制器能较易完成的,也不需要重标过度。
第五,注意高维状态重标的一致性。如果替代目标是一个图像,那在技术上说重标就是把当前图像当成目标图像,但是图像像素空间里的距离判据往往不如低维坐标好使。所以HER最擅长的场景,仍然是目标空间可定义、可达状态可量化的:位置、姿态、角度、关节角等都行。图像级目标,我一般建议先做表征学习,把图像压缩成隐向量再当目标用。
3. 实操过程:我要怎么把HER接进自己的训练管线
3.1 一个具体可运行的最小案例设计
与其抽象地说HER,我想用“小球在二维平面被机械臂推动”这个简化版场景来演示。假设我们的环境状态空间为:
observation:机械臂末端坐标、当前推动小球的位置achieved_goal:当前小球的二维坐标desired_goal:我们希望小球到达的二维坐标
机器人动作不是连续控制那么复杂的动力学模型,就简化为:每一步朝某个方向推动小球,步长为0.05。奖励函数设定为如果小球与目标点的欧氏距离小于0.1则奖励1,否则奖励0。初始目标每次episode重采样。这个设定下,如果没有HER,纯随机探索的成功率差不多是1%不到,就算加epsilon-greedy也难学。用上HER后再做DQN或者DDPG,通常在几万步内就能看到成功率明显上升。
3.2 关键代码框架:目标是重标,不是算法
我先给一个“目标重标”的伪代码框架,实际项目直接用就行,语言用Python,结合通用的强化学习库接口:
# 每条episode收集完成后,执行her_relabel import numpy as np def her_relabel(episode_buffer, future_k=4): # episode_buffer: list of dict # 每个dict含 keys: observation, achieved_goal, desired_goal, action, reward, next_observation, next_achieved_goal new_transitions = [] T = len(episode_buffer) for t in range(T): # 原始样本入池 new_transitions.append(episode_buffer[t]) # 用future策略采替代目标: # 在当前时间步之后,等间隔抽取k=4个未来状态,再随机挑一个 future_steps = np.linspace(t+1, T-1, num=future_k).astype(int) future_steps = future_steps[future_steps > t] if len(future_steps) == 0: continue chosen_idx = np.random.choice(future_steps) future_goal = episode_buffer[chosen_idx]["achieved_goal"].copy() # 构造重标样本 trans = episode_buffer[t].copy() trans["desired_goal"] = future_goal # 重算奖励 trans["reward"] = float(np.linalg.norm(trans["achieved_goal"] - future_goal) < 0.1) # done判定:如果现在或下一时刻达成目标,则置1 trans["done"] = float(np.linalg.norm(trans["next_achieved_goal"] - future_goal) < 0.1) new_transitions.append(trans) return new_transitions这里有几个容易踩的点需要专门说一下。
第一个,future_goal不要直接用achieved_goal以外的量,比如不要拿next_observation整个向量。我一开始偷懒直接取了一个状态向量当目标,后来发现维度不对,因为目标是子空间维度,状态是整个高维观测,混在一起会让目标维度爆炸。
第二个,done标志的重算不能漏。如果你只是把奖励重标了而done还是原来的0,那么当小球正好在重标目标附近时,这条样本不会触发序列终止逻辑,导致价值函数里面的终态特征学不到。相反,重标后done为1的样本,在DQN更新时没有next_state的Q项,这是算法收敛的关键。
第三个,这里是等间隔采样再加随机,而不是纯随机采样。因为未来状态之间具有较强的时序相关性,纯随机采样容易集中到那些物理上可达但语义上“脱节”的位置。等间隔采样可以让替代目标覆盖一个更广的时间尺度。具体K值:我发现3~10比较合理,太小等于final,太大等于random,这两种极端下训练曲线都明显更慢。
3.3 与DDPG/DQN等主流算法的整合
HER本质上是数据增强方法,不是独立的优化器,所以任何一个off-policy算法都能接。我自己最常用的是DDPG加HER,在连续控制环境中最稳。这里给一个训练主循环的骨架:
# 训练主循环伪代码,简化版 buffer = ReplayBuffer(capacity=200000) for episode in range(50000): obs = env.reset() ep_transitions = [] while True: action = policy(obs["observation"], obs["desired_goal"]) next_obs, reward, done, info = env.step(action) t = { "observation": obs["observation"], "achieved_goal": obs["achieved_goal"], "desired_goal": obs["desired_goal"], "action": action, "reward": reward, "next_observation": next_obs["observation"], "next_achieved_goal": next_obs["achieved_goal"], } ep_transitions.append(t) obs = next_obs if done: break relabeled = her_relabel(ep_transitions, future_k=4) for t in relabeled: buffer.push(t) if len(buffer) > warmup_steps: batch = buffer.sample(batch_size=256) # 标准DDPG更新 update_actor_critic(batch)这里有个细节:在HER框架下,exploration noise的选择比普通DDPG更重要。因为替代目标往往给了策略一个相对丰富的梯度方向,探索噪声太大反而会把“移动到位”的成功动作覆盖掉;噪声太小则无法在episode内产生足够多样的未来状态。我实验里标准差取0.2为初始值,训练后期为了精细控制再线性降到0.05左右。
还有一个所有HER实现都会涉及的“buffer容量”问题。因为每个episode每条样本都会产生一份重标样本,实际存的有效经验条数会翻倍。如果你的环境episode特别长,比如500步,那么存储量就要按1000步来规划。不要设小了才发现历史经验被覆盖得太快,导致目标重标多样性不足。
3.4 训练指标怎么看
光盯着成功率曲线也不行,因为HER特有的“重标成功”会让曲线产生错觉。我在训练中最常用的是两个指标组合:
第一,重标样本的即时奖励均值。这个值如果持续为0,说明重标后的样本依旧稀疏,通常是因为替代目标都选得太远了,或者是阈值太严格,action space太大导致根本移动不动。这个值如果快速上升,说明策略已经开始能达成“附近目标”,这是好信号。
第二,原始目标的rollout成功率。在每个epoch结束,固定几个目标任务,关闭探索噪声,跑若干rollout统计真正达成目标的频率。这个不受重标干扰,直接反映最终任务执行效果。如果重标奖励均值上升速度远远快于原始成功率,那大概率说明策略在“跟着目标走”这一层学会了,只是在原始目标的最终精度上还不够,这时把阈值慢慢收紧依然很有效;如果重标奖励上升但原始成功率完全不动,有可能动作空间探索还不够,或者目标维度与观察维度之间的映射没有对齐。
4. 常见问题与排查技巧实录
4.1 加了HER还是学不会,先查这几个地方
我见过最多的一种情况是,有人往自己的环境里加了HER,但训练还是死水一潭。每次遇到这种事情,我都建议按这个顺序排查。
先看替代目标的选取是否落在了“实际可发生的状态集合”里。如果你把目标定成了机械臂前端某时刻的末端位置但任务目标是“推动方块到某处”,那么替代目标实际上应该从方块的位置坐标里选。如果你把机械臂位姿选成目标,方块位置仍然是随机初始,样本重标之后奖励判断根本对不齐。很多人默认achieved_goal就是从observation里随便挑一个分量,结果挑错了维度,问题就锁在这个地方。
再看当前算法是不是on-policy。HER天然适配off-policy算法。如果你在PPO或者A2C里用HER,会碰到非常尴尬的局面:重标后的样本违背了on-policy采样分布,但又带有一个明确奖励偏差,训练起来方差巨大。我的建议是:要么把PPO的更新改成off-policy形式的策略优化(等于重写核心引擎),要么直接换成SAC、DDPG等算法。我不推荐在PPO管线里强行加HER,除非你只是想做一个学术对比实验。
再看目标空间的“距离度量”是否合理。HER框架下的稀疏奖励往往用的是欧氏距离。如果目标是旋转角度,欧氏距离对角度可能不连续(比如350度和10度用欧氏距离通常很远但实际只差20度)。我处理旋转类目标时用弧度差值并对2pi取模,把距离函数改掉。不要小看这个问题,旋转变量的维数如果没做wrap-around,HER重标出来的奖励信号直接是反的。
4.2 “目标重标”和“奖励函数”两全策略
还有一个常见误区,以为用了HER就能完全放弃奖励塑形。我实际使用中的倾向是混合方案:在HER重标的基础上,再做一层小而温和的稀疏奖励,比如当步骤距离小于0.5时奖励0.1,小于0.1时奖励1。
这样做的理由很简单:HER生成的新样本并不总是严格映射到“接近某个替代目标”的轨迹,早期替代目标本身可能很稀疏,纯0/1奖励也会让价值网络初期难收敛。加一层梯度强度极低的中间奖励,相当于给价值网络一个“斜坡”而不是“悬崖”,网络更容易拟合。但这里务必注意,中间奖励的增量不要大于最终成功奖励,否则策略会过度保守,只愿意刷那一点零星距离奖励,而不敢去尝试高风险高获利的路线。
4.3 常见问题速查表
| 故障现象 | 可能原因 | 我的处理办法 |
|---|---|---|
| 重标样本奖励长期为0 | 替代目标离当前时刻太远,或者阈值太严格 | 调大阈值、改用future的短偏移范围 |
| 原始成功率不动,重标奖励上升 | 策略学到了“达成替代目标”但没学到“柔和到达最终目标” | 检查目标维度、收紧阈值或加少量曲线奖励 |
| 训练后期震荡严重 | 重标样本占比过大,目标分布太杂 | 重标样本比例从1:1降为1:0.3 |
| 用上了但buffer清空后不收敛 | 重标目标一旦被覆盖,真实成功样本还是太少 | 调大buffer容量或提高探索强度 |
| 多目标环境里每个episode的目标切换太频繁 | HER伪装出“成功”导致跟踪混乱 | 用log轨迹统计每个替代目标的出现频率,如果过度集中在同一个目标则改成随机采样的prior |
4.4 关于“重标时机”的隐藏细节
另一个容易翻车的地方,是重标是在线做的,还是episode结束后做的。很多实现为了实时性,会在transition节点直接对未来状态抽样重标。但这样可能踩一个时序漏洞:在episode还没结束时,你选择了“未来状态”作为目标,然而这个未来状态可能因为中途done而根本不存在,或者距离终点过远。我使用episode结束后做完整重标的方案,能保证任意一个替代目标一定在轨迹中存在,且其时间索引明确有序。
如果你真的需要在线重标(比如某些内存极度受限的移动端训练场景),那至少要保证:不重标done之前的Transition,并且对替代目标对应的reward做“事后判断”。在在线过程中没法预测未来,你在当前时刻把某状态设为目标但下一时刻动作已经变化,重算奖励和done会面临滞后。所以宁可牺牲一点实时性,在episode缓冲区里组织好再统一处理。
5. 经验扩展与场景适配
5.1 什么情况下别用HER
我在前面强调了HER的适用范围。这里说说我亲眼见过“用HER反而变糟”的场景。
第一类是环境轨迹不可逆的场景,比如货币交易策略,状态是价格序列,目标可能是“获得收益率超过X”。但是每一条历史轨迹都是唯一且不可重演的,你没法在同一个episode里重标出一个“实际发生过的未来状态”,因为“实际发生”本身已经是孤立的样本了。HER重标依赖状态空间中同一轨迹相邻一致性,这在纯序列决策里基本失效。
第二类是目标空间和观测空间严重不对齐的环境。比如自然语言对话任务,目标可能是“让用户说出特定回答”,但是被选为替代目标的“实际用户回复”是一段自由文本,它的生成空间远远大于我们能判断成功的条件。神经网络很难通过文本距离判断“目标是否达成”,HER在这里就变成了一个性价比很低的近似。
第三类是本身奖励已经足够稠密的环境。HER的收益源于把稀疏变成相对密集,如果奖励本来就每步有值,还硬要加HER,反而引入冗余的“伪目标”噪声,干扰原来稳定的梯度方向。我的原则是:当随机策略在最初5000采样步里能找到5%以上的正向奖励时,不需要HER。
5.2 HER与多目标强化学习的扩展思路
HER的重要理论启发是:一个在目标A上失败的轨迹,在目标B上可能是一个成功的示例。把这个思想进一步扩展,就有了多目标强化学习的各种变体。比如,你可以不只做后见式重标,还可以做“目标空间分布自动扩充”:每训练一段时间,用胜利状态聚类生成新的伪目标,再把它们注入到目标分布里。HER本质上给这样的自奖励机制提供了一个廉价可扩展的信号源。
再比如,HER可以被整合进基于模型的强化学习方案。我之前试过一个组合:用动力学模型做推演,预测未来N步的可能状态,然后把预测状态当成候选替代目标。这样,替代目标不再局限于当前现实轨迹,而是扩展到“模型想象中的可行状态”,相当于给HER加了一层想象空间,样本效率更进一步。
5.3 我在实际项目中总结的实验配置
最后一个部分,我把自己经常作为默认配置的一组参数给出来,不是标准答案,但能满足大部分物理仿真控制任务:
| 超参名 | 我的默认值 | 备注 |
|---|---|---|
| future_k | 4 | 太少接近final,太多接近random |
| 重标比例 | 每样本额外生成1条 | 可在训练中后期降为0.5 |
| buffer容量 | 训练episode总数估计值 x 平均步数 x 2 | 为重标预留双倍空间 |
| 距离阈值 | 任务允许误差的2倍 | 训练后期收紧 |
| 奖励结构 | 成功1.0 + 距离小于0.5奖励0.1 | 后一项不要超过成功奖励的20% |
| 采样批次 | 256 | 偏低时价值网络偏差明显 |
| 目标更新频率 | 每100步更新一次target网络 | 太频繁会震荡 |
这些数值并不是从论文抄来的,而是调参时逐渐感觉比较合理的区间。你换成自己的场景时,可以在这个基础上左右拨动,观察比较的对象主要是两个指标:重标奖励均值和最终成功率曲线。
有一件事,我在自己的项目中体会很深:HER这类数据增强算法,真正发挥作用的前提,是你能把环境状态拆得足够清楚。很多强化学习任务调不动,不是算法不够新,而是不知道“当前状态哪些维度和目标相关”。HER逼着你去定义achieved_goal,逼着你去想状态空间结构,这个梳理过程本身的价值,有时比HER带来的几个百分点提升更大。如果看到这里的读者恰好正在纠结某个环境学不会,我建议你先别急着堆模型,把状态和目标写下来,想想“如果这条轨迹失败了,它到底在哪个别的目标上成功了”。想通这层,离训练通就不远了。