☰
稀疏奖励下的强化学习救星:HER算法原理、实现与应用实践
2026/10/2 14:47:21 网站建设 项目流程

hindsight 这个词,英文原意是"后见之明",也就是我们常说的"事后诸葛亮"。第一次看到它被用作算法名,是几年前翻到 OpenAI 那篇 Hindsight Experience Replay(HER)论文的时候,当时我愣了几秒:把"后见之明"用在一个稀疏奖励强化学习算法身上,实在太贴切了。HER 解决的是很多强化学习实践者都会撞到的那堵墙——环境只在任务完成时给奖励,其他时候全是零,智能体在巨大的动作空间里摸索半天,什么都学不到。它的做法是让智能体"重新解释"失败:既然刚才没有到达目标,那就把实际到达的地方当作目标,并据此把整条轨迹的奖励重写一遍。这是一个可以直接复现的思路,无论你是研究机器人抓取、自动驾驶决策,还是想做游戏 AI,都会用得上。这篇文章从原理到代码再到踩坑,分享我自己的完整实操记录。

1. 从一个单词说起:hindsight 到底解决了什么问题

1.1 稀疏奖励:那个让所有策略都失效的零分

我在刚接触强化学习那会儿,收到过不少"看起来很美好"的任务,比如让一个机械臂把方块推到目标位置。环境只会干一件非常残酷的事:如果你把方块推到了目标点,给你一个 +1;差一毫米,对不起,0 分。所有中间过程都没有任何奖励信号。也就是说,智能体要在一片茫茫的状态空间里,凭概率找到那种"一毫米都不能差"的动作序列。

这种 setup 带来的结果很尴尬:无论是 DQN、DDPG 还是 PPO,在训练初期都会因为几乎收不到正奖励而表现得像无头苍蝇。我们用普通 DDPG 在点目标环境里训练过,跑了上千个回合,成功率仍然接近 0。为什么?因为策略梯度依赖奖励信号来调整动作概率,如果 99% 的轨迹都是 0 奖励,网络根本不知道往哪个方向挪一步才是有利的。

有人会想到奖励塑形,就是自己给中间状态设计一个"距离目标越近奖励越大"的稠密奖励。这确实能缓解问题,但它有一个致命缺陷:奖励形状一旦设计得不对,智能体就会学会钻漏洞,比如绕着目标转圈但就是不推进,或者为了满足中间奖励做出完全不符合直觉的动作。而且很多真实任务里,中间奖励本身就难以定义:你怎么量化"一个倒车入库的动作进行到一半"是好是坏?

HER 当初吸引我,就是因为它不需要你多聪明地设计奖励。你只需要一个很简单的二值奖励,甚至不需要任何中间引导,剩下的它自己搞定。简单到让人怀疑:真能有用吗?事实证明,在大量目标导向任务里,它真的有效。

1.2 HER 这个名字为什么起得漂亮

"hindsight"在英文俗语里最有名的用法是hindsight is 20/20,意思是回头看的时候一切都是那么清晰。放在强化学习环境里,HER 做的事恰好就是让智能体"回头看"整条轨迹,然后把"本来想达成但没达成"的目标,替换成"实际达成"的目标。

举个例子。你训练一个机器人走迷宫,目标是到达出口,但它这次走到了一个死胡同。普通强化学习会觉得这条轨迹没有任何学习价值,因为最终没拿到奖励。HER 会怎么做?它会说:这条轨迹虽然没有到达人类定义的"出口",但它确实是从起点走到了那个死胡同。于是它把"死胡同"这个点标记成一个临时目标,把这次失败的轨迹"重新解释"为一次成功的到达训练。

这种方式看起来有点自欺欺人,但本质上是在告诉策略:你刚才那一系列动作,确实有能力完成"从一个地方走到另一个地方"这件事。只要这种能力被反复强化,当目标换成真正的出口时,智能体就已经具备了寻找出口的底层技能。名字起得妙,算法也漂亮。

2. HER 的核心思路:把失败也变成经验

2.1 一个还没到手的奖励,等于没有奖励

要理解 HER,先得理解它面对的问题:稀疏奖励下的探索困境。在大多数目标导向任务里,我们可以把环境抽象成三个要素:状态 s、目标 g、奖励函数 r(s, g)。奖励函数通常写成一个指示函数:只有当 s 与 g 足够接近时,输出 1,否则输出 0。

这样的奖励函数让经验回放机制失效了一半。普通强化学习从之前的经验样本里学到"哪个动作带来高价值",如果一条轨迹里所有 reward 都是 0,那这条轨迹对价值网络来说几乎是"全零教案",除了让网络更确定"这些状态没价值"之外,提供不了任何正面的因果信息。坏就坏在,这些"没价值"的状态里其实藏着大量有用的因果联系:从 A 走到 B 的动作序列是稳定的,只是 B 不是我们要的出口而已。

HER 从哲学上做了一次倒转:既然你说 B 不是目标,好,那我宣布它就是目标。这就解决了"没有奖励"的问题——因为一旦目标被替换成实际到达的状态,那么这条轨迹的最后一步自然就是"成功"的,奖励立刻变成 1。

你不用再担心智能体死循环,因为它的训练目标变成了"到达它已经到达过的地方"。这听上去像作弊,但这就是课程学习的本质:先学会简单的,再挑战难的。

2.2 事后重标:把"没做成"改写成"学会了到那里"

具体操作层面,HER 在每一条轨迹结束后,会额外生成若干条"重标样本"。假设一条轨迹由状态序列 s1, s2, ..., sT 组成,原本的目标是 g。训练时,算法先正常存储一条轨迹:每个 transition 都是 (s_t, a_t, r_t, s_t+1, g),其中 r_t 表示这个 step 是否完成了原始目标 g。

接着,算法从轨迹里挑出一个"事后目标" g',这个 g' 通常来自轨迹中出现过的某个状态,比如最后状态 sT。然后它再生成一组新 transition:除了目标从 g 变成 g' 之外,还要重新计算每个 step 的奖励 r'_t,计算公式变成:状态后一步 s_t+1 是否达到 g'。因为 g' 是轨迹中出现过的状态,至少轨迹的末尾一定满足了"接近 g'"这个条件,所以这条新轨迹里一定存在正奖励。

生成完重标样本之后,把它们和原始样本一起丢进经验回放池。策略和价值网络的更新,就同时从"原目标样本"和"事后目标样本"里学习。原目标样本让智能体不要忘记真正的任务是什么,事后目标样本则让它在频繁的零奖励环境中依然能获得学习信号。

这个过程可以用一个公式概括:对每条原始 transition,额外生成 k 个重标 transition,每个重标 transition 把原始目标 g 替换为 g',并把奖励按照新目标重新计算。k 通常取 4,这也是论文里的默认值。

2.3 从失败轨迹里挖出来的信号,为什么这么宝贵

我一开始有个疑问:既然重标目标都是"已经达到过的状态",那训练出来有什么意义?智能体不该只是一遍遍练习它已经会的事情吗?后来在实践中才想明白,这个看似无用的过程,其实是在给策略网络做"技能锚定"。

假设一个策略从没拿到过正奖励,它的输出和梯度之间的关系完全是乱的。HER 通过重标,让那些本应被丢弃的轨迹重新产生"\n好的,你刚才的动作序列导致你到达了这个位置,我给你正反馈"的信号。于是网络里面开始出现一些稳定的 actor 输出——至少它知道这些动作对应着某一个状态域是"有价值"的。

这也解释了为什么 HER 一般和 off-policy 算法搭配使用,比如 DDPG、TD3、SAC。因为 HER 需要把所有历史轨迹都存进经验回放池反复采样,on-policy 算法天生不兼容这种大量重放的模式。我用 DDPG + HER 跑导航任务时,明显感受到前期探索效率比普通 DDPG 高出一个量级,不是因为它更聪明,而是因为它能在一堆"零奖励轨迹"里榨出学习信号。

3. 复现 HER 前必须搞清楚的几个关键细节

3.1 状态与目标:分得清才能拆得散

HER 不是所有强化学习任务都能直接套用的,它对状态和目标的表示有一个隐藏要求:目标和状态必须能拆开。如果观察空间里已经混入目标信息,或者奖励函数依赖于一个不可分解的全局状态,那重标目标时就会很麻烦。

举个反例:如果状态 s 是"当前画面像素",目标 g 是"目标物体的一小块图像区块",那你可以把 g 单独作为网络输入的一部分,和 s 拼接在一起。这时重标就可行,因为你只需要替换 g 那一部分。但如果你把目标直接编码进环境状态里,让状态变成 s=[位置, 目标位置],那重标时就不只是替换目标向量,还要确保奖励计算逻辑从状态中正确读出新目标,否则网络会被搞糊涂。

我在实现时最常用的做法是:状态表示只包含与目标无关的"自车状态"(比如机械臂关节角、末端位置、速度),而目标 g 单独作为一个向量输入。两者在进入网络前拼接成一个长向量。这样重标时只需要换掉目标向量,然后按新目标重算奖励,非常简单清晰。

3.2 四种目标替换策略,别再只盯着 final

HER 原论文里给出了四种选择"事后目标 g'"的方法:

策略做法我实际测试的感受
final直接取轨迹最后一个状态作为目标最简单,但目标永远只有一个,样本多样性差
random从回放池里随机挑一个状态作为目标目标来源多样,但可能和当前轨迹关联太弱
episode从当前这段轨迹里随机挑一个状态比 final 丰富一些,但仍和这条轨迹强相关
future从当前轨迹中当前时刻之后的状态里随机挑一个最符合时序逻辑,学习信号最自然,推荐

很多新手刚接触 HER 时默认用 final,因为它最好理解。但我在自己的点目标环境里做了对比,final 策略虽然也能收敛,但需要的 episode 数量几乎是 future 策略的两倍。原因很简单:final 重标出来的样本,都指向同一个目标,而 future 会在一条轨迹里生成多个不同目标,让网络一次看到"到达不同地方"的多种成功经验,泛化能力自然更好。

3.3 奖励函数和网络结构设计

HER 的奖励函数适合用稀疏设定,也就是"到达目标给 1,没到给 0"。你可能会想,既然重标样本已经把目标改成已到达状态,那大部分样本奖励都是 1,会不会导致网络过拟合?不会。因为奖励函数仍然保持"距离阈值判断"的机制,比如判断距离是否小于 0.01,如果事后目标选得太远,有些过渡状态还是 0 奖励,网络依然能学到边界。

网络结构方面,我沿用 DDPG 的标准 Actor-Critic 架构。Actor 输入是拼接后的"状态+目标",输出动作;Critic 输入是"状态+目标+动作",输出 Q 值。两层隐藏层,每层 256 个神经元,激活函数用 ReLU。目标网络用软更新,系数 tau 取 0.005。整个结构不需要针对 HER 做任何特殊改造,HER 完全工作在采样层,网络只是负责正常 off-policy 更新。

有一点容易被忽略:Critic 的输入包含目标向量,这让 Q 函数变成了"在目标 g 下,状态 s 执行动作 a 的价值"。重标样本能起作用的前提,就是 Critic 能区分不同目标下的价值差异。所以你在搭建网络时,务必保证目标向量是作为一个独立的输入通道进入网络,而不是藏在某个全连接层里被过早压扁。

3.4 藏在论文角落里的几个超参数

HER 能跑通,很多时候靠的是几个容易被忽略的参数:

  • k:每条轨迹额外生成多少条重标轨迹。k=4 是常用值,太大容易让重标样本淹没有效的原始目标经验,太小又学不到东西。
  • 回放缓冲大小:我习惯设到 1e6。稀疏奖励环境下,你需要保留足够多的历史失败轨迹,否则重标之后样本多样性还是不够。
  • gamma:短 episode 任务可以设小一点,我用 0.9,太长会让价值估计过度平滑。
  • batch size:128 是比较稳的选择。

这些参数没有绝对最优,我的建议是固定其他条件,只动一个变量,然后看成功率曲线。很多复现失败不是因为算法逻辑错,而是因为这些参数搭配导致学习信号太弱或者太震荡。

4. 动手实现一个最小 HER 项目

4.1 环境选型:一个简单到可以手写代码的点目标环境

为了验证 HER 的效果,我没有直接跑现成的机器人仿真,而是自己写了一个 2D 点目标环境。很简单:智能体是一个点,初始位置在 (0, 0),目标位置随机生成在 [-1, 1] 区间内。每一步它输出两个动作值(x 方向速度、y 方向速度),速度被 clamp 到 [-1, 1]。状态就是当前坐标 (x, y),耗时超过 50 步或到达目标附近(欧氏距离 < 0.01)则本回合结束。

奖励设置:每一步给 0,到达目标给 +1。之前我还试过每步给一个很小的 -0.01 代价,结果发现会拖慢收敛,因为重标样本本身已经提供了稀疏的正信号,不需要额外惩罚来逼它走捷径。

这个环境看起来简单,但足够暴露问题:没有中间奖励,普通 DDPG 在里面几乎学不到任何策略。你把这个环境换成机械臂抓取,核心挑战完全一致,只是状态维度更高、目标更复杂而已。

4.2 核心实现:经验采样、目标替换、网络更新

下面这段是我在项目里实际用到的核心逻辑,压缩成了最小可读版本。重点是看两个环节:轨迹结束后如何生成重标样本,训练循环里如何采集数据。

import numpy as np from collections import deque class HERBuffer: def __init__(self, capacity=1000000): self.buffer = deque(maxlen=capacity) def store_transition(self, state, action, reward, next_state, goal, done): self.buffer.append((state, action, reward, next_state, goal, done)) def sample(self, batch_size): indices = np.random.choice(len(self.buffer), batch_size) return [self.buffer[i] for i in indices] def augment_trajectory_with_hindsight(trajectory, k=4): # trajectory: [(state, action, reward, next_state, goal, done), ...] augmented = [] states = [t[0] for t in trajectory] for _ in range(k): future_state_idx = np.random.randint(1, len(states)) new_goal = states[future_state_idx] for state, action, _, next_state, original_goal, done in trajectory: new_reward = 1.0 if np.linalg.norm(next_state - new_goal) < 0.01 else 0.0 augmented.append((state, action, new_reward, next_state, new_goal, done)) return augmented

训练循环里,每次跑完一条 episode 后把原始轨迹存进 HERBuffer,再调用augment_trajectory_with_hindsight把生成的重标样本也存进去。之后就是正常的 off-policy 更新,从 buffer 里随机采样一个 batch,分别更新 actor 和 critic。

我踩过的一个坑是:future_state_idx必须取当前时刻之后的某个状态,而不是随便从整条轨迹里抽。如果从轨迹开头抽一个状态作为新目标,那前面的 transition 在重标后可能会不满足时间因果,导致价值网络学到错误的自举关系。这就是为什么原论文里 future 策略是"在未来状态中随机选一个",而不是"从全轨迹随机选一个"。

4.3 训练效果对比:有 HER 与无 HER

我在同样环境、同样超参数下做了两组实验:一组用普通 DDPG,一组用 DDPG + HER。两组各跑 1500 个 episode,评价指标是最近 100 个 episode 的平均回合成功率。

实验结果非常鲜明:

训练方式前 300 回合成功率中 800 回合成功率1500 回合成功率
DDPG0%0%0%
DDPG + HER12%41%87%

普通 DDPG 几乎趴在地上不起身,因为它的价值网络只有在碰到目标时才会收到 +1,而 2D 连续状态空间里的随机动作碰到目标的概率实在太低了。HER 版本在前 300 回合就已经能看到零星的成功突破,这正是重标样本在起作用——它让网络在几乎没有原始正奖励的情况下,也获得了稳定梯度。

我后来又试了一个小改动:把每次 episode 的重标轨迹数量从 4 增加到 8,结果前 300 回合成功率提升到 17%,但 1500 回合时反而掉到了 79%。这说明重标样本过多会挤压原始目标样本的比例,让智能体太专注于"到达已到达的地方",忘了真正的任务目标。k 合适的才是好的。

4.4 我的参数配置和实测记录

这里是我最终稳定跑通的一组配置,可以直接抄:

参数值
算法DDPG + HER(future 策略)
k(重标轨迹数量)4
网络隐藏层256 x 256
激活函数ReLU
actor 学习率1e-3
critic 学习率1e-3
软更新系数 tau0.005
回放缓冲容量1e6
batch size128
gamma0.9
每次 episode 最大步数50
目标判定距离0.01

训练时我在每 100 个 episode 打印一次当前成功率曲线,同时记录 actor 输出幅度的变化。刚开始几百步,actor 输出会有一点混乱,这是正常的。如果看到 actor 输出慢慢变得有方向性,比如在目标在右下方时动作向量倾向右下,说明 HER 正在把重标信号转化为"走对方向"的偏好。我个人习惯以 1500 个 episode 作为一次完整实验周期,用时在单张 GTX 3060 上约 20 分钟,非常轻量。

5. 常见问题与踩坑实录

5.1 目标替换后损失不下降,梯度去哪了?

我有一次在跑 HER 时,发现 critic loss 从一开始就没怎么动,演员网络输出也像死水一样。排查了很久,最终发现是重标样本里的next_state和new_goal用的是同一个状态数组的引用,但因为环境是逐步 reset 的,旧数组在下一轮被覆盖,导致 buffer 里存的数据全变成了同一个值。

这种 bug 在 Python 里特别隐蔽,因为states[future_state_idx]返回的是 ndarray 的一行,如果你在后续代码里直接修改了state,原始 buffer 里的内容也会变。解决办法很简单:在存储之前调用np.copy()给每个状态和目标都做一份显式拷贝。现在我把这条原则写成了自己的代码规范:凡是存进经验回放池的数组,一律 copy。

5.2 训练初期就学会"躺平"怎么办?

这里说的"躺平",是智能体发现让 episode 尽快结束能获得相对更好的结果。比如我的环境里设定 50 步超时自动结束,有人还加了每步 -0.01 的惩罚,于是智能体学会了一直输出超大的速度,直接冲进边界外,回合快速结束,总惩罚反而更小。

HER 的一个好处是重标样本会在"加速到达某个状态"这个方向上提供正信号,但如果原始奖励里包含过大的惩罚项,这些正信号会被抵消,导致策略收敛到一个"什么都不做"的局部最优。我的建议是:HER 配合纯稀疏奖励使用,不要画蛇添足加中间惩罚。如果你确实需要限制动作幅度,最好通过 clip 动作输出本身,而不是靠奖励惩罚。

5.3 非目标状态学多了,反而拖慢收敛?

这其实是 HER 的另一个特性:重标样本让策略学会到达各种各样的"非目标状态",如果这些状态离我们真正关心的目标区域太远,智能体可能会花大量精力去精进"到达某个犄角旮旯"的技能,冲淡了原目标方向的训练。

怎么判断你遇到了这个问题?看训练曲线:如果前 500 回合成功率涨得很快,之后开始停滞甚至下降,大概率是因为重标样本中的目标分布太散。解决办法:优先用 future 策略而非 random,因为 future 至少保证了目标是从同一段轨迹中采样的,和当前策略的行为分布有一定关联;其次可以适当降低 k 值,减少重标样本在回放池中的占比。

5.4 踩坑速查表

现象可能原因我的解决办法
训练完全不变buffer 里存了数组引用,数据被覆盖存入前np.copy()
开始涨一点就崩溃actor 学习率过大,训练不稳定学习率降到 3e-4,或换用更稳定的 TD3
成功率上不去但 loss 正常目标阈值太严格,重标样本也大多 0 奖励把目标判定距离调大到 0.05 试试
后期过拟合原始目标原目标样本占比太小增加原始轨迹存入比例,或降低 k
训练特别慢回放缓冲太小,重标样本被频繁挤出缓冲容量加到 1e6

这些坑我每条都真实踩过,尤其数组引用那条,我一度以为是算法写错了,差点把整个网络推翻重来。所以如果你跑 HER 时遇到诡异问题,先检查 buffer,再检查奖励,最后才怀疑网络结构。

6. 一点个人体会和后续方向

我在一次次复现 HER 的过程中,越来越觉得"hindsight"这个词值得细品。机器学习和人生不少道理是相通的:我们当然希望每次都能直奔目标,但现实中大量行动注定会偏离预期。HER 的高明之处,就是不让这些"偏离"白费,而是立刻把偏过去的点当成新的参照,重新校准自己的动作。这跟我们做项目复盘、做季度总结时的思路几乎一模一样——先承认失败,再从失败里挖出可用的技能点。

如果你打算把 HER 用到更复杂的任务里,我的建议是从我这个 2D 点目标环境开始验证逻辑,确认能跑通之后,再迁移到机器人仿真平台。后续可以做两个方向的扩展:一是把 HER 和 SAC 结合,替代我这里的 DDPG,通常会得到更平滑的训练曲线;二是在策略里加入"目标自动生成"的模块,让智能体自己决定什么时候该把某个状态当成临时目标,而不是等一个回合结束后才整段重标。HER 的思想还能和 meta-learning、课程学习结合,算是它最有挖掘空间的地方之一。

说实话,我直到现在看到 HER 重标样本在训练曲线上激起的那些小峰值,还是觉得这个思路既简单又狡猾。希望这篇文章能帮你把它变成自己手里的一件趁手工具,也祝你在自己的项目里,能把那些看似无用的"失败轨迹"都变成后见之明的养料。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询