提到 hindsight 这个词,搞机器学习的同行第一反应多半是 Hindsight Experience Replay,也就是常说的 HER。我第一次读 Andrychowicz 那篇论文的时候,觉得这个名字起得实在太妙——把“事后聪明”这个人类认知里非常普遍的毛病,直接变成一种提升强化学习采样效率的算法。说到底,就是让智能体从失败的轨迹里翻旧账:这次没抓到目标不要紧,把目标换成“实际到达的位置”,这条轨迹反而成了一条成功轨迹。如果孩子每次投篮没进都只得到“没进”的惩罚,他很难学会投篮;但如果他每次都记住“球最后飞到哪”,他会慢慢建立起“朝那个方向用这个力度”的手感。HER 干的就是这件事。
这篇文章我会从概念讲起,把 HER 为什么能解决稀疏奖励问题、网络和代码怎么搭、参数怎么调、以及实际跑训练会踩哪些坑都摊开讲一遍。适合正在做机器人控制、抓取、导航这类 goal-conditioned 强化学习任务的朋友,也适合刚接触稀疏奖励问题、想知道除了奖励塑形和课程学习还有哪些思路的入门读者。我会尽量写得像操作笔记一样,你照着做基本能复现出可用的训练曲线。
1. 项目解读:这个“hindsight”到底在做什么
1.1 为什么用“事后聪明”来命名一个算法项目
先扯一下这个词本身。hindsight 的字面意思是“后见之明”,日常语境里常带点自嘲:事情发生之后,谁都能看清当时该怎么做。放在强化学习里,这个词精准得让人拍大腿——因为 HER 的核心动作,就是在 trajectory 结束之后,回头把“当初设定的目标”替换成“后来实际达到的状态”,然后用这个新的目标重新解释整段经历。
我见过不少第一次接触 HER 的人,都会卡在这个直觉上:替换目标之后,这条轨迹还是原来那条轨迹吗?答案是物理轨迹没变,但学习信号完全变了。原来每一步 reward 都是 -1,因为最终没达成目标;替换目标之后,轨迹终点恰好就是新目标所在位置,所以最后一步 reward 变成 0,前面的步骤也因为“离目标越来越近”而有了明确的改进方向。原本一条毫无信息量的失败轨迹,经过这次“事后重写”,就变成了一条教会智能体“如何逼近一个目标”的正样本。
这正是 HER 这个名字的妙处:它不是让智能体变聪明,而是让智能体学会像人一样“事后复盘”。人犯了错会反思“要是当时这么做就好了”,HER 直接把这个反思过程自动化,而且不需要任何额外的人工标注。
1.2 HER 解决的核心痛点:稀疏奖励
为什么需要这种“事后反思”?因为强化学习里最难啃的骨头之一就是稀疏奖励问题。很多真实任务,比如机械臂抓取、移动机器人导航、游戏里找钥匙开门,环境只在任务真正完成的时候给一个非零奖励,其他所有时刻都是 0,甚至是 -1。在这种设定下,智能体随机探索一百步,九十九步都得不到任何正反馈,它根本不知道往哪个方向动才能离奖励更近。
传统解法有三板斧。第一板斧是奖励塑形,也就是人为设计一个中间奖励函数,比如“距离目标越近奖励越高”。但奖励塑形最大的坑是容易学歪:智能体会找到让你给的函数数值最大、但任务根本没完成的投机行为,而且设计奖励函数本身非常耗时,换一个任务就要重新设计。第二板斧是课程学习,先让智能体学简单任务再慢慢变难,但课程难度的自动调整也是个难题,设计不好反而拖慢训练。第三板斧是模仿学习,需要大量专家演示数据,很多场景根本拿不到。
HER 绕开了这三条路。它不修改环境奖励,不设计课程,也不需要专家数据,仅仅靠修改 replay buffer 里数据的“解释方式”,就把稀疏奖励变成了密集且有效的学习信号。这个思路听起来简单,但它对采样效率的提升是实打实的,在 Fetch 系列机器人操作环境上,HER 配合 DDPG 可以把成功率从几乎为零拉到 80% 以上。
1.3 适用边界:什么任务真正需要 HER
说句实在话,HER 不是万金油。它适用的任务有一个关键前提:必须能定义一个“goal”,而且这个 goal 必须是可观测、可判定的。比如机械臂抓取,goal 就是“物体位置到达目标点”,这个可以用物体当前位置和 desired goal 的欧氏距离来判断;又比如迷宫导航,goal 就是“智能体到达终点坐标”。
反过来,如果你手里的任务根本没有清晰的目标定义——比如“让对话更自然”“让画面风格更高级”——那 HER 就很难直接套用,因为你说不清楚“实际到达的状态”到底算不算完成了目标。还有一个隐性的适用条件:任务最好允许 agent 在 episode 结束前“无限接近目标”。如果任务是二值的,比如“按按钮成功或失败”,失败之后智能体什么都没做到,那 hindsight 重写出来的目标也接近随机噪声,帮助有限。
另外提一句,HER 和 off-policy 算法是天然搭档,因为它依赖 replay buffer 做目标重标注;on-policy 算法比如 PPO 用起来就比较别扭,虽然可以硬套,但收益远不如 DDPG、SAC 这类算法明显。后面我讲实操也默认是 HER + DDPG 的组合。
2. HER 的核心原理:让智能体从失败里“翻旧账”
2.1 目标重标注:把“没做到”改成“我做到了”
先看数学上的操作。标准的 goal-conditioned 强化学习里,一条轨迹由一系列状态、动作、目标、奖励组成。智能体在当前状态 s_t 下,面对目标 g,执行动作 a_t,得到奖励 r_t = R(s_t, a_t, g),然后转移到 s_{t+1}。当目标没有达成时,奖励恒定是 -1 或者 0,学习信号非常虚。
HER 在 trajectory 结束之后,额外采样一个新目标 g',这个 g' 从轨迹中某个实际到达的状态里取,比如轨迹终点 s_T 的位置。然后整条轨迹的 reward 全部用 g' 重新计算一遍:
def recompute_reward_with_goal(state, goal): # 以机械臂抓取为例,距离小于阈值算成功 distance = np.linalg.norm(state - goal) return 0.0 if distance <= 0.05 else -1.0 # 原始轨迹: goal=g, 在 s_T 处失败 # 重标定轨迹: goal=g', s_T 距离 g' 为 0, 所以奖励不再是全 -1 for t in range(len(states)): rewards_new[t] = recompute_reward_with_goal(states[t], g_prime)注意一个细节:替换 g 之后,轨迹里的 s_t 和 a_t 一个都不变,变的只是“目标”这个条件。也就是说,同一个物理过程,被“解释”成了另一个过程:本来是从起点乱撞一通最后失败,重标定之后变成了“不断逼近一个目标、最后一刻正好抵达”。
这就是 HER 最优雅的地方。它没有改变环境,没有改奖励函数,只改变了经验池里数据的语义。用大白话说,它让智能体从“失败”中提炼出了“阶段性成功”——你虽然没有达成原始目标,但你实际到的那个位置,本身就是你此刻能做到的最好结果;把它当作目标来学,就是在学“如何稳定复现当前水平”,这是能力提升的第一步。
这里有一个很容易踩的误区:重标定目标时不能随便从轨迹里挑一个状态。如果选的目标在轨迹起点就已经满足了,那这条“成功轨迹”里几乎所有步的奖励都改成 0,同样没有学习信号。所以目标通常要从轨迹中段或末段的位置采样,才能在“开始没达到、后来达到了”的过程中形成清晰的梯度。
2.2 四类虚拟目标采样策略
论文里给出了四种从轨迹中采样 g' 的方式,分别是 final、episode、random、future。我用实际效果排序说明一下,你可以直接抄作业。
final 策略最简单,拿轨迹最后一个状态作为 g',整个 episode 只重写一次。我的实际体验是它适合很早期或者轨迹特别短的任务,但一旦智能体学得好、大部分 episode 都成功时,hindsight 的增益就消失了,因为“最后状态”和“原始目标”往往已经重合。
episode 策略是从整条轨迹的所有状态里均匀采样一个作为 g'。这样能产生更多样化的虚拟目标,但有个风险:如果采样到的位置出现太早,可能出现“轨迹刚开始就已经达到目标”的假成功,需要额外过滤。
random 策略是从 replay buffer 里其他 episode 的状态中随机抽一个。这种策略的理论含义是让智能体知道“任何状态都能成为潜在目标”,但使用时要小心,它可能生成一些不太合理的组合,导致 critic 学习难度加大。
future 策略是我个人最推荐的,它要求采样位置在当前时刻 t 之后,也就是“从未来状态中取目标”。这样能天然保证轨迹在某个时间点之后达到目标,时间上的因果顺序也是合理的。baselines 的默认配置里,normal 模式就是用 future 的某个变体。实际项目里,我通常先用 final 跑通流程,再切到 future 配合一个合适的 k 值来提升最终成功率。
2.3 为什么 HER 能提升采样效率:一条轨迹的多次复用
聊采样效率之前,先做个简单对比。普通 DDPG 在稀疏奖励环境里,一条失败轨迹提供给 critic 的都是负样本,actor 的梯度更新方向几乎是随机的。HER 把一条轨迹额外改写 k 次,意味着同一条物理轨迹可以产生 k+1 条带不同目标的训练数据。一条本来要扔掉的失败记录,变成了 k+1 条对“逼近目标能力”的训练样本。
这个 k 值直接决定数据量的放大倍数。k=4 是比较常规的起点,论文实验里多数任务用 k=4 到 k=8 就能看到明显效果。我试过把 k 调到 16,成功率没有继续显著上升,训练时间倒是线性涨上去了,所以不建议盲目加大。从信息论角度说,一条轨迹里能提取出的“阶段性成功”信号是有上限的,超过这个上限,重复的目标只会让数据冗余。
另外要说清楚,HER 并不改变算法的 bias-variance 性质。它引入的“事后目标”会让价值函数学习到的是“在所有可能目标上的泛化能力”,而不是“仅仅在原始目标上的能力”。这就是为什么 HER 学出来的策略往往比普通 DDPG 更鲁棒:它在训练中见过各种各样的目标和对应的到达状态,对环境的动态特性理解得更充分。
3. 实操:从零训练一个会抓取的目标条件智能体
3.1 环境准备与依赖安装
实操环节我们直接用 OpenAI baselines 仓库里的 HER 实现,虽然这个仓库有点年头了,但它的实现干净、逻辑清晰,特别适合用来理解算法细节,比很多包装精美的现代框架更适合学习。你需要准备 Python 3.6 左右的解释器环境,以及一个能跑 MuJoCo 的机器。
# 建议用 conda 建一个干净环境 conda create -n her python=3.6 conda activate her # 安装 baselines 和配套依赖 git clone https://github.com/openai/baselines.git cd baselines pip install -e .Fetch 系列环境依赖 MuJoCo 和 mujoco-py。MuJoCo 现在已经对个人免费,安装流程比前几年简单多了,但 baselines 旧代码对新版 mujoco 的兼容性一般。如果你遇到 import mujoco_py 报错,通常需要装一个特定版本的 mujoco-py:
pip install mujoco-py==2.1.2.14装完之后先跑一个最小验证,确认环境能正常加载:
import gym import baselines.her env = gym.make('FetchReach-v1') obs = env.reset() print(env.observation_space) print(env.action_space)如果这一步不报错,说明环境链路是通的。FetchReach 是 HER 入门的最佳环境,动作空间只有 4 维,目标就是移动机械臂末端到一个 3D 坐标点,训练速度快,非常适合用来验证你的代码和参数是否正常。
3.2 理解观测与动作空间
Fetch 系列环境的 observation_space 不是简单的 Box,而是一个 Dict,里面包含三个字段:observation、achieved_goal、desired_goal。这是 HER 能工作的基础,因为算法必须能从 obs 里读出“当前实际到达的 goal”,才能进行目标重标注。
以 FetchReach 为例,observation 是机械臂末端的位姿和速度信息,achieved_goal 是末端的 3D 坐标,desired_goal 是目标点的 3D 坐标。对于 FetchPush 这类带物体的任务,achieved_goal 就是木块的位置,desired_goal 是木块的目标位置。
baselines 的 HER 实现会把这些字段拼成一个长向量作为网络的输入。具体拼接方式在代码里体现为将 observation 和 goal 拼在一起,再用一个全连接网络处理。有一个细节我要重点提醒:如果你自己写网络而不是用 baselines 封装,一定要保证输入里同时包含 achieved_goal 和 desired_goal 两个信息。critic 需要知道“当前实际在哪儿”以及“要求去哪儿”,才能判断“距离目标有多近”。很多人复现 HER 效果差,就是网络输入里漏了 achieved_goal,导致价值函数根本没法正确估计“到目标的距离”。
3.3 用 baselines 跑起一个 HER+DDPG 训练
baselines 里 HER 的实现和 DDPG 强耦合,入口可以直接通过 train 脚本启动。老仓库的用法是这样:
python -m baselines.her.experiment.train \ --env_name=FetchReach-v1 \ --n_epochs=50 \ --n_cycles=50 \ --num_cpu=1如果你不想直接用命令行脚本,也可以自己写一段 Python 调用逻辑,方便在 Jupyter 里调试。核心配置项在 experiment/config.py 里,比如:
# 以下是简化后的配置示意 config = { 'env_name': 'FetchReach-v1', 'batch_size': 256, # 每次更新采样的经验条数 'buffer_size': int(1e6), # replay buffer 容量 'gamma': 0.98, # 折扣因子 'tau': 0.05, # target 网络软更新系数 'actor_lr': 1e-3, 'critic_lr': 1e-3, 'k': 4, # 每条轨迹额外重标定的次数 'goal_selection_strategy': 'future', 'n_epochs': 50, 'n_cycles': 50, }我自己习惯把训练封装成一段循环,每一轮做四件事:采集 rollout、把轨迹存入 buffer、对轨迹做 HER 重标定、从 buffer 采样更新 actor 和 critic。baselines 的实现里,重标定发生在存入 buffer 之前,由sample_her_transitions函数完成,它会对每条轨迹额外生成 k 条“虚拟目标版本”的 transition 一起放进 replay buffer。
DDPG 部分的网络结构默认是 256-256 的两层全连接。actor 输出 4 维动作,用 tanh 限制在 [-1, 1] 区间,对应机械臂各关节的增量控制。critic 输入是拼接后的状态和目标,输出一个 Q 值。这些结构不需要改,训练前跑一两个 epoch 确保 loss 没有爆掉就好。
3.4 核心参数设置与表格
我把几个直接影响训练效果的核心参数整理成一张表,都是跑 Fetch 系列环境的经验值:
| 参数 | 推荐值 | 含义 | 调整心得 |
|---|---|---|---|
| k | 4 | 每条轨迹额外重写的目标数 | 4 起步够用,上 8 收益不大,16 纯浪费算力 |
| goal_selection_strategy | future | 虚拟目标采样策略 | future 最稳,final 适合快速验证管道 |
| batch_size | 256 | 每次更新的样本数 | 小任务 128 也行,但 256 更稳 |
| buffer_size | 1e6 | 经验池容量 | 不要设太小,否则目标重标定的多样性不够 |
| gamma | 0.98 | 折扣因子 | 机器人任务一般 0.95~0.99 |
| tau | 0.05 | 软更新系数 | 0.05 是 baselines 默认,别动太猛 |
| actor_lr / critic_lr | 1e-3 | 学习率 | 两个网络都 1e-3 起步,loss 震荡时降到 3e-4 |
| n_cycles | 50 | 每个 epoch 的采集轮数 | 控制训练时长,FetchReach 50 个 epoch 足够 |
| clip_range | 5 | 奖励裁剪范围 | baselines 默认,防止异常奖励干扰 |
这里最值得花心思调的是 k 和 goal_selection_strategy,其他参数基本沿用默认就能出结果。我见过太多人在 actor_lr 上纠结半天,结果发现 k 设成 1 才是成功率上不去的根本原因。
4. 训练曲线解读与调参心得
4.1 怎么看成功率的收敛曲线
训练跑起来之后,最重要的观测指标不是 loss,而是 evaluate 阶段算出来的成功率。baselines 的 train 脚本会在每个 epoch 结束后跑若干次确定性策略的 rollout,统计成功比例。FetchReach 上,大概 10 个 epoch 左右成功率就会爬到 0.5 以上,40 个 epoch 冲到 0.9 算是正常水平。
看曲线有几个要点。第一,成功率曲线初期可能有较长的“平台期”,看上去一点不动,这通常是 critic 还在学习距离函数,策略没有明确改进方向,不用慌。第二,中期曲线可能突然跳升,这是因为 critic 对“距离-价值”的建模突然变得准确,actor 的梯度从混沌中稳定下来。第三,成功率到 0.8 之后可能长期在 0.7~0.9 之间抖动,这时候优先检查是不是动作噪声太大,或者 explore 阶段的随机扰动没有衰减。
我踩过一次很典型的坑:训练到 30 个 epoch 成功率卡在 0.6 不动,我把 k 从 4 调到 6,再把 tau 从 0.05 调到 0.02,两个 epoch 内成功率就突破了 0.85。这两个参数一个增加有效样本量,一个让 target 网络更新更平滑,往往能解决“后期不涨”的问题。
4.2 参数优先级:先调哪个,后调哪个
面对一个不收敛的 HER 训练任务,我的排查顺序是这样的:先确认环境配置和 reward 函数没写错,再看网络输入是否包含 achieved_goal,然后调 k 和未来目标采样策略,最后才碰学习率和网络结构。
为什么把学习率放在后面?因为在这个项目里,成功率上不去的首要原因通常是“有效学习样本不足”,而不是“梯度更新步长不对”。你把学习率调得再精细,样本里全是“失败且无信息”的轨迹,actor 还是学不到东西。反而是把 k 加大、把目标重标定做好,直接解决样本质量的问题,效果立竿见影。
网络结构也不是主要瓶颈。256-256 的两层 MLP 在 Fetch 系列上足够用了。我有一次把网络加到 512-512 还加了 BatchNorm,结果训练不仅没变快,反而因为网络容量过大开始过拟合经验池里的噪声轨迹。记住:HER 的价值在于高效利用数据,不是靠大网络硬啃。
4.3 算力有限时的实用策略
很多朋友没有多 GPU 集群,只能用一台台式机或者云端 CPU 实例。这种情况下我的建议是:不要一上来就碰 FetchPickAndPlace,那玩意儿训练量很大,先跑 FetchReach 把整个流程走通。FetchReach 在单核 CPU 上大概一两个小时就能看到成功率上涨,完全可以用来验证代码正确性。
如果目标确实需要跑更复杂的任务,有几个省钱省时的做法。一个是降低 n_cycles,比如从 50 降到 20,先跑 10 个 epoch 看成功率有没有上涨的趋势,有趋势再拉长训练;另一个是调小 buffer_size 到 5e5,虽然理论上会影响性能,但能减少内存占用,让单机训练不那么吃力;还有一个是用已经训好的 FetchReach 模型做初始化,再迁移到 FetchPush 上,比从头训快不少。
这里我必须提醒一句:迁移初始化不是简单地把权重加载进去就完事,FetchReach 的动作空间和 FetchPush 一样是 4 维,但观测维度不同,输入层要改。更实用的做法是只加载 critic 的底层特征提取层,或者干脆把 FetchReach 作为“预训练经验”的来源,把它的 replay buffer 导出来灌进 FetchPush 的初始经验池,这个操作在 baselines 里没有现成接口,需要自己写几百行代码,但效果确实好。
5. 常见问题与排查技巧实录
5.1 训练很久成功率不上涨
这是被问得最多的问题。我的排查步骤很固定:先打印一条原始轨迹和一条重新标定后的轨迹,对比它们的 reward 序列。如果重标定后的 trajectory 的 reward 还是全 -1,说明目标重写环节出了问题——最常见的原因是从轨迹里采样的 g' 不在 agent 实际到达的位置上,或者 g' 的坐标没有正确对齐状态里的坐标单位。
另一个隐蔽的原因是 reward 阈值写错。Fetch 环境默认 0.05 的判定距离,如果你把环境自己包了一层,可能使用的是 0.01 或 0.1,导致重标定后“假成功”或“真失败”的数据分布和算法预期不一致。我建议新手先用原始环境跑通,再考虑自定义 reward。
还有一个场景是成功率卡在某个值长期不动,比如 FetchPush 卡在 0.4。这种往往是任务本身难度导致的价值函数局部最优,或者探索噪声不够。试着把 exploration 的高斯噪声标准差从 0.1 提到 0.2,让智能体多尝试不同的策略轨迹,有时候就能跳出平台期。
5.2 训练早期 NaN 或崩溃
NaN 问题在强化学习里十有八九是数值溢出。Fetch 环境的奖励是 -1 或 0,理论上不会爆 reward,所以 NaN 基本来自梯度爆炸。检查三个地方:网络是否用了不合适的初始化、critic 输入的 goal 和 observation 是否尺度差异过大、学习率是不是太高。
baselines 默认的输入没有做归一化,所以动作范围是 [-1, 1],observation 里某些维度可能是物体坐标,范围在 0~1 之间,这还好说;但如果你自己拼接了额外特征,比如机械臂角度、速度数值,一定要先做归一化。我的习惯是把所有输入压缩到 [-1, 1] 或 [0, 1] 再喂给网络,能省掉很多 NaN 烦恼。
另外注意 explorer 的噪声实现。baselines 用的是自适应参数噪声,有时候种子设置不当会导致噪声标准差一路涨到非常大的数值,action 超出物理约束,环境重置时崩溃。如果你在训练日志里看到 action 序列突然变成极端值,直接把噪声初始标准差调小一个量级。
5.3 复现已开源项目时的环境兼容问题
复现 baselines 老代码,最大的成本往往不是算法本身,而是环境版本兼容。gym 0.26 之后改了 reset 接口,老代码里env.reset()返回的还带 info,直接拿旧写法会报错;numpy 1.24 之后移除了np.bool,老代码里如果引用就会被绊住。我踩这些坑的时候,解决方案很粗暴:直接进入到 conda 环境里固定关键依赖版本,不要追求最新。
pip install numpy==1.19.5 gym==0.21.0 mujoco-py==2.1.2.14 tensorflow==1.15没错,这套组合拳打下来基本能稳。tensorflow 1.15 在 Python 3.6 环境下运行很流畅,baselines 的原始实现就是基于 TF 1.x 的。如果你实在不想用老版本 tensorflow,也可以用 PyTorch 重写 HER 的 researcher 逻辑,但那就不是“复现”而是“重写”了,工作量完全不一样。
有一个实用的小技巧:跑训练之前先跑通一个极短的 smoke test,比如 n_epochs=1、n_cycles=1,确保从环境采集、重标定、更新网络的整条链路是通的,再放心去跑长时间训练。这个习惯能帮你省下大量排查时间。
5.4 常见问题速查表
| 现象 | 直接原因 | 处理方案 |
|---|---|---|
| 重标定后奖励仍全为 -1 | 目标采样策略/位置不对 | 检查 g' 是否对应轨迹中实际状态 |
| 成功率长期为 0 | 网络输入缺少 achieved_goal | 确认输入拼接包含 achieved_goal 与 desired_goal |
| 训练中断/NaN | 梯度爆炸或噪声过大 | 降低学习率,缩小噪声 std,归一化输入 |
| 老代码 import 报错 | 环境版本新、接口变了 | 固定 numpy/gym/tensorflow 版本 |
| FetchPush 卡 0.4 | 探索不够或 k 偏小 | 提高噪声 std,k 从 4 调 6,检查采样策略 |
最后再分享一个小技巧。很多人在博客或论文里只看成功率最终值,却忽略了一个关键指标:确定性策略和带探索噪声的策略之间的差距。如果确定性策略成功率已经很高,但训练时因为噪声影响导致 reward 曲线一直波动,可以先调低噪声,而不是急着改网络结构。HER 的整个设计思路就是“从过往经历中提取改进信号”,只要你把这个信号源的闭环打通,后面所有调参都是在锦上添花。
我个人在实际操作中最深的体会是:HER 不是银弹,但它是稀疏奖励工具箱里最值得先试的一把锤子。面对一个 goal-conditioned 任务,先别急着设计复杂的奖励函数,先跑一版 HER,看看它能把采样效率提到什么程度。很多时候你以为需要更多智能、更复杂网络,其实只需要换个方式“看待过去”。