资讯动态

HER算法解析:用事后聪明破解强化学习中的稀疏奖励难题

发布时间:2026/10/3 4:36:44 来源:尧图企业网站定制
做强化学习的人第一次看到 hindsight 这个词大概率是从 OpenAI 那篇《Hindsight Experience Replay》的论文标题里。但它在英文世界里其实是个特别日常的词——事后聪明马后炮。英语里有句老话叫 hindsight is 20/20翻译过来就是“事后看谁都是千里眼”。我最初被这个词击中不是在读自然语言的时候而是在实验室里折腾稀疏奖励任务被虐到怀疑人生的那段时间。机器人从零开始抓取一个物体随机晃了几万个 step得到的奖励始终是同一个负数策略网路的梯度几乎没有任何有用方向。后来我把 HER 接进 experience replay事情才开始变得可控。这篇文章就想把 hindsight 在强化学习里的这层“后见之明”聊透它为什么能成代码上怎么落地以及我在实操中踩过哪些坑。这篇东西适合谁看如果你正在做机器人控制、多目标强化学习或者被稀疏奖励问题折磨到头秃那它至少能帮你少走几个月的弯路。就算你只是刚接触强化学习的小白只要懂一点 Python 和基础的 DQN/SAC也能跟着把 HER 的核心逻辑完整复现出来。1. 整体设计拆解HER 凭什么把失败变成经验1.1 “目标条件策略”这个前提要理解 HER你得先接受一个前提这里的强化学习任务不是“单目标”而是“多目标条件任务”。策略输入不再只是状态 s而是状态加上目标 g写成 π(a|s, g)。奖励函数也带目标写作 r(s, a, g)。举个例子会直观很多。你在厨房做饭桌上有鱼、青菜、鸡蛋。如果目标是“蒸鱼”你看到鱼会去拿蒸锅如果目标是“炒蛋”你就会去拿碗和筷子。同一个状态不同目标最优动作完全不同。这种设定在机器人领域特别常见比如机械臂要把物体推到指定位置目标位置每次可能是不同的点。目标条件策略最大的难点在于你不仅要学会“从状态出发做动作”还要学会“在不同目标之间泛化”。HER 的思路并不是直接去解这个难题而是让训练数据本身变得更聪明一点。1.2 稀疏奖励到底难在哪常规强化学习依赖奖励信号做梯度回传。可一旦任务给的奖励非常稀疏比如“只有达成最终目标才给 1其余每一步都是 0 或 -1”那么大多数 transition 的奖励信息几乎为零。我做过一个非常简单的 8x8 网格实验起点在 (0,0)目标在 (7,7)动作是上下左右挪一格每步有步数上限。用随机策略跑 50 步能踩中目标格子的概率非常低大约只有百分之几。这意味着什么你用 DQN 去训大部分 batch 里的 target 全都一样Q 值根本学不到任何有区分度的东西。训练曲线从头到尾像一根平线不是代码 bug而是奖励稀疏得离谱。这种场景下传统的 reward shaping 可以缓解但需要人来设计中间奖励而且设计得不好会引入局部最优。HER 不想改奖励函数它想改的是“数据怎么标”。1.3 从“事后聪明”到算法设计人类天生就有事后聪明这个毛病。考试砸了你复盘的时候会说“我要是当时多看一眼那道题就好了”项目上线出 bug你也会说“我早知道应该先做压测”。但在强化学习里这种反事实思维被 HER 变成了正经的算法工具。核心逻辑一句话一条没达到目标 g 的失败轨迹如果换个角度看它其实成功到达了某个后续状态。那这个“后续状态”就可以被视为一个替代目标 g然后我们重新计算奖励把这条原本毫无学习价值的轨迹变成一条有正反馈的训练样本。这就像教练看球员投篮录像一个球没投进但如果把篮筐自动移到球实际落点的位置那这次出手动作其实是“标准”的。球员通过大量这样的“虚拟成功”经验提高了对自身动作的控制力。 HER 的设计思路就是这样它不改变环境动力学只改变经验池里样本的“身份说明”。2. 核心细节解析目标重标注里的关键参数与公式2.1 HER 的算法骨架标准 HER 流程可以拆成四步。第一步用当前策略在某个目标 g 下跑完一条完整轨迹存下每一步的 (s_t, a_t, r_t, s_{t1})以及这一局的目标 g。第二步把这条轨迹里的原始转换全部写入 replay buffer。注意原始样本一定不能丢。HER 是在原始经验基础上做加法不是拿替代样本替代原始样本。第三步从这条轨迹的“未来状态集合” {s_{t1}, ..., s_T} 里随机挑出 k 个状态。每个状态通过映射函数 φ 转换成替代目标 g。这个 φ 取决于任务比如在推箱子任务里就是提取物体的位置坐标。第四步对每个替代目标 g重新计算奖励r 0如果 φ(s_{t1}) 与 g 的距离小于阈值 τ否则 r -1。然后把重标注后的样本 (s_t, a_t, r, s_{t1}, g) 写进 buffer。为什么这里几乎总是选“未来状态”而不是过去状态因果一致性。当前动作只能影响之后的状态不能影响之前的状态。你把一个已经发生过的过去状态当成目标来反推当前动作相当于让策略为一个它根本推动不了的结果负责这是逻辑悖论。而未来状态确实可以被后续动作逐步达到所以“目标可达”在动力学上是成立的。2.2 奖励选择与目标函数设计稀疏奖励的常见姿势有两种一种是“成功给 1失败给 0”另一种是“成功给 0失败给 -1”。两种都可以但在 HER 里我更建议用后者也就是每走一步都惩罚成功终止时给 0。原因很简单在步数有限的任务里-1 惩罚会隐式地引导策略找最短路径而 1/0 的组合会让策略对“早点结束”不敏感。虽然这个差别在奖励足够稠密时才明显但既然本来就缺奖励为什么不把步数压力也塞进信号里这里有一个特别微妙的地方也是新手容易忽略的HER 重标注后一条失败轨迹的尾部一定存在非零奖励样本。假设替代目标取到了轨迹最终状态 g φ(s_T)那么对最后一步转换 (s_{T-1}, a_{T-1}, s_T) 来说φ(s_T) 和 g 完全相等奖励立刻变成 0。这个“天然存在的正样本”是整个算法能启动的火种。实际实现里替代目标不一定只取最终状态。OpenAI 论文里最常用的策略叫 future即从当前转换之后的所有时间步里均匀采样 k 个状态作为目标。另一种是 final只取最终状态。final 更保守适合目标特别难达成、未来状态多样性过高的场景future 更激进能带来更多样化的训练信号也是我实测下来最稳的方案。2.3 关键超参数和那些没人写的坑第一个超参数是 k每个转换额外生成几个替代目标。OpenAI 的默认值是 4。k 太小信号密度不够k 太大buffer 里重标样本太多原始目标会被稀释而且存储和训练开销都往上走。我自己的习惯是先设 4等成功率爬到五成以上再试 8观察曲线是否更稳而不是一上来就堆数量。第二个容易被低估的是目标判定阈值 τ。很多任务里判断“是否到达目标”用的是欧氏距离小于阈值τ 大小的利害关系很大。τ 太大明明离目标还有十万八千里也算成功网络学到的策略会很糙τ 太小成功样本几乎不存在HER 和白写没区别。建议从任务物理尺寸出发比如机械臂抓取任务我常用 0.02 到 0.05 米。网格世界反而好办压到同格距离即可。第三个坑跟状态表示有关。重标注后的替代目标是某个“状态”但这个状态很可能包含速度、姿态等额外信息不能直接当 goal 用。你需要一个 φ 函数把原始状态映射到“目标空间”。比如状态里有机器人位置、物体位置、物体速度目标空间通常只取物体位置。如果忘记做映射直接把整个状态塞进 goal网络要记住的东西会爆炸训练大概率发散。3. 实操过程与核心环节实现一份可复现的 HER 代码3.1 环境设计最小可跑的网格推方块为了把 HER 的机制讲明白我建议你自己动手跑一个 8x8 的网格环境。起点固定 (0,0)目标固定在对角 (7,7)动作就是上下左右撞边界不动每步上限 50。状态 obsagent 的 (x, y) 目标 goal目标格子的 (x, y) 每步奖励0到达目标或 -1没到代码可以这样写import numpy as np class GridEnv: def __init__(self, size8, max_steps50): self.size size self.max_steps max_steps self.goal (size - 1, size - 1) self.agent (0, 0) self.steps 0 def reset(self): self.agent (0, 0) self.steps 0 return np.array(self.agent, dtypenp.float32), np.array(self.goal, dtypenp.float32) def step(self, action): x, y self.agent if action 0: x min(x 1, self.size - 1) elif action 1: x max(x - 1, 0) elif action 2: y min(y 1, self.size - 1) else: y max(y - 1, 0) self.agent (x, y) self.steps 1 reached (x, y) self.goal reward 0.0 if reached else -1.0 done reached or self.steps self.max_steps return np.array(self.agent, dtypenp.float32), reward, done, {}环境本身非常简单但完全能复现稀疏奖励的困境。你会看到不带 HER 的 DQN 训练过程像睡着了一样成功率长时间在低位徘徊。3.2 把 HER 写进经验回放核心代码真正关键的是经验池这边的重标注逻辑。我先把 HER 重标注函数单独拎出来它接收一个 episode 的所有转换返回原始样本和重标样本import numpy as np from collections import deque import random class ReplayBuffer: def __init__(self, capacity): self.buffer deque(maxlencapacity) def add(self, obs, action, reward, next_obs, goal): self.buffer.append((obs, action, reward, next_obs, goal)) def sample(self, batch_size): batch random.sample(self.buffer, batch_size) return batch def __len__(self): return len(self.buffer) def her_relabel(episode_trans, k4, tau0.5): episode_trans: list of (obs, action, reward, next_obs, goal) obs 和 goal 都是 (x, y) 的 numpy 数组 states [t[0] for t in episode_trans] [episode_trans[-1][3]] extra [] for t_idx, (obs, action, reward, next_obs, goal) in enumerate(episode_trans): future_idx list(range(t_idx 1, len(states))) if not future_idx: continue chosen np.random.choice( future_idx, sizemin(k, len(future_idx)), replaceFalse ) for idx in chosen: alt_goal states[idx] alt_reward 0.0 if np.linalg.norm(next_obs - alt_goal) tau else -1.0 extra.append((obs, action, alt_reward, next_obs, alt_goal)) return extra这里要解释几个我在实际开发里觉得特别容易错的地方。states 数组的长度是 episode_trans 长度加一因为要把每一步的 obs 都存下来最后还要补一个最终 next_obs。如果你忘了补最后一帧那么对最后一步转换做 HER 时future_idx 会是空的尾部天然正样本就丢了。alternate goal 取自 states[idx]但奖励是拿 next_obs 和 alt_goal 比的。这里 next_obs 是 t_idx 这一步动作执行后的状态idx 一定大于 t_idx保证我们是在拿“未来的某个状态”当目标。有的刚上手的同学会手滑写成拿 obs 去和 alt_goal 比那就完全变了味道因为还没执行当前动作你当然到不了未来状态所有样本立刻全部变成 -1。接下来是训练主循环的示意。为了控制篇幅我只写跟 HER 相关的核心部分buffer ReplayBuffer(capacity100000) env GridEnv() policy DQNPolicy(state_dim4, action_dim4) for episode in range(4000): obs, goal env.reset() episode_trans [] done False while not done: # 这里把 obs 和 goal 拼接后输入网络 s np.concatenate([obs, goal]) action policy.select_action(s, epsilon0.3) next_obs, reward, done, _ env.step(action) episode_trans.append((obs, action, reward, next_obs, goal)) obs next_obs # 原始样本先进 buffer for trans in episode_trans: buffer.add(*trans) # HER 重标样本进 buffer for trans in her_relabel(episode_trans, k4, tau0.5): buffer.add(*trans) # 每 10 个 episode 更新一次网络 if episode % 10 0 and len(buffer) 256: batch buffer.sample(256) losses [] for obs, action, reward, next_obs, goal in batch: s np.concatenate([obs, goal]) ns np.concatenate([next_obs, goal]) target reward 0.98 * policy.target_q(ns).max() current policy.q(s)[action] losses.append((current - target) ** 2) policy.update(np.mean(losses))注意一个细节重标样本里的 goal 是 alt_goal但 obs 和 next_obs 还是原来的。网络输入要把 obs 和 goal 拼在一起因此重标样本天然地改变了网络的输入标签。如果你用的算法是 DDPG 或 SAC同样要把目标拼进 actor 和 critic 的输入而不是只在奖励层面做文章。3.3 训练记录与调参观察我在这个网格环境里跑过一组对比记录比较稳定。无 HER 的 DQN 跑到 4000 个 episode成功率在 3% 以下基本是随机水平。上了 HER 之后前 500 个 episode 同样看不出动静但从 1000 个 episode 左右开始训练曲线上开始出现明显的抬头2500 个 episode 时成功率大概到了 45%4000 个 episode 时能稳定在 78% 左右。k 值的差异也很明显。k8 时成功率爬升更快1500 个 episode 就到了一个肉眼可见的高位但中后期波动更大k4 更稳只是前期稍微慢一点。另一个让我印象深刻的实验是把 τ 从 0.5 调小到 0.1结果整个训练直接退化。原因不复杂网格 8x8坐标差 0.1 意味着两个格子几乎必须完全重合才算成功原本能在轨迹尾部产生的正样本大幅减少HER 也就失去了动力源。我建议你动手跑的时候先打印每一批 buffer 里“奖励为 0”的样本占比。这个数字在 HER 加持下会明显高于普通经验池。如果它始终低得可怜先别调网络结构回头检查 τ 和替代目标采样逻辑。4. 常见问题与排查技巧实录你的 HER 为什么没效果4.1 问题速查表我把实操中遇到的典型问题整理成了一个表格解决问题的时候可以按图索骥。现象可能原因处理方式加了 HER 后成功率反而下降目标没有拼进网络输入或者只在 reward 里改目标状态里看不到 goal检查 actor/critic 的输入是否都是 obs goal训练初期 loss 剧烈震荡k 过大替代目标分布太杂梯度方向互相打架先降 k 到 2 或 4等策略有一定基础后再加大重标样本的奖励几乎全为 -1τ 太严成功判定几乎不可能满足把 τ 调到任务物理尺寸对应的合理范围正样本有了但策略不泛化到新目标训练中目标太单一或者替代目标集中在最容易到达的状态混入更多原始目标样本或引入目标课程学习episode 很短时 HER 报错未来索引为空在 t_idx1 超过 states 长度时跳过训练曲线一段时间后突然崩掉buffer 中重标样本比例过高原始目标被淹没限制每个 episode 额外生成的 HER 样本数量4.2 最容易出错的三个细节第一个坑是把 HER 用在非目标条件的策略上。HER 要求策略输入里有目标 g多目标环境里才成立。如果你在普通单目标任务里硬套 HER替代目标没有任何载体等于往经验池里塞了大量互不兼容的数据。判断方法很简单看网络输入里有没有 goal没有就别用。第二个坑是关于替代目标索引错位。很多实现里会把“是否达到目标”写成 distance(next_obs, goal) tau但在 HER 样本里goal 是未来某个状态next_obs 是当前转换的下一个状态。如果代码里误把未来状态取成了 states[t_idx-1] 或者 states[t_idx]然后和 next_obs 比较某些情况下也能碰出正样本但位置错乱的样本会让策略学习到错误的因果关联训练过程会变得极其不稳定。我的经验是加一句断言assert idx t_idx一旦 future 索引取到过去时间步直接报错。这个小断言帮我省了很多调试时间。第三个坑是 buffer 里原始样本和重标样本的比例。OpenAI 的实现会把原始样本全部保留再额外塞入 HER 样本。我不建议为了扩大 HER 的效果把原始样本丢一半那会让策略的注意力偏离真实目标分布。我试过一次仅保存 HER 样本的精简版训练前期还行后期对真实目标的成功率反而不如混合版。原因很直白混合经验池相当于既让学生做“低难度但贴近失败”的修正题又不让他忘掉考试原题。5. 个人实操经验与可以继续扩展的方向5.1 我的几条实测体会HER 不是一个万能开关。它对“状态本身可以当目标”的任务效果极好比如推动物体、到达位置、倒水高度这一类。但如果你要优化的目标是一个抽象评价指标比如“报告写得好不好”“视频画面美不美”你很难从状态里抽出一个物理量来当替代目标这时候 HER 就不太好使。另外HER 跟 off-policy 算法是天作之合DDPG、SAC、DQN 都能直接接。但如果你的主力算法是 PPO 这类 on-policy 方法原生 HER 的“事后重标样本会破坏同策略分布”我个人的建议是先换成 off-policy 的框架或者至少在收集轨迹时保留更多同策略轨迹再在 buffer 里加 HER 样本。真实机器人部署是另一个让人头秃的环节。模拟器里 HER 重标样本的“实际状态”是完美的真实系统总归有观测噪声和执行误差替代目标可能并不像模拟器那样精确可达。我一般会在 sim2real 之后加一小段真实环境微调并且降低 k 值避免大量带噪声的未来状态把策略带偏。5.2 可以继续玩的方向HER 之后有不少工作围绕“怎么选择更好的替代目标”展开。你可以不用均匀采样未来状态而是学一个目标生成模型生成那些当前策略“差一点就能完成”的目标这种 Hindsight Goal Generation 思路在难度递增场景里表现更好。也可以把它和课程学习结合起来。先让策略在“容易达成的状态”上猛刷 HER等 Q 值估得差不多了再逐步解锁更远的目标。实际操作时我会把目标的生成范围从近到远分配给不同训练阶段效果比一次全部铺开稳定很多。如果你做的是机器人操作还有一个进阶玩法把 HER 和阶段化策略结合。比如机械臂推物块光有 HER 可能只学会推一次但加上“先移动到位再推再归位”的层次化目标分解替代目标可以直接取子目标的完成状态训练会顺很多。最后分享一个我保存已久的调试技巧每训练几百个 episode把经验池里所有样本的目标画成散点图。你大概率会看到替代目标集中分布在一条轨迹附近这是正常的但如果散点图上几乎所有点都堆在状态的起点区域说明策略已经退化到只在原地打转了。这时候别急着调网络先确认是不是随机探索的噪声设置太小或者 τ 判定得过于苛刻把策略逼成了“宁可不做也不错”的状态。HER 的本意是让算法学会从失败里总结经验但前提是它还能大胆地失败。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑