强化学习中的HER算法:事后经验回放破解稀疏奖励难题 hindsight直译过来就是“后见之明”我这次手头一个项目偏偏就起了这么个名字。说实话最初看到这个名字我第一反应是“数据处理里的事后镜像”但真正做完以后我发现它其实是强化学习里一个极具启发性的算法代称——Hindsight Experience Replay事后经验回放简称 HER。这个项目主要是复现并改造 HER用一套稀疏奖励的机械臂推方块任务验证了算法的效果。如果你正被“智能体怎么都拿不到第一次正奖励”这种问题折磨或者翻来覆去调参仍看着 loss 纹丝不动这篇东西应该能帮到你。1. 项目思路拆解为什么一个“事后诸葛亮”能训练好智能体1.1 稀疏奖励到底难在哪强化学习的核心信号就是奖励但现实中很多任务压根不是每一步都给反馈的。以机械臂推方块到指定坐标为例只有方块和目标点的距离小于某个阈值比如 5 厘米才会得到正向奖励其他时候奖励要么是 0要么是 -1。这种设计很常见毕竟真实世界的抓取任务不可能每一步都有人告诉你“你距离目标又近了 0.3 毫米”。问题在于稀疏奖励让整个探索过程变成大海捞针。智能体一开始完全是随机动作它几乎不可能恰好把方块推到目标位置。于是会出现一种让人崩溃的状态训练了几万步replay buffer 里存的 transition 全部是负奖励或者零奖励价值网络根本学不到任何梯度信号策略也永远在原地打转。这不是调参能解决的问题而是学习信号本身缺失了。1.2 “事后”经验的魔法HER 的核心直觉HER 提出的出发点特别简单一次任务失败了预设目标没达成但这段轨迹里智能体其实经过了无数个“可达状态”。假设任务目标是挪到 (1, 1)结果方块最后停在 (0.7, 0.7)那这段回合是不是也没用HER 说别浪费把这次轨迹的目标“重标”成 (0.7, 0.7)这条轨迹立刻就从“失败轨迹”变成“成功轨迹”了——因为方块最后恰好停在了目标点上。这就像一场考试你原本想拿满分最后只考了 70 分。表面上是失败但如果你重新定义“目标就是及格线 60 分”那这次考试就是一次成功经验它能告诉你哪些复习策略是有效的。HER 就是把“失败”轨迹换个视角变成“正样本”再塞回经验回放池里去训练。我在项目里第一次看到这个思路时有一种“这么简单怎么没人早想到”的感觉。这个算法的提出者是 OpenAI 团队2017 年左右放出来随后被广泛用到机器人操作、导航这类目标达成任务中。它不需要修改网络结构不需要改奖励函数只需要在数据采集后做一步重标注就能显著提升稀疏奖励场景下的收敛能力。1.3 项目选型为什么是 DDPG 搭配 HERHER 本身不是一个独立的训练算法它是一种“经验包装技术”必须配合 off-policy 强化学习算法使用。这里有个根本原因HER 的核心操作是改 reward、改 goal然后把修改后的 transition 放入 replay buffer。如果是 on-policy 算法比如 PPO、TRPO样本用完一次就要丢掉回放池根本留不下事后重标的数据HER 就没有施展空间。因此项目基座选择了 DDPGDeep Deterministic Policy Gradient。DDPG 本身是确定性策略的 actor-critic 结构天然带一个经验回放池和 HER 的组合几乎零嫁接成本。后来的 TD3、SAC 也能和 HER 配合但 DDPG 实现简单、参数少更适合用来验证 HER 的思想。我当时也考虑过直接用 SAC但被“歇斯底里”的超参数劝退了DDPG 做实验最省心线程模型简单训练流程里任何一个时刻插进去重标记逻辑都不违和。实际效果也印证这个选择没错。同样的二维点导航任务普通 DDPG 跑了 200 个 episode成功率始终是 0%加上 HER 之后大约 120 个 episode 左右就开始出现连续成功的轨迹。有了这个直观对比后面的实验就顺畅了。2. HER 算法核心细节与实现要点2.1 目标重标注的具体操作HER 的算法流程不是“整个 episode 全改目标”而是有策略地重标。它先正常采样一个 episode存下状态序列、动作序列、奖励序列。episode 结束后针对每条 transition按照一定概率选择一个“替代目标” g然后用 (s_t, a_t, s_{t1}, g) 这四个元素重新算一条 transition再塞进 replay buffer。这里有个容易被忽略的点重标注时不仅要替换 goal还要把 reward 一并重算。原轨迹里奖励可能是 0 或 -1但换了一个更近的目标后这个动作轨迹可能正好达成了新目标奖励就变成 1。如果不重算奖励等于告诉智能体“这一步动作很差”但实际上在新目标定义下这步动作就是完美的信号就错了。伪代码层面我项目里核心的重标过程可以写成这样def relabel_transitions(episode, k4): episode: dict, 包含 obs, act, next_obs, 以及原始 goal k: 为每条 transition 采样 k 个 future state 作为新目标 relabeled [] horizon len(episode[obs]) for t in range(horizon): original_transition { obs: episode[obs][t], act: episode[act][t], next_obs: episode[next_obs][t], goal: episode[goal], } # 保留原始目标不能全改成替代目标 relabeled.append(original_transition) # 以一定概率从 future 状态中采样新目标 if np.random.rand() 0.5: # 从 t 之后的某个时刻随机挑 k 个未来状态作为新目标 future_indices np.random.randint(t, horizon, sizek) for future_idx in future_indices: alt_goal episode[next_obs][future_idx] new_reward compute_reward(episode[next_obs][t], alt_goal) trans { obs: episode[obs][t], act: episode[act][t], next_obs: episode[next_obs][t], goal: alt_goal, reward: new_reward, } relabeled.append(trans) return relabeled批注一下compute_reward 必须和真实环境的奖励函数保持一致只是传入的目标变成了替代目标。很多新手在这里会踩坑奖励函数里写死了“目标必须在 episode 最开始设定”重标注后所有奖励都不对训练直接崩。2.2 三种采样策略future、final 和 episodeHER 的原始论文里给了三种替代目标的采样方式项目里我应该把每一种都测一遍才能体会它们的差异。策略替代目标来源特点适用场景final整个 episode 的最后一个状态最简单方差较大如果最后状态距离较远重标意义有限计算资源少、任务 horizon 较短的场景future当前时刻 t 之后的某个状态目标总是落在“当前轨迹还未到达但即将可能到达”的位置和后续状态有因果关系经验利用率高大多数机器人控制任务episode从整个 episode 里均匀随机挑一个状态目标可包含轨迹早期状态方差适中多样性好需要更多探索多样性的任务实测下来final 策略实现最简单但效果不稳定因为很多 episode 的末状态可能是“卡在墙边不动”的无效状态拿它做目标反而让策略学到一堆垃圾。episode 策略随机性太强有时候会选中一个非常远的状态相当于重标后还是负样本。我最终选的是 future 策略也就是上面代码里写的从 t 到 horizon 之间采样 K 个状态。论文里 K 一般取 4每条 transition 以 0.5 的概率触发重标。这个参数不用调得太精细基本能覆盖常见场景。有一点要注意future 策略要求状态具备可达性假设也就是智能体从某个时刻开始有能力达到 t 之后出现的状态在仿真环境里这个假设基本成立。2.3 网络输入与训练细节DDPG HER 不是简单地把 goal 作为一个外部变量塞进网络就行具体的输入拼接方式会影响训练结果。我的做法是策略网络Actor输入是 (state, goal) 拼接后的向量输出是动作价值网络Critic输入是 (state, goal, action) 拼接后的向量输出一个 Q 值。这里有个不太能忽视的细节状态和目标的特征尺度要统一。很多仿真环境里状态是三维坐标 (x, y, z)范围在 [-1, 1] 之间但有些自定义环境里 goal 可能是角度、速度或者拼了别的量纲直接拼接会导致网络对某一维度过度敏感。我在项目里给状态和目标分别做了一次 normalization统一映射到 [-1, 1] 区间训练稳定性明显提高。另外一个经验是DDPG 本身的探索噪声不能省。常见做法是在动作上加高斯噪声或者用 Ornstein-Uhlenbeck 噪声。HER 能够增加样本多样性但如果没有探索噪声智能体永远走同一条路径重标目标也只是反复看到相似的状态效果大打折扣。我的项目里使用标准差逐渐衰减的高斯噪声从 0.2 线性降到 0.02比固定噪声效果更好。3. 实操复现从搭建环境到训练收敛3.1 环境准备与安装如果要复现论文级的实验最经典的选择是 OpenAI Gym 的 Robotics 系列环境比如 FetchReach、FetchPush、FetchPickAndPlace。但这类环境依赖 MuJoCo 物理引擎安装有点麻烦。我的建议是第一轮跑通整个算法流程直接用自定义的二维点到达任务就够了省去物理引擎的安装折腾还能把注意力全部集中在 HER 的重标逻辑上。我项目里定义了一个简化环境一个二维平面上有 100 个随机位置的目标点智能体是一个质点动作是二维速度每步更新一次位置。任务目标是让质点与目标位置的距离小于 0.05否则奖励为 0。这个环境 100 行以内的 numpy 代码就能写完训练速度快适合第一时间验证 HER 的实现正确性。安装依赖也简单一个 conda 环境就够了conda create -n hindsight python3.9 conda activate hindsight pip install numpy torch gymnasium matplotlib如果后续要挑战 Fetch 系列可以考虑用 gymnasium-robotics 配合新版 MuJoCo安装包命令是pip install gymnasium-robotics用 MuJoCo 之前要注意需要下载对应版本的 mujoco-py 或者直接安装 mujoco 的 Python 绑定版本匹配问题容易让人心态爆炸。我建议第一轮项目先忍住把二维环境玩明白再说。3.2 关键超参数配置表HER 算法很神奇的一点是它对超参数的要求并不苛刻但还是有几个值会直接影响结果。我整理了一份自己实测用的配置方便抄作业参数名取值说明replay buffer 容量1,000,000足够大否则未来状态采样很容易覆盖掉旧经验batch size256不宜太小重标后的样本多样性需要 batch 撑起来actor/critic 学习率1e-3使用 Adam 优化器折扣因子 gamma0.98任务长度 50 步gamma 不用太接近 1Kfuture 采样数4每条 transition 生成 4 个新目标样本重标概率0.5触发重标的概率过高会让原始目标占比偏低目标网络 soft update tau0.05DDPG 常用 0.001-0.005这里实际项目里我用了 0.05收敛更快训练步数上限2e5二维任务大概 10 分钟内能看到效果每 episode 最大长度50太短导致任务无法完成太长导致训练变慢这份配置在二维环境里效果很明显。如果换成 FetchPush 这类机械臂任务K 可以保持 4buffer 容量越大越好训练步数要乘以 10 起步。不要照搬论文里的所有参数论文是为了泛化对比实际项目要根据任务长度和状态维度调整。3.3 训练主循环与 HER 重标注代码实现训练主循环最需要注意的一点HER 重标不是实时进行的而是在一个 episode 结束后统一处理。如果你的代码里每采样一条 transition 就把 state 塞进 replay buffer那就无法做重标了因为你需要知道 episode 后续的所有状态才能用 future 策略。所以正确做法是先开一个临时列表收集当前 episode 数据结束后再统一清空进入 buffer。我项目里的训练主循环大概长这样# 伪代码 memory ReplayBuffer(capacity1000000) for episode in range(2000): obs env.reset() episode_buffer [] for t in range(max_episode_steps): goal obs[goal] action actor.select_action(obs[observation], goal) exploration_noise next_obs, reward, done, _ env.step(action) episode_buffer.append({ obs: obs[observation], act: action, next_obs: next_obs[observation], goal: goal, reward: reward, }) obs next_obs if done: break # episode 结束后先保存原始 transition for trans in episode_buffer: memory.store(trans[obs], trans[act], trans[rew], trans[next_obs], trans[goal]) # 这里注意episode 的临时 dict 需要在最后一并重标 # 重标逻辑 relabeled_buffer relabel_episode(episode_buffer, k4) for trans in relabeled_buffer: memory.store(trans[obs], trans[act], trans[rew], trans[next_obs], trans[goal]) # 从 memory 里采样 batch 训练 DDPG for _ in range(40): # 每 episode 训练 40 次 batch memory.sample(batch_size256) ddpg_update(batch)这里有个很多人容易犯的错误以为只要 replay buffer 里“有”重标后的 transition 就行于是把所有原始 transition 重标一遍。我建议原始目标和替代目标之间保持 50% 以上的占比原始目标能帮策略记住真正的任务分布替代目标则是用来填补稀疏奖励的共享梯度。全改成替代目标智能体确实会学会“如何到达某个状态”但它会忘掉“如何到达预设目标”。还有个工程细节ReplayBuffer 的容量是按“transition状态转移”计数的不是按 episode 计数。HER 的 future 策略里一次重标会生成 K 个新 transitionbuffer 很容易被重标样本塞满。我实测后建议buffer 里原始样本和重标样本的比例控制在 1:1 左右比较合理如果感觉重标样本太多可以降低重标触发概率到 0.3。4. 训练中的坑与排查实录4.1 训练不收敛、奖励一直为 0先说最常见的现象loss 在下降但成功率一直是 0%智能体就像无头苍蝇。这种时候先别怀疑 HER 没用先去检查两件事。一是替代目标的范围是否合理。我犯过一次低级错误自定义环境里目标坐标是无界的重标时会选中一个极其远的 future state相当于给智能体定义了一个“永远完不成的目标”导致所有重标样本依然是负样本HER 彻底失效。解决方法是把目标范围限制在 episode 实际可达的区域内或者更保险的方法是只从 next_obs 序列中采样而不是从整个状态空间采样。二是检查 reward 计算函数是否把 nxt_obs 和 goal 给组合对了。HER 的 reward 必须是基于“当前 next_obs 与替代 goal 的距离”来计算的如果你写成了“当前 obs 距离替代 goal”那每一步的奖励都滞后一个动作周期策略学到的是错误的时序关系。这两类问题都是硬编码细节错误比算法问题更隐蔽半天排查不出来很正常。4.2 重标注目标太多导致策略“精神分裂”另一个典型情况是训练前期还行后期成功率停滞甚至倒退。我观察下来这通常是重标目标占总样本比例过高导致的。原因很好理解如果 replay buffer 里大部分样本都是“来自轨迹不同位置的目标”那策略会被迫去迎合一堆没出现过的目标。今天学的是去轨迹中段的位置明天学的是去末位置甚至同一个状态对应了好几个不同 goal 的样本actor 的梯度方向被拉扯得乱七八糟。解决办法有几招实测下来都有效第一降低重标概率从 0.5 降到 0.3优先保证原始目标的样本量第二减少 K 值K2 就已经能满足大部分需求K4 适合状态空间特别大的任务第三在同一个 batch 中刻意保证原始目标和替代目标的比例是 1:1而不是随机采样。第三招在工程上最直接但不回归复现论文实验的话第一招最省事。实际上我在项目里调好之后K4 和 0.5 的重标概率还是能稳定跑但前提是 batch size 足够大256让每个 batch 里原始目标不至于被淹没。如果 batch 只有 64重标样本占比过高的问题会立刻暴露。4.3 训练速度慢、显存占用异常很多人忽略 HER 对内存的消耗。一条原始轨迹经过重标后会变成 1 K 条 transitionK4 意味着数据量直接扩大了 5 倍。如果 replay buffer 容量开 100 万实际存储的物理数据可能是 500 万条以上。我之前在机器上跑 FetchPickAndPlacebuffer 没存多少 episode 内存就快满了训练周期被磁盘交换拖慢。解决方式是用“episode 级存储”代替“transition 级存储”。也就是说replay buffer 只存整个 episode 的序列训练时再从 episode 里动态采样、动态重标。这样做的好处很明显原始数据和重标目标是惰性生成的内存占用只与 episode 数量挂钩而且每次训练时还能随机生成不同的重标目标样本多样性反而更高。代价是实现复杂度高一点但收益非常可观。我项目后期就是用这种实现才把机械臂环境的训练时间压到了可接受范围。这里还涉及到另一个调试小技巧——观察 replay buffer 里重标样本的 reward 分布。如果发现大量重标样本的 reward 都是 0也就是“成功”说明替代目标太容易了策略可能只学到“投机取巧”的方式。反之如果绝大多数 reward 还是 -1说明替代目标选得太刁钻需要往 final 策略靠一靠。把 reward 分布打印出来是诊断 HER 是否正常工作最直观的手段。4.4 一份基于实操的调参速查表最后整理一份我踩坑后调好的“问题-解法”对照表希望能让你少走弯路。症状可能原因项目中的调法成功率一直为 0reward 恒为 0目标范围过大或 reward 计算错误限制目标范围检查 reward 使用的转移下标训练初期正常后期波动大重标样本占缓冲过高降低重标概率到 0.3或降低 K 值策略只向“一个方向”走探索噪声太小提高初始噪声幅值或使用 OU 噪声样本收集慢、buffer 满transition 级存储扩展了 5 倍改 episode 级存储训练时动态重标替代目标全在轨迹末端future 策略采样偏向晚期改为从完整 episode 均匀采样或混合 final 策略两个目标相距极远训练震荡状态归一化缺失对 state 和 goal 分别做标准化写在最后我的两点体会项目做到后期我最大的感悟是HER 这个算法真正厉害的地方不在于“事后重标”这个技巧本身而在于它揭示了奖励信号缺失场景下的一种通用思考方式——不要只盯着预设目标要善于从已有轨迹里“重新定义目标”。我在跑通二维环境后甚至尝试把 HER 用在了超参搜索的日志分析里把每一次失败的训练试错都看作一次“重标后的经验”结果帮我排除掉了好几个无效超参区间。最后再分享一个小技巧就算你不做强化学习把 HER 的“重标思想”用在日常工作复盘里也特别有意思。每次任务没达到预期目标先别急着否定整个过程问一句“这次经验如果换一个评价标准哪些环节其实是成功的”往往能挖出不少被忽略的增量价值。这也是我为什么愿意把这个项目命名的含义写下来——hindsight 不只是算法名更是一种值得迁移的思维习惯。