拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

HER:稀疏奖励下的目标重标记与强化学习实战解析

HER:稀疏奖励下的目标重标记与强化学习实战解析

hindsight 这个词,直译过来是“后见之明”。做强化学习的人听到它,大概率会想到 OpenAI 那篇 Hindsight Experience Replay。之前我在复现多目标机器人控制任务时,被稀疏奖励问题折腾得够呛,试过手塑奖励、课程学习,最后真正解决问题、让我觉得“想通了”的,就是 HER。这篇文章围绕 hindsight 这个主题,把算法从原理到实现完整拆一遍,适合已经在跑 RL 实验、准备接触 Goal-Conditioned RL,或者被“奖励永远为 0”困扰的读者。

1. 项目概述:hindsight 的身份确认

1.1 它是什么:从英文词义到强化学习算法

hindsight 日常用的意思是“后见之明”,心理学里还有个 hindsight bias,指人们在事情发生后倾向于觉得自己当时就能预料到。OpenAI 把这个“事后视角”用到了强化学习里,形成了 HER 这个名字背后的核心思想:当智能体没有达成目标时,不要只看到“失败”,而是换个角度,把实际到达的状态当作目标,再从这条已经发生的轨迹里重新学习。这个做法改变了 reward 信号的生成方式,也让原本完全无法学习的任务变得可学。

HER 最核心的价值是解决稀疏奖励问题。在普通 RL 设置里,任务成功率低的时候,奖励几乎全是 0,梯度信号稀疏到可以忽略,策略更新基本靠运气。HER 通过目标重标记(goal relabeling)改变了返回的奖励,让看似失败的轨迹也能产出有效的学习指令,从而显著提升采样效率。它不是一个新的 RL 框架,而是一种经验增强方法,通常搭在 DDPG 等连续动作算法之上。

1.2 它能在哪些任务里派上用场

不是所有任务都适合 HER,它主要面向多目标强化学习(Goal-Conditioned RL),或者有明确期望状态的任务。典型例子包括机械臂推物体(Push)、抓取放置(Pick & Place)、滑块摆位(Slide)。这些环境里,每个 episode 会随机生成一个目标,比如桌子上的某个坐标点,智能体需要学会“无论目标在哪里,都能把方块推过去”。

这类场景的共同特征是:目标空间大、奖励极其稀疏,随机探索几乎产生不了正奖励。HER 之所以有效,是因为它不再把“目标没达成”当作毫无价值的经验,而是把已经发生的状态作为学习素材。换句话说,HER 把学习信号从“目标”迁移到“经验”,越探索、样本越丰富,学习越高效。这也是为什么它特别受机器人领域欢迎——真实机器人采样成本高,能把失败数据用起来,是实打实的降本增效。

2. 核心原理拆解:为什么“事后”能变成“事前”

2.1 稀疏奖励为什么会让 RL 学不动

假设我们让智能体控制一只虚拟机械臂,目标是把桌面上的方块推到坐标 (x, y)。奖励函数设置成:如果方块最终位置与目标距离小于 5cm,返回 1,否则返回 0。问题是,机械臂初始位置和目标位置可能相距很远,随机动作大概率把方块推到离目标更远的地方,奖励永远是 0。

从强化学习的公式角度看,Q 函数更新依赖 TD 误差,而 TD 误差中 reward 是核心信号。如果 reward 恒为 0,那么不同状态动作对之间没有区分度,Q 网络只能输出一片平坦的估计,策略也没有方向可以优化。有些人会尝试手塑奖励,比如把“距离目标有多远”作为 reward。但手塑奖励有两个毛病:第一,任务形态一变,奖励函数要重新设计,工作量大;第二,智能体很容易走捷径,学会推出一段固定动作,而不是真正根据目标调整行为。

HER 的解法很有意思。真实目标没法达成,那就换一个“实际上已经达成”的目标。比如想推动方块到 (x,y),实际推到了 (x',y'),那这次 episode 至少说明:从起始状态出发,用这一串动作,我可以把方块推到 (x',y')。这本身就是一条有效经验。把一个失败样本改写成带正奖励的学习样本,这就是“事后视角”的力量。

2.2 目标重标记的完整逻辑

为了把“失败”变成“学习材料”,数据层面需要做完整的重标记。原本一条经验在回放缓冲区里存储为四元组 ( (s_t | g, a_t, r_t, s_{t+1} | g) ),其中 ( r_t ) 表示在目标 g 下、执行动作 a_t 后到达状态 s_{t+1} 的奖励。HER 的流程分五步:

  1. 采样一个真实目标 g。
  2. 智能体按照条件策略 ( \pi(a|s,g) ) 执行一整轮 episode,记录完整轨迹。
  3. 把原始目标经验存进缓冲区,即使奖励全为 0。
  4. 从轨迹中选替代目标 g',最常用的是从未来状态中采样。
  5. 针对 g' 重新计算每一步的奖励,生成新的四元组再存缓冲区。

这里有个关键约束:替代目标 g' 必须来自“当前这条轨迹中真实到达过的状态”。如果随机选一个从未到达过的状态当作目标,那依然无法产生有效奖励信号,因为智能体根本没有接触过那个状态对应的转移。这也是 HER 名称的由来——只使用已经确认发生过的事情作为目标,绝不做无根据的假设。

2.3 替代目标选择的四种策略

论文里给出了四种从轨迹中采样替代目标的方式:

  • final:直接用轨迹最终状态作为替代目标。
  • random:从轨迹任意时间步随机抽状态。
  • episode:从当前时间步以后的所有状态中随机抽。
  • future:从当前时间步之后的未来状态中随机抽 k 个。

实验结果里,episode 和 future 明显优于 final 和 random。final 的问题在于,长 episode 的最终状态往往和中间状态差异很大,对时间步靠前的样本来说,重标记目标离得太远。random 则是完全没有考虑时间连续性,可能把一个早期状态当成目标,导致前后奖励矛盾。future 保证了“从 t 时刻往后看,目标 g' 是未来某个时刻真到达过的状态”,这种时序相关性让重标记后的奖励更合理,策略学习也更稳定。

一个直观类比是投篮。你第一次没投进,但球撞到篮板弹到了一个具体位置。这个“弹到的位置”对你理解“用这个力度和角度,球会飞向哪里”非常有价值。如果只盯着篮筐,这次投篮毫无意义;但如果把“球的实际落点”当作这次投篮的目标,你反而获得了关于动作结果的一手资料。HER 做的事情,本质上就是把这个类比搬进强化学习。

3. 算法细节与关键参数选择

3.1 状态、动作、目标如何组织

实现 HER 时,第一步是把“状态”和“目标”统一编码。UVFA(Universal Value Function Approximators)是核心思想:把目标向量拼接到状态向量后面,作为一个整体输入 Q 网络和策略网络。这样 Q 函数可以表达为 ( Q(s, a, g) ),策略也可以写成 ( \pi(a|s, g) ),网络就不需要为每个目标单独训练一次。

拼接输入看起来很直观,但实际操作中有几个容易翻车的细节。第一,目标向量和观测向量的量纲往往不同,最好分别做归一化,否则数值尺度不均衡很容易让训练震荡。第二,拼接后输入维度变化了,网络第一层要根据新的维度定义。第三,在 Fetch 这类环境里,observation 里面既有 achieved_goal 也有 desired_goal,千万别把 desired_goal 和观测里其他部分搞混,更不要重复拼接。我就见过一个复现项目把 desired_goal 和 achieved_goal 同时拼了两次,表面上看维度没报错,实际训练效果一塌糊涂。

目标重标记后,奖励函数的对应关系也必须重新计算。原来的 ( r_t ) 基于目标 g 计算,重标记之后要用新目标 g' 重新算,不能直接沿用。这个细节看起来不起眼,却是几乎所有复现项目的重灾区。

3.2 future 策略与 k 值的影响

实际实现里,每个时间步不只采样一个替代目标,通常采样 k 个。论文推荐 future 策略 k=4。这个值很有讲究:k 太小,重标记经验比例不够,稀疏奖励问题依然突出;k 太大,缓冲区里重标记经验占比过高,原始目标对应的 reward 分布会被淹没,策略可能过度拟合“事后目标”,对真实目标的敏感度下降。

k=4 是论文在 Fetch 系列环境上实验出来的经验值,不同任务可以微调。我建议先固定 k=4 跑通流程,之后做一次小范围网格搜索,比较 k=1、2、4、8。从我的实践看,这个超参对收敛速度的影响相当明显,在 FetchPush 上 k=4 和 k=1 的收敛速度能差出约 30% 的 epoch。这里说的不是最终成功率差异,而是达到相同成功率所需交互次数,HER 的主要价值本来就体现在采样效率上。

替代目标采样的具体范围也要注意。future 策略要求从 ( t+1 ) 到 episode 末尾的状态中采样,而不是从整个轨迹中采样。很多精简实现会随手写成 random 策略,同样能跑,但效率差不少。如果目标是从 ( t ) 时刻看未来,那么 ( t ) 时刻之前到达过的状态对当前时刻的决策没有参考价值,这一点在做代码审查时要特别小心。

3.3 与基线算法 DDPG 协同工作的机制

HER 不是一个从零开始的 RL 算法,它更像一个“经验增强插件”,通常搭在 DDPG 这类连续动作空间算法上。原因是机器人类任务动作空间天然连续,DQN 处理连续动作很不方便,而 DDPG 用 actor-critic 结构可以直接输出连续动作值。

在 DDPG 框架下,Q 函数接收状态、动作、目标三个输入,策略网络同样要把目标拼接进输入。训练时,从回放缓冲区随机采一个 batch,里面既有原始目标经验,也有重标记目标经验。TD 误差的计算、目标网络的软更新、动作噪声的添加,都跟标准 DDPG 保持一致。一个值得注意的地方是:HER 对探索策略有更高要求。如果动作噪声过小,智能体永远只访问一小部分状态,那么即使重标记目标来自真实状态,这些状态也缺乏多样性,重标记的价值会大打折扣。

我通常会先把探索噪声调得比纯 DDPG 时稍大一些,比如高斯噪声标准差 0.2,确保初始阶段目标位置周围和机械臂可达范围内都有足够的访问覆盖。后面随着训练推进,再按 DDPG 常见的做法慢慢降低噪声。

4. 从零复现:HER 的实现要点

4.1 环境与依赖准备

用 Python + PyTorch 就足够实现完整流程。环境方面,OpenAI Gym 的 Fetch 系列是测试 HER 的标准场所,拿 FetchPush 练手最合适,任务难度适中,收敛速度比 FetchPickAndPlace 快很多。

需要准备的依赖包括:PyTorch、numpy、gym 以及对应的 mujoco 物理引擎相关绑定。安装好环境后,第一步是读懂 observation space 的结构。Fetch 环境的观测字典里包含 observation、achieved_goal、desired_goal 三个字段。observation 描述机械臂本身的关节角度、末端位置等;achieved_goal 是当前实际达成的位置;desired_goal 是希望达成的目标位置。

我们在构造网络输入时,把 observation 和 desired_goal 拼接起来作为网络输入,把 achieved_goal 用于奖励函数计算。一个常见的误区是把 achieved_goal 也拼进网络输入,但目标重标记之后,achieved_goal 实际上是不断变化的,把它作为环境状态的一部分会导致训练过程不稳定,建议按照 HER 论文的标准做法来组织数据。

4.2 核心数据结构、重标记逻辑代码

我直接放一段简化但完整表达核心逻辑的代码。为了便于理解,省略了部分网络结构细节,保留了 HER 最关键的存储和重标记过程。

import numpy as np class HERBuffer: def __init__(self, capacity, k_future=4): self.capacity = capacity self.k_future = k_future self.storage = [] def store(self, episode): length = len(episode['obs']) # 1. 保存原始目标经验 for t in range(length): self.storage.append(( np.concatenate([episode['obs'][t], episode['goal']]), episode['act'][t], episode['rew'][t], np.concatenate([episode['obs_next'][t], episode['goal']]) )) # 2. 目标重标记:future 策略 for t in range(length): future_states = episode['obs_next'][t:] if len(future_states) == 0: continue for _ in range(self.k_future): g_prime = future_states[np.random.randint(len(future_states))] r_prime = compute_reward(episode['obs_next'][t], g_prime) self.storage.append(( np.concatenate([episode['obs'][t], g_prime]), episode['act'][t], r_prime, np.concatenate([episode['obs_next'][t], g_prime]) )) def sample(self, batch_size): idxs = np.random.randint(0, len(self.storage), size=batch_size) return [self.storage[i] for i in idxs]

这里的关键点在于future_states = episode['obs_next'][t:],也就是从 t 时刻到 episode 末尾的状态中随机采样,保证替代目标一定在时间上是“未来”的。compute_reward通常实现为:当状态与目标的欧氏距离小于阈值时返回 1,否则返回 0。如果环境本身提供了 reward 计算函数,重标记后也必须重新调用,不能直接复用原始 reward。

存储结构上最常见的问题是混用 list 和 numpy array 导致速度瓶颈。episode 数量大时,每次 append 都做拼接和计算,性能压力不小。实际项目里可以用固定大小环形缓冲区,把四元组改成 numpy 数组批量存储,训练前的采样效率能提升好几个量级。

4.3 训练流程与超参配置参考

接下来是完整训练循环。

for epoch in range(total_epochs): for episode_idx in range(num_episodes_per_epoch): goal = env.sample_goal() obs = env.reset() episode = collect_trajectory(env, actor, obs, goal) her_buffer.store(episode) for step in range(num_train_steps): batch = her_buffer.sample(batch_size) update_critic_and_actor(batch)

这里 collect_trajectory 会调用 actor 网络生成动作,并添加探索噪声与环境交互,记录完整轨迹。训练步里更新 Q 网络和策略网络的方式,和标准 DDPG 完全一致,唯一区别是输入向量里拼了目标。

参考超参我直接列一个在 FetchPush 上相对稳定的组合:

  • 网络结构:两层 256 隐藏层 MLP,ReLU 激活。
  • batch size:128 到 512。
  • 训练频率:每个 epoch 每存储一条轨迹后做 40 到 80 次梯度更新。
  • 动作噪声:高斯噪声 std=0.2,或者 OU 噪声。
  • 学习率:critic 和 actor 都从 1e-3 开始,也可尝试 3e-4。
  • target 网络软更新参数 tau:0.05。
  • 折扣因子 gamma:0.98。

这些参数不是标准答案,只是在我的实验里比较稳定的一组起点。策略是:先固定一组能工作的参数,跑通后再逐个调整 k、噪声、更新次数,不要一上来就让所有参数同时漂移。

效果观察主要看两个指标:平均 episode 长度和 rollout 成功率。HER 在 FetchPush 上通常几十个 epoch 内就能看到成功率明显上升,我自己的随机种子下大约 30 到 50 个 epoch 就能达到 80% 以上成功率。如果到了 50 个 epoch 还没动静,大概率是代码有 bug 或者超参不合适。

5. 常见问题与调试经验

5.1 加入 HER 后曲线仍然平的

这是最让人头疼的情况。常见原因有三个:

  • 缓冲区里没有足够多样性的经验。替代目标虽然来自真实状态,但如果动作噪声太小,智能体来来回回只访问一小片区域,重标记目标的覆盖范围有限,奖励照样稀疏。
  • 替代目标采样策略写错,误用了 random 而不是 future。这个 bug 很难发现,因为程序不报错,训练也能跑,就是效率差很多。
  • 网络输入拼接维度不一致。UVFA 要求目标与观测拼接,但很多新手把 desired_goal 和 achieved_goal 搞混,输入维度对不上,训练能跑但持续不振。

排查方式很简单:打印几条重标记后的样本,人工检查四元组是否自洽。把 ( (s_t|g', a_t, r't, s{t+1}|g') ) 拿出来,手动算一下 r'_t 是否符合定义。这一步能定位 80% 以上的实现问题。我还习惯在训练前写一个单元测试:构造一个只包含单步转移的简单 episode,检查 buffer 是否生成了预期数量的重标记样本。

5.2 重标记的比例和发送时机

我之前专门做过一次小实验对比 k=1 和 k=4 的效果。在 FetchPush 上,k=4 的收敛速度明显领先。重标记经验并不是越多越好。缓冲区里重标记经验过多,Q 函数对“真实目标”的估计可能失去精度,导致策略在真实目标上的表现变差。

比较稳的做法是:原始经验全部保留,重标记经验按 future 策略每个时间步生成 k 个。这样一个 50 步的 episode 会产生 50 条原始经验 + 50×k 条重标记经验,重标记经验占比天然高于原始经验。实际训练效果依然很好,说明这个比例是可行的。如果训练后期出现 Q 值震荡,可以尝试把 k 调小到 2,重新观察稳定性。

重标记发送的时机也很重要。HER 的标准实现里,一个 episode 结束后立即完成重标记并存入缓冲区,而不是等到后续某个时刻。这样能保证重标记样本与最新策略的分布一致性。如果延迟太多轮次再重放,旧轨迹对应的是旧策略行为,新策略可能已经不会访问同样状态了,样本的有效性会降低。

5.3 HER 与 PER、策略噪声的组合坑

很多复现项目会把 HER 和 Prioritized Experience Replay(PER)叠加,理论上两者确实互补:PER 关注“哪些经验值得多学”,HER 关注“如何产生更有信号的经验”。但实际操作中,PER 的优先级要基于 TD 误差计算,而重标记目标下的 TD 误差与原始目标下的 TD 误差差异很大。如果每次采样后优先级变化剧烈,训练会非常不稳定。

我的建议很直接:先用朴素均匀采样把 HER 跑通,再加入 PER。如果一定要同时用,PER 的 exponent(即 α)从 0.4 起步而不是常用的 0.6,beta 退火也要更慢一些。对重标记经验,优先级计算时一定要使用重标记后的 Q 值和 reward,不要和原始目标的数据混用,否则优先级会把训练引入错误的方向。

策略噪声方面,DDPG 常用的做法是给目标策略输出加上截断的高斯噪声。HER 环境下,这个噪声的标准差可以适当放大到 0.2 至 0.3,主要目的不是提高最终的策略精度,而是保证探索阶段状态覆盖足够广。很多用户一上来就用 0.1 的噪声,结果发现探索不足,HER 的效果完全发挥不出来。

6. 个人体会与后续扩展

6.1 我用 HER 的感受和踩过的坑

从开始读论文到完全跑通 FetchPush,我中间卡了大概两周,最大的坑是一个非常低级的细节:重标记后的 reward 忘记重新计算,导致 Q 值几乎没有任何起伏。还有一个比较反直觉的体验是,HER 的收敛曲线经常不是光滑上升的,而是突然跳变。原因在于重标记经验的数量在某个 epoch 后突然达到足够密度,Q 函数对目标的泛化能力在临界点被激活。所以看到平缓甚至下降的曲线,不用急着放弃,先把噪声调大、把 k 固定到 4,再给算法更多 epoch。

我的体感是,HER 已经可以算作多目标稀疏奖励问题的“默认解法”,但如果探索质量上不去,它的上限也会受限。想在现有任务上继续加速,可以考虑从状态采样方式入手,比如用 density model 选更有信息量的状态作为替代目标,或者结合模型预测控制来引导探索。

6.2 适合进一步尝试的方向

  • HER + Curiosity / ICM:让智能体优先访问新奇状态,补充重标记目标的多样性。
  • Hierarchical HER:在高层规划和底层动作之间同时做重标记,适合时间跨度更大的任务。
  • 离线 RL 场景下的 HER:把历史失败数据通过重标记转化为有效数据集,这个方向在机器人学演示数据集上已经看到了不少成果。
  • 把 HER 思想扩展到语言目标:将目标从状态向量换成自然语言描述,重标记时用语言模型生成事后的指令。

最后分享一个小技巧:如果要在新任务上快速验证 HER 实现是否正确,可以先挑一个目标奖励很容易达成的简单任务,比如只有两个离散目标点的小环境。如果在这个简单场景里 HER 都不能加速学习,那问题大概率出在代码而不是环境复杂度。等简单场景跑通,再切到 Fetch 系列或者真实任务,心里就有底了。

返回列表