
跑过 DDPG、PPO 这类深度强化学习算法的人大概都体验过一种让人特别绝望的场景环境配置好了网络结构搭好了超参数也参考别人的仓库调过一轮了训练跑了一整晚第二天早上打开 tensorboard 一看——reward 曲线像一条做心电图失败的直线从头到尾趴在初始值附近一动不动十几万步的探索毫无学习信号。这时候你大概率会怀疑环境、怀疑代码、怀疑显卡驱动、甚至怀疑人生。但问题往往不在这些地方而在一个更根本的设定上你给的这个任务奖励稀疏到 agent 根本不知道自己离成功近了一步。我这边说的 hindsight就是专门解决这个问题的思路。它在强化学习里对应的是一个非常知名的方法Hindsight Experience Replay后见经验回放简称 HER。这套方法 2017 年由 OpenAI 团队提出核心思想特别朴素把失败的轨迹重新解释为对另一个目标的成功轨迹。听起来像“事后诸葛亮”但这恰恰是把稀疏奖励任务变成稠密奖励任务的关键切入点。这篇内容会从问题本身讲起把 HER 的原理、代码实现、踩坑经验和适用边界全部过一遍适合正在被稀疏奖励折磨、或者在调研目标条件强化学习方案的读者参考。1. 为什么需要 hindsight从“永远学不会”的任务说起1.1 稀疏奖励是强化学习的死穴先把问题具象化。想象一个七自由度的机械臂任务是把桌面上的杯子从位置 A 推到位置 B。你给环境的奖励函数是杯子中心点距离目标位置小于 5 厘米给 1否则给 0。听起来很合理对吧但跑起来就发现问题了机械臂从初始姿态开始做随机探索绝大多数动作产生的位移幅度都很小一百步之内杯子可能根本没怎么动杯子和目标位置之间的距离可能一直停留在初始的 30 厘米开外。于是每一个 episode 的累积奖励都是 0每一个 transition 的即时奖励也都是 0。这时候策略网络的梯度更新靠的是什么靠的是 TD 误差而 TD 误差的来源就是即时奖励和自举估计的 Q 值。如果一个 batch 里全都是 r0 的数据critic 网络很快会把所有状态的 Q 值都预测成接近 0actor 网络拿到的梯度就变成一片噪声或者干脆趋近于 0。你加大探索噪声、调大学习率、换网络宽度都没有用因为问题不在拟合能力在于整个经验池里完全没有“正反馈”样本可供学习。有人会想那我把奖励做成连续的比如负的欧氏距离这样每次机械臂靠近一点就有奖励变化了。这个思路没错但属于“人为制造稠密奖励”它要求你对任务有足够的领域知识去设计一个理想的势函数。在很多真实场景里你根本不知道什么样的中间状态值得奖励设计得不好还会诱导 agent 钻奖励函数的空子。所以 HER 的思路是不改变奖励函数的定义而是换一种方式制造正样本。1.2 hindsight 的核心思想事后重新解释目标HER 的思想源头特别直观就是人类在学习和复盘时天然具备的一种能力。篮球运动员投篮没进但球碰到了篮板他虽然没达成“进球”这个目标但可以把这个结果重新解释为“我完成了触碰篮板这个子目标”下次训练时就会对如何控制球的飞行弧线多一点把握。你不会因为一次投篮没进就否定整个动作的全部信息你会从失败里提取出有用的部分。把这个逻辑搬进强化学习HER 的做法是这样的在一个 episode 中agent 本来被给定了一个目标 g例如“把杯子推到位置 B”。但它整个 episode 下来都没有达成 g最终杯子停在了位置 B。正常情况下这个 episode 里所有 transition 的奖励都是 0直接丢进回放缓冲区就是一堆垃圾数据。HER 的做法是把“杯子最终到达的位置 B”重新定义为一个目标 g然后把这整个 episode 的所有 transition 都改写成“目标为 g 的轨迹”。因为杯子最后确实停在 B所以这段轨迹在目标 g 下就是一段成功轨迹最后一步的奖励是 1前面的每一步距离目标 B 也越来越近天然附带了一段稠密的“接近奖励”信号。注意这里面没有任何伪造或者欺骗的意思。agent 确实完成了“让杯子到达 B”这件事只是这件事不是它原本被要求的任务。HER 只是发现了这些真实发生过的成功并把它们放进经验池里作为学习素材。这就是“hindsight”这个名字的真正含义用后见之明把失败重新解释为成功。1.3 HER 解决什么、不解决什么HER 解决的是目标条件强化学习goal-conditioned RL下的稀疏奖励问题。它的前提条件是任务必须能用一个明确的目标状态来描述并且环境能给出这个目标的观测。比如机械臂抓取、机器人导航、迷宫寻路、推箱子这些都可以。不适用的情况也很明显如果你只有一个固定目标而且随机探索根本无法产生任何与目标中间状态相关的信息HER 也无能为力因为它需要从实际到达的状态里“取材”来生成新目标。另外HER 不解决探索的终极难题比如 Montezumas Revenge 那种需要发现特定隐藏房间才能推进的任务HER 没有内在机制帮你发现那个从没见过的状态。所以你可以把 HER 理解成一把专门打开“稀疏奖励 多目标”这把锁的钥匙它不是万能工具但在合适的锁上效果惊人。2. Hindsight Experience Replay 算法拆解2.1 算法全流程HER 的完整流程可以拆成四个大的阶段。第一步从任务的目标分布 p(g) 里采样一个目标 g作为这个 episode 的原始目标。第二步让 agent 在这个目标下和环境交互跑完整整个 episode记录下轨迹里的每一个状态、动作、奖励、下一状态、目标、任务完成标志。第三步把这条完整 episode 数据先存进一个临时的 episode 缓冲区。第四步是 HER 的关键动作从这个 episode 里按照一定的策略再选出若干替代目标 g以 g 对轨迹中的每一条 transition 重新解释目标、重新计算奖励和 done 标志然后把新旧两组 transition 都写进总体的回放缓冲区。之后的学习流程就和普通 off-policy 算法完全一致了。从回放缓冲区 sample 出一个 batch用 DDPG、DQN、SAC 之类的算法更新 actor 和 critic。区别只在于回放缓冲区里多了一批通过 hindsight 重标记出来的成功样本。这些样本虽然在原始目标下是“失败”但在重标记目标下是“成功”它们的奖励分布不再是全 0而是存在大量的小负数未完成的过渡步骤和少量 1最后一步的成功。这里有个重要的工程细节HER 的回放缓冲区必须按 episode 存储不能像普通 DDPG 那样逐 transition 地存储。原因很简单重标记需要知道整条轨迹的走向和最终到达的状态你只有在 episode 结束之后才能做 hindsight 操作。所以代码结构上通常分两层一个 episode 缓存用来收集当前这一条轨迹的全部数据一个 replay buffer 用来存放已经重标记好的 transition。2.2 四种目标重标记策略有了完整 episode 之后怎么挑选替代目标 g 是有讲究的。原论文里对比了四种策略我在实际复现的时候也挨个试过这里直接说结论和我的感受。第一种叫 final就是直接把 episode 结束时 agent 最终到达的状态当成替代目标。这个策略最简单但产生的监督信号最少因为只用了轨迹末端的一个状态作为目标大部分 transition 距离这个最终目标其实很远中间几乎没有逐步接近的引导。第二种叫 random就是从整个回放缓冲区里随机抽一个已出现过的状态作为替代目标这个策略的问题在于随机抽出来的目标可能和当前任务完全不相关难度忽高忽低学习效率一般。第三种叫 episode就是从当前 episode 内部随机抽一个在未来某一步会到达的状态作为替代目标。第四种叫 future采样方式和 episode 类似但有一个额外约束对轨迹里的每一个 transition t替代目标只能从 t 之后的时间步里随机抽取且要采样 k 个不同的目标。四种策略的对比结果在原论文里非常明确future 最好episode 次之random 和 final 明显偏弱。为什么 future 效果最好因为它的逻辑最符合逐步接近的学习过程对某个 transition在未来的状态里选目标意味着从这个 transition 出发后续的轨迹里有连续的段落是朝着这个目标前进的中间存在大量奖励逐渐增大的中间信号相当于天然构造了一段密集的“走向目标”的引导数据。而 episode 策略因为允许从过去的状态里采样目标可能出现目标在“身后”的倒车情况信号没那么连贯。策略采样范围信号质量我的复现感受final轨迹终点状态差只有末端有信号收敛慢适合简单任务random整个回放缓冲区差目标无关性强不稳定容易成绩波动episode当前 episode 内任意状态中有倒车风险效果一般不如 futurefuture当前 transition 之后的 k 个未来状态好连续引导推荐首选k4 比较稳2.3 为什么 HER 有效从奖励密度看本质从理论层面看HER 有效性的核心在于它改变了回放缓冲区里正样本的比例。在稀疏奖励任务里普通经验池的即时奖励分布是退化的一坨零TD 学习在这种情况下收敛到一个全零解的盆地。而 HER 通过重标记在缓冲区里人为制造了一批“目标可达”的成功样本它们的奖励存在层级变化这就让 critic 有机会学习到“状态距离目标越近Q 值越大”的连续映射关系。有了这个映射actor 的梯度更新就不再是零向量策略就有了明确的改进方向。换个角度理解HER 本质上是构建了一个自适应的课程学习机制。普通课程学习需要人手动排好任务的难度阶梯而 HER 自动选择 agent 实际能到达的状态作为目标这些目标天然分布在当前策略的可达域内。随着策略逐渐变强它到达的状态越来越接近真实目标范围重标记出来的新目标也随之向真实目标靠拢。目标分布和策略能力一起演进整个学习过程就有了自主升级的节奏。这也是为什么 HER 不需要额外设计课程表的原因。还有一个小细节值得注意HER 不只是给经验池增加成功样本它还改变了“目标”的边缘分布。原来的目标分布是任务给定的 p(g)可能非常集中且困难而 HER 重标记后的目标分布更多落到了 agent 的真实行为分布上。从多任务学习的角度看这相当于让 agent 在一个更均匀、更可达的目标空间里训练学会的是“从任意状态到达任意可达目标”的通用控制能力这种能力迁移到原本的目标上也更容易。3. 从零实现 HERPyTorch 加持的完整实操3.1 环境与基线选择我建议第一次接触 HER 时别一上来就上 MuJoCo 的 Fetch 系列虽然那是原论文的标准实验环境但配置成本高、渲染依赖多初学者容易被环境问题劝退。用 openai/gym 里自带的一个简单任务或者干脆自己写一个二维点机器人推球环境。我这里用的是 gym 里改造过的二维迷宫一个圆形的 agent 在二维平面上目标是一个固定坐标agent 每步可以输出 x、y 方向的推力观测包括自身位置和目标位置当欧氏距离小于 0.05 时视为成功并给 1 奖励。选择这个环境的原因一是维度低、训练快几万步就能看到明显效果二是状态空间完全可控方便打印日志检查重标记后的奖励数值三是它能非常直观地暴露“没有 HER 时永远学不会”的现象对比效果显著。基线就用 DDPG 加不加 HER 两个版本跑同一套超参对比成功率和学习曲线。3.2 核心数据结构与回放缓冲区实现先写基础的回放缓冲区这里直接给出核心代码逻辑。注意HER 版本的缓冲区存储的是“episode 分片”但为了方便随机采样我会在训练时再把完整 episode 展开成逐 transition 存储并保留每个 transition 所属的 episode 索引方便做 future 采样。import numpy as np from collections import deque class EpisodeBuffer: def __init__(self, capacity500): self.capacity capacity self.buffer deque(maxlencapacity) def push(self, episode): # episode: dict of lists, 包含 obs, action, reward, next_obs, goal, done self.buffer.append(episode) class HERReplayBuffer: def __init__(self, capacity1_000_000): self.capacity capacity self.obs [] self.next_obs [] self.actions [] self.rewards [] self.goals [] self.dones [] self.episode_id [] def add_transition(self, obs, action, reward, next_obs, goal, done, ep_id): # 存储时用原始 transition 先占位 self.obs.append(obs) self.actions.append(action) self.rewards.append(reward) self.next_obs.append(next_obs) self.goals.append(goal) self.dones.append(done) self.episode_id.append(ep_id) def relabel_and_store(self, ep, ep_id, achieved_goals, future_k4): # 原始 transition 已经存过了这里只做重标记并追加 T len(ep[obs]) for t in range(T): for _ in range(future_k): # future 策略: 从 t1 到 T-1 中随机采一个状态作为替代目标 future_idx np.random.randint(t 1, T) new_goal achieved_goals[future_idx] # 重算奖励 new_reward self.compute_reward(ep[next_obs][t], new_goal) new_done float(new_reward -0.05) # 阈值判断 # 直接把重标记后的 transition 写到主回放区 self.add_transition( ep[obs][t], ep[action][t], new_reward, ep[next_obs][t], new_goal, new_done, ep_id )这里有个容易写错的地方future 采样时np.random.randint(t 1, T)需要保证t 1 T也就是最后一步不参与 future 采样。我见过有人在最后一步报错后直接改成randint(t, T)这样就把“当前状态”本身作为目标了虽然不算致命错误但会让重标记后的最后一步奖励变成 1和原始节奏对不上会带来一点偏差。稳妥做法是只对t T - 1的 transition 做 future 额外重标记最后一步只用 final 策略补一条就行。3.3 目标重标记与奖励重算奖励重算是 HER 里最关键的细节。原始奖励函数基于真实目标 g重标记后目标变成了 g那么奖励必须用 g 重新计算否则就会出现“目标是 A奖励却按 B 算”的错位整个经验池的标签就是乱的。我的奖励函数用的是简单形式的二值奖励距离小于阈值给 0表示成功否则给 -1。负的常数奖励比全 0 要好一些因为它给 critic 提供了一定的“推进压力”让 Q 值能够区分不同距离的状态。def compute_reward(self, achieved_goal, desired_goal, threshold0.05): # 欧氏距离作为成功判据 dist np.linalg.norm(achieved_goal - desired_goal) if dist threshold: return 0.0 else: return -1.0注意我说奖励“给 0”是指成功时给 0失败时给 -1。这和常见的 1/0 形式本质上等价只是平移了一下不会影响最优策略。但有一个地方会因此受影响done 标志。如果按“奖励是否为 0”来判断任务是否完成那么重标记后最后一步通常满足dist threshold此时应该把 done 置为 True。如果你直接把原始 trajectory 里的 done 复制过来那这批重标记样本在 critic 眼里就成了“未完成但奖励为 0”的诡异数据会造成 Q 值错误外推。所以重标记时必须同步重新计算 reward 和 done这两者是绑定在一起的。3.4 训练闭环与超参选择主体流程我用 DDPG 作为 off-policy 基座和标准 DDPG 的区别只在于回放缓冲区多了一层 episode 缓存、以及每次把新 episode 写入主缓冲区时执行 future 重标记。完整训练循环长这样for episode in range(max_episodes): obs, goal env.reset() ep_obs, ep_actions, ep_next_obs, ep_goals, ep_dones [], [], [], [], [] ep_achieved [] for step in range(max_steps): action select_action(obs, goal, noise_scale0.1) next_obs, reward, done, info env.step(action) ep_obs.append(obs) ep_actions.append(action) ep_next_obs.append(next_obs) ep_goals.append(goal) ep_dones.append(done) ep_achieved.append(info[achieved_goal]) obs next_obs if done: break # 先把原始 transition 写入主回放区 ep_id len(her_buffer.episode_id) for t in range(len(ep_obs)): her_buffer.add_transition( ep_obs[t], ep_actions[t], compute_reward(ep_achieved[t], goal), ep_next_obs[t], goal, ep_dones[t], ep_id ) # 关键: HER 重标记并追加样本 her_buffer.relabel_and_store( {obs: ep_obs, action: ep_actions, next_obs: ep_next_obs}, ep_id, ep_achieved, future_k4 ) # 正常 off-policy 更新 if her_buffer.size() batch_size: for _ in range(update_steps): batch her_buffer.sample(batch_size) # DDPG 网络更新逻辑同标准实现超参方面比较关键的是future_k也就是每条 transition 额外生成几个重标记样本。原论文用的 4我在二维环境里试过 2、4、8感觉 4 是一个性价比很高的点样本量翻倍的同时不会让缓冲区里“假目标”占比过高。还有一个值得注意的细节主回放区容量尽量设大一点比如 50 万到 100 万条因为 HER 会显著增加样本写入速率容量太小会导致重标记样本被快速淘汰无法充分发挥作用。DDPG 本身的学习率、exploration noise、网络结构我直接沿用了标准配置没有额外调优。实测下来同样的 10 万步预算无 HER 的 DDPG 成功率一直趴在 0% 附近加了 HER 的版本大概在 3 万步时开始出现稳定成功到 8 万步成功率接近 90%。这个差距就是重标记带来的。4. 实战踩坑与调试实录4.1 我踩过的五个坑第一个坑忘了重算奖励。听起来很蠢但特别容易在写代码时发生。你为了省事把原来的transition直接复制了一份换了个 goal 字段就丢进缓冲区奖励字段还是按照原始目标算出来的。结果就是经验池里存储的“成功样本”实际上并不成功critic 被这些错位数据带偏训练刚开始的时候 loss 疯狂飙升然后整个策略崩掉。排查方法也简单重标记完成后随机打印几条 transition肉眼检查 goal 和 achieved_goal 的欧氏距离确认小于阈值时 reward 确实是 0 或 1。第二个坑done 标志没更新。这个我在前面已经强调了。重标记以后最后一步的 done 应该是 True否则 critic 在自举时会错误地继续估算未来回报。一个很隐蔽的现象是训练时 success rate 有上涨趋势但到后期反复震荡上不去这就是 done 标志不一致留下的隐患。把 done 改成随 reward 同步计算后曲线一下就稳了。第三个坑把 episode 数据直接切成单条 transition 存入缓冲区导致做 future 采样时拿不到未来的状态。HER 必须按 episode 存放原始数据这也是它的工程实现和普通 DDPG 经验池最大的不同。如果你一开始设计数据结构时没注意后面要补就很被动要么重新录数据要么把 episode id 作为额外的字段塞进去。我的建议是第一步就设计成两层结构别图省事。第四个坑目标维度没做归一化。很多真实环境里目标状态是多个物理量的拼接例如位置坐标和速度量纲差异很大。如果不归一化距离计算会主要被大数值的那个维度主导小数值维度直接失效。HER 的目标采样和 distance 判据都依赖状态间的距离这个问题会被放大。我习惯在环境返回值之后做一次标准化或者至少对目标向量做 min-max 缩放。第五个坑重标记目标全选高难度样本。有人会把 future 的 k 值调得特别大比如 20导致缓冲区里大部分都是 agent 实际达不到的远端目标学习信号反而被淹没。HER 的核心是让目标落在可达域附近过度重标记会稀释信号密度。我现在的经验是 k 不要超过 8且 future 策略生成的样本占总体样本比例控制在 30% 到 50% 之间。4.2 怎么判断 HER 是否在起作用判断 HER 有没有生效最直接的指标是回放缓冲区里即时奖励的分布。你可以定期统计一下奖励非零的比例。在我的二维环境里没有 HER 时这个比例是严格 0%因为所有真实轨迹都不可能到达目标加了 HER 之后非零奖励比例大概在 20% 到 30% 之间这说明重标记确实在制造有效信号。如果这个比例太低比如低于 5%多半是重标记逻辑有问题或者目标采样难度失衡。第二个判断维度是 Q 值的分布。训练稳定后抽样统计 critic 预测的 Q 值中位数和最大值如果最大值明显大于中位数说明 critic 学会了区分不同状态的好坏。如果所有 Q 值都挤在同一个值附近说明信号没有传进去。第三个判断维度是成功率曲线的斜率。HER 的一个特征是成功率通常不是均匀提升的而是先经历一个很长的平台期然后突然出现一段陡峭上升。这个平台期是 agent 在积累了足够的重标记样本、逐步学会了“接近目标”的基础行为一旦这个行为成形实际成功率就会爆发式增长。我见过很多次这种情况所以如果你看到前面两万步毫无起色也别急着杀进程先确认缓冲区里非零奖励样本比例是正常的再多等等看。4.3 常见问题速查表现象可能原因排查手段训练一开始 critic loss 就异常大重标记后奖励没重算旧奖励与新目标错位打印随机 transition 人工检查 reward 与 goal 的匹配性成功率长时间为 0缓冲区无正样本future 采样索引越界导致重标记代码被跳过在 relabel 函数入口加计数日志确认每 episode 确实写入了新样本成功率曲线不停震荡上不去done 标志未随奖励同步更新检查最后一跳 transition 的 done 是否为 True测试时策略行为异常动作幅度过大critic 被错误数据带偏Q 值外推失真清空缓冲区修正重标记逻辑后从头训练训练速度明显变慢future_k 过大缓冲区写入量暴涨调小 k 到 4并增大缓冲区容量更多时候HER 的调参问题不是出在算法本身而是出在数据管线的细节上。我强烈建议在完整训练之前先写一个十几行的小脚本专门验证重标记逻辑模拟一条随机轨迹执行 relabel然后打印原始目标和重标记目标下的奖励、done、距离对比。这一步花 10 分钟能省下后面两天调参的时间。5. HER 的适用范围与后续演进5.1 什么时候该用 HER什么时候不该用适合用 HER 的任务有个共同特征目标可以用低维或中维向量明确表示且存在“在行为轨迹中实际到达的状态可作为候选目标”的空间。比如机械臂的位姿抓取、移动机器人的导航、游戏中需要到达特定地点的任务、以及一部分自动驾驶决策场景里的目标状态规划。凡是任务本身带有多目标属性且你关心的是“能否学会一个通用的目标达成策略”HER 都是非常值得优先尝试的基线方案。不太适合的情况我总结为三类。第一类是单目标且目标极难到达的纯探索型任务比如“找到地图里唯一隐藏的宝藏”因为 agent 从来没接近过宝藏重标记找不到任何有价值的替代目标也就是没有“hindsight”的素材。第二类是目标定义本身不清晰的任务比如对话生成里的“让用户满意”你很难抽出明确的目标向量HER 的 relabel 无从下手。第三类是你已经有很好的人工密集奖励HER 带来的边际提升会很小反而增加存储和计算的复杂度。另外要注意的是HER 对 off-policy 算法是必需品因为它必须依赖回放缓冲区来反复利用重标记样本。如果你用的是 PPO 这种 on-policy 算法强行套 HER 需要改成重要性采样或者维护一个很大的旧策略缓冲区工程复杂度会直线上升效果还不一定好。除非有特殊理由否则我建议在 policy gradient 系算法上不要硬套 HER。5.2 从 HER 出发的后续扩展HER 提出之后围绕“自动生成更有价值的目标”这个方向衍生出了不少工作。比较有参考价值的有CHERCurriculum Hindsight Experience Replay它不再均匀地重标记而是根据 agent 当前的学习进度动态调整重标记目标的难度分布让课程推进更平滑HGGHindsight Goal Generation专门面向多智能体或多人协作场景在重标记时加入对其他智能体行为的考虑还有 GoalGAN、LfDHER 这类把生成对抗网络和示范学习结合起来的方案用学到的目标生成器替代朴素的随机采样。这些改进方向本质上都是在回答同一个问题除了“从未来状态里抽目标”有没有更聪明的选目标方式如果你项目里已经有了一个可工作的 HER 基座往这些方向扩展的路径是相当顺畅的。因为核心的数据管线、重标记机制和奖励重算逻辑都不用动只需要替换“目标采样策略”这一层。这也是我特别喜欢 HER 的一个原因它的模块化程度很高算法演进不会要求你推翻重来。最后分享一个从实际项目里总结出来的体会HER 最大的价值其实不只是它带来的成功率提升而是它改变你设计任务的思路。以前遇到稀疏奖励第一反应是加奖励塑形、加中间奖励、加人工引导现在我会先问一句——这个任务的“成功”能不能被重新定义成若干个子目标如果答案是肯定的那很多让人头疼的奖励函数设计就不必要了。每次训练失败的数据也不再是垃圾桶里的废品而是可以反复挖掘的监督信号。这种“把失败当资源”的思路放到强化学习之外的工程问题里同样成立。