恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
HER算法详解:用事后经验回放破解强化学习稀疏奖励难题
首页
资讯中心
/
HER算法详解:用事后经验回放破解强化学习稀疏奖励难题
HER算法详解:用事后经验回放破解强化学习稀疏奖励难题
发布时间:2026/10/1 6:42:21
如果你在搜索引擎里敲下 hindsight会先看到词典里后见之明的解释接着是认知心理学里的 hindsight bias数字取证圈还有一款同名工具。但如果你是在训练强化学习智能体时搜这个词那你大概率已经被稀疏奖励折磨过了——后见之明经验回放Hindsight Experience Replay简称 HER是我在机器人操作类项目里复用率最高的一招没有之一。这篇文章我会围绕 HER 掰开揉碎讲它解决的到底是什么问题、那个事后复盘失败轨迹的机制为什么能大幅提升样本效率、从零实现一个 HER 训练管线要怎么写代码以及我实际跑实验时踩过的坑和试过的变体。目标读者是正在做机器人控制、游戏 AI、仿真导航这类 goal-conditioned 任务的同学。如果你只是听说过这个名词想去彻底搞懂它这篇同样适用我会把必要的 RL 前置知识都铺开讲不需要你之前接触过 HER。1. 稀疏奖励的训练困局为什么模型会越训越傻接触 RL 的人几乎都会在一个看起来很简单的任务上怀疑人生机械臂推一个方块目标位置离初始点就几厘米训练了很久成功率纹丝不动。问题不在模型在奖励信号。1.1 从全程零奖励说起RL 智能体最常见的死法在 goal-conditioned 任务里每个 episode 会给智能体一个 desired goal期望目标同时观测里包含 achieved_goal当前实际状态。典型的奖励函数长这样r(s, a, s) -1 如果 ||achieved_goal(s) - g|| 0.05 0 否则对 Fetch 这种连续控制环境动作空间是 4 维末端位置控制加夹爪一个 episode 有 50 个时间步目标空间是 3 维位置。初始随机策略能推到目标半径 0.05 的小球里的概率极低于是整个 episode 的奖励序列就是整整一页 -1。这带来一个很隐蔽的恶性循环on-policy 算法比如 PPO采一条轨迹发现奖励全负梯度算出来接近一个常数策略更新约等于随机游走off-policy 算法比如 DDPG虽然把样本存进回放池但池子里正样本比例几乎为零Q 函数根本没法区分这个动作比那个动作好学到最后就是输出动作均值。更直白地说智能体从头到尾没见过成功长什么样自然不知道往哪个方向改进。很多入门玩家在这阶段的第一个反应是把奖励改密一点于是开始手工设计 reward shaping往往越改越乱。遇到这种模型越训越像个傻孩子的现象先别怀疑网络容量极大概率是监督信号本身就断了。1.2 人类的后见之明怎么变成算法的可执行逻辑想想人类是怎么处理失败经验的。投篮没进目标当然是进球但实际结果是球砸在篮筐左侧弹了出来。这时候你不会觉得这条轨迹毫无价值你会本能地注意到偏左了下次手腕再抬一点。换句话说这条没进球的轨迹在球最终落在左侧这个事后目标下其实是一套非常连贯、可复用的动作模式。HER 的核心就是把这种事后聪明变成算法机制。它问了一个反直觉的问题既然这条轨迹没有完成目标 g那它总归到达了某个状态 g。如果把 g 当作目标那么这条轨迹从后半段开始就是一条带正奖励的成功轨迹。那我们为什么不顺手把它当成成功样本存下来这个想法能成立有一个关键前提策略必须是目标条件化的也就是写成 π(a | s, g) 的形式。策略不只是根据当前状态决定动作还要知道自己此刻在追哪个目标。只要策略能接受任意目标作为输入我们就可以在训练数据里安全地把目标换掉——反正测试时我把真正的目标塞进去就行。1.3 为什么 reward shaping 和 curriculum 不够用在 HER 流行之前大家对付稀疏奖励主要靠三招我分别试用过说下真实感受。Reward shaping 是最常用的把稀疏奖励改成距离的负函数比如 r -||achieved_goal - g||。听起来合理但势函数设计是个坑太陡智能体学会在半路摆烂太平又回到稀疏问题。更麻烦的是它会改变最优策略的定义智能体经常找到刷距离分的漏洞而不是真正完成任务。Curriculum learning 是从易到难。问题在于连续控制任务里难度很难自动度量课程设计本身就是一篇论文的工作量。而且课程切换时智能体经常出现灾难性遗忘前面学好的策略到难任务上直接崩掉。加大探索噪声是最省事的但稀疏任务里你面对的往往是高维动作空间纯靠噪声撞到目标区域概率低到可以忽略。这也是我对 HER 评价高的原因它不改环境、不改奖励函数、不需要课程设计只改回放池里的数据分布就凭空把样本效率抬了一个数量级。接下来就看这套机制到底是怎么运作的。2. 后见之明经验回放把失败轨迹改写成学习样本的那套机制HER 在 2017 年由 Andrychowicz 等人提出论文题目就叫《Hindsight Experience Replay》。当时解决的问题非常具体在 OpenAI Gym 的 Fetch 系列机器人任务上DDPG 单独训练几乎学不动加上 HER 之后成功率能从接近 0 拉到 50% 甚至更高。2.1 目标条件化策略HER 变革的前提普通 RL 的策略是 π(a | s)只看状态。目标条件化策略则是 π(a | s, g)把目标作为条件输入。对应的 Q 函数也是 Q(s, a, g)。这样做虽然简单却是 HER 的基石。原因在于如果我事后把 g 改成 gtransition 里的状态转移和动作都没变但整个数据的语义变了——它从一条失败轨迹的一部分变成一条朝 g 成功轨迹的一部分。如果策略不是目标条件化的改目标没有任何意义因为策略压根不知道目标是什么。在实现层面这通常意味着 actor 和 critic 的输入层要把状态和目标拼接起来。Fetch 环境里目标一般只有 3 维所以拼接的成本很低。2.2 relabel 的完整流程目标替换、奖励重算、数据入库一条 Episode 结束后HER 不是直接把它丢进回放池而是做一次数据清洗。完整流程分四步记录整条轨迹的观测、动作、奖励、下一观测。从轨迹中按某种策略挑出一个替代目标 g见下一节。对轨迹里每一个时间步用 g 重新计算奖励 r。由于 Fetch 这类环境的奖励只依赖 achieved_goal 和 goal 的距离重算就是一个 L2 距离加阈值判断成本几乎为零。把改写后的 transition (s, a, r, s, g) 连同原始 transition 一起存入回放池。注意relabel 只改两个字段goal 和 reward。状态、动作、下一状态全部保持原样。换句话说我们是在用真实的物理轨迹配上事后选定的目标人为制造出大量带正奖励的样本。这种做法的本质是把目标 g 当作可替换的标签而不是环境给定的事实。放在监督学习里相当于你给一张照片换不同的标注每换一次就多一个训练样本。正因如此某个被推歪的方块位置既能作为想去左边却推过头的失败样本存在也能作为目标就是推到右边的成功样本存在。2.3 final / random / future四种替代目标采样策略的取舍HER 的论文里比较了四种替代目标的选取方式这个细节直接决定了效果策略目标来源特点final轨迹最后一个状态实现最简单目标多样性低random轨迹中随机一个状态多样性好但可能采样到过去的状态future当前时间步之后的一个状态论文中效果最好episode future变体每个时间步按概率从未来状态中采样常用于工程化调优我在实际实验里只用 future几乎没有例外。原因可以从分布匹配的角度解释future 采出来的目标 g 来自当前时刻之后的状态它描述的是从当前状态出发将来可能到达哪里。测试时你给智能体的目标恰恰也是从当前状态出发希望到达哪里。训练与测试的目标分布一致策略学到的就是一个通用的走向未来可达状态的控制律。而 random 策略可能选到当前时刻之前的状态等于要求智能体学会回到过去这和测试时的目标分布明显不一致效果自然差。final 只有一个目标多样性太低样本利用率上不去。2.4 它和 reward shaping 的本质差异很多人第一次接触 HER 时会误以为它就是高级奖励塑形其实两者的哲学完全不同。Reward shaping 是修改奖励函数它的先验来自设计者我认为中间状态也该得分的假设。这个先验一旦错了最优策略就会被带偏。HER 则完全不引入外部假设它只重复利用环境里已经发生的事实轨迹到达过某个真实状态那个状态是物理上可达的。你可以理解为HER 没有告诉智能体你应该怎么走只是把已经走过的轨迹重新归类标成如果当时目标是这里你其实做得不错。所有数据都来自真实交互因此不会引入任何虚假的最优性。这也是它能和 DDPG、SAC 等一堆 off-policy 算法无缝结合的根本原因。3. 从零实现 HER环境选型、回放缓冲区与 relabel 代码原理讲得再多不落地总是虚的。这一章我给出一个能直接跑的 HER 核心实现代码风格参照 OpenAI Baselines 里 her 目录的简化版去掉了工程外壳保留算法骨架。3.1 环境选型为什么推荐 Gym 的 Fetch 系列学任何 RL 算法环境选错都会非常痛苦。我的建议是用 Gym 内置的 Fetch 系列原因有三观测是 dict 结构天然区分 observation、achieved_goal、desired_goal和 HER 的接口严丝合缝奖励函数已经按稀疏加阈值定义好不用自己造模拟环境跑得快调通逻辑只需半天。环境任务内容难度FetchReach机械臂末端到达指定点入门HER 效果极快FetchPush把方块推到指定位置标准难度区分度高FetchSlide用机械臂滑动方块到远处较难需要长距离规划FetchPickAndPlace抓取并搬运方块最难训练时间最长如果你是第一回写 HER强烈建议用 FetchReach 起手。我在给团队培训时观察到大部分初始失败都出在代码 bug 上而不是算法理解上先用简单环境把所有逻辑调通再换 FetchPush 做正式实验。3.2 观测结构先看懂 Fetch 的 dict 和 reward 定义Fetch 环境每步返回的 obs 是一个字典包含三个字段observation机器人关节角度、速度、末端位置等维度较高25 维achieved_goal当前操作物体或末端的位置3 维desired_goal任务目标位置3 维奖励函数不是从 obs 里直接读的而是用同一个工具函数 compute_reward 计算。这个函数接收 achieved_goal 和 desired_goal返回 0成功或 -1失败阈值一般是 0.05。实现 HER 时重算奖励必须复用这个环境自带的 compute_reward不能自己另写一套距离判断否则会把环境分布弄偏。3.3 HindsightReplayBuffer 核心实现HER 和普通回放池最大的区别是按整个 episode 存储而不是按单条 transition 存储。因为 relabel 需要看到整条轨迹才能挑选替代目标。采样时再从 episode 里随机抽时间步即时生成原始样本和改写样本。下面是核心类我加了详细注释import numpy as np import random from collections import deque class HindsightReplayBuffer: def __init__(self, buffer_sizeint(1e6), k4, strategyfuture): self.buffer deque(maxlenbuffer_size) self.k k self.strategy strategy def add_episode(self, obs_list, action_list, reward_list, obs_next_list): 按 episode 存入缓冲。 obs_list 中每个元素是 Fetch 环境的 dict observation。 episode { obs: obs_list, actions: action_list, rewards: reward_list, obs_next: obs_next_list, } self.buffer.append(episode) def sample(self, batch_size): 返回一个 batch。对每个原始 transition 额外生成 k 个 relabel transition所以实际 batch 大小是 (k1)*batch_size。 transitions [] for _ in range(batch_size): ep random.choice(self.buffer) t random.randint(0, len(ep[obs]) - 1) # 原始样本目标用 episode 的 desired_goal g_orig ep[obs][t][desired_goal] transitions.append(self._build_transition(ep, t, g_orig)) # 额外 k 个 HER 样本目标从轨迹中重新采样 for _ in range(self.k): g_new self._sample_goal(ep, t) transitions.append(self._build_transition(ep, t, g_new)) return transitions def _sample_goal(self, ep, t): 根据 self.strategy 选择替代目标。 if self.strategy final: return ep[obs][-1][achieved_goal] elif self.strategy future: # 关键只从 t 之后的时间步里采样避免回到过去 future_idx random.randint(t, len(ep[obs]) - 1) return ep[obs][future_idx][achieved_goal] elif self.strategy random: idx random.randint(0, len(ep[obs]) - 1) return ep[obs][idx][achieved_goal] else: raise ValueError(fUnknown strategy: {self.strategy}) def _build_transition(self, ep, t, goal): 构造一条 transition。如果 goal 是替代目标用环境 reward 函数重算奖励。 obs ep[obs][t] action ep[actions][t] obs_next ep[obs_next][t] # 计算奖励直接用环境同一套规则 reward self._compute_reward(obs_next[achieved_goal], goal) # transition 里要带上 goal供目标条件策略使用 return { obs: obs[observation], goal: goal, action: action, reward: reward, obs_next: obs_next[observation], goal_next: obs_next[desired_goal], # 通常等于 goal保留备用 } staticmethod def _compute_reward(achieved_goal, desired_goal, threshold0.05): dist np.linalg.norm(achieved_goal - desired_goal) return 0.0 if dist threshold else -1.0这段代码有几个实现细节值得注意。第一_sample_goal 里 future 策略必须限定在[t, len-1]区间这是整个 relabel 逻辑里最容易写错的地方。我见过不止一次有人写成了随机全区间采样结果模型学会转圈。第二替代目标取的是 achieved_goal不是 observation。理论上观测里包含机器人的所有关节信息但那不是任务目标强行拿整段观测当目标会让状态空间维度爆炸语义也完全不对。第三_build_transition 里计算奖励用的是环境一致的 L2 距离和阈值。建议直接把 Gym 环境内部的 compute_reward 导出来复用避免手写漂移。3.4 把 relabel 接入 DDPG 训练循环有了这个回放池接入 DDPG、SAC 这类 off-policy 算法就非常顺。训练主循环的结构大概是replay HindsightReplayBuffer(buffer_size1_000_000, k4, strategyfuture) for episode_idx in range(total_episodes): obs env.reset() ep_obs, ep_actions, ep_rewards, ep_obs_next [], [], [], [] done False while not done: # 训练阶段加探索噪声DDPG 一般用高斯噪声或 OU 噪声 action actor.get_action(obs) exploration_noise() obs_next, reward, done, info env.step(action) ep_obs.append(obs) ep_actions.append(action) ep_rewards.append(reward) ep_obs_next.append(obs_next) obs obs_next # Episode 结束后整条存入内部会做 relabel replay.add_episode(ep_obs, ep_actions, ep_rewards, ep_obs_next) # 每 episode 更新多次 for _ in range(update_steps): batch replay.sample(batch_size256) ddpg_update(batch) # 这里把 obs、goal、action 拼起来喂给 actor/critic关于 ddpg_update 里的拼接方式我的习惯是 actor 输入 concat([obs, goal])输出动作critic 输入 concat([obs, goal, action])输出 Q 值。目标网络的处理和普通 DDPG 完全一致relabel 样本也参与同一套更新不需要额外区分来源。一个容易忽略的点评估阶段不要用 replay.sample也不要加噪声。把真实 desired_goal 从环境 obs 里取出来直接喂给 actor统计成功率。如果你在测试时忘了关探索噪声成功率会忽高忽低白白浪费时间去调一个其实没坏的系统。3.5 超参建议k、buffer 大小、网络尺寸怎么定我整理了一张速查表取值来自论文和我在不同任务上的复现参数建议值备注buffer_size1e6Fetch 任务建议太小目标多样性不够k4每个 transition 额外改写 4 次strategyfuture默认通常不用改网络结构两层 256足够加深收益不大batch_size256实际更新时是 (1k)*256注意显存探索噪声 std0.2高斯噪声归一化到动作空间学习率1e-3AdamFit 得不好可降 5e-4评估间隔每 10 个 episode每次固定 50 个测试目标取平均k4是论文里选的我在 FetchReach 上试过 k1 也能学但 FetchPush 上 k 太小会明显变慢。k 越大buffer 里成功样本比例越高代价是每一个真实样本消耗更多存储batch 也变大。如果训练速度吃不消优先减 batch_size 而不是减 k。4. 复现实验与调参心得成功曲线背后那些反直觉的坑原理和代码都有了接下来是实验。这一章我来复盘自己从跑不起来到稳定出效果的过程包括那些让我半夜爬起来改 bug 的瞬间。4.1 对照组怎么设同一份代码只切 HER 开关实验设计上我强烈建议做一对严格对照同一套 DDPG 实现同一个环境所有超参一致唯一区别是采样时是否调用 relabel 逻辑。用 FetchPush 做测试任务原因是它的难度恰到好处——FetchReach 太简单不加 HER 也能靠运气学会FetchPickAndPlace 又太慢调试周期长。评估指标用固定次数的成功率每训练 10 个 episode冻结策略随机初始化 50 个目标位置跑 50 次测试取平均成功率。这里有个细节目标位置要固定一个随机种子生成保证每次评估的是同一组目标否则不同轮次之间比较意义不大。4.2 成功率曲线的两种形态从贴地到陡峭爬升跑完 50 万步左右你会看到两条截然不同的曲线。不加 HER 的 DDPG成功率全程贴着地板偶尔跳到 0.05 又掉回来几百个 episode 过去了还是老样子。不是因为网络容量不够而是因为 Q 函数从一开始就没有得到过像样的正样本整个模型在往所有动作都一样烂的方向收敛。加 HER 的版本前 10 万步也不好看成功率同样很低。但过了某个临界点后曲线会以肉眼可见的速度陡峭爬升最终在 FetchPush 上稳定到 60% 到 80% 区间。这个先平后陡的形态不是异常说明前期积累的替代目标样本开始发挥梯度作用价值函数终于能区分好动作和坏动作了。如果你的曲线 20 万步还趴在地上我建议先跑一遍核心自检确认采样 batch 里真的有 relabel 样本打印 reward 分布看看有多少 0确认 goal 确实拼进了网络输入确认评估时用的是无噪声动作。这三个地方各占一半的翻车概率。4.3 踩坑记录几个几乎毁掉实验的细节这里写几条我印象最深的坑每条都对应一段真实调试经历。第一个坑是把整个 observation 当 goal。早期版本我图省事直接用obs[observation]当目标结果 25 维目标让网络容量瞬间吃紧训练速度奇慢而且奖励基本算不出来。后来才意识到目标必须是任务语义上的 achieved_goal也就是物体的 3 维位置。这个 25 维和 3 维的差别是 HER 理解里最基础、也最容易犯的错。第二个坑是 relabel 奖励函数写漂了。我没有复用环境自带的 compute_reward自己写了个带归一化的距离函数结果训练曲线上来后不稳定。检查了半天才发现relabel 出来的样本奖励和环境真实奖励的尺度不一致。后来统一改为导入环境内部计算函数问题立刻消失。原则很简单无论 HER 怎么改写目标奖励的计算规则必须和环境完全一致。第三个坑是忘了归一化。Fetch 的 observation 里有些量纲很大关节速度、角速度目标位置反而是 0 到几的小数值。把它们拼在一起直接喂进 MLP网络很容易被大数值维度主导训练发散。我的做法是对输入做 running mean / running std 归一化状态和目标分开统计。这个改动在不少任务上能把成功率提高 10 个百分点以上。第四个坑是 buffer 开太小。一开始为了省内存把 buffer 设成 1e5结果发现策略学一段时间就开始震荡。原因是 relabel 增加了样本多样性小 buffer 会频繁覆盖旧经验等于把好不容易产生的多样目标又冲掉了。换回 1e6 后稳定很多。第五个坑是评估时没关探索噪声。这个坑最丢人浪费了我一整天。明明是训好的策略测试成功率一直在 30% 左右上不去我以为是超参问题后来才发现训练循环里加噪声的代码在评估分支也被执行了。零均值噪声在动作空间上不可忽视目标区域本来就小动作抖一下成功率就掉一大截。4.4 调参速查表症状、原因与解决方向为了让你少走弯路我把常见现象和调整方向整理成表症状可能原因调整方向成功率一直为 0 且 loss 不降relabel 没进 batch、奖励重算错误打印 batch 中 reward 分布确认有 0 值样本曲线爬到一半开始大幅震荡buffer 太小、学习率太高增大 buffer或把学习率降到 5e-4evaluation 成功率忽高忽低残留探索噪声、目标集合不固定评估分支去掉噪声固定随机种子训练很慢模型不收敛目标用了高维 observation确认 goal 用的是 3 维 achieved_goal前期正常后期突然崩归一化统计被 epoch 覆盖用单独的归一化器或采用 offline mean/std这些坑看着都很初级但每一个我都真实遇到过而且每踩一个至少要花半天排错。记录下来是希望你能在第一次跑 HER 时避开它们把时间花在真正有价值的调参上。5. HER 的适用边界与进阶组合别在错误场景里硬套HER 很好用但它不是万灵丹。很多人在自己的任务上套 HER 没效果不一定是实现有问题而是任务本身的假设不满足。我把适用边界和进阶玩法放在最后讲方便你判断自己的场景适不适合。5.1 适合的场景goal-conditioned 任务的基本盘HER 能发挥最大效力的场景有三个特征任务可以写成从某状态出发把某属性变成目标值的形式目标值可以从轨迹的真实状态里采样到底层算法是 off-policy。机器人操作是典型的基本盘。Fetch 系列的推、抓、放全部符合。导航任务也适合目标是坐标位置轨迹上每个点都可以视为潜在目标。游戏里的到达某区域消灭某类型敌人如果状态可观测同样可以建模成 goal-conditioned。一个判断标准是如果任务的目标和轨迹状态是同一个空间里的元素HER 大概率有效。比如方块位置既是可达状态又是任务目标relabel 顺理成章反之如果目标是开门但状态里根本没有门的位置表达HER 就无从改写。5.2 慎用或不适用的场景HER 不是万能胶水第一个不适用场景是 on-policy 算法。HER 改写了样本的奖励和目标样本的来源是行为策略而不是当前策略这破坏了 on-policy 算法的假设。硬套进 PPO、A3C数据分布乱套训练必崩。想在 on-policy 体系里利用事后经验需要做重要性采样之类额外的修正复杂度高很多。第二个是目标不可观测或不可采样的任务。比如评价指标是图片和参考图相似度但状态里没有显式的目标编码再比如目标是先抓 A 再抓 B这类带时序约束的组合目标单纯 relabel 成抓到 B会让轨迹语义错乱。还有一个很多人忽略的隐性限制HER 不能凭空创造成功。如果初始探索产生的轨迹都挤在起点附近relabel 出来的目标也就集中在起点附近策略只能学会原地不动。这种情况要先扩大探索范围或者用多目标初始化让轨迹覆盖更大区域否则 HER 的收益会大打折扣。5.3 值得试的进阶组合SAC、优先回放与多目标扩展如果你的任务满足基本盘我建议在跑通基础 HER 之后再考虑两个进阶方向。第一个是 HER SAC。SAC 自带熵正则探索更稳定和 HER 配合往往比 DDPG 更耐调。我在 FetchPush 上对比过SACHER 的曲线普遍更平滑最终成功率也略高代价是训练时间稍长。第二个是 HER Prioritized Experience ReplayPER。HER 产生了大量改写样本但并不是每条样本都同样重要PER 可以对 TD 误差大的样本赋予更高采样权重。实现时要额外维护每条样本的优先级relabel 样本也需要参与更新注意别把 buffer 结构写复杂了。第三个扩散方向是多目标 HER把单点目标换成目标分布训练时每次 relabel 不只看实际到达状态还结合任务的目标先验。这个方向常见于真实机器人场景能进一步提升迁移到未知目标的泛化能力。5.4 一个三步上手 HER 的个人建议最后给一个我长期带人时使用的上手路径。第一步在 FetchReach 上调通整套 HER 管线目标只有一个看到成功率快速跳到接近 100%。第二步换 FetchPush用 future 策略加 k4 的默认配置跑 50 万步不要急着改参数先逼自己读懂曲线形态。第三步换到自己的任务开始碰目标表示、归一化、阈值这些细节。我就是靠这个路径从一个听说过 HER 概念的人变成能在三天内把 HER 嫁接到新机器人环境上的熟练工。你不需要把论文里的所有数学推导都吃透但一定要亲手把 relabel 逻辑写一遍。只有自己写过一次你才会真正理解那条成功率曲线为什么会在某个临界点开始陡峭爬升也才能在自己的任务出问题时知道该去查哪一行代码。