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_{t1} | g) )其中 ( r_t ) 表示在目标 g 下、执行动作 a_t 后到达状态 s_{t1} 的奖励。HER 的流程分五步采样一个真实目标 g。智能体按照条件策略 ( \pi(a|s,g) ) 执行一整轮 episode记录完整轨迹。把原始目标经验存进缓冲区即使奖励全为 0。从轨迹中选替代目标 g最常用的是从未来状态中采样。针对 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 时第一步是把“状态”和“目标”统一编码。UVFAUniversal 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 策略 k4。这个值很有讲究k 太小重标记经验比例不够稀疏奖励问题依然突出k 太大缓冲区里重标记经验占比过高原始目标对应的 reward 分布会被淹没策略可能过度拟合“事后目标”对真实目标的敏感度下降。k4 是论文在 Fetch 系列环境上实验出来的经验值不同任务可以微调。我建议先固定 k4 跑通流程之后做一次小范围网格搜索比较 k1、2、4、8。从我的实践看这个超参对收敛速度的影响相当明显在 FetchPush 上 k4 和 k1 的收敛速度能差出约 30% 的 epoch。这里说的不是最终成功率差异而是达到相同成功率所需交互次数HER 的主要价值本来就体现在采样效率上。替代目标采样的具体范围也要注意。future 策略要求从 ( t1 ) 到 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_future4): 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), sizebatch_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 隐藏层 MLPReLU 激活。batch size128 到 512。训练频率每个 epoch 每存储一条轨迹后做 40 到 80 次梯度更新。动作噪声高斯噪声 std0.2或者 OU 噪声。学习率critic 和 actor 都从 1e-3 开始也可尝试 3e-4。target 网络软更新参数 tau0.05。折扣因子 gamma0.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, rt, s{t1}|g) ) 拿出来手动算一下 r_t 是否符合定义。这一步能定位 80% 以上的实现问题。我还习惯在训练前写一个单元测试构造一个只包含单步转移的简单 episode检查 buffer 是否生成了预期数量的重标记样本。5.2 重标记的比例和发送时机我之前专门做过一次小实验对比 k1 和 k4 的效果。在 FetchPush 上k4 的收敛速度明显领先。重标记经验并不是越多越好。缓冲区里重标记经验过多Q 函数对“真实目标”的估计可能失去精度导致策略在真实目标上的表现变差。比较稳的做法是原始经验全部保留重标记经验按 future 策略每个时间步生成 k 个。这样一个 50 步的 episode 会产生 50 条原始经验 50×k 条重标记经验重标记经验占比天然高于原始经验。实际训练效果依然很好说明这个比例是可行的。如果训练后期出现 Q 值震荡可以尝试把 k 调小到 2重新观察稳定性。重标记发送的时机也很重要。HER 的标准实现里一个 episode 结束后立即完成重标记并存入缓冲区而不是等到后续某个时刻。这样能保证重标记样本与最新策略的分布一致性。如果延迟太多轮次再重放旧轨迹对应的是旧策略行为新策略可能已经不会访问同样状态了样本的有效性会降低。5.3 HER 与 PER、策略噪声的组合坑很多复现项目会把 HER 和 Prioritized Experience ReplayPER叠加理论上两者确实互补PER 关注“哪些经验值得多学”HER 关注“如何产生更有信号的经验”。但实际操作中PER 的优先级要基于 TD 误差计算而重标记目标下的 TD 误差与原始目标下的 TD 误差差异很大。如果每次采样后优先级变化剧烈训练会非常不稳定。我的建议很直接先用朴素均匀采样把 HER 跑通再加入 PER。如果一定要同时用PER 的 exponent即 α从 0.4 起步而不是常用的 0.6beta 退火也要更慢一些。对重标记经验优先级计算时一定要使用重标记后的 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 系列或者真实任务心里就有底了。