
Hindsight让强化学习从失败中“捡”出经验第一次听到 hindsight 这个项目名估计很多人以为是“后见之明”“事后诸葛亮”的心理学讨论。但搞强化学习的朋友应该立刻会心一笑——这是 OpenAI 在 2018 年提出的Hindsight Experience ReplayHER事后经验回放算法一个专门解决稀疏奖励问题的经典思路。我最初接触 HER 是被机器人抓取任务折磨得够呛智能体在三维空间里探索几百轮训练下来什么都学不到奖励函数给得再细也有拦不住的死角。后来把 HER 接进自己的训练流程才发现“让失败样本也能变成学习材料”这件事比我想象中更简单也远比我想象中有用。这篇内容我面向三类人写一是刚接触强化学习、正被稀疏奖励问题折磨的学生或爱好者二是实际做机器人操控或游戏 AI 的工程师三是想在自己项目里快速试跑 HER 但不想重新造轮子的“实用派”。我会把自己踩坑的过程、调参的经验、看论文时容易忽视的细节全部整理出来争取你看完就能在 Gym 环境里跑通一套 HER 训练流程。1. 为什么正反馈稀疏会让强化学习寸步难行1.1 稀疏奖励问题的本质先聊个最直接的场景你让一个机械臂把桌子上的方块推到一个固定位置。动作空间是连续的机械臂每一步可以选择不同的推的方向和力度。如果方块没到目标位置每一步的奖励都是 0只有当方块最终与目标位置的距离小于某个阈值才给一个正奖励比如 1。这种设计在学术上叫二值稀疏奖励整个回合只有两个结果成功给 1失败给 0。问题在于强化学习的核心是“用奖励信号引导策略改进”而奖励为 0 意味着没有任何梯度方向。智能体随机探索一百步百分之九十九的概率什么都碰不到。DQN 也好 DDPG 也好它们在面对这种环境时价值网络的误差信号几乎处处为零训练根本推不动。我用一个生活化的类比解释你让一个从没玩过飞镖的人站在三米外投飞镖靶心只有硬币大小投不中就没有任何反馈。他连续投一百次可能连靶子边都没碰到。如果有一个教练告诉他“你刚才偏左上了一点”“再往下压一点”他学起来就快得多。HER 干的事情就是给这个“教练”提供一种特殊的观察方式虽然你没投中目标靶心但你投中了别的某种位置这个信息本身也能变成一次有效的学习。1.2 强化学习为什么拿“零奖励”没办法从数学上看策略梯度类算法依赖Q(s, a)来估计动作的优劣而Q(s, a)的更新依赖贝尔曼方程Q(s, a) r(s, a) γ * max_a Q(s, a)当r(s, a)恒为 0 时Q 值的唯一信息源就是下一状态的 Q 值。如果整个状态空间里几乎不存在“被访问过的成功状态”Q 函数就是一个“全是 0 的平原”。智能体在这个平原上随机游走没有山丘、没有斜坡自然没有方向可言。更麻烦的是很多实际任务不仅奖励稀疏还有episode 长度限制。比如机械臂推方块每个回合给 50 步50 步内推不到目标区域就重置。从角落出发到目标位置可能需要差不多刚好 50 步随机策略根本不可能在预算内完成。你不断调大 episode 长度训练时间呈指数级增长还没等智能体学会你的训练预算已经爆炸了。我早先做过一个室内导航任务智能体要在一个 20mx20m 的平面环境里走到一个固定点每一步的奖励是-0.01时间惩罚 终点到达 1。看起来已经很温和了对吧但实际训练时我发现DQN 探索了两个月步长simulation steps都没走到终点。原因很简单区域太大目标点占整个状态空间的比例太小随机探索撞到目标的概率跟买彩票差不多。这就是我后来转投 HER 的直接原因。2. 算法核心拆解HER 到底改了什么2.1 从“一个目标”到“目标条件策略”传统强化学习处理的是单任务状态s动作a奖励r(s, a)。HER 最关键的改动是把问题重写成目标条件强化学习Goal-Conditioned RL。在这个框架下每个 episode 不仅有一个状态序列还有一个额外的变量g表示目标。状态变成(s, g)动作变成a奖励变成r(s, a, g)。机械臂推方块的任务里g就是目标位置坐标奖励函数判断的是“方块当前位置距离g是否小于阈值”。目标是哪里来的每个 episode 开始时随机采样一个目标。比如把目标位置设成(x, y) (1.2, 0.8)机械臂就从起始位置开始推。初始策略只能乱推大概率推不到(1.2, 0.8)。但是注意HER 的核心洞察来了虽然这个 episode 没完成“原始目标”但这个 episode 的结果并不算没有意义。如果方块最终停在了(1.0, 1.1)那这意味着“方块从初始位置被推到了(1.0, 1.1)”——这是一个真实发生的、已经成功了的轨迹。如果我们重新定义目标为g (1.0, 1.1)那么这条轨迹在新的目标下就是一个成功轨迹我们可以用这个方式去更新目标条件策略让智能体学会“把方块推到它最终到达的位置”。这个思想就是“事后”的含义你用已经发生的轨迹结果来重新标注目标把失败的轨迹重新解释为成功的经验。2.2 目标重标注的核心机制HER 在实现上并不复杂每个 episode 结束时除了使用原始目标g来更新一次还会额外构造k个“假目标”来更新。常用的重标注策略有三种策略做法特点final把 episode 结束时的状态作为假目标最简单只用了最终状态random从轨迹中随机采样一个状态作为假目标覆盖更广的状态空间future从当前时刻之后的某个状态采样作为假目标兼顾时序信息最常用论文里认为future 策略效果最好因为它既保留了时序一致性假目标在未来可被当前轨迹达成又不会像 final 那样把所有轨迹都指向同一个终点。实际实现中未来时间点通常从当前步数 1 到 episode 结束步数之间均匀采样。举个例子一条轨迹长度为 50原始目标g失败。我们选择k 8也就是额外生成 8 条“虚拟成功轨迹”每条轨迹的假目标从轨迹未来时刻的状态里随机抽一个。加上原始目标这一条经验就被用来更新了 9 次。意味着什么原本一条失败轨迹根本没进入训练池现在它以“成功样本”的身份被反复学习。在 replay buffer 里成功轨迹比例显著提升Q 函数开始“看到”成功的样子。2.3 注意HER 不是什么都能配套这可能是很多人看论文时忽略的要点。HER 只能用在off-policy 算法上因为它的重标注逻辑要求我们保存 episode 轨迹、事后修改目标再把这些修改过的数据放进 replay buffer 里供后续采样。如果是 on-policy 算法比如 PPO、TRPO策略更新依赖当前策略产生的样本事后重标注的样本与当前策略分布不匹配处理起来非常别扭。最常见的搭配是DDPG HER。OpenAI 的原始实现就是在 DDPG 基础上加的 HER用 actor-critic 结构处理连续控制任务靠目标网络稳定训练。如果你用的是标准 DQN 做离散动作任务也能加 HER核心逻辑不变。我还试过用 SAC 替换 DDPG。效果整体更好因为 SAC 的熵正则项提升探索效率配合 HER 的重标注能力在 Fetch-Reach 任务上收敛速度比 DDPGHERV 快不少。代价是调参参数多了几个alpha 的温度系数需要自动调训练时间也会变长。如果你不想折腾先跑通 DDPGHER 再考虑升级到 SACHER 是比较稳的路径。3. 从零跑通 HER环境搭建、代码结构、调参实战3.1 环境与依赖准备我目前最常用的组合是Python 3.8PyTorch 2.xGym 0.25注意不要用 GymnasiumAPI 改动很多OpenAI 原版代码不兼容mujoco-py 2.1。如果你要直接跑 OpenAI 的baselines仓库里的 HER 实现需要繁琐的环境版本匹配我建议直接用下面这套简化版结构代码量不大改动也容易。conda create -n her_env python3.8 pip install gym0.25.2 pip install torch2.0.1 pip install mujoco-py2.1.2.14mujoco-py 这里有坑新版的mujoco和mujoco-py是两个不同的包前者底层的渲染引擎和 API 全变了后者才是老版本 Gym 能直接调用的。如果你装的是新版mujocoGym 0.25 会找不到mujoco_py接口直接报错。个人经验在 Linux 上安装时还要注意patchelf、gcc的版本兼容建议先跑一行python -c import mujoco_py验证环境没问题再继续下一步。3.2 测试环境FetchPush 与 FetchReachOpenAI Gym 的FetchReach系列实际在gym.envs.robotics里就是专门为这类目标条件任务设计的非常适合用来验证 HER。FetchReach-v2最简单的任务机械臂末端直接移动到目标点动作空间是 3 维位置增量几乎没有接触物理。FetchPush-v2机械臂把方块推到目标点有接触力和摩擦难度明显上升。FetchPickAndPlace-v2抓取并搬运方块到目标位置包含夹爪控制是所有任务里最难的。第一次实验建议用 FetchReach因为它的状态空间简单、奖励密集时容易收敛跑 50 万步50 万 env steps就能看到明显效果。FetchPush 大约需要 100-200 万步FetchPickAndPlace 则需要 200 万步以上单卡 GPU 训练一个晚上才能看到稳定下降。状态空间结构FetchPush 为例直接告诉你怎么用observation: { observation: 初始物体位置 物体线速度 夹爪位置 夹爪线速度 (10 维) achieved_goal: 物体当前位置 (3 维) desired_goal: 目标位置 (3 维) }注意这里的achieved_goal和desired_goal就是 HER 重标注时需要用到的核心信息。标准 reset 函数会在每个 episode 开始时随机采样一个目标desired_goal而achieved_goal是实际状态中物体的位置。3.3 策略网络与目标重标注代码实现我把自己精简过的 HER 核心代码贴出来。这里我把重标注逻辑抽成单独函数方便你理解数据流。import numpy as np def sample_her_transitions(episode_batch, max_episode_len, k4): 对一整条 episode 数据做 HER 重标注。 episode_batch 包含以下 key: obs: [episode_len, obs_dim] achieved_goal: [episode_len, goal_dim] desired_goal: [episode_len, goal_dim] actions: [episode_len, action_dim] rewards: [episode_len, 1] her_transitions {} episode_len episode_batch[obs].shape[0] her_obs [] her_achieved_goal [] her_desired_goal [] her_actions [] her_rewards [] for _ in range(k): # 核心从未来时间步随机采样一个状态作为假目标 future_t np.random.randint(0, episode_len - 1) fake_goal episode_batch[achieved_goal][future_t] # 构造新的transition her_obs.append(episode_batch[obs]) her_achieved_goal.append(episode_batch[achieved_goal]) her_desired_goal.append(np.repeat(fake_goal.reshape(1, -1), episode_len, axis0)) her_actions.append(episode_batch[actions]) her_rewards.append(compute_reward( episode_batch[achieved_goal], np.repeat(fake_goal.reshape(1, -1), episode_len, axis0) )) return { obs: np.concatenate([episode_batch[obs]] her_obs), achieved_goal: np.concatenate([episode_batch[achieved_goal]] her_achieved_goal), desired_goal: np.concatenate([episode_batch[desired_goal]] her_desired_goal), actions: np.concatenate([episode_batch[actions]] her_actions), rewards: np.concatenate([episode_batch[rewards]] her_rewards), }这段代码看起来简单但里面有三个关键的细节容易踩坑第一个假目标必须来自achieved_goal而非obs。Fetch 系列里achieved_goal已经是物体的三维位置而obs里包含的是物体位置 速度 夹爪状态直接用后者做目标会导致维度不匹配和训练发散。第二个重标注的 reward 必须重新计算不能沿用原始 reward。上面代码里我用compute_reward重新算了一次如果直接复制原始 reward假目标下奖励全是 0等于没有学习信号。第三个原始目标desired_goal也要保留在训练数据里不能只用假目标。否则策略会退化成“只会推到任意位置”丢失任务本身的指向性。所以代码里her_transitions[desired_goal]是原始目标 假目标的拼接。3.4 DDPG 网络部分的关键参数网络结构其实没什么秘密两层全连接隐藏层 256 个神经元ReLU 激活DDPG 经典结构完全够用。真正影响训练效果的是下面这几个参数我曾经在不同任务上试过不同组合效果差异很大。参数FetchReachFetchPushFetchPickAndPlacereplay buffer 大小100 万100 万100 万HER 采样数量 k488Q 网络学习率1e-31e-31e-3actor 网络学习率1e-31e-31e-3batch size256256512目标网络更新率 tau0.050.050.05动作噪声0.20.20.1训练步数50 万150 万300 万k这个参数对训练影响很大。开太小重标注提供的成功样本不够训练慢开太大一个 episode 会被反复重放多次可能让策略对假目标过拟合反而忽略了原始目标。我的实验感受是 FetchPush 用 8 比较均衡FetchPickAndPlace 也用 8但如果你的 GPU 显存足够12 会更好一点。动作噪声用的是高斯噪声不是 OU 过程。DDPG 原论文用的是 OU 噪声但 OpenAI 后来在实践中发现高斯噪声更简单也更好调。我自己的感受也一样OU 过程参数太多theta 和 sigma高斯噪声只需要调一个 sigma 就够效果差别不大。3.5 训练日志怎么解读HER 与普通 DDPG 的核心差异我拿 FetchReach 做了对比实验普通 DDPG 和 DDPG HER各跑 50 万步。普通 DDPG 的 success rate目标距离小于 5cm 的比例在 50 万步后只有 20% 左右而 HER 版本在第 10 万步左右就到 90% 以上最终接近 100%。发现有意思的现象HER 版 DDPG 的Q 值曲线一开始会上下剧烈波动因为重标注的假目标会让 Q 网络看到大量“非平稳”的数据分布。你不要一看到 Q 值抖动就以为代码 bug 了这是 HER 的正常现象随着训练推进Q 值会逐渐收敛到与 success rate 一致的区间。真正值得监控的指标有三个一是actor 网络的采样子成功率rollout 时实际推到的目标成功率二是训练 batch 里的平均 reward经过 HER 重标注后数据分布里 reward1 的比例三是Q 值与真实 return 的 TD-error。如果你想在 TensorBoard 里看进度建议把这三个指标全部打出来。4. 踩坑记录当我试图复现 HER 时遇到的问题4.1 目标偏移导致 Q 值爆炸这是我第一次跑 HER 时踩的最大的坑。现象训练刚开始几千步Q 值突然冲到几万然后 loss 变成 NaN。排查了很久发现是desired_goal的归一化出了问题。FetchPush 的目标空间是三维坐标范围在[0.5, 1.5]左右。如果直接把原始坐标喂给神经网络Q 网络和 actor 网络都会被大数值的输入搞崩。常见的做法是对目标做归一化goal_mean np.array([1.2, 0.8, 0.5]) goal_std np.array([0.5, 0.5, 0.5]) normalized_goal (goal - goal_mean) / goal_std而且归一化要放在重标注之前做否则假目标分布和原始目标分布不一致等于给网络喂了两套不同尺度的数据。这个问题你可以在 Gym 环境里用env.reset()后随机执行几步打印desired_goal的坐标范围来确认。4.2 稀疏奖励下采样太少导致不收敛第二个常见问题即便加了 HER如果 replay buffer 里成功样本比例还是太少比如只有 1%训练照样慢。原因在于 HER 的重标注是每个 episode 结束后一次性生成 k 条假样本如果 k 值太小或者 buffer 里成功样本被后续失败样本冲刷掉效果依然有限。我后来加了一个成功样本优先级采样从 replay buffer 采样时给 reward1 的样本额外加权。具体实现可以用 Prioritized Experience ReplayPER的思想设计或者在随机采样时按一定概率强制采样成功样本。我用了一个比较粗暴但有效的方式采样时以 0.3 的概率只在成功子集中采样0.7 的概率全量随机采样效果提升显著。还有种做法是在训练过程中定期清理 buffer把太老的失败样本淘汰掉确保 buffer 里保存的是近期的、分布更接近当前策略的样本。我用过后发现有一定效果但要注意不能太激进否则 buffer 多样性下降训练更不稳定。4.3 目标条件策略的“目标混淆”还有一个有趣的问题发生在 FetchPickAndPlace 上训练中途成功率达到 60% 后卡住不再上升。我检查日志发现机械臂学会了抓取但搬运到目标位置时总是差一点。问题出在网络对目标位置的分辨率不够。FetchPickAndPlace 的目标空间是 3 维而网络最后一层是 256 个隐藏单元对连续的三维坐标回归能力有限。我做了两个改进效果立竿见影第一把目标从绝对坐标改成相对坐标desired_goal改成目标位置 - 物体当前位置。这样网络只需学习一个相对位移向量不需要记住绝对坐标的绝对数值泛化性能好很多。第二把 critic 网络改成目标拼接进状态向量而不是在特征层面做乘法。拼接操作简单直接网络容量不受限制是 OpenAI 实现里默认的融合方式。你也可以试试做乘法融合理论上两者都能用但我实测在简单任务上区别不大。4.4 训练时间太长试试降低 k 配合并行如果你发现 FetchPickAndPlace 训练到 300 万步要十几个小时车等不起可以考虑两个优化方向。一是向量化环境并行采集数据比如用gym.vector.make创建多个并行环境每个环境独立跑 episode把经验统一收集后训练。DDPG 是 off-policy 算法对数据并行天然友好我这里用 8 个并行环境时训练时间差不多能降到原来的 1/5。二是用 GPU 版的 replay buffer 采样。这个在样本量大的时候很明显。普通 numpy 随机采样在 100 万条数据里取 256 个样本大约需要几毫秒但对于一个训练步来说这是不小的开销。如果你用 PyTorch可以直接在 GPU 上用torch.randint做索引省掉 CPU 到 GPU 的数据拷贝。5. HER 的边界与后续扩展5.1 HER 解决不了什么问题虽然 HER 很强但确实有它的边界。最典型的两个场景第一目标空间与状态空间不重叠的任务。HER 的核心假设是“带重标注的目标是从achieved_goal里采样的”如果 achieved_goal 本身不能代表任务目标比如目标是“开门”而 achieved_goal 只是机械臂手部位置重标注就没有意义。我自己做过一个插线板插孔任务物体有姿态变化仅仅用末端位置做 goal 无法表达“插塞要对准孔”这种约束HER 的效果就非常有限。第二目标过于稀疏且时间跨度过长的任务。比如多步推理型任务一步做错后面全部白费重标注只能覆盖 episode 内的局部轨迹但无法改变长期依赖的问题。这种场景更多要靠奖励塑形reward shaping或分层强化学习hierarchical RL来拆解任务。5.2 从 HER 到 Hindsight Instruction Relabeling现在很多新算法借鉴了 HER 的思想把“事后重标注”从目标层面扩展到了指令层面。代表性工作是分解式指令学习比如语言条件任务里用语言模型给轨迹自动生成文字指令再把它当成额外的监督信号。这本质上还是 HER 的骨架——只不过把假目标从坐标换成了句子。如果你做的是多模态控制任务我建议先理解清楚 HER 的底层逻辑再考虑是否迁移到语言指令重标注。因为语言指令的多样性和语义复杂性远远超过坐标目标重标注时如果生成指令质量不高反而会引入噪声比直接用坐标目标效果还差。5.3 一个低成本但有效的提升在 HER 基础上加对手损失如果你已经跑通了 HER 想要进一步提升我推荐一个比较小众但有效的技巧在 actor 的损失函数里加一个额外项惩罚“假目标下策略输出差异过大”的行为。这个技巧来自我在类似任务上排查 Q 值时发现的规律——HER 让 Q 网络见过多种目标分布但 actor 网络容易在假目标分布上拟合得过快。具体实现很朴素在一个 batch 里同时计算“原始目标”和“假目标”下 actor 的输出给两者差值加一个 L2 惩罚系数设成 0.05 左右。效果上是把策略的输出分布约束到更平滑不会因为假目标干扰导致动作产生剧烈抖动。这个方法没有论文支撑是我在多个任务上反复测试出来的有效组合你不妨拿一个简单任务先验证一下。6. 关于复现 HER 的最后一些体会回想这段时间和 HER 打交道的过程让我最意外收获的是它对随机目标采样的敏感度。很多人把 HER 当成一个即插即用的工具其实目标空间怎么采样、要不要做归一化、要不要做相对坐标直接影响训练效果五六倍甚至更多。我的建议是每次换新任务前先在小的子任务上做一个快速消融光验证“目标表示方式”这一项就能省下后面几天调试的时间。另一个值得留意的地方是HER 对训练基础设施的要求其实很低代码量不多不需要分布式不需要混合精度一张消费级 GPU 完全可以跑。这使得它成为强化学习稀疏奖励问题的最佳入门算法之一。相比手工设计密集奖励函数的反复试错HER 的收益完全不成比例。如果要从这篇文章里带走一个观念我希望是不要把失败经验丢掉把它们换一个角度重新解释它们会变成最有价值的学习材料。这既是 HER 算法的精神也是我做实验时候最常用到的一条经验。最后分享一个小技巧别在 FetchReach 上调通一套参数就以为万事大吉等换到 FetchPush 上时你会发现 k 值、噪声幅度和 replay buffer 比例都要重新调。把这个过程记录下来其实比算法本身更值钱因为一套“怎么调”的直觉在你未来遇到新任务时才是真正能复用的资产。