☰
HER算法实战:目标重标注如何破解强化学习稀疏奖励难题
2026/10/1 14:10:00 网站建设 项目流程

1. 从"后见之明"说起:这个词在技术圈到底指什么

hindsight 直译过来是"后见之明",好听点叫"事后复盘",难听点就是"事后诸葛亮"。有意思的是,技术圈里至少有三种完全不同的东西都叫这个名字。前端领域它曾是 Google 提过的一种高容量本地存储方案,那时候 Chrome 想做一个比 localStorage 更简单的大容量 API,后来无疾而终;交互设计领域它是可用性测试里常用的 retrospective think-aloud,让用户回看操作录像、边看边复盘自己当时的想法;而强化学习领域,它是一项改变训练效率的经典算法——Hindsight Experience Replay,缩写 HER。今天要聊的是第三个,但它的思考方式其实跟前两个一脉相承:都是利用"事后才知道的信息",回头改造原来的过程。

HER 这个名字起得非常妙。你设了一个目标,让智能体去够,它没够到,但它在失败的过程中其实"碰巧完成了另一个目标"。如果事后我们把这个碰巧达成的状态当作目标重新训练,原本的失败轨迹就变成了一条成功轨迹。这个思路 2018 年被 Andrychowicz 等人写成论文发表在 NeurIPS,论文标题就叫《Hindsight Experience Replay》。当时它解决的是一个让无数强化学习工程师头疼的顽疾:稀疏奖励任务里,智能体随机探索半天一个正样本都遇不到,训练过程完全停滞。

这篇文章不是给你念论文翻译,而是把我自己实际跑 HER 时理解透的东西、踩过的坑、验证过的工程细节全部讲清楚。适合三类人看:刚入门强化学习、想搞清楚 HER 到底是什么的小白;已经在用 DDPG、PPO、SAC 这类算法、想把 HER 接进自己代码里的工程师;以及做机械臂抓取、路径规划这类连续控制任务、天天对着"奖励一直为零"发愁的算法同学。

1.1 三种不同语境下的 hindsight,为什么我一直强调这个词

先说存储 API 和交互设计这两个分支,是为了避免你搜资料的时候被带偏。你搜 "hindsight API",搜出来的可能是一堆过时浏览器提案;搜 "hindsight usability",出来的是用户体验测试方法。只有在强化学习上下文里,"hindsight" 才指经验回放技巧。所以对初学者,第一步不是看公式,而是先确认你搜到的资料跟你想学的是不是同一个东西。

交互设计里的"回顾式出声思考"其实最能帮助我们建立直觉:用户在完成任务后回看屏幕录像,边看边说"我这一步本来想点哪里""我以为这个按钮是干嘛的"。这些事后吐露的信息,比操作过程中的实时反应更完整,因为人在操作瞬间往往来不及解释自己的意图。HER 的思路与之惊人相似:智能体在 Episode 运行中根本来不及"思考"目标是否合理,只有跑完了我们才能回头说"你刚才其实成功到达了这里"。这种"事后补标签"的哲学,就是 HER 的灵魂。

1.2 这篇文章要解决的核心问题

HER 的官方定义很短:一种用于多目标强化学习的经验回放技巧,通过目标重标注(goal relabeling)把失败的 Episode 转变成有学习信号的数据。但定义短不代表好懂。绝大多数人看完公式后依旧不知道代码该怎么写:重标目标到底选哪个状态?奖励要不要重新算?done 要不要跟着改?重标后的样本跟原始样本按什么比例混合?这些问题论文里不会手把手教你,只有自己把代码写一遍、实验跑崩几次,才会真正记住。

我写这篇文章的目标很直接:让你看完之后能自己动手实现一个最小可运行的 HER,并理解每个设计选择背后的原因。我会从稀疏奖励这个痛点讲起,拆解目标重标注的原理,给出可复现的 PyTorch 代码框架,再列一份我在实际工程里整理的踩坑清单。读完全文你至少能明白:为什么 HER 能奏效,在什么条件下它会失效,以及怎么把它接到你自己的算法里。

2. HER 是怎么把"失败"变"教学样本"的:核心原理解析

2.1 稀疏奖励问题到底难在哪

先看一个典型困境。假设你训练一个机械臂去抓取桌面上一个位置随机的小方块,奖励设置很"稀疏":只有机械爪成功抓住方块并抬到指定高度时,给 +1,其余每一步都是 0。这个设定非常自然,因为它不需要人为设计中间奖励,但训练起来却极其痛苦。

原因在于强化学习的本质是"由奖励驱动的试错"。随机初始化的策略在绝大多数情况下碰不到方块,于是你收集到的所有经验,奖励都是 0。Q 网络的更新公式里,目标值是 r + γ max Q(s', a'),当 r 恒为 0 时,网络只是在反复拟合"下一步的 Q 约等于当前 Q",根本学不到"哪个动作离成功更近"。就算你让智能体跑了几百万步,它连一次正反馈都没见过,梯度方向完全随机,策略自然原地踏步。

更麻烦的是,许多工程师遇到这种情况的第一反应是加 reward shaping,比如"离方块越近奖励越大"。这确实能缓解稀疏问题,但 shaping 函数的设计本身就是一门玄学:距离度量选欧氏距离还是曼哈顿距离?要不要考虑机械臂关节角度?奖励系数多大才不会压制真正目标的信号?我见过太多项目在 shaping 上一调就是几个月,最后策略学会了在目标附近"抖动"骗取 shaping 分数,真实任务成功率反而上不去。HER 的价值就在于:它不需要任何 shaping,直接让"失败经验"自己长出奖励来。

2.2 目标重标注(goal relabeling)的两步逻辑

HER 能成立的前提是:任务必须是"目标条件化"(goal-conditioned)的。换句话说,环境里存在一个明确的目标变量 g,策略的输入是 (状态, 目标),奖励函数 R(s, g) 负责判断"当前状态是否接近目标"。很多真实任务天然满足这个条件:机械臂抓取任务的目标是物体最终位置;导航任务的目标是目的地坐标;游戏任务是通关,但也可以改造成"到达某个关键点位"。

现在想象一个具体场景。智能体的目标是到达坐标 A = (2.0, 3.0),但它这一轮实际走了个弯,最终停在了 B = (1.5, 1.2)。从"目标 A"的视角看,这条轨迹完完全全是失败的,50 步里每一步奖励都是 0。但从"目标 B"的视角看呢?这个智能体最后就站在 B 点上,如果它一开始的目标就是 B,那么它确实成功了,最后一步应该给 +1。

HER 的核心操作就是把这个"视角切换"落到数据上:把轨迹中每一步的 goal 字段从 A 改成 B,并用 R(s, B) 重新计算每一步的奖励。于是原来一条零奖励的失败轨迹,变成了一条末尾带 +1 的成功轨迹。这就叫目标重标注,英文叫 goal relabeling。

但它不是把整条轨迹所有样本都简单粗暴地塞进 Buffer 就完事了。实际操作分两路:一路保留原始目标 A 的经验,让策略始终知道用户真正要的是什么;另一路生成重标目标 B 的新经验,用来提供学习信号。两条数据流混在一起,智能体既不会忘记真正的目标,又能从失败中获得梯度。这个"混合"的比例和采样策略,直接影响效果,后面代码部分我会细说。

2.3 为什么重标目标没有破坏 Q 学习的正确性

第一次接触 HER 的人都会有一个合理质疑:你把目标从 A 换成 B,这不是篡改环境吗?Q 学习的收敛性不就毁了吗?这里的关键在于,HER 并没有修改环境的转移函数和奖励结构,它只是额外增加了"对另一个目标的经验估计"。

打个比方。一个学生做题,题目要求解得 x=5,他算错了得到 x=3。老师改卷时不会说"啊你算出了 3,那这道题算你对"。但如果另外出两道题,一道问"3 的平方是多少",一道问"2+1 等于多少",学生可能都能答对。错误答案本身包含正确信息,只是对应的题目不同。HER 做的不是把"解 x=5"改成"解 x=3",而是额外生成了一批以 x=3 为目标的新题目,用学生刚才的错误过程来回答这些新题目。对于 Q 函数而言,每个 (s, a, g') 的奖励都是根据既定规则 R(s, g') 重新计算的,数学上依然自洽。

严格来说,论文里论证了重标目标最好取自"与当前状态相对独立"的分布,所以"future 采样"(从未来时间步的状态里挑一个当目标)通常比"final 采样"(只用最后一个状态当目标)效果更稳定,因为时间距离越远,目标跟当前状态的相关性越低,越不会引入大的估计偏差。这个细节是 HER 实现效果好坏的分水岭,新手往往忽略,后面实操部分会再次强调。

3. 手把手实现 HER:一个可跑通的最小代码框架

3.1 环境准备:用二维 reaching 任务做试验田

在跳到机器人环境之前,强烈建议先在一个几秒钟就能跑完的最小任务上验证 HER 代码。我常用的是一个二维 reaching 环境:智能体是一个点,状态是它当前的二维坐标;目标是从屏幕随机位置采样出的一个二维点;动作是 x、y 方向的速度,范围 [-1, 1]。只有智能体在一步之后与目标的欧氏距离小于 0.1,奖励为 +1,否则为 0。每轮 Episode 最多 50 步,超时自动终止。

这个环境虽然简单,却完整包含了稀疏奖励的所有要素:随机探索很难精确落在一个半径 0.1 的小圆里,如果不加 HER,DDPG 大概率长时间学不出任何东西。你可以在它上面快速验证你的重标逻辑、调试网络结构,跑通后再迁移到 Fetch 这类机械臂环境。我把环境接口设计成跟 OpenAI Gym 的标准 goal-conditioned 接口一致:reset(goal=...)接收目标参数,step(action)返回(obs, reward, done, info)。这样后续接任何强化学习库都不需要改太多代码。

3.2 核心代码:her_relabel 与训练循环骨架

HER 的全部精髓都在一个函数里:对 Episode 里每个时间步,除了保留原始样本,再生成若干条以"未来实际达到状态"为目标的新样本。下面这段代码是我在项目里反复使用后精简出来的,你可以直接抄走改改用。

import numpy as np def her_relabel(episode, compute_reward, strategy='future', k=4): """ episode: list of (obs, act, reward, obs_next, done, goal) compute_reward(obs_next, goal) -> float strategy: 'future' / 'final' / 'episode' / 'random' """ horizon = len(episode) transitions = [] for t, (obs, act, _, obs_next, done, goal) in enumerate(episode): # 原始样本永远保留,让策略记住真实的用户目标 transitions.append((obs, act, compute_reward(obs_next, goal), obs_next, done, goal)) # 构造候选目标集合:从未来状态里挑 candidates = [] if strategy == 'future': for i in range(t + 1, horizon): candidates.append(episode[i][3]) # episode[i][3] 是 obs_next elif strategy == 'final': if horizon > 0: candidates = [episode[-1][3]] elif strategy == 'episode': for i in range(horizon): candidates.append(episode[i][3]) elif strategy == 'random': candidates = [sample_random_goal() for _ in range(horizon - t)] if not candidates: continue # 从候选中无放回抽 k 个,作为新目标 idxs = np.random.choice(len(candidates), size=min(k, len(candidates)), replace=False) for idx in idxs: new_goal = candidates[idx] new_reward = compute_reward(obs_next, new_goal) # 注意:done 保持不变,不因为重标成功就提前终止 transitions.append((obs, act, new_reward, obs_next, done, new_goal)) return transitions

然后是在训练循环里怎么用它。每跑完一个 Episode,把它交给her_relabel得到一批扩展后的样本,全部推进 replay buffer。网络训练时从 buffer 里随机采样一个 batch,每个样本都带有自己的 goal,更新Q(s, a, goal)和策略。

def train_one_episode(env, policy, replay_buffer, goal, her_k=4): episode = [] obs = env.reset(goal=goal) done = False while not done: act = policy.sample_action(obs, goal) # 加探索噪声 obs_next, reward, done, info = env.step(act) episode.append((obs, act, reward, obs_next, done, goal)) obs = obs_next if len(episode) >= 50: # 超时 break # HER 重标 extended = her_relabel(episode, env.compute_reward, strategy='future', k=her_k) for trans in extended: replay_buffer.push(*trans)

如果你用的是 PyTorch 写的 DDPG,batch 里通常包含obs、goal、act、reward、obs_next、done六个量。拿 Q 网络更新举例,输入要把状态和目标拼接起来:

import torch import torch.nn.functional as F # 采样 batch obs, goals, acts, rewards, obs_next, dones = replay_buffer.sample(batch_size) # 让 target Q 网络计算目标值 with torch.no_grad(): next_actions = target_policy(obs_next, goals) # actor 输入也拼接 goal q_next = target_q(obs_next, next_actions, goals) q_target = rewards + (1 - dones) * gamma * q_next q_loss = F.mse_loss(q(obs, acts, goals), q_target)

这里最容易犯的错是:Q 网络输入漏掉 goal。很多人从普通 DDPG 代码起步,网络输入只写了obs和act,完全忘了把goal拼进去,结果 HER 的样本即使换了目标,网络也根本感知不到差异,训练自然没有任何进展。记住三个输入位:Q(s, a, g)、actor(s, g)、target_Q(s', a', g),都不能少。

3.3 效果观察:加 HER 与不加 HER 的差别

把同一个 reaching 任务分别在普通 DDPG 和 DDPG+HER 下各跑一遍,你会看到近乎教科书式的差别。普通 DDPG 在几万步之内成功率往往一直是 0,Q loss 曲线看起来像一条几乎没有变化的水平线,因为所有 batch 里的 reward 都是 0,网络只是在学"输出一个固定值"。而 DDPG+HER 往往几千步之后就能看到成功率开始爬升,一开始是偶尔成功,然后稳步提高。

我自己实测下来的经验是:观察训练不能只看成功率,更值得盯的是 replay buffer 里"正样本比例"。HER 起作用的时候,同一个 batch 里会有一定比例的 reward=1 样本,Q 网络才能真正学到"某些状态是好的"。如果跑了很久 buffer 里正样本依然是 0,那说明重标逻辑根本没生效,得回头检查奖励函数或者未来状态索引有没有写错。

另外,我建议你做一个简单消融:把 strategy 从future改成final,看看成功率变化。在很多连续控制任务里,final策略因为只使用最后一个状态当目标,目标与中间状态的相关性高,效果往往不如future。这不是玄学,而是因为future随机性更强,相当于给数据做了更多元化采样。这也是论文里重点强调的发现。

4. HER 能用在哪些真实场景:工程应用与扩展思路

4.1 机器人操作任务里的经典用法

HER 目前最有名的应用场景是机械臂操作,尤其是 OpenAI 论文里那组 Fetch 环境:FetchReach、FetchPush、FetchPickAndPlace、FetchSlide。这些任务里机械臂自由度很高,随机策略几乎不可能直接把方块推到目标位置。论文给出的曲线显示,单纯用 DDPG 在这些任务上成功率接近 0,而 DDPG+HER 在训练后期能达到 90% 以上。我第一次看到那个对比图的时候,第一反应是"怎么可能差距这么大",但自己复现之后发现确实如此。

道理并不复杂:机械臂每推一次方块,方块都会停在一个新位置。从"要把方块推到目标点 P"的角度看,这一次尝试失败了;但从"方块现在在位置 Q"的角度看,这次尝试完美地把方块推到了 Q。HER 把这次失败经验转化成了"如何把方块从起始位置推到 Q"的成功经验,等于每一次试错都在为附近的目标点积累有效训练数据。在一个目标密集的任务空间里,学习效率自然暴涨。

值得提醒的是,真实机器人场景里 Episode 数据的收集成本很高,所以 HER 的"一鱼多吃"效果特别宝贵。我见过不少实际项目拿历史失败轨迹做离线 HER 重标,效果堪比额外的在线探索。只要你有足够多样的历史状态覆盖,重标出来的样本质量是有保证的。

4.2 HER 与其他强化学习算法的组合方式

HER 最初搭配的是 DDPG,但它并不挑食。只要是离策略(off-policy)算法,理论上都能接 HER:TD3、SAC、DQN(处理离散动作目标任务时)都可以。核心要求只有一个:你的经验可以进 replay buffer,并且可以重复采样训练。PPO、A2C 这类同策略(on-policy)算法就不太适合直接加 HER,因为重标改变了经验分布,同策略算法对数据来源有严格假设。

与 SAC 结合时,网络输入依然要拼上 goal。SAC 的更新公式里熵项、Q 项、策略项都要带上 goal 维度。我在实际项目里通常把obs和goal先分别过一个小编码器,再拼接起来,这样高维状态和稀疏目标能各自抽象一部分语义。另一个工程细节是,HER 的样本量会成倍增加,buffer 容量要给足,不然原始目标经验和重标目标经验互相挤掉,效果反而下降。

顺便提一句,如果你用的是 TD3,它本身双 Q 网络的设计可以缓解 Q 值过估计,而 HR 提供的正样本又能解决稀疏奖励问题,两者互补性很好。所以现在很多人直接把 TD3+HER 作为连续控制基线,而不是原来的 DDPG+HER。

4.3 状态空间和 goal 空间不一致时的处理

HER 最舒服的情况是 state 和 goal 在同一个空间,比如目标就是某个坐标位置,重标时直接把未来状态当作新目标就行。但真实任务不是都这么幸运。比如机械臂抓取任务的目标可能是"把红色物体抓起来放到蓝色区域",目标很难直接用机械臂状态表达;又比如导航任务的目标可能是"到达某个房间",而状态是激光雷达点云。

这时候重标不能直接拿状态当目标,而是需要一个从状态到目标空间的映射函数 φ(s)。例如机械臂任务里,你可以定义 φ(s) = 物体当前抓取位置,然后目标重标就是把未来状态的 φ 值当作新目标。逻辑上没变,只是多了一道转换。如果映射很难手工设计,也可以用学习到的编码器:训练一个 encoder 把高维观测映射成隐向量,goal 也映射到同一隐空间,再在隐空间里算奖励。这个思路在很多视觉机器人任务里已经被验证是可行的。

但这里也藏着 HER 的边界:如果成功条件根本无法从状态判断,或者目标空间离散且与实际达到的状态没有自然映射,HER 就失效了。比如"写一首关于秋天的诗"这种开放目标,你没法说"这首诗恰好到达了某个状态"。所以使用 HER 前,先问自己一个问题:我把未来时刻的状态映射成目标,奖励函数还能公允地判断成功吗?能,就可以上 HER;不能,就得换方案。

5. 踩坑实录:HER 实现中的常见问题与排查方法

5.1 重标之后 reward 没更新

这是我第一次自己写 HER 时犯的错误。当时我把原始样本直接复制,只改 goal 字段,忘记用新目标重新调用奖励函数。结果 replay buffer 里塞满了"目标明明是 B,奖励却还是 0"的样本。更坑的是这种错误不报错,训练也不崩,只是学习效率没有提升,让你怀疑 HER 是吹出来的。

排查手段非常简单:从 replay buffer 里随机抽一批样本,打印new_goal和reward,手动算一遍compute_reward(obs_next, new_goal),对比是否一致。只要不一致,说明重标后奖励没更新。统计上还有个小技巧,实时打印每个 batch 的正样本比例,HER 生效时这个比例应该随着训练上升,如果始终为 0,优先怀疑这里。

顺带说一个容易被忽略的细节:compute_reward里的阈值要跟环境真实判断成功用的阈值保持一致,否则会出现"HER 觉得成功了、真实环境判定没成功"的尴尬局面。让策略学到的"成功"和环境的"成功"不同频,后续评估肯定拉胯。

5.2 done 标志重标错误导致训练崩溃

这个坑非常隐蔽。很多实现为了图省事,重标后看到compute_reward为正,就顺手把done也改成 True。这在单步任务里问题不大,但在多步任务里是灾难。为什么?因为 Q 更新时done=1会截断未来回报,让目标值变成reward本身。重标样本本来就容易产生正奖励,如果同时把done置为 1,等于告诉策略"你到达这个新目标,任务就结束了,不用管后续"。

但真实环境里,智能体在 50 步之内不会因为到达某个中间位置而终止,它后面还可以继续运动。错误地把done设为 1 会让策略出现"到点就停"的惰性,甚至在真实评估时不敢走出成功区域。我在一次抓取任务里就吃过这个亏,训练曲线前几万步看着挺好,一换评估环境直接崩到 0。记住 HER 重标只改 goal 和 reward,永远不要动done。除非你的环境本身定义就是"达到任意状态立即结束",否则这条铁律不要破。

5.3 future 策略的索引与采样细节

future策略的代码看起来只有几行,但边界情况很多。比如在 Episode 的最后一步,range(t+1, horizon)是空的,候选集为空,此时应该直接跳过重标,而不是报索引越界。另一个常见问题是,如果你把obs_next存成了 tuple(比如(x, y)坐标),而 goal 需要 numpy array,直接拿 tuple 当 goal 会把维度搞混。我建议在环境返回的状态上统一套一层np.asarray(obs, dtype=np.float32),保证所有数据都是 numpy 数组。

采样数量 k 的选择也值得说道。k 太小,正样本密度不够;k 太大,buffer 里有大量重复度高且高度相关的新样本,训练效率不升反降。论文里常用 k=4,很多开源实现也默认 4,我自己做过小范围扫参,4 到 8 之间差别不大,但明显小于 2 或大于 16 都会让效果变差。另外,候选集较大时用np.random.choice(..., replace=False)无放回采样,避免同一个未来状态反复出现。

5.4 HER 和 on-policy 算法混用的误区

我见过有人把 PPO 训练循环里每个 Episode 都做 HER 重标,然后像 off-policy 那样重复训练几十轮,结果策略完全不收敛。根源在于 PPO 的损失函数依赖当前策略产生数据的概率比值,你用重标后的目标重新回放旧数据时,数据分布已经和当前策略不一致了,但 PPO 没有 off-policy 纠正机制,强行多轮利用会导致优化方向失真。

这不是说 on-policy 场景完全不能用 HER,而是不能像 off-policy 那样直接往经验池里堆。正确的姿势是:先用普通 on-policy 方式采样一批数据,重标后只用来额外训练一个"目标判别器"或"奖励模型",再把这些信号合并进 PPO 的奖励计算里。这已经是更高级的改法了,新手不要一上来就这么折腾。最稳妥的建议是:想体验 HER 的效果,直接用 DDPG、TD3 或 SAC 起步,先把离策略这条路走通再说。

6. 一点个人心得

我在实际项目里用 HER 解决过不少稀疏奖励问题,最大的感悟其实是思维方式的转变。以前看到一个任务学不动,第一反应永远是调奖励函数、加 shaping、设计课程学习,恨不得把"怎么算成功"这句话写进代码里。用了 HER 之后,我开始习惯反过来问:这次失败里,有没有一个隐含的成功?智能体没达到我指定的目标,但它到达了什么?能不能把这个"到达"变成教学信号?

这个小问题威力巨大。它不仅适用于强化学习,也适用于日常的模型迭代和数据处理:一次失败的实验、一批无标签的数据、一段跑偏的日志,背后往往藏着别处用得上的规律,关键在于你愿不愿意换个目标重新审视它们。HER 这个算法名字,本身就是给所有工程师的一句提醒——事后聪明不是贬义词,懂得从失败里重标目标,才是真正能落地的智慧。

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

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

立即咨询