☰
强化学习稀疏奖励实战:HER事后经验回放原理与实现
2026/9/30 8:38:42 网站建设 项目流程

一直觉得“hindsight”这个词特别有意思。字面意思是回头看清,通俗讲就是“事后诸葛亮”。但放到强化学习这个领域,它却是一个正经到不能再正经的学术概念——Hindsight Experience Replay,事后经验回放,OpenAI 在 2017 年提出的一种专门对付稀疏奖励问题的训练技巧。我做这个项目的时候,核心就是把这一套算法在机器人操作环境里完整落地,从原理推演到代码实现全部跑通。这篇文章就把整个项目的来龙去脉拆一遍,适合正在做机器人控制、游戏 AI、以及一切奖励信号又稀又少任务的 RL 玩家参考,尤其是被稀疏奖励折磨到怀疑人生的那种。

1. hindsight项目到底是什么?

1.1 一个项目标题背后的技术内核

有些项目标题一眼就能看出作者在搞什么,“hindsight”这个命名其实非常直白——它指向的就是强化学习里那个经典的“事后经验回放”算法。做这个项目之前,我一直在折腾机械臂抓取任务,从仿真环境到真实硬件都踩过不少坑。最让人崩溃的是:智能体在绝大多数回合里根本拿不到任何奖励信号,训练几万步之后策略依然跟随机游走差不多。这个问题在学术上叫稀疏奖励问题,在实际工程里则是所有机器人学习任务绕不开的坎。

当时我注意到 OpenAI 的论文《Hindsight Experience Replay》,标题里就带着 hindsight 这个词。它解决稀疏奖励的思路,用一个生活化的类比就是:你没射中靶心,但射中了靶子旁边的木板,你不能说这一箭完全无用,至少你学会了“往这个方向使力”是差的比较近的。HER 的做法就是把这些“接近成功但不完全成功”的轨迹,通过替换目标的方式变成有效学习样本。这个项目就是围绕这条主线展开的,我用 Gym 里的 Fetch 系列环境做了验证,把 HER 和 DDPG 组合起来训练,跑通了整套流程。

做这个项目的过程中我最大的感受是:HER 的算法逻辑并不复杂,真正复杂的是理解它为什么有效,以及在工程实现层面如何把“目标替换”这个操作做得干净利落。所以这篇文章不会只贴代码,我会把原理、实现细节、调参经验一条条讲清楚。

1.2 解决什么问题:稀疏奖励场景

先说说稀疏奖励到底有多讨厌。强化学习的核心是智能体通过与环境交互获得奖励,然后优化策略。如果奖励信号太稀疏,智能体在初期几乎得不到任何正反馈,梯度信号接近为零,训练就变成了盲人摸象。举个例子,你让一个机械臂去抓取桌面上一个物体,动作空间可能是四维、七维甚至更高。物体在某个特定位置,机械臂的末端执行器只有运动到物体附近并且准确合拢手指,才能拿到“抓取成功”这个奖励。

用概率算一下就明白了。假设机械臂每次尝试抓到物体的概率是 0.01,那么它平均需要 100 次才能吃到一次正奖励。在 0.01 这个概率下,前 50 次尝试中至少成功一次的几率只有约 39%。更惨的是,抓取任务里奖励往往不是渐进给的,只有成功和失败两种结果。成功概率低、奖励间隔长,策略网络根本学不到“接近目标”这种中间概念。

常见应对方案是奖励塑形——手动设计一个连续奖励函数,比如根据机械臂末端与目标之间的距离给一个负奖励,离得越近惩罚越小。这种方法在某些场景下有效,但存在两个大问题。第一,工程量大,每个新任务都要重新设计奖励,而且设计得不好很容易让智能体学会钻空子,也就是所谓的奖励黑客。第二,过于细密的引导奖励会让策略变得脆弱,换一个环境、换一个目标位置,原本好用的奖励函数可能就失效了。

HER 的巧妙之处在于它绕开了奖励塑形这个坑,不需要设计任何中间奖励,而是从失败经验里“挖掘”有价值的信息。它做的事情本质上是一个视角转换:一条失败的轨迹,在原始目标下是没用的,但如果把轨迹的最终状态当作目标,这条轨迹就变成了一条成功轨迹。这个思路听着有点“自欺欺人”,但在数学上完全站得住脚,而且被大量实验验证了高效性。

2. 核心机制拆解:事后经验回放为什么有效

2.1 从“事后诸葛亮”到算法

要理解 HER 为什么有效,得先回头看强化学习中经验回放的基本单元。标准的 off-policy 算法,比如 DQN、DDPG,都会维护一个经验池,里面存放着一条条四元组或者五元组:(状态 s、动作 a、奖励 r、下一状态 s')。训练时从经验池里随机采样,用这些历史数据更新网络。经验池存在的意义是打破样本之间的时间相关性,提高数据利用效率。

但问题是,在稀疏奖励环境下,经验池里 99% 的样本都是“没吃到奖励”的失败样本,每条样本的奖励都是 0。梯度方向被这些零奖励样本主导,有价值的信息少得可怜。传统经验回放在这里基本失效,其实不是回放机制本身有问题,而是样本的质量太差——目标定得太高,绝大部分探索行为都够不着。

HER 做的事情是修改样本的“目标”。它引入了一个新的变量 g,代表目标。原来的四元组变成 (s, a, r, s', g),训练时不仅要告诉智能体“你在这个状态做了这个动作”,还要告诉它“你当时的意图是想达成目标 g”。关键点在于:HER 在存储样本时,会额外生成一些“事后目标”,并基于这些事后目标重新计算奖励。

比如机械臂抓取任务,原始目标是“把物体抓到位置 A”,某条轨迹里机械臂把物体推到了位置 B。站在原始目标的角度,这是一个失败轨迹,奖励是 0。但如果事后把目标改成“把物体推到位置 B”,那这条轨迹的每一步就都变成了成功状态下的经验——因为物体最后确实到了 B。虽然 B 不是我们最终想要的位置,但至少训练数据里多了一批“知道如何完成任务”的样本。

这种做法的本质是让智能体从每一次尝试中学到点东西,而不是只有碰到最终目标才算数。用工程的话说,就是提高了经验池里“正样本”的比例,让策略网络有更多有效梯度可以更新。

2.2 目标替换策略详解

HER 论文里提出了四种事后目标的选取策略,我把它们整理成一个对比表格,方便你看清楚区别:

策略事后目标选择方式特点
final取轨迹的最终状态作为事后目标最简单,但信息量有限,每条轨迹只产生一个额外样本
future取轨迹中未来某一时刻的状态作为事后目标最常用,一条轨迹可以生成多个额外样本,效率高
episode取同一条轨迹中随机一个状态作为事后目标比较激进,数据利用充分,但噪声大
random取训练过程中其他轨迹的随机状态作为事后目标相当于增大目标采样空间,探索性强,但不够稳定

我实测下来,future 策略是工程里的首选。它不取轨迹的最终状态,而是从当前时刻之后的某个状态里随机挑一个当作事后目标。这样做的优势是:一条长度为 T 的轨迹,理论上可以产生 O(T^2) 个额外的样本组合,数据利用效率远高于 final 策略。

但 processing 的时候需要注意一个细节:事后目标不能选得太随意。HER 论文里建议,future 策略里事后目标应该从“同一轨迹中未来 k 步之后的状态”里选,k 是一个超参数。太小了,状态和目标几乎一样,学不到迁移信息;太大了,状态和目标差异过大,样本质量下降。实际操作中 k 取 4 是一个比较稳妥的默认值,后面我会再详细讲怎么调。

2.3 奖励重标注的数学过程

理解了目标替换策略,接下来要搞清楚奖励重标注到底怎么算。这一步是 HER 的实质性操作,直接关系到训练是否稳定。

假设有一个二值奖励函数 f(s, g),用来判断状态 s 是否达到目标 g:达到则奖励 0,未达到则奖励 -1。在原始目标 g_orig 下,轨迹里每个时刻的奖励都是 r = f(s_t, g_orig)。当 HER 把某个状态 s_target 选作事后目标时,它会重新计算这条轨迹上所有时刻的奖励。

具体来说,对于原轨迹里每一个状态-动作对 (s_t, a_t),当目标被替换为 g_new = s_target 时,新的奖励是 r' = f(s_t, g_new)。因为 s_target 是轨迹里确实到达过的状态,所以在这条轨迹的最后一个时刻,智能体总是满足 r' = 0。换句话说,这条轨迹在事后目标的视角下,是一条有始有终的成功轨迹。

在实现层面,还有个重要的点:目标信息 g 要拼接到状态向量里一起输入网络。大多数情况下动作策略依赖于当前状态和目标,所以要构造一个“增强状态” [s, g]。当目标从 g_orig 变成 g_new 时,增强状态就变成 [s_t, g_new],网络看到的是不同的输入,从而学到“在这个状态下,朝这个目标去,应该这么做动作”的策略。

这就是 HER 的完整闭环:通过事后目标,把失败轨迹变成成功轨迹;通过奖励重标注,让网络获得正确的监督信号;通过目标拼接,让策略网络知道目标在哪里。三者缺一不可。

3. 完整实操:从零搭建HER训练管线

3.1 环境与工具选型

我做这个项目用的是 OpenAI Gym 的 Fetch 系列环境,具体是FetchPush-v1。选这个环境的原因是它具备几个很适合验证 HER 的特点:任务目标随机化(每一轮的目标位置都不同)、奖励二值化(只有成功和失败两种)、状态空间里直接包含观测目标信息,不用自己额外拼接太多东西。

算法选型上,我用了DDPG + HER的组合。DDPG 是经典的连续控制 off-policy 算法,适合机械臂这类连续动作空间的任务。当然你也可以用 SAC、TD3 替代,但如果是跑通流程、验证 HER 思路,DDPG 是最直接的。

为了不让你被代码细节淹没,我把整套实现的核心模块拆成三部分:经验池、目标重标注逻辑、训练循环。下面一段代码给出的是经验池与重标注逻辑的核心实现,这是整个项目里最关键的部分。

import numpy as np import random from collections import deque class HERReplayBuffer: def __init__(self, capacity, k_future=4, strategy='future'): self.buffer = deque(maxlen=capacity) self.k_future = k_future self.strategy = strategy def add(self, episode_transitions): # episode_transitions: list of (obs, action, reward, next_obs, goal) # 先把原始轨迹以原始目标存储 for trans in episode_transitions: self.buffer.append(trans) # 对每条轨迹随机选 k_future 个事后目标,生成额外样本 for _ in range(self.k_future): self._add_future_goal_samples(episode_transitions) def _add_future_goal_samples(self, episode_transitions): T = len(episode_transitions) if self.strategy == 'future': # 随机选一个时间戳作为事后目标的来源 future_time_idx = random.randint(0, T - 1) # 从该时间戳之后的状态中随机选一个 if future_time_idx < T - 1: goal_idx = random.randint(future_time_idx + 1, T - 1) else: return elif self.strategy == 'final': goal_idx = T - 1 elif self.strategy == 'episode': goal_idx = random.randint(0, T - 1) else: return new_goal = episode_transitions[goal_idx]['next_obs'][:3] # 取物体位置作为目标 for t, trans in enumerate(episode_transitions): obs, act, _, next_obs, _ = trans.values() # 计算新奖励:达到新目标则 0,否则 -1 new_reward = 0.0 if np.linalg.norm(next_obs[:3] - new_goal) < 0.05 else -1.0 new_obs = np.concatenate([obs, new_goal]) new_next_obs = np.concatenate([next_obs, new_goal]) self.buffer.append((new_obs, act, new_reward, new_next_obs, new_goal)) def sample(self, batch_size): batch = random.sample(self.buffer, batch_size) return map(np.array, zip(*batch))

这段代码有几个地方值得解释一下。第二行的 deque(maxlen=capacity) 是经验池的容器,容量到了之后会自动弹出旧的样本。我不建议用普通列表,因为训练到后期经验池会很大,列表的随机采样性能会很差。

再看 _add_future_goal_samples 方法。这里的核心是先随机选一个时间戳 future_time_idx,再从这个时间戳之后的状态里随机选一个作为新目标。这样的好处是,新目标和轨迹末段的状态比较接近,重标注出来的奖励曲线更平滑,不会出现“目标选得太早导致前半段和后半段割裂”的问题。

新目标我取的是 next_obs[:3],也就是物体在三维空间的位置。在 Fetch 系列环境里,位置信息通常占据观测的前几个维度,实际使用时要根据环境定义调整。重标注的奖励计算用了一个距离阈值 0.05,这个值不是拍脑袋定的,Fetch 环境本身的成功判定阈值就是这么定的。

3.2 核心代码:经验重放实现

经验池建好之后,还有一个重要的点:网络训练时要把原始样本和目标重标注样本混合采样。如果只采样重标注样本,策略会过度偏向“达成任意目标”而不是原始目标;如果只采样原始样本,又回到了稀疏奖励的老问题。我采用的是每条原始轨迹存储时额外生成 k_future 个重标注样本,这样采样时天然就是混合的。

DDPG 那边的实现我用了四个网络:两个 actor(在线网络和目标网络)、两个 critic(在线网络和目标网络)。训练循环里,critic 的 loss 用的是标准的 TD 误差:

def update(model, replay_buffer, batch_size=256, gamma=0.98): obs, act, rew, next_obs, goal = replay_buffer.sample(batch_size) # 计算目标 Q 值 target_action = model.target_actor(next_obs) target_q = model.target_critic(next_obs, target_action) y = rew + gamma * target_q # 当前 Q 值 current_q = model.critic(obs, act) critic_loss = ((current_q - y) ** 2).mean() # 更新 critic 网络 model.critic_optimizer.zero_grad() critic_loss.backward() model.critic_optimizer.step() # 更新 actor 网络 actor_loss = -model.critic(obs, model.actor(obs)).mean() model.actor_optimizer.zero_grad() actor_loss.backward() model.actor_optimizer.step()

需要特别注意的是目标网络的软更新,也就是通过 polyak 系数 tao 缓慢把在线网络的参数复制到目标网络。这个系数一般取 0.05,太小了训练容易不稳定,太大了网络收敛变慢。Fetch 任务里我试过 0.005 到 0.1 之间的几个值,0.05 表现最均衡。

如果你的场景是稀疏奖励特别极端,比如目标是“开门”而奖励只有在门完全打开时才有,可以考虑用 SAC 替代 DDPG。SAC 自带熵正则化,探索性更强,配合 HER 在一些硬核任务上表现更好。不过代价是实现的复杂度更高,超参数也更多。第一次跑 HER 的话,还是建议 DDPG 起步。

3.3 超参数设置与训练流程

下面是我在这个项目里最终采用的超参数组合,基本可以直接复现:

参数名取值说明
经验池容量1e6Fetch 任务的数据量比较大,容量小容易覆盖掉早期有效样本
采样批次大小256实验过 128,效果差不多;256 更稳
actor 学习率1e-3用了 Adam 优化器
critic 学习率1e-3与 actor 保持一致,方便调
polyak 系数0.05目标网络软更新系数
折扣因子 gamma0.98任务步数不长,不需要太接近 1
噪声策略高斯噪声,标准差 0.2动作探索噪声,训练后期可以逐步降低
k_future4每条轨迹额外生成的事后样本数量
每轮最大步数50Fetch 环境默认的 episode 长度
总训练步数100 万通常 60 万步左右开始看到明显效果

训练流程分成几个阶段,可以看作是五个循环,逻辑上很像“探索—存储—重标注—更新—评估”。我在实际项目中还加了一个每 5000 步跑一次评估的逻辑,固定一个随机种子和环境初始状态,统计连续 20 个 episode 里机械臂抓取的成功率。这个评估逻辑非常关键,因为训练 loss 下降不代表任务成功率上升,只有跑固定环境的评估才能反映真实效果。

3.4 训练曲线解读与效果验证

训练完成后,我画了两条曲线:一条是 DDPG 单独训练的成功率,一条是 DDPG + HER 的成功率。两条曲线放在一起看效果非常明显。

纯 DDPG 的曲线基本在 0% 附近徘徊,最多偶尔冒出 2% 到 5% 的尖峰,那是运气好的几个 episode。到了训练后期成功率依然没有稳定上升的趋势,说明单纯加大训练步数也无法解决稀疏奖励问题。

加了 HER 之后,曲线大约在 20 万步开始缓慢爬升,40 万步之后进入快速上升期,60 万步左右成功率稳定在 80% 以上。有些 run 运气好能做到 90% 以上,运气差一点也能保持在 75% 左右。这里值得说的是,HER 不是万能的,它解决了“有效样本不足”的问题,但策略能不能学到精细操作,还取决于网络结构和动作维度。

我还尝试过把 HER 应用到 FetchReach(机械臂到达指定点)这个更简单的环境上,成功率几乎能在 10 万步内冲到 90%+。这进一步验证了 HER 对稀疏奖励问题确实有效,而且任务的动作精度要求越高,HER 带来的提升越明显。

提示:如果你在复现时发现曲线始终不上升,优先检查经验池里重标注样本的奖励值是否计算正确。用打印日志或抽样检查的方式,确认经验池里存在正样本。这个检查比调任何超参数都重要。

4. 常见问题与排查技巧实录

4.1 典型问题速查表

我在项目过程中踩了不少坑,有些坑不看日志根本发现不了。这里整理成一张速查表,对照着排查能省掉很多时间:

症状可能原因排查方式解决方案
训练损失不下降经验池正样本为 0打印经验池中 r=0 的样本比例,检查重标注逻辑确认新目标的计算维度正确;检查距离阈值是否设置过小
成功率跳变剧烈噪声标准差过大记录每个评估轮次的平均回报降低探索噪声,或加入噪声退火策略
网络收敛到局部最优事后目标选取策略不佳对比 final 和 future 两种策略优先切换到 future 策略,k_future 从 4 开始调
训练后期过拟合经验池容量不足统计经验池样本总数和采样分布增大容量到 1e6 或以上
动作输出超出边界DDPG 的 tanh 输出处理不当检查 actor 输出层激活函数输出层用 tanh,并按环境动作范围缩放

4.2 调参心得与避坑指南

先说 k_future 这个参数。我之前提到默认取 4,但实际调试时值得多试几个值。k_future 越大,重标注样本越多,经验池里正样本比例就越高,训练信号越充足。但你也会发现,当 k_future 超过某个临界值后,性能提升不再明显,甚至可能下降。原因很简单:事后目标过多,会让训练分布偏离原始目标太多,策略会变得“太擅长达成任意目标,却忘了原始任务”。这是一个权衡,不能盲目加大。

另一个容易被忽视的点是观测拼接的顺序。HER 要求把目标信息拼接到观测上,但拼在前面还是拼在后面,在实现上没有区别,但要注意网络结构里对应维度的初始化和归一化。我之前踩过一个坑:对原始观测做了归一化,却忘记对目标维度做同样的归一化,导致训练前期梯度方向被目标维度主导。目标空间和观测空间往往量纲不同,拼接后最好整体做一次归一化。

还有一点关于奖励阈值。Fetch 环境的成功判断是基于物体和目标之间的欧氏距离,阈值 0.05 是环境默认值。但如果是自定义任务,你需要先搞清楚“成功”的物理含义,再设定重标注奖励的阈值。阈值设得太小,重标注后的成功样本依然很少;设得太大,智能体可能学一个“差不多得了”的粗放策略,精确操作永远学不会。

最后想强调一下随机种子。HER 这类算法的方差比较大,同一个超参数配置,换一个随机种子可能差出 20% 的成功率。我做实验时固定了三个种子分别训练,取评估成功率的中位数,而不是只跑一次就下结论。如果你想复现这个项目的效果,也建议至少跑三个随机种子。

5. 这个项目后续还能怎么扩展

HER 不是只能用在机械臂抓取上。我做完 Fetch 系列之后,把同样的思路迁移到了两个新任务上:一个是障碍物环境里的导航任务,一个是需要多步操作的桌面整理任务。导航任务里,智能体的原始目标是到达某个特定坐标,HER 会让它把“走过的轨迹终点”当作事后目标,很快就学会了绕开障碍物的基本路径策略。桌面整理任务稍微复杂一点,因为涉及多个物体和多个子目标,HER 需要和分层强化学习结合起来,这时候单纯靠 HER 就不够了。

我个人的建议是:如果你是在做真实机器人或者高精度操作任务,HER 可以当作基线方法,但它解决不了所有稀疏奖励问题。目标空间的设计、奖励信号的形式、探索策略的选择,每一项都值得单独深挖。做这个项目的过程中我最大的体会是:所谓“事后诸葛亮”,并不是真的要把失败当成功,而是从每一次尝试里榨取出可用的监督信号。这个思想不仅在强化学习里适用,在很多工程优化问题上也适用——把“这次没成功”变成“至少知道哪条路更接近成功”,本身就是一件很有价值的事。

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

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

立即咨询