如果把强化学习比作爬山,稀疏奖励环境就像一座被云层锁死的山峰:只有在山顶踩到终点的那一瞬间,你才知道自己对不对,而在此之前,所有的中间状态都一片漆黑。传统算法在这种环境里几乎寸步难行,因为学习信号太稀缺,模型连“哪一步值得庆祝”都不知道。直到遇到 hindsight 这个思路,我才真正意识到,很多时候不是环境太难,而是我们没有学会用自己的失败去教育自己。
这篇文章要聊的,是我在复现和改造 hindsight experience replay(HER,后见之明经验回放)过程中的完整思考与踩坑记录。hindsight 是一套非常反直觉的训练理念:它允许智能体在没达成原定目标时,把实际到达的状态“伪装”成目标去学习。适用于任务型机器人控制、路径规划、带稀疏奖励的任意强化学习场景。如果你正在为奖励函数设计头疼,或者你的智能体总是在空荡荡的奖励信号里原地打转,这篇文章应该能让你少走几条弯路。
1. 整体设计拆解:为什么“失败”反而是最好的老师
1.1 稀疏奖励到底难在哪
先明确一下我要解决的问题。所谓的稀疏奖励,指的是智能体在执行一条很长的轨迹时,直到终止才收到一个非零反馈的环境。典型例子是:机器人手臂要从桌面抓取一个杯子,只有手指真正捏住杯子的瞬间才得到 +1,其余时间全部是 0;又比如翻转比特串,只有完全匹配目标比特串才返回 +1,否则什么都不给。
这种设定在真实世界里比比皆是,因为现实任务天然就是目标导向的。可对强化学习来说,这意味着样本利用率低得吓人。假设一条轨迹有 50 步,第一步开了个头,后面 49 步全被当作“0 步奖赏”,没有任何梯度信息,策略就只能靠随机的偶然命中去碰运气。简单比特翻转任务里,目标串越长,随机命中概率指数级下降,你再怎么增加采样量,标准 DQN、策略梯度依然崩得一塌糊涂。
传统缓解思路无非两条:一是手工雕刻奖励函数,给每一步一个密集的中间激励,比如“离终点越近给分越高”;二是做课程学习,从简单状态教起,慢慢提高难度。这两条路都能用,但都有代价。手工奖励函数往往是研究者对任务的主观预设,一旦换个环境,那套“经验”就未必通用;课程学习设计起来又繁琐,需要反复调难度曲线。
hindsight 提供的是第三条路:不再假装每一步必须有反馈,而是重新定义“什么算成功”。
1.2 后见之明经验回放的核心思路
HER 的英文全称是 Hindsight Experience Replay,核心观点很朴素:这条轨迹没达到原本设定的目标 g,但在接近终点的时候,模型实际到达了另一个状态 g'。如果我把训练时的目标从 g 临时替换成 g',那么这段数据立刻就从“失败样本”变成了“成功样本”。
举个例子会更直观。你想让机械臂抓红色球,但这次它抓到了蓝色球。传统强化学习就拿这条数据当作失败,把奖励写成 0,信息白白浪费。HER 会想:反正机械臂已经成功抓到了蓝色球,我干脆把这次 episode 的“目标”改成“抓蓝色球”再重放一次。这样算法就能从这段轨迹里学到“当目标恰好是蓝色球时,这个动作序列是正确的”,从而把稀疏信号转成密集的、可以反向传播的学习材料。
在实操时,这种目标替换不是只发生在最后一步,而是对整条轨迹都做。轨迹的每一步都会被存下来,替换时选择轨迹中某个未来的实际状态 s_t 作为虚拟目标,然后重新计算每一步的动作价值。这样一段失败轨迹可以被重放四次、八次甚至更多次,每次都对应一个不同的、已成事实的目标,学习效率自然陡增。
第一眼看,这会让人担心:把目标随意改掉,会不会教模型走捷径?其实不会。HER 不是让策略放弃原始目标,而是让它学会“状态-动作对”在不同目标语境下各自意味着什么。换个说法,它是在给算法补充“如果目标是这个,那刚才的动作就算成功”的数据,而不是把原有目标删掉。
1.3 为什么电脑需要“骗”自己才能学
我最初并不理解为什么这样的“欺骗”能让策略收敛,后来想明白了一个关键点:强化学习的困难,并不是某个具体目标任务本身有多难,而是有效经验的密度太低。人类摔倒了,也知道自己“没有走稳”,这种“反馈”本身就是信号。可机器不会做这种抽象归纳,它需要一个明确的目标标签来判断动作的好坏。
HER 的实质是在给这段经验“立一个新的牌坊”。它通过后见之明把“失败”重新归档成“成功”,既保存了轨迹的全部动作细节,又提供了正向奖励。这个逻辑放在真实世界里也不陌生——团队复盘时,我们经常说“当时目标是做 A,虽然没做成 A,但我们在过程中顺手做好了 B,你看 B 其实很有价值”。把 B 记成功劳,这种复盘方式能带来动力,也更有信息量。
在实现上,HER 必须和经验回放配合使用。离线经验库会存下每一句经历,训练时随机抽取一段,再在抽中的轨迹上选择虚拟目标,而不是在真实交互时替换目标。这个设计让数据集天然包含了大量来自失败经验的正向例子,从而显著提升了样本效率。可以说,HER 是最早把“失败是成功之母”这句老话变成可计算公式的算法。
2. 核心细节解析与实操要点
2.1 虚拟目标选择策略的四个选项
HER 论文中,重放目标不是随机选的,而是有一套精心设计的采样策略,常见的有四种:
- future:从轨迹中当前位置之后的某个状态作为目标;
- final:直接用轨迹最后的状态作为目标;
- episode:从轨迹中随机选一个状态作为目标;
- random:从整个经验池里随机挑一个状态作为目标。
我实验中比较稳的是 future 策略,因为它在“目标难度”和“目标有效信息量”之间取得了平衡。final 虽然实现最简单,但当轨迹很长时,最后状态和目标初始状态可能差得很远,等价于给模型布置了一个难度过高的任务;episode 虽然随机性大,但它是均匀采样的,很多时候选择的中间状态并不具备代表性。
具体来说,假如轨迹长度为 T,当前位置是 t,future 策略是从区间 [t, T-1] 里随机选一个时间步对应的观测状态作为替换目标。这样选出来的目标总是“未来可达”的,误差上界更小,训练也就更稳定。很多开源框架默认把 future 作为首选,不是没有原因的。
还有两个设置需要当心:一个是替换频率,论文推荐的重放比例是 4:1,也就是每段轨迹会额外重放 4 次,其中有 1 次用原始目标,有 4 次换掉目标再学;另一个是是否在原始目标和心理目标之间做策略,我习惯保留原始目标的重放,否则模型可能会越来越偏好那些“容易达成”的状态,导致原任务完不成。
2.2 经验回放库的数据结构设计
HER 依赖经验回放,但这里的经验池比 DQN 的稍微复杂一点。每条经验不仅仅保存四元组 (s, a, r, s'),还需要额外记录该轨迹对应的原始目标 g。读取时,我们要能取到轨迹级的长度和状态序列,才能做虚拟目标替换。
我用 Python 实现时用了 dict 列表,结构大概是:
{ "observations": trajectory_observations, # 每一时刻的环境状态 "actions": trajectory_actions, # 每一时刻执行的动作 "rewards": trajectory_rewards, # 原始奖励,基本都是 0 "next_observations": trajectory_nexts, # 下一步状态 "desired_goals": [original_goal] * T, # 原始目标复制一份 "achieved_goals": achieved_goal_seq # 每个时刻实际达到的状态 }这个结构有两个关键点:一是必须保留 achieved_goals 序列,因为后见之明是靠它来“生成”新目标的;二是奖励不能前热,初始 r 可以全部是 0,替换目标后我们要根据“替身目标”重新计算奖励,不能直接用存下来的旧奖励。
很多新手在实现时把 reward 也算进回放池,替换目标后忘记重新算,结果模型学到的完全是错乱信号,这个坑我栽了两次,才把逻辑彻底总结清楚。
2.3 奖励重算公式
当把目标替换成 g' 之后,奖励必须按照新目标重新生成。通用做法是二元奖励:判断替换后的目标是否达成。用函数表示为:
def compute_reward(achieved_goal, desired_goal, sparse=True): if sparse: return float(np.all(np.abs(achieved_goal - desired_goal) <= threshold)) else: # 也可以使用形如 -np.linalg.norm(achieved_goal - desired_goal) 的密集奖励 return -np.linalg.norm(achieved_goal - desired_goal)如果是连续型动作任务,比如机械臂抓取,目标是一个三维坐标,判断成功的阈值需要合理设定,比如距离小于 0.05 就认为成功;如果是 BitFlipping 离散任务,成功条件就是所有比特位完全一致。
这里其实还有一层哲学:HER 并不要求一定要用稀疏奖励来判断成功。你完全可以把奖励设计成“欧氏距离的负值”,这样替换目标后,轨迹每一步都有连续的、关于新目标的反馈信号。但论文的实验表明,HER 本身是给稀疏奖励环境设计的,通常还是保持简单二元信号,让算法自己去学“距离”的概念,效果反而更稳。
3. 实操过程与核心环境实现
3.1 环境选择:先用 BitFlipping 验证逻辑
在学习细节之前,我强烈建议先用玩具环境跑通逻辑,不要一上来就上机械臂仿真。我使用的是 OpenAI Gym 里经典的 BitFlipping 环境,状态由一串随机比特组成,动作为按位取反,目标是对应长度的目标比特串,只有完全一致才奖励 1。这个任务直观、快速、调试方便,还在 hindsight 原论文里被用作标准 benchmark。
如果你环境里没有现成的 wrappers,也可以自己写一个很短的环境类。核心只要实现 reset、step、compute_reward 三个接口即可。reset 时随机生成目标串和初始串,step 时对状态中某一位取反,然后判断是否等于目标,返回奖励。
用这个环境,你能在几分钟内看到 HER 带来的明显效果:不使用 HER 时,在较长的比特串上几乎无法提升成功率;使用 HER 后,曲线会像新手骑自行车一样,突然就从乱扭变成能跑起来。
3.2 一个最小可跑的 HER 训练框架
我为了验证思路,写了一个精简版 HER + DQN 的训练循环。代码只保留核心逻辑,方便你作为自己的脚手架:
import random import numpy as np from collections import deque class ReplayBuffer: def __init__(self, capacity=100000): self.buffer = deque(maxlen=capacity) def push(self, trajectory): self.buffer.append(trajectory) def sample(self, batch_size): return random.sample(self.buffer, min(batch_size, len(self.buffer))) def her_sample(trajectory, k=4): """对一条轨迹做目标替换,返回 k 条重放样本""" samples = [] for _ in range(k): t = random.randint(0, len(trajectory["states"]) - 1) # 从 t 之后选一个未来状态作为目标 goal_idx = random.randint(t, len(trajectory["states"]) - 1) fake_goal = trajectory["states"][goal_idx] for step in range(len(trajectory["states"])): state = trajectory["states"][step] action = trajectory["actions"][step] next_state = trajectory["next_states"][step] reward = float(trajectory["achieved"][step] == fake_goal) samples.append((state, action, reward, next_state, fake_goal)) return samples这段代码最核心的是 her_sample 函数。它先用 random 选一个“时间锚点” t,再从 [t, 终点] 中选某个未来状态当虚拟目标,然后把轨迹每一步都按这个虚拟目标重放。这里要特别注意,varying 目标的虚拟目标对每一步而言都是同一个,这样才能保证轨迹逻辑闭环。
实际跑起来,你会发现这个简单的替换逻辑,配合一层很浅的 Q 网络,在 BitFlipping 长度 20 的时候可以达到 90% 以上的成功率。原则上,你自己如果觉得替换目标之后奖励全为 1 反而不真实,还可以加一点噪声或者只替换部分轨迹,比如有 20% 的概率保留原始目标,其余 80% 做后见之明替换,这个比例可以作为一个超参数调优。
3.3 训练循环和评估指标
训练主循环可以用伪代码这样写:
for episode in range(5000): trajectory = env.run_episode(agent) buffer.push(trajectory) # HER:对每条轨迹额外生成虚拟目标经验 for fake in her_sample(trajectory, k=4): buffer.add(fake) batch = buffer.sample(batch_size=64) agent.update(batch) if episode % 100 == 0: eval_success = agent.evaluate(env, n=100) print(f"Episode {episode}: success rate={eval_success:.2f}")第 4 行到第 6 行就是 HER 的全部秘密,也是这篇文章的题眼所在。值得记住的是,buffer 里的样本不一定都是新的离线轨迹,你把替换后的目标也塞进去,但原始目标样本仍然保留,这两种数据混合训练,策略才能既学会“怎么成功”,又不忘记“自己本来要干什么”。
对评估的成功率,我用的是 100 局随机初始状态的平均成功率,不是在训练环境里直接看即时 reward。如果你也想复现,我的建议是单独写一个评估函数,入场完全不调用任何探索策略,只使用当前 agent 的确认性动作,这样才能看到一个真实学习效果。
3.4 超参选择与训练曲线观察
hindsight 在超参选择上有几个地方比较敏感。首先是重放比例 k。论文使用的是 4,这个数字大概意味着对每条轨迹做 4 次虚拟目标替换。如果 k 太小,后见之明提供的正向样本不够,训练很容易回到稀疏困境;如果 k 过大,比如设置成 32,会占用大量显存和内存,也会导致模型过度关注“后见之明目标”,原生目标反而被稀释。
其次是 buffer 尺寸。HER 的 buffer 保存的是完整轨迹,不是单步样本,所以内存压力比传统方法大很多。我建议容量至少要够存 100 条完整轨迹,不然容易出现“学一段忘一段”的现象。
训练曲线方面,我踩过最典型的坑是“前期暴涨、中期卡壳、后期乱抖”。前期暴涨是因为后见之明让模型迅速学到了一些简单策略,比如一口气把某个比特翻转到位;中期卡壳是因为简单策略已经饱和,而复杂策略需要更多探索;后期乱抖通常是我把学习率调得太高,导致 Q 值估计振荡。要是你在自己的实验里看到这种三段式曲线,不用慌,先调低学习率,再考虑增大 k,总比东搞西搞找不到北强。
4. 常见问题与排查技巧实录
4.1 训练崩溃:NaN 和极端 Q 值
我在跑 HER 任务时,经常会遇到训练崩溃的问题,表现是 loss 突然变成 NaN,或 Q 值跑到几千几万。排除常见的网络结构或学习率问题后,十有八九是奖励重算时出的错。比如用浮点数比较两个坐标时,如果阈值设成了 0,就会出现大部分样本奖励为 0、偶尔奖励为 1,价值函数很难学稳定。
排查方法很简单:每个训练周期打印一条采样出的奖励分布,看看替换目标后到底有多少奖励是正的。如果正奖励比例低于 5%,大概率是虚拟目标取得太偏或成功判定太严;比例高于 50%,说明你的虚拟目标太容易达成,模型学不到真正有用的动作细节。
聚合这两个方向的调节,我一般会把成功阈值设成目标空间特征宽度的 1/20 到 1/5,并在经验池里维护一个滑动窗口统计正负奖励占比。
4.2 替换目标后策略“忘记”原目标
HER 最容易被新手误伤的问题,不是学不到东西,而是模型越学越“满足现状”:在 BitFlipping 任务里,它学会了翻转到一半就停止,因为后见之明把“一半状态”也标成了成功。这种偏差的出现,关键在于 original goal 的重放比例没有保证。
我沿用论文的做法,将训练数据切成两部分:一部分是原始目标下的轨迹,另一部分是替换目标后的轨迹。每个 batch 里原始目标数据的占比最好不低于 20%。如果发现模型“绕过”最终目标,把中间状态当终点,我优先把原始目标数据比例提升到 30% 或 40%,同时把 k 调小至 2,效果立竿见影。
另外还有一个更隐蔽的原因:虚拟目标可能会越选越“简单”。比如使用 final 策略时,轨迹终点常常就是最容易达成的那个,模型当然倾向于走向终点的方向。future 策略太激进时也存在这个隐患,建议在替换时对虚拟目标做一次限制,只允许选择距离当前状态一定范围内的未来状态,而不是随便挑一个远在天边的位置。
4.3 成功率在评估时远低于训练时
训练时动作包含随机探索率 epsilon,评估时如果把 epsilon 调成 0,可能表现变差,这在 HER 世界里尤其常见。原因是 HER 的目标替换逻辑让模型学会了对“随机探索动作”做补偿,但真正的确认性策略并没有被认真训练好。
解决思路有两种:一是在训练时就采用确定性策略加噪声动作,比如 DDPG 里的做法:动作 = 真实策略输出 + OU 噪声,而不是用 epsilon-greedy 随机选择。二是评估时不要只跑一次,而是对同一初始状态跑 5 遍,取平均成功率,因为很多任务本身有随机初始噪声,单次评估波动太大。
我在调试时有一个习惯:在训练之后把模型静默运行 20 个 episode,把每一步的预测动作和实际动作差异打印出来。如果这种差异很大,优先怀疑目标替换导致的不一致;如果差异很小但成功率仍然低,那就得回头检查状态特征是否归一化了。
4.4 和环境 reward 函数的坑
最后一条,来自我踩过的比较深的坑:不要自己“好心”增加密集奖励。HER 的思想是让模型从稀疏奖励中学会目标达成,一旦你在环境里自己加了每一步的中间奖励,比如“接近目标给 0.1 分”,整个后见之明的体验回放就会被扭曲,因为模型会开始拥戴那些“在原始轨道上接近目标”的行为,反而忽略了真正的目标达成。
大部分真实案例里,奖励函数越干净,HER 效果越拔腿扎实。哪怕你觉得给一点点中间奖励能加速,在一开始也不要那么干,等验证了 HER 本身有效之后,再增量式地评估是否真的需要中途奖励。这也是为什么 hindsight 这个项目我特别推崇——它给你的是一个“重新定义成功”的机会,而不是让你自己想破脑袋去模拟工具人。
根据我个人的实操经验,HER 这类方法最先要战胜的往往不是算法,而是我们自己的直觉。我一开始也总觉得“失败数据就该是失败”,可后来看到纯靠目标替换就能把稀疏任务跑出来的效果,这种直觉就被彻底刷新了。如果你也想做强化学习里的稀疏奖励任务,我的建议是把 hindsight 作为第一优先级去试,绝大多数情况下,它能用最少的方法带来最实在的提升。