
想象一下你刚入坑深度强化学习看着 DQN 的公式觉得真简单可一动手写代码却连经验回放里先 pop 还是先 push 都能搞错。更别提目标导向的强化学习goal-conditioned RL里那种稀疏奖励任务环境几乎永远不给你正反馈模型怎么都训不动。这也是我当初在 FetchPush 这类场景里卡了快两周的问题后来把 HERHindsight Experience Replay事后经验回放算法的 PyTorch 版本从零复现了一遍才算把整个思路彻底理顺。这篇文章就把我的代码视角和踩坑经验梳理出来给同样想从代码学习深度强化学习的你一些参考。整篇文章会从一个可运行的 PyTorch 实现切入先讲清楚 HER 到底在解决什么问题再逐段拆解经验回放、目标重标记、Actor-Critic 网络这些核心模块最后给出一份我实测过的训练与排查建议。适合已经有基础的强化学习概念、线性代数和 PyTorch 基本用法但还没能独立把 DDPG 或 HER 写出来的读者。1. 从代码视角理解 HER先弄清楚它要解决什么问题1.1 目标导向强化学习为什么容易卡住目标导向的强化学习和普通强化学习最大的区别在于智能体需要同时学会两件事一是“怎么操作”的能力二是“应对不同目标”的泛化能力。举个简单例子机械臂要把桌面上的积木推到红色圆形区域里红色区域每次开局都可能换一个位置智能体必须根据当前目标位置来调整推的方向和力度。这种问题里环境给的奖励通常是稀疏的。多数情况下只要没把积木推到目标区域奖励就一直是零一旦推进去了才给一个正奖励。这种极端稀疏的信号会带来一个严重问题随机探索时智能体几乎不可能碰巧完成目标所以整个训练过程中连“正向样本”都见不到。回想一下标准 DQN 或者 DDPG 的训练机制它们都依赖经验池里有足够多的正样本来传播奖励如果一整个训练轮次里一个正样本都没有梯度更新基本就是原地打转。我刚开始踩这个坑的时候试着调大探索噪声、提高学习率、增加网络容量都没什么用。原因不是优化器不给力而是“正样本缺失”这个结构性问题没有解决。也就是说在目标导向的任务里常规的经验回放机制失效了它要求经验本身带有有价值的目标信息但稀疏奖励任务中大量经验的目标都是“失败”的这些失败经验几乎不携带学习信号。1.2 HER 的核心思想把失败经验重新“翻译”成成功经验HER 的出发点非常朴素在一段 rollout 里虽然智能体没有达成它最初想要的目标 g但它实际上“达成”了一个真实发生的状态——也就是眼前的 achieved goal g。既然已经做出了这个状态为什么不把这段轨迹当作“目标恰好是 g”的成功轨迹来学习呢举个例子机械臂尝试把积木推去位置 A结果最后积木落在位置 B。原来的目标是 A奖励是 0如果我们将目标重新标记为 B那么这一次行为“达成目标”了奖励变成 1。HER 就是把这句“把目标换成当前已经实现的状态”用一个采样策略落到算法里利用这些重标记后的成功经验来训练策略。这里要注意HER 并没有改变环境本身也没有改变真实目标分布它只是在训练时“篡改”了经验池中的目标字段和奖励字段。这种篡改之所以合理是因为目标导向策略本质上是一个条件策略用不同的目标去查询应该给出不同的动作。只要重标记后的目标仍然是一个合法的目标那么这段轨迹对学习“如何达成该目标”就是有信息量的。1.3 从公式到代码为什么用代码学起来更扎实说实话看公式的时候HER 的 idea 我五分钟就能理解替换目标、重算奖励、存进 buffer。但真到了写代码问题一个一个冒出来目标重标记应该在采样时做还是在存储时做一条 episode 里的 transition 是不是可以被不同目标重复使用done 信号要不要跟着目标一起重算这些细节公式里往往一笔带过但代码里每一个都决定算法能不能收敛。所以我建议想深入算法的人不要只看理论文章最好自己动手把关键模块敲一遍。代码能帮你把抽象的数学关系落到具体的 tensor 形状和变量赋值上。一旦你见过“原来的 goal 是 (0.5, 0.1, 0.3)重标记后的 goal 变成 (0.42, 0.15, 0.28)奖励从 0 变成 1”这个真实过程你就再也不会忘记 HER 的工作原理了。2. PyTorch 版 HER 的整体设计环境、结构、算法选型2.1 环境选择先用 FetchPush再考虑轻量替代经典的 HER 论文Hindsight Experience Replay在 OpenAI Gym 的 Fetch 系列环境里做验证其中最常用的就是 FetchPush、FetchSlide、FetchPickAndPlace。这些环境都是 7 维状态、4 维连续动作奖励函数是“是否到达目标”的稀疏信号。稍微有点麻烦的是它们依赖 MuJoCo 物理引擎早期版本还需要 license安装起来特别让人头疼。我的建议是如果你想快速理解算法可以用一个更轻量的自定义环境比如二维平面上控制一个点到达随机目标位置状态量只有 2 或 4 维。这个环境大约几十行代码就能写出来方便你在计算图上调试变量形状。等把 HER 跑通后再切换到 FetchPush 验证完整效果。这样可以避免被环境安装劝退。如果电脑里已经能跑 MuJoCo那么直接用 FetchPush 也是没问题的注意选对 gym 和 mujoco 的版本就好。2.2 代码结构怎么划分模块职责先理清一个完整的 PyTorch HER 实现一般包括这几个模块models.py定义 Actor 和 Critic 网络。her_buffer.py实现支持目标重标记的经验池。agent.py封装 DDPG 智能体包含策略更新和目标网络软更新。train.py主训练循环负责采样、存储、重放与更新。evaluate.py定期评估成功率。我建议从一开始就把工程结构理清不要把所有内容都堆在一个 train.py 里。原因有两点第一调试时你能快速定位是网络问题、buffer 问题还是环境交互问题第二后面做扩展实验比如换成 SAC 或不同的重标记策略时不需要重写接口。代码首先是给人看的其次才是给机器跑的。2.3 算法选型为什么 HER 通常搭配 DDPGHER 是一个经验回放层面的技巧它可以套在不同的 off-policy 算法上。但绝大多数示例实现都选择 DDPG 作为底层算法原因很实际DDPG 是面向连续动作的 actor-critic 算法结构比 SAC、TD3 简单得多核心只有两个网络加一个经验池非常适合教学。而且 Fetch 系列环境的动作空间是连续的DQN 完全用不了DDPG 是最短的可行路径。当然用 TD3 或 SAC 做基础算法也能结合 HER而且通常会表现得更稳定只是网络多了几个超参数也更敏感。作为从代码学习的第一站我建议先用 DDPG 跑通完整流程之后再换更好的 base learner这样你才能分清楚“到底是 HER 在起作用还是底层的 SAC 更优秀”。3. HER 核心实现细节这一部分是整个代码的命门3.1 经验池设计按 episode 存储而不是按 transition普通强化学习里经验池一般以 transitions, a, r, s, done为单位每次训练时随机采样一小批。而 HER 必须按 episode 存储原因在于目标重标记需要在同一条轨迹内选择一个未来的状态作为新目标。如果你把 transition 打散存进一个公共 buffer这条信息就丢了。所以 HER 的 buffer 典型实现是先按 episode 缓存整个轨迹等一条 episode 跑完后再生成多条“重标记后的 transition”写入 buffer。这里有一个很重要的工程细节同一段 episode 可以被多次使用对应不同目标。比如你采样 future 策略的重标记时从同一个未来状态池中抽多个目标这样一条轨迹能扩展成好几条带不同目标的训练样本。经验池的容量因此要适当放大。我习惯把 episode 先缓存在一个临时列表里跑完后再统一处理避免在步进过程中反复操作 numpy 数组。3.2 目标重标记的实现future 策略怎么写HER 论文里研究了多种目标采样策略包括 final、future、episode 等。其中最常用、效果也最稳的是 future 策略对于轨迹中的第 t 步从 t 之后不包含 t的状态中随机挑一个位置 k然后用那个位置的 achieved_goal 作为新目标。这样做的好处是新目标一定是当前轨迹未来真实到达的状态能保证重标记后的轨迹在物理上是合理可达的。PyTorch 代码里这个逻辑并不复杂。假设 rollout 里有 n 步每个 step 记录的 observation、achieved_goal、desired_goal 都是数组那么核心函数大概长这样def sample_inplace_future(episode_transitions, k4, future_prob0.5): 对一条episode做基于future策略的目标重标记。 n len(episode_transitions) for t in range(n): if np.random.rand() future_prob: # 在 t1 到 n-1 之间选择一个未来索引 future_idx np.random.randint(t 1, n) new_goal episode_transitions[future_idx][achieved_goal] else: new_goal episode_transitions[t][desired_goal] # 直接用原始 transition 的 state、action、next_state # 但用 new_goal 重新计算奖励 r compute_reward(episode_transitions[t][achieved_goal_next], new_goal, episode_transitions[t][action]) ...关键在于奖励必须永远基于“目标与当前状态的距离”来计算而不是简单沿用环境返回的原始奖励。因为环境返回的奖励是对着原始目标算的目标一换奖励就必须重新算。同理done 标志也要用新目标重新判断是否已经到达目标。如果不重算训练就会出现“奖励说是成功done 却说你没成功”的矛盾信号策略会变得非常不稳定。3.3 网络结构把状态和目标拼在一起输入HER 的目标导向策略是一个条件策略所以在网络输入层面最自然的方式就是把 observation 和 desired_goal 拼成一个向量然后送进和普通 DDPG 一样的全连接网络。在 Fetch 系列环境里observation 通常包含 gripper 位置、物体位置、物体相对位置等信息desired_goal 是目标位置的向量二者拼接后维度大概是 25 维左右具体看环境版本。以 PyTorch 为例Actor 的网络结构可以写成class Actor(nn.Module): def __init__(self, obs_dim, goal_dim, action_dim, hid256): super().__init__() self.net nn.Sequential( nn.Linear(obs_dim goal_dim, hid), nn.ReLU(), nn.Linear(hid, hid), nn.ReLU(), nn.Linear(hid, action_dim), nn.Tanh() ) def forward(self, obs, goal): x torch.cat([obs, goal], dim-1) return self.net(x)Critic 的输入一般包括 state、action、goal 三部分。有的实现会把 action 单独过一层再和 stategoal 拼接但最简单的方式也是直接拼接然后再加一个 BatchNorm 或 LayerNorm 来稳定训练。DDPG 的 Q 网络对输入尺度很敏感实践经验是目标向量和状态向量如果量纲差距大可以先做标准化或者至少对拼接后的输入加一个 LayerNorm能明显改善收敛。3.4 训练循环与关键超参数HER 的主训练循环大体如下初始化环境和智能体。循环若干 episode每个 episode 内policy 根据当前 state 和 goal 输出动作环境返回下一个 state、reward、done、info。把整条 episode 的 transition 缓存起来。episode 结束后用 future 策略重标记目标生成多条 transition 写入 buffer。从 buffer 随机采样 batch更新 Critic 和 Actor。软更新 target 网络。定期评估成功率。超参数方面我给出一个我调得比较稳的组合供参考参数推荐值说明batch_size256经验池里轨迹很多batch 大一点有助稳定buffer_size1e6按 transition 数量算越多越好但不是越大越有用重标记比例50%一半原始目标一半 future 重标记目标future 采样窗口4在论文中就是 k4效果不错gamma0.98Fetch 类任务步数不长稍低一些反而更敏感tau0.05软更新系数我习惯比默认 0.005 略大policy 学习率1e-4Critic 学习率可以稍大比如 3e-4探索噪声高斯噪声 std0.2OU 噪声也行但高斯噪声简单省事记住这些超参数不是金科玉律只是帮你在跑通代码时少走弯路。真正的调参方向应该根据成功率曲线来调整。4. 实操记录把一个 HER 代码从零跑通4.1 第一步先让“普通”DDPG 在理想环境中跑起来很多同学一上来就同时调 DDPG 和 HER结果模型不收敛时根本无法判断哪部分出了问题。我的习惯是分两步走。第一步先不接 HER用全是成功样本的“理想环境”测试 DDPG 本身是否正常。怎么做呢可以在 FetchPush 环境里把目标直接设成初始物体位置附近这样环境几乎瞬间给正奖励确保经验池里的正样本占比很高。如果这种理想情况下 DDPG 都训不动那问题大概率出在网络结构或超参数上而不是 HER。第二步回到原始目标分布再接上 HER。这时候如果训练变差问题就出在 HER 的存储或重标记逻辑上。这种逐步递进的排查方式能帮你把 bug 隔离在一个很小的范围内。我自己第一次跑的时候就是因为跳过了第一步花了大量时间在调参结果最后发现是 Critic 的输入里少拼了一个 goal 维度导致整个 Q 网络根本学不到有用的东西。4.2 第二步验证重标记后的奖励和 done 是否正确HER 里最容易出 bug 的地方就是重标记后的奖励和 done 没有同步重算。我建议在训练脚本里加一段诊断代码手动构造一小段轨迹打印出重标记前后的 goal、reward、done 变化人工检查一遍。比如# 诊断代码仅在 debug 时运行 trans rollouts[0] old_goal trans[desired_goal][0] new_goal trans[achieved_goal][10] old_r trans[reward][0] new_r compute_reward(trans[achieved_goal_next][0], new_goal) print(fgoal change: {old_goal} - {new_goal}) print(freward change: {old_r} - {new_r}) print(fdone change: False - {is_success(new_goal)})如果发现 new_goal 和 new_r 不匹配比如目标明明等于 achieved_goal_next但 reward 还是 0那就要检查 compute_reward 是不是用了错误的“当前状态”来算距离。Fetch 环境里计算奖励用的是“下一时刻的物体位置”而不是当前时刻这个细节特别容易搞混。4.3 第三步观察成功率曲线而不是 loss 曲线在强化学习里loss 下降不等于学到了策略尤其是 HER 这种重标记机制下loss 更不可靠。重标记会让经验池里出现很多“假成功”样本尽管这些样本对训练有益但 Critic 的 loss 会因此发生诡异波动。所以我强烈建议训练过程中每 20 个 episode 就跑一次评估连续运行 100 个原始目标分布的 episode统计成功率。成功率曲线的形状很有代表性。训练刚开始成功率基本是 0这时千万不要怀疑代码有问题。HER 的特点是前期很慢等经验池里积累了足够的正样本后成功率会突然从 0.1 跳到 0.8 甚至更高呈现一个阶跃上升。如果你的成功率在前 1000 个 episode 里都没有任何动静再回去检查 buffer 的容量是否太小或者重标记概率是否设成了 0。4.4 第四步利用向量化环境提升采样效率PyTorch 的深度学习部分通常跑得很快真正慢的是环境交互。Fetch 系列环境依赖 MuJoCo单步仿真消耗较大如果只开一个环境训练一个像样的 HER 智能体可能要几个小时。我的经验是用 multiprocessing 并行开 4~8 个环境收集 rollout每条 episode 结束后汇总到同一个 replay buffer。加速之后训练时间能缩短 3 倍以上。不过并行环境也有代价那就是代码复杂度上去了。首次跑通代码的时候先别急着并行用单环境把逻辑摸清楚再考虑提速。免得出现“代码并行之后跑得飞起但每次结果完全不可复现”的尴尬局面。5. 常见问题与排查技巧实录5.1 Q 值和 reward 的尺度灾难DDPG 的 Critic 输出的是 Q 值如果 reward 的量级和网络权重初始化不合适Q 值很容易爆炸或塌缩。在 HER 里奖励往往只有 0 和 1或者 0 和 -1本身量级不大但 Q 网络在训练初期会输出很大的预测值导致 loss 巨大。我的经验是对最终输出层做小随机初始化或者对 Critic 输出加一个 tanh/缩放同时配合梯度裁剪能显著降低早期发散概率。5.2 重标记后的未来索引越界future 策略需要在 t1 到 n-1 之间采样如果索引写错很容易出现 index out of range。这种错误在 Python 里不会崩溃但可能导致目标永远是最后一个状态从而让策略只能学“推到任意终点”而不是“推到目标”。建议写一个简单的单元测试构造长度为 10 的假 episode断言所有重标记后的索引都满足 t index n。5.3 版本兼容性问题gym 从 0.20 到 0.26API 变动很大比如 reset 返回值从 state 变成了 (state, info)step 的返回值也从 4 个变成了 5 个。很多老代码因此无法直接运行。如果你用的是新版 gymnasium需要相应调整环境交互代码。建议在 requirements.txt 里固定 gym、mujoco_py、pytorch 的版本避免所谓的“可复现性”被环境版本悄悄破坏。5.4 经验池重复使用导致的过拟合如果重标记做得太多一条原始轨迹可能被复制出几十条变体经验池里大量样本来自同一条轨迹导致策略对某几条轨迹过拟合。我的做法是限制每个 episode 最多生成 2~3 个重标记目标并且在采样时做随机 shuffle同时让 buffer 容量保持在大约 1e6 级别这样同一条轨迹的变体在整个 buffer 中的占比不会太大。5.5 终极排查思路最小化复现如果你还是卡在某些诡异行为上我推荐一个很老套但有效的方法把环境换成只有 2 个状态和 1 个动作的超简单网格世界每一步都能看到完整的转移和奖励。然后把 HER 逻辑照搬过去跑。如果简单环境下 HER 有效说明算法实现没问题是环境或规模问题如果简单环境下也失败那就可以逐行 debug。在我看来从简单到复杂才是学习强化学习最踏实的路径。最后再分享一个我个人的实操体会HER 的代码量不大核心甚至只有几十行但每一个细节都值得反复推敲。从目标重标记的索引范围到 done 信号的重算再到经验池的容量设置所有看起来不起眼的小决定最终都会在成功率曲线上暴露出来。如果你也在从代码学习深度强化学习我建议你多花一点时间在复现和修改 HER 上不要只满足于调通别人的代码。你亲手把它重写一遍之后再回头看论文里的公式会有一种豁然开朗的感觉。后续你也可以试着把 HER 和 TD3 或 SAC 结合或者在更复杂的机器人操作环境里部署这些扩展都会比从零开始简单很多。