☰
HER算法:让强化学习从失败中捡出经验
2026/9/30 3:47:28 网站建设 项目流程

Hindsight:让强化学习从失败中“捡”出经验

第一次听到 hindsight 这个项目名,估计很多人以为是“后见之明”“事后诸葛亮”的心理学讨论。但搞强化学习的朋友应该立刻会心一笑——这是 OpenAI 在 2018 年提出的Hindsight Experience Replay(HER,事后经验回放)算法,一个专门解决稀疏奖励问题的经典思路。我最初接触 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 任务上收敛速度比 DDPG+HERV 快不少。代价是调参参数多了几个(alpha 的温度系数需要自动调),训练时间也会变长。如果你不想折腾,先跑通 DDPG+HER 再考虑升级到 SAC+HER 是比较稳的路径。

3. 从零跑通 HER:环境搭建、代码结构、调参实战

3.1 环境与依赖准备

我目前最常用的组合是Python 3.8+PyTorch 2.x+Gym 0.25(注意不要用 Gymnasium,API 改动很多,OpenAI 原版代码不兼容)+mujoco-py 2.1。如果你要直接跑 OpenAI 的baselines仓库里的 HER 实现,需要繁琐的环境版本匹配,我建议直接用下面这套简化版结构,代码量不大,改动也容易。

conda create -n her_env python=3.8 pip install gym==0.25.2 pip install torch==2.0.1 pip install mujoco-py==2.1.2.14

mujoco-py 这里有坑:新版的mujoco和mujoco-py是两个不同的包,前者底层的渲染引擎和 API 全变了,后者才是老版本 Gym 能直接调用的。如果你装的是新版mujoco,Gym 0.25 会找不到mujoco_py接口,直接报错。

个人经验:在 Linux 上安装时还要注意patchelf、gcc的版本兼容,建议先跑一行python -c "import mujoco_py"验证环境没问题,再继续下一步。

3.2 测试环境:FetchPush 与 FetchReach

OpenAI 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, k=4): """ 对一整条 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, axis=0)) her_actions.append(episode_batch["actions"]) her_rewards.append(compute_reward( episode_batch["achieved_goal"], np.repeat(fake_goal.reshape(1, -1), episode_len, axis=0) )) 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 经典结构完全够用。真正影响训练效果的是下面这几个参数,我曾经在不同任务上试过不同组合,效果差异很大。

参数FetchReachFetchPushFetchPickAndPlace
replay buffer 大小100 万100 万100 万
HER 采样数量 k488
Q 网络学习率1e-31e-31e-3
actor 网络学习率1e-31e-31e-3
batch 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 重标注后数据分布里 reward=1 的比例),三是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 采样时,给 reward=1 的样本额外加权。具体实现可以用 Prioritized Experience Replay(PER)的思想设计,或者在随机采样时按一定概率强制采样成功样本。我用了一个比较粗暴但有效的方式:采样时以 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 比例都要重新调。把这个过程记录下来,其实比算法本身更值钱,因为一套“怎么调”的直觉,在你未来遇到新任务时才是真正能复用的资产。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询