HER后见经验回放:稀疏奖励下强化学习如何利用失败经验

发布时间:2026/10/2 9:37:42
HER后见经验回放:稀疏奖励下强化学习如何利用失败经验 年初调模型的时候重新翻了 Hindsight Experience ReplayHER这套思路。Hindsight 这个词英语里就叫“后见之明”我们常说 hindsight is 20/20事后事情看的一清二楚。但“事后复盘”在强化学习里并不是一个顺理成章的事尤其是稀疏奖励环境下模型常常输得一塌糊涂却连“为什么输”都得不到信号。HER 这个名字厉害的地方在于它把“事后”直接变成了训练信号让智能体从失败轨迹里提取出能学的经验。这篇文章就是围绕这套技术把背后的设计思路、核心实现和我在实际跑实验时踩过的坑一起讲清楚。先说清楚这东西适合谁。如果你在做机器人控制、机械臂抓取、导航这类任务奖励很稀疏跑 DDPG 或 SAC 半天奖励曲线纹丝不动那 HER 大概率值得试一下。如果你刚接触强化学习只想弄明白“后见经验回放”到底是什么这篇文章也能让你少走弯路。它解决的痛点很明确在二值奖励或者极稀疏奖励的任务里模型几乎拿不到正向反馈常规 off-policy 算法很难学会复杂操作。HER 的做法是重新定义目标让那些“失败”的经验也可以变成学习资源。整个话题我会按四个部分展开先解释为什么要引入后见之明再拆解 HER 的核心设计和目标采样策略接着聊落地时参数与工程细节最后整理我实际跑实验时遇到的问题和排查方法。1. 为什么需要 Hindsight稀疏奖励下的学习困境1.1 稀疏奖励到底难在哪先想想一个最简单的情境让一个机械臂把桌上的杯子推到目标点如果最终位置离目标点很近给奖励 1否则给 0。这个设定在真实任务里很常见因为它省去了设计复杂奖励的功夫。但问题是智能体在一开始完全随机探索时绝大多数 episode 都拿不到任何奖励哪怕它已经“接近”目标了也没有中间梯度的反馈。这就像学生做题老师只告诉你“满分”和“零分”不告诉你中间错在哪一步。对于一次尝试你无法判断哪些动作是对的、哪些动作是错的因为所有动作看起来都没带来任何变化。深度学习模型依赖梯度梯度需要误差误差来自预测和真实奖励之间的差异。如果奖励大多数时候都是 0那误差信号就极其稀疏模型基本学不到东西。在连续控制任务里这种情况尤其致命。动作空间是无限维度的组合要把手臂从一个位置挪到另一个位置需要精确控制每个关节的角度和力矩。没有中间反馈探索出现有效动作的概率极低模型只能在原地打转。即使偶尔碰巧成功了一次样本量也不足以让策略稳定下来。1.2 事后诸葛亮怎么变成学习信号HER 想了一个很讨巧的角度既然这次任务没成功但智能体确实从初始状态跑到了另一个状态那“到达这个实际状态”本身能不能被当作一个新的目标来学习举个例子。机械臂的目标是把杯子推到位置 A但实际操作中杯子最终停在位置 B。这当然算失败。但换个角度想在这个 episode 里智能体其实知道如何让杯子从起点移动到 B因为它亲眼看完了这个过程。如果把“杯子在 B”重新定义成目标那么整段轨迹就是一条成功轨迹每一步移动到 B 的动作都得到了正向奖励。这就是后见之明的直觉上帝视角下你已经知道“实际到了哪里”于是可以把这个实际结果倒推成目标再把整条轨迹标注为成功。智能体虽然没学会达到 A但至少学会了达到 B。随着无数个这样的“B、C、D”不断出现模型能积累大量“成功”经验逐步建立起“如何通过一连串动作改变物体位置”的知识。你以为这只是在自欺欺人恰恰相反这是一种非常强的学习信号。因为智能体在探索过程中总会碰到各种状态这些状态虽然离原始目标很远但它们之间往往存在因果联系。把这些因果联系用目标重标记的方式暴露给模型模型就能利用失败经验去拟合“状态到动作”的映射。学习“如何到达任意状态”比单纯学习“如何到达唯一指定状态”要容易得多因为前者有密集的正反馈后者几乎无反馈。2. Hindsight Experience Replay 核心设计拆解2.1 成功经验回放与目标重标记HER 是建立在经验回放之上的。传统的经验回放只存(观测, 动作, 奖励, 下一观测, 是否结束)HER 额外引入“目标”维度存储形式变成(观测, 动作, 目标, 奖励, 下一观测, 是否结束)。智能体在环境中推进时带着一个原始目标去选动作但这条轨迹在存入回放池时除了保留原始目标还会额外生成几个替代目标并按照这些替代目标重新计算奖励和终止信号。重标记之后的奖励不是随便给的。计算方式和你环境里的奖励函数保持一致只不过把目标的参数替换掉。比如原始奖励函数是reward 1 if distance(obs, goal) threshold else 0那重标记之后就用同样的函数只是把goal换成替代目标。这样替代目标是否成功完全取决于状态是否满足“目标达成”的条件。如果满足奖励就是 1不满足就是 0。关键点在于替代目标取自“后续时间步实际到达的状态”。这保证了在任何时刻后续某个状态大概率是可达的因此重标记后的 reward 更可能等于 1。比如轨迹共 50 步选择第 30 步的实际状态作为第 10 步的目标那智能体在第 10 步之后确实走到了第 30 步的状态所以这一段可以被标记为成功。模型学习的是“在当前观测下如果要到达未来的那个状态应该采取这个动作”。这个训练信号完全来自真实发生的 transition所以具有很强的可学习性。每次转换最多能生成多少个替代目标是超参数K。常设K 4表示每个转换额外加 4 个重标记目标。加上原始目标一个 transition 就可以在回放池里以 5 份不同目标的形式参与训练。样本量不变但效率大幅提升。2.2 目标采样策略final、future、episode、random重标记目标不是随便选的目标采样的方式直接影响效果。原始论文里比较了几种策略我用表格把它们的区别列出来。策略名称采样方式特点与适用场景final取轨迹最后一个状态作为替代目标最简单但所有 transition 都指向同一个目标多样性差random从整个回放池中随机采样一个状态作为目标目标分布广但容易引入不相关的状态训练初期噪声大episode从当前这条轨迹中随机采样一个状态作为目标目标来自同一条轨迹相关性强比 random 稳定future只从当前 transition 的未来时间步中采样一个状态作为目标最符合因果顺序能保证目标“从后续可达状态中产生”经验上效果最好我实际用下来future是最稳的选择。原因很容易理解如果一个目标出现在当前 transition 的之前那这个目标之前可能已经发生过不对应当前动作的结果而来自未来的目标天然是当前动作链后续会到达的状态因果上成立。episode也还凑合因为整条轨迹的状态都和当前 episode 相关但future更精细。还有一个容易被忽略的点final虽然只取最后状态但如果轨迹很长、比较曲折后期所有 transition 都在学习“如何到达终点”会丢失中间过程的多样性。对于长 horizon 任务建议至少用future或episode不要偷懒只写 final。2.3 算法伪代码与网络结构HER 本身不是独立的强化学习算法它是改造经验回放的方法必须搭配 off-policy 的模型比如 DDPG、TD3、SAC 或者 DQN。原因后面章节细说。先来看整合流程的伪代码。# 伪代码HER 与 off-policy 算法的整合 for episode in range(total_episodes): episode_transitions [] obs env.reset() goal sample_initial_goal() for step in range(max_steps): action policy.select_action(obs, goal) next_obs, reward, done, info env.step(action) episode_transitions.append((obs, action, reward, next_obs, goal, done)) obs next_obs if done: break # 1. 原始 transition 入池 for t, trans in enumerate(episode_transitions): buffer.store(*trans) # 2. 重标记替代目标入池 for t, trans in enumerate(episode_transitions): obs_t, action_t, reward_t, next_obs_t, goal_t, done_t trans for _ in range(K): if strategy future: future_idx random.randint(t 1, len(episode_transitions) - 1) new_goal episode_transitions[future_idx][3] # next_obs 作为目标 elif strategy episode: new_idx random.randint(0, len(episode_transitions) - 1) new_goal episode_transitions[new_idx][3] elif strategy final: new_goal episode_transitions[-1][3] elif strategy random: new_goal sample_state_from_buffer(buffer) new_reward compute_reward(next_obs_t, new_goal) new_done check_success(next_obs_t, new_goal) buffer.store(obs_t, action_t, new_goal, new_reward, next_obs_t, new_done) # 3. 常规 off-policy 更新 for _ in range(update_steps): batch buffer.sample(batch_size) policy.update(batch)注意伪代码里有个细节重标记目标时new_goal用的是next_obs而不是更早的状态因为next_obs是当前动作直接造成的状态用它作为目标当前动作自然就是“成功动作”因果链条最干净。在future策略里目标也可能取更远的未来状态但无论如何当前 transition 确实沿轨迹走向了那个未来状态。网络结构上DDPG 类算法的输入通常会把obs和goal拼接成一个向量。比如机械臂任务里观测是手臂关节位置和物体位置目标是期望物体位置拼接后一起送入 actor 和 critic。没有特殊结构不需要额外的 goal 编码器。如果观测是图像那要考虑目标图像与当前观测的通道拼接或者用一个小 encoder 压缩目标表示但基本思路一致。3. 实操落地关键参数与工程细节3.1 基于 off-policy 算法的集成方式HER 最常搭配的是 DDPG、TD3、SAC。为什么只能是 off-policy关键在于重标记后的 transition动作并不是根据新目标选出来的。原始 episode 里智能体是在原始目标goal_original下采样了动作action_t。重标记后这条 transition 被标记成“在目标new_goal下执行了action_t”。但实际上如果一个策略真想达到new_goal它未必会选择action_t。这就导致重标记 transition 的行为分布与新目标下的真实策略分布不一致。off-policy 算法本身可以容忍这种不一致因为它利用回放池里的历史数据做 TD 更新不需要过渡正好来自当前策略。DDPG、SAC 等算法用经验回放就是为了打破时间相关性让更新可以用非当前策略产生的数据。HER 正是利用了这个特性把“旧数据”改头换面变成学习样本。如果你硬要把 HER 接到 PPO 上会出大问题因为 PPO 依赖当前策略收集的轨迹做重要性加权更新重标记后的目标分布变化会破坏重要性采样假设训练直接崩溃。我一般推荐在 TD3 或 SAC 上接 HER。SAC 的随机策略对探索有帮助在稀疏奖励环境下更容易碰触到多样状态TD3 更稳定收敛后动作更平滑。如果你在低维连续控制上做实验两者都行看个人偏好。3.2 参数选择与调参参考HER 不涉及复杂网络改造但有几个参数值得认真调。我放一张自己常用的参数参考表对应 OpenAI 那套 Fetch 机器人环境。参数参考值说明K每种替代目标数4每个 transition 多存 4 份重标记样本K 过大训练变慢K 过小效果有限目标采样策略future实测效果最好优先选择回放池大小100 万稀疏奖励下需要足够大的池子保存历史状态batch size128回放池大时 batch 太小会不稳定actor 学习率1e-3与 off-policy 算法默认值一致critic 学习率1e-3常用SAC 可以适当降到 3e-4探索噪声0.2用于 DDPG/TD3SAC 不需要额外调成功判定阈值根据任务设太严格会让重标记“成功”样本过少太松则目标质量差K 是很关键的数字。K 越大同一段轨迹被重复使用的次数越多样本效率理论上更高但也会造成回放池过度重复更新时会反复用相似数据容易让偏差累积。我用下来K4是性价比最高的选择任务复杂时可以试到K8但训练速度会明显变慢。还有一点重标记样本占总回放池比例并不是直接控制的它取决于 K。K4 意味着重标记样本占比为 80%4 份替代目标 1 份原始目标。这个比例本身是合理的如果追求更保守可以设 K2让原始目标样本占 1/3如果任务确实非常稀疏K8 让成功样本概率大增但代价是模型可能过度适应“事后目标”对原始目标的学习会慢一些。3.3 Python 实现要点工程上实现 HER 并不需要很复杂的代码困扰人的通常是数据结构。一个干净的做法是让transition带上goal字段回放池直接存dict或namedtuple。from collections import namedtuple import numpy as np Transition namedtuple( Transition, [obs, action, next_obs, goal, reward, done] ) class HindsightReplayBuffer: def __init__(self, capacity, K, strategyfuture): self.capacity capacity self.K K self.strategy strategy self.buffer [] self.pos 0 def push_episode(self, episode, compute_reward, check_success): # episode 里每一项是 (obs, action, next_obs, goal, reward, done) for trans in episode: self.push(trans) for idx, trans in enumerate(episode): for _ in range(self.K): if self.strategy future: future_idx np.random.randint(idx 1, len(episode)) new_goal episode[future_idx][2] elif self.strategy episode: new_idx np.random.randint(0, len(episode)) new_goal episode[new_idx][2] elif self.strategy final: new_goal episode[-1][2] elif self.strategy random: # 从整个 buffer 中随机取一条轨迹的 next_obs random_trans self.buffer[np.random.randint(len(self.buffer))] new_goal random_trans.next_obs else: raise NotImplementedError obs, action, next_obs, original_goal, original_reward, done trans new_reward compute_reward(next_obs, new_goal) new_done check_success(next_obs, new_goal) self.push(Transition(obs, action, next_obs, new_goal, new_reward, new_done)) def push(self, transition): if len(self.buffer) self.capacity: self.buffer.append(transition) else: self.buffer[self.pos] transition self.pos (self.pos 1) % self.capacity def sample(self, batch_size): indices np.random.choice(len(self.buffer), batch_size, replaceFalse) batch [self.buffer[i] for i in indices] return batch实现时有个很容易踩的坑episode里的goal我通常存的是当前 episode 的原始目标但重标记时如果把new_goal直接设成未来next_obs要注意未来next_obs的维度要和obs保持一致。如果环境返回的观测里既有机械臂状态又有目标位置需要先把观测拆开把目标部分单独抽出来否则重标记目标会混入机械臂自身关节角度反而干扰学习。比较稳妥的做法是环境返回(observation, achieved_goal, desired_goal)这样的三元组和 OpenAI gym 的 GoalEnv 接口保持一致。另外check_success 和 compute_reward 尽量共用一个距离函数。比如目标达到与否取决于np.linalg.norm(achieved_goal - desired_goal) threshold那么奖励可以直接写成reward 1.0 if dist threshold else 0.0。不要在 compute_reward 里用稀疏奖励在 check_success 里却用另一个阈值二者不一致会让训练信号完全混乱。4. 常见问题与排查技巧实录4.1 问题速查表实际调参时遇到的问题很多我整理成一张速查表后面再挑几个重点说明。现象原因解决办法训练早期 reward 很低且长时间不变原始目标成功率极低重标记目标也未被充分利用检查 K 是否太小增大到 8检查替代目标是否真的被标记为成功模型学会了到替代目标但到原始目标一直失败原始目标样本占比太少模型被“事后成功”带偏适当降低 K增加原始目标样本比例在目标采样中混入部分原始目标训练时 loss 震荡剧烈重标记导致回放池内目标分布不均匀改用 future 采样减小 K增大回放池容量接在 PPO 上完全学不动HER 不兼容 on-policy 算法换成 DDPG/TD3/SAC或者设计稠密奖励替代 HER奖励函数里有距离函数但效果反而差重标记目标后距离奖励导致目标越远回报越低信号变形检查是否必须使用二值奖励如用距离奖励考虑保留原始距离而不重标记环境返回结构不统一obs 和 goal 拼接出错没有区分 achieved_goal 和 desired_goal统一用 Gym GoalEnv 的observation三元组接口4.2 为什么 HER 在 on-policy 算法上容易翻车很多人第一次接触 HER会想把它接到 PPO 上试试。我明白这种冲动毕竟 PPO 在很多连续控制任务里表现稳定。但 HER 要求算法拥有一个可以反复使用历史任意数据的更新机制PPO 的更新公式里带着重要性采样权重前提是当前策略与采样策略不要差得太远。重标记会做一件 PPO 不能接受的事把过去的 transition 改换目标后当作新数据扔给策略。这个新目标和当前策略的行为分布不一致导致重要性权重膨胀PPO 的 clip 机制虽然能限制单步更新幅度但架不住整个样本分布被持续扭曲。最终结果是策略在“假目标”上反复震荡原始目标却没有任何进展。如果项目里已经选定 PPO不能换算法那建议别在 PPO 上接 HER。更实际的做法是把奖励设计得更稠密一些比如用distance potential shaping让智能体每个时间步都能拿到梯度。HER 的舞台就是 off-policy 家族没必要硬掰。4.3 重标记目标会不会让策略陷入局部最优这是个容易被忽视的问题。HER 会大量创造“在替代目标上的成功经验”但替代目标往往比原始目标更容易达成模型可能被这些容易的“虚假成功”带偏长期停在原地。这个现象在 K 较大时更明显。比如抓取任务里模型发现只要把手移动到物体旁边就能获得替代目标奖励它可能就不再尝试把物体拿起因为“纠正”下的奖励已经让它满足了。解决思路是控制替代目标与原始目标之间的平衡。我在工程上常用两个手段一是把 K 控制在 2 到 4避免替代目标样本淹没原始目标样本二是在训练中周期性统计成功率真实验证环境下的原始目标成功率如果超过阈值比如 20%再逐步减小 K甚至后期只保留原始目标样本让模型从“尝试成功”转向“刻意精化”。另外future 策略本身也缓解了这个问题。它只从当前 transition 的未来时间步采样目标目标一定在真实轨迹中可达不是随意的一个状态。不会出现“目标很远但实际上不存在对应轨迹”的情况所以模型学到的是真实可达的状态转移。我自己跑抓取任务时还试过把compute_reward里加一个很小的距离惩罚项比如reward 1.0 if dist threshold else -0.1 * dist帮助模型在替代目标间比较优劣。这个办法有点偏离 HER 原版二值奖励的设定但在一些混合任务里确实有效可以当作锦上添花。4.4 回放池中的状态被反复重标记样本多样性会丢失吗理论上会。每条轨迹最多产生 K 份重标记样本如果任务总是从少数几个初始状态出发重标记目标也会集中在少数可达状态上经过几百轮 update回放池里不同目标的分布会偏向高频访问状态多样性降低。解决起来不难重点在环境侧。训练时给sample_initial_goal增加随机化让初始目标分布不要太窄。比如机械臂任务里目标位置可以在整个操作空间均匀采样不要固定在某一个点。目标多样了失败轨迹自然多样重标记目标池也就多样。回放池容量尽量给到 100 万级给样本多样性留足空间。还有一个容易忽略的是“成功样本稀释”。如果环境本身成功率已经很高比如原始目标达到 30%HER 的优势就没那么明显了因为正常经验里已经有充足的成功 transition盲目重标记反而可能引入冗余数据。这时候可以动态降低 K甚至关闭 HER直接用普通回放池。最后说一点个人体会我最初跑 HER 时总觉得“重标记目标”有点作弊嫌疑毕竟把没做到的事当作做到了。但后来发现这其实是把客观因果和主观目标解耦的过程因果链条是真实的你确实从状态 A 走到了状态 B只是“目标设为 B”这件事是后验选择的。强化学习要学的不只是“完成某个具体目标”更是“如何通过一系列动作把状态变更到期望状态”。HER 做的正是让模型在稀疏环境里先学“变更能力”再逐步逼近“指定目标”。给还在纠结的朋友一个建议不要在 reach 类任务上死磕太久先跑通一个开源实现再回你自己的环境。OpenAI 开源代码看起来复杂但核心逻辑就这么十几行。自己手动实现一遍比看十遍论文都有用。等你把 future 策略、K、回放池这三个点调顺了再回头看稀疏奖励会觉得它其实没有想象中那么可怕。