
做强化学习的人十有八九都被稀疏回报坑过。让一个机械臂去抓取目标物体连续几千步碰不到目标每一步的奖励都是 0策略梯度直接失去方向网络权重原地打转。我这次从零复现的项目叫 hindsight技术核心就是深度强化学习里那套著名的 Hindsight Experience Replay事后经验回放简称 HER。这套思路说白了就一句话既然失败是常态那就别让每一次失败白费把没达到目标的轨迹改写成达到另一个目标的成功样本让 agent 从自己的失误里真正学到东西。这篇文章我会把整个项目的来龙去脉、核心机制、完整的代码实现、以及我踩过的坑全部摊开讲。适合两类人看一是刚接触 goal-conditioned RL、被稀疏奖励折磨得想换方向的入门者二是已经在用 HER 但训练效果不稳、想搞清楚参数和细节的老手。我不打算堆概念全程按实际调代码的顺序来写你照着抄也能跑起来。1. 项目概述hindsight 到底在解决什么问题1.1 稀疏奖励这个老问题到底难在哪先聊清楚痛点否则你不明白 HER 为什么值得复现。在 goal-conditioned 的强化学习场景里agent 的目标通常由外部指定比如把木块推到 (1.0, 2.0) 这个位置。奖励设计最常见的是稀疏模式距离小于某个阈值给 0否则给 -1。这意味着在训练初期agent 的动作完全随机绝大多数 episode 根本碰不到目标每一步拿到的都是 -1整个回合的累计回报约等于路径长度取负。这个数值有什么问题问题大了。价值网络学出来的 Q 值全在 -1 附近徘徊策略梯度虽然也在更新但信号就像在深海里听回声方向完全是被噪声主导的。你加大训练轮数、扩大网络容量都只是让模型更熟练地在原地打转。我最早在这个项目里试过把奖励改成连续的距离函数shape reward也就是距离越近奖励越高结果训练是能训了但 agent 学会了绕着目标走——因为距离的梯度被引导到了局部路径上而不是真正解决问题的全局策略。这就是手写 reward shaping 的经典翻车现场你以为在帮它实际上在教它钻空子。当时我整理了一下手里的牌能走的路无非这么几条第一继续加大探索噪声让随机动作碰目标但这在连续高维空间里概率低到可以忽略第二手工设计课程任务从简单目标逐步过渡到复杂目标但这需要大量领域知识第三用 HER。它的思路完全不同——不改奖励、不拆任务只改训练数据的构成方式。1.2 为什么 hindsight 思路行得通HER 的核心洞察其实非常朴素任何一条轨迹对于某个目标来说都是成功的轨迹。设身处地想一下你练罚球每次都投不进。一个只会盯着篮筐的教练会说你水平太差但换个思路如果每次出手之后把篮筐挪到球实际落点那你的每一投都是空心入网。HER 在训练时干的事就是把这个挪篮筐的操作作用到经验回放上。具体来说一个 episode 里 agent 原本的目标是 A结果它没到达 A途中和终点经过了一串状态。HER 随机挑几个后续状态把它们重标注成新的目标然后重新计算这批样本的奖励。因为新目标是从轨迹里真实出现的状态所以对这段轨迹而言新目标的完成度是 100%奖励一定是成功的那一档。这批改写后的样本被塞进回放缓冲区策略网络就从原来的全失败经验库里凭空多出一批成功样本可以学。这套机制最早出自 OpenAI 那篇 2017 年的论文Andrychowicz et al., Hindsight Experience Replay论文里在机械臂抓取、推箱子等环境上证明了 HER 配合 DDPG 能从全稀疏奖励下学会复杂操作任务。我这次做的 hindsight 项目本质上就是把论文里的方法用 PyTorch 从头实现、跑通、并整理出可复用的代码。它解决的核心问题就是让 agent 在完全没有外部奖励信号的条件下靠事后给失败找意义的方式完成目标导向学习。2. 核心细节解析与实操要点2.1 HER 的核心机制目标重标注HER 最关键的操作用一个词就能概括relabel重标注。但要实现得对有三个细节必须抠死选什么状态当新目标、改完目标后怎么重算奖励、以及这几个动作要和采样流程怎么配合。先看目标来源。论文里对比了四种策略final用轨迹最后一个状态、random随机状态、episode当前 episode 里的随机状态、future当前 episode 里时间靠后的某个状态。实践下来future 策略是效果最好也最常用的原因很直观用轨迹后半段的状态做目标意味着 agent 那些看似失败的动作其实都离目标不远学到的策略更接近从现实出发逐步逼近目标而不是拿一个天马行空的随机位置当靶子乱学。再看重算奖励。这一点我见过的初学者几乎人人踩坑你改了目标就必须用新目标重新算 reward绝不能沿用旧 reward。否则价值函数会收到目标没达到但奖励是成功的错乱信号网络直接学疯。奖励的计算公式通常是套距离阈值提示重标注样本的 reward 必须重新计算。用二值奖励-1 / 0时核心逻辑就是判断新的目标状态和该步的 next_state 是否足够接近接近则给 0否则给 -1。下面这是我项目里最初版的重标注逻辑我故意把注释写详细方便你对照检查自己的实现import numpy as np import random from collections import deque class HeraldBuffer: 一个简化但结构完整的 HER 经验回放缓冲区。 存储粒度是 episode而不是 transition这一点非常关键。 def __init__(self, capacity, k4, distance_threshold0.5): self.episodes deque(maxlencapacity) self.k k self.distance_threshold distance_threshold def store_episode(self, episode): # episode 是一个 list元素是 (state, action, reward, next_state, goal) self.episodes.append(episode) def _judge_reward(self, next_state, goal): dist np.linalg.norm(next_state - goal) return 0.0 if dist self.distance_threshold else -1.0 def _relabel(self, transition, new_goal): state, action, _, next_state, _ transition new_reward self._judge_reward(next_state, new_goal) return (state, action, new_reward, next_state, new_goal) def sample_batch(self, batch_size): raw, her [], [] for _ in range(batch_size): ep random.choice(self.episodes) idx random.randint(0, len(ep) - 2) raw.append(ep[idx]) # 从该 episode 的未来状态中挑选 k 个作为新目标 future_indices random.choices(range(idx 1, len(ep)), kself.k) for future_idx in future_indices: new_goal ep[future_idx][3] # 用 future state 作为新目标 her.append(self._relabel(ep[idx], new_goal)) return raw, her注意sample_batch返回的是raw和her两个列表前者是原始经验后者是改写后的经验。训练时两者都会送进网络只是标签reward不同。这里的k是每个原始样本额外生成的 HER 样本数后面会单独讲。2.2 关键参数这样调k、目标策略与距离阈值HER 之所以让人觉得看起来简单但效果飘多半是因为参数没调对。我把自己测试过的参数组合整理成了一张表都是基于这个 2D 点到点任务的实测结果换到高维环境趋势类似但绝对值会变。参数典型取值我实测的效果备注k每个样本的 HER 改写数4收敛速度和稳定性最佳论文默认值适用于大多数任务k 11训练能跑但曲线抖动明显适合快速验证代码正确性k 88样本量翻倍前期收敛快显存和计算开销大后期收益有限目标来源策略future成功率高学到的行为自然优先选择除非任务有特殊时间结构目标来源策略final只学到“最终位置附近”的行为目标多样性不够探索效率低距离阈值0.5环境尺度 10适中太大则奖励过早放水太小则无从学起距离阈值这个参数特别容易被忽略。阈值设太大比如 2.0agent 在离目标很远的地方就能拿到成功奖励策略学出来是差不多先生阈值设太小比如 0.05成功信号几乎永远不出现HER 改写出来的样本同样全是失败。我的经验是先把环境动作步长摸清楚然后让阈值大约是单步最大移动距离的 2 到 4 倍这样既不会放水也不会让成功概率太低。2.3 一个容易被忽略的细节目标表征与距离度量很多人以为 HER 的难点在算法本身其实一半的坑在目标怎么表示上。我用的是绝对坐标也就是把目标坐标直接拼进观测向量观测是[x, y, gx, gy]的四维向量。但如果你做的是机械臂或者机器人导航目标往往要换成相对坐标即goal - current_pos的相对位置。相对坐标有个好处它让策略对当前我在哪、目标在哪的相对关系更敏感天然适合平移不变的任务绝对坐标则在目标精度要求很高的场景下更直接。距离度量方面我默认用欧氏距离算阈值判断。但要注意在高维目标空间里欧氏距离的三个分量如果量纲不同比如角度和位置混在一起那就要先做归一化或者改用逐分量的距离判断每个维度各自判断是否在阈值内。我在项目里加过一个调试开关分别用两种距离度量跑同一环境结果成功率能差出 20 多个百分点。别小看这个细节它在你的实验对比里可能直接决定结论。3. 实操过程与核心环节实现3.1 准备环境与依赖我把项目环境收敛到最少依赖方便任何人复现Python 3.9 PyTorch 2.x NumPy不需要 MuJoCo也不需要 OpenAI Gym 全家桶。训练环境是我自己写的一个 2D 点到达任务一个点在 [-5, 5] 的方形区域里移动每一步可以加一个二维速度指令目标固定放在 (5, 5)初始位置固定在 (-5, -5)。这个环境足够简单能把 HER 的效果差异看得清清楚楚又不会因为环境本身就自带太多随机性干扰判断。import numpy as np class PointReachEnv: 极简二维点到点任务。 观测 [x, y, goal_x, goal_y]动作 [vx, vy]。 奖励稀疏距离小于阈值给 0否则给 -1。 def __init__(self, threshold0.5): self.threshold threshold self.pos None self.goal np.array([5.0, 5.0]) self.action_dim 2 self.obs_dim 4 def reset(self): self.pos np.array([-5.0, -5.0]) return self._get_obs() def _get_obs(self): return np.concatenate([self.pos, self.goal]).astype(np.float32) def step(self, action): action np.clip(action, -1.0, 1.0) self.pos np.clip(self.pos action, -5.0, 5.0) dist np.linalg.norm(self.pos - self.goal) reward 0.0 if dist self.threshold else -1.0 done dist self.threshold return self._get_obs(), reward, done, {dist: dist}选择这个环境而不是直接上 Gym 的 FetchReach原因有两个第一Fetch 系列依赖 MuJoCo 许可证很多同学装上就跑不起来复现门槛太高第二2D 点到达任务状态维度低收敛快训练一轮只要几分钟非常适合验证 HER 的机制是否在你的代码里真正生效。等你把这个环境调通了再迁移到复杂任务也不迟。3.2 实现 HER 回放缓冲区两个关键设计缓冲区代码在上文已经给出了主体这里补充两个容易忽略的设计决策。第一个决策缓冲区存的必须是 episode不是 transition。这个我在代码注释里特意标了红色警告因为它直接决定了 HER 能不能工作。HER 改写样本时需要当前 episode 里 time step 更靠后的状态如果你像普通 DQN 那样只存(s, a, r, s)四元组改写时根本找不到同一 episode 的后续状态整个算法就退化成普通回放稀疏奖励问题原样存在。存 episode 会牺牲一点内存但这是 HER 的地基没有商量余地。第二个决策采样时 raw 和 her 分开返回而不是混在一起。我这样设计是为了在训练循环里能分别统计原始样本和改写样本的 loss方便调试。比如你发现策略在某个阶段突然崩了先看是 raw 的 loss 爆了还是 her 的 loss 爆了能快速定位问题出在价值函数还是策略网络。3.3 接入 DDPG 训练流程的完整套路HER 论文里用的 off-policy 算法是 DDPGDeep Deterministic Policy Gradient因为它适合连续动作而且天然支持从回放缓冲区采样。我把训练循环的骨架贴出来重点看数据流actor 和 critic 在计算时都要用到 goal 信息因此输入是拼接好的obs [state, goal]。import torch import torch.nn as nn class Actor(nn.Module): def __init__(self, obs_dim, action_dim, hidden256): super().__init__() self.net nn.Sequential( nn.Linear(obs_dim, hidden), nn.ReLU(), nn.Linear(hidden, hidden), nn.ReLU(), nn.Linear(hidden, action_dim), nn.Tanh() ) def forward(self, obs): return self.net(obs) class Critic(nn.Module): def __init__(self, obs_dim, action_dim, hidden256): super().__init__() self.net nn.Sequential( nn.Linear(obs_dim action_dim, hidden), nn.ReLU(), nn.Linear(hidden, hidden), nn.ReLU(), nn.Linear(hidden, 1) ) def forward(self, obs, action): return self.net(torch.cat([obs, action], dim-1))训练循环的核心逻辑只有四步跑一个 episode 存进缓冲区从缓冲区采样 raw 和 her 两批数据把两批数据合并后更新 critic再用 critic 的梯度更新 actor。伪代码如下def train_one_epoch(agent, buffer, env, noise_scale0.3): episode [] obs env.reset() for t in range(50): # 限制 episode 最大长度 action agent.select_action(obs, noise_scale) next_obs, reward, done, _ env.step(action) episode.append((obs, action, reward, next_obs, env.goal.copy())) obs next_obs if done: break buffer.store_episode(episode) raw, her buffer.sample_batch(128) transitions raw her # 更新 criticloss MSE(Q(s,g,a), r gamma * Q_target(s,g,a)) # 更新 actorloss -mean(Q(s,g, actor(s,g))) # 每训几步就软更新 target 网络tau 0.05请注意env.goal.copy()这一行我在第一版代码里漏了 copy导致同一个 goal 数组被反复引用缓冲区里存的目标全变成同一个了。这类 Python 引用陷阱遇到一次就长记性。3.4 训练观测与性能对比我分别跑了纯 DDPG和DDPG HER两组实验每组 200 个 epoch每个 epoch 只跑 1 个 episode。结果非常直观纯 DDPG 的成功率从头到尾都是 0%价值函数输出的 Q 值始终在 -1 附近策略网络完全没学到任何东西。加上 HER 之后大约从第 30 个 epoch 开始出现第一次成功到 100 个 epoch 左右成功率稳定在 80% 以上。这里有个数字值得注意虽然加了 HER 成功率上去了但前 30 个 epoch 其实和纯 DDPG 一样沉闷。原因是 HER 改写的那些成功样本在策略还没成型时学到的也只是随机目标附近的行为需要等真正碰到几次原始目标策略才会被拉向正确的方向。所以如果你跑 HER 发现前半程没起色不要急着砍掉先让它多跑一阵这是正常现象。配置首个成功 epoch200 epoch 最终成功率纯 DDPG无0%DDPG HER (k4)约第 3082%DDPG HER (k8)约第 2586%4. 常见问题与排查技巧实录4.1 目标重标注顺序错了训练直接废如果你的训练曲线完全不动或者 loss 异常震荡第一件事就查重标注逻辑。我遇到过两次致命 bug全在这个环节。第一次我把 new_goal 换进了 next_state 但忘了换 state导致同一组观测里 state 和 goal 对不上。价值函数学出来的规律变成无论目标是什么状态都一样整个策略直接放弃治疗。第二次我改了目标但没重算 reward原始样本和 HER 样本的 reward 含义完全错乱训练时 loss 直接爆 NaN。排查方法也很简单写一个单元测试手动构造一条轨迹跑一遍_relabel人工检查 state、next_state、goal、reward 四者的对应关系是否自洽。4.2 k 值到底选多少别盲目翻倍论文默认 k4我一开始不信邪直接拉到 k16觉得样本越多越好。结果训练速度没快多少显存先爆了而且因为缓冲区里 HER 样本严重过剩策略开始过度依赖事后找补的成功样本真实的 goal-conditioned 能力反而变差。后来我按 k1 快速验证代码正确性再切到 k4 正式训练最后用 k8 做精度冲刺。k 值本质是在数据量和数据分布偏差之间做权衡不是越大越好。4.3 原始经验与 HER 经验的配比问题HER 样本和原始样本的比例直接由 k 值决定每个原始样本配 k 个 HER 样本。但要注意缓冲区里还混着不同时间段的旧 episode。随着训练推进早期那些质量很差的 episode 如果没有及时被挤出缓冲区HER 会持续从里面生成低质量改写样本拖慢后期收敛。我的做法是把缓冲区容量调小一点比如 200 个 episode强制淘汰旧经验让缓冲区的数据分布紧跟当前策略的水平。另外噪声策略也要配合调整。我用的噪声是动作空间的高斯噪声幅度 0.3 起步随着成功率提升逐步衰减到 0.05。噪声太大HER 改写出来的目标都是东一榔头西一棒子的乱状态噪声太小探索不够原始目标的成功率上不去。观察曲线的技巧是如果 HER 样本的 loss 很低但原始样本的 loss 很高说明策略在自欺欺人问题往往出在噪声太小、原始成功经验太少。4.4 一个提升稳定性的小改动目标归一化最后一个坑也最隐蔽目标坐标如果不做归一化价值函数的输入尺度差异过大会导致训练不稳定。我的环境坐标范围是 [-5, 5]动作范围是 [-1, 1]直接用的话 critic 的最后一层 weights 要同时消化大数和小数容易出现梯度震荡。我的做法是观测里 state 和 goal 都除以 5缩放到 [-1, 1]同时把距离阈值也按同样比例缩放。做完这个改动之后同样超参数下成功率大概又涨了 5 到 8 个百分点而且曲线平滑很多。5. 写完这个项目之后我的一些体会说句实话HER 的论文我几年前就看过当时觉得不就是把失败当成功来学嘛思路简单到有点怀疑它是不是真的有用。这次亲手从零实现一遍才发现简单背后的精妙全在细节里episode 粒度的存储、目标改写的策略、reward 的重算、还有 k 值和噪声的配合每一个环节做错一点效果就往后退一大步。如果你也想复现类似的实验我的建议是别急着上复杂环境。先用我这个 2D 点到点任务把 HER 的整套链路跑通确认你的缓冲区确实在生成正确的改写样本再迁移到机械臂或者导航任务。调试 HER 和调试普通 RL 最大的区别就是普通 RL 你盯着 reward 曲线就能判断问题HER 你还要多盯一眼改写后的样本分布是否合理。最后再补一个小技巧训练过程中把 HER 改写出来的样本定期 dump 成文件看一眼那些新目标在空间里长什么样。如果它们密密麻麻堆在起点附近说明探索不足如果它们均匀散布在可行区域说明噪声水平健康。视觉化判断比盯着 loss 数数字直观得多我后面所有 RL 项目都保留了这个小习惯。