“hindsight”这个英文词,直译过来是“后见之明”,听起来总带点“事后诸葛”的意思。但在强化学习圈子里,只要聊到稀疏奖励、机器人抓取、多步决策这些话题,hindsight就会以另一个身份高频出现——Hindsight Experience Replay,也就是大名鼎鼎的HER算法。这篇博文就围绕这个名字展开,把HER是什么、解决了什么问题、怎么复现、有哪些坑一次讲清楚,适合正在入门强化学习、或者想在手头项目里解决“训练半天学不动”问题的算法工程师和研究者参考。
1. 先从“稀疏奖励”说起:HER到底拯救了什么
1.1 为什么机器人“抓个方块”会比看起来难一百倍
我最早看到“hindsight”这个词,是在OpenAI那篇关于机器人操作的研究里。当时第一反应是:这不就是人类再熟悉不过的“事后想想当时该怎么做”吗?但放到强化学习环境里,这背后是一个让人非常头疼的问题——稀疏奖励。
想象一下这样的场景:机械臂七自由度,尽头是一个夹爪,它要完成的任务是“把红色方块推到桌子上的指定位置”。这个过程中,环境只在最后一步给出奖励:成功接近目标就+0,差得太远就-1。在整个训练过程中,智能体需要连续输出几十个动作,但绝大多数情况下什么反馈都拿不到,只有“错”没有“对”。问题来了:如果每一步拿到的都是同一个失败信号,那么这些动作之间怎么比较优劣?策略网络怎么知道“往左偏了一点”比“完全没动”更接近目标?梯度信号根本没法区分每一步的好坏,等于没有梯度。
这就是强化学习里谈之色变的稀疏奖励问题。更麻烦的是,这种任务不是特例,反而是很多真实场景的常态:机器人抓取、环境探索、迷宫寻路、多轮任务型对话生成,早期阶段都很难遇到正奖励。传统经验回放在这个阶段基本处于“原地踏步”状态,因为回放池里存的全是“失败样本”,用它们算出来的TD误差趋近于零,策略参数更新的幅度微乎其微。
1.2 与其费心设计奖励,不如“换一个目标重新评价”
既然奖励太稀疏,很多人的第一反应是:做奖励塑形,给中间状态加分。这个思路本身没错,比如检测到夹爪靠近方块就加分,检测到方块移动就加分。但做过工程的人都知道,奖励函数一旦复杂起来,前后牵一发动全身,稍不留神就会触发reward hacking——智能体不做任务,专门找奖励函数的漏洞刷分。更别说有些任务的目标状态根本没法用简单规则描述,真想设计出好的稠密奖励,工程成本高得吓人。
HER换了个思路——既然精调奖励太麻烦,那干脆改数据。核心主张很朴素:这次任务的目标是A,但智能体最终做到的是B,那不如把样本里的目标A全部替换成B,重新计算奖励。这样一来,原本一条全程负奖励的失败轨迹,就变成了一条“成功轨迹”。在Hindsight Experience Replay的框架里,这个操作叫作轨迹重标注,或者说目标重标注。
举一个具体例子:机械臂想抓住红色方块放到坐标(1, 1, 1),结果推到半路方块滑出去,最终停在了(0.8, 1.1, 1.2)。如果坚持原目标,这条轨迹是100%的失败记录,除了浪费存储空间几乎没有训练价值。但HER会把目标改写成(0.8, 1.1, 1.2),然后重新审视整条轨迹:在这个过程中,机械臂其实始终在接近这个“新目标”,所以每一步都能变成正反馈样本。智能体学到的技能就被沉淀下来:虽然没完成原任务,但学会了如何把方块推向某个位置——这种能力在下一次尝试中非常可能继续沿用。
1.3 HER不是玄学,它依赖“以目标为条件”的策略假设
HER能做这个操作,前提是任务本身必须是一个“以目标为条件”的问题,也就是goal-conditioned RL。
什么意思?就是说,策略网络的输入除了当前状态s,还必须把目标g也拼进去。智能体学到的不是“看到这个状态就出这个动作”,而是“看到这个状态,并且我想要达成目标g,才出这个动作”。这样,同样的状态轨迹,放在目标A下可能毫无意义,放到目标B下就是标准训练样本。
这个逻辑其实在日常决策里特别常见:同样是一条走错路的历史轨迹,如果原计划是去火车站,它就是失败案例;但如果改一下目标,说“我就是想去公园散步”,那么之前所以为的“绕路”每一步都成了合理选择。HER就是把这个逻辑搬到了算法里,让回放池中的数据利用率翻了几倍。也正因如此,HER天然不能用在所有场景——如果任务本身没有显式目标输入(比如普通的Atari游戏),那就要先改造环境定义,把“目标”这个概念引入状态空间。
2. 核心细节拆解:目标重标注到底怎么设计才合理
2.1 经验元组从四元组变成五元组,重标注分三种玩法
常规强化学习的经验缓冲池里,每一条transition是一个四元组:状态s、动作a、奖励r、下一时刻状态s'。HER里的transition则升级成五元组:状态s、动作a、奖励r、下一时刻状态s'、目标g。
算法运行过程中,每一轮episode采集完成后,除了把原始目标g的轨迹原样丢进回放池,还会额外生成若干条“重标注版本”的经验。重标注最核心的一步,是从轨迹里挑选一个状态来充当替代目标。常见策略主要有三种:final、future和random。
| 策略 | 做法 | 特点 | 典型适用场景 |
|---|---|---|---|
| final | 把整个episode结束时的最终状态作为替代目标 | 实现最简单,但最后一个状态往往距离轨迹前段太远,学习信号偏弱 | 短轨迹、状态空间不大的任务 |
| future | 把轨迹中当前时刻之后的某个状态作为替代目标 | 与原经验的时间结构一致,OpenAI论文里实践效果最好 | 长轨迹、连续控制、机器人操作 |
| random | 从整条轨迹中随机挑一个状态作为替代目标 | 增加样本多样性,但噪声偏大,容易引入不一致 | 目标空间比较简单、需要覆盖更多目标时 |
我在实际复现中发现,future策略确实最稳。原因也好解释:替代目标越接近当前时刻,这条经验与策略当前行为之间的“因果距离”越短,就好像你刚摔了一跤爬起来,立刻被问“如果刚才目标是避免摔倒,你哪一步没做好”——这时候教训是最清晰的。final策略等于过了很久才回顾,很多中间动作对应不上“最后的成功”,学习信号自然稀释很多。
2.2 “未来”怎么选:采样窗口和数量都有讲究
future策略的细节值得单独说一说。具体操作是:在一条长度为T的轨迹中,遍历每个时间步t,从后面任意一个时刻t'(满足t < t' ≤ T)随机抽出状态s_{t'},把它作为这条transition的新目标g'。这里的t'不一定非要选最终时刻,而是允许在轨迹后段任意游走,这相当于给重标注加了一层随机扰动,让替代目标覆盖更丰富的情况。
还有一个参数需要关注——每条轨迹到底额外保存多少条重标注经验。OpenAI论文里常用每个epoch保存4份HER样本用于训练,但在工程里我更习惯用“50%原始目标 + 50%重标注目标”的比例来配置回放池。这个比例不能太偏:如果重标注样本占比过高,智能体会被大量“做到了的事”淹没,慢慢丧失挑战困难目标的动力;如果占比太低,HER的效果又出不来。我自己调试下来,k在4左右或者50%这个档位比较平衡,大家在落地时可以从这个起点往外调。
2.3 奖励的重新标定:阈值、距离范数与评价一致性
目标换了,奖励当然要重新计算。HER里最常用的做法是把奖励定义成稀疏二值奖励:如果当前状态s'与目标g'之间的距离在阈值ε以内,奖励设为0(表示“达到目标”);否则设为-1。注意这里的0不是“没有信号”,而是所有成功样本统一为0,失败样本统一为-1。这个设计让critic网络可以稳定地学到“这个状态下做这个动作是否朝目标更近了一步”,因为它看到的模式是:成功轨迹在末段的Q值会逐步抬升。
距离度量方面,最常用的是L2范数。但在不同任务里,比较状态和目标的实际语义并不一样,比如位置误差、姿态误差、像素误差,不能一律照搬欧氏距离。一个经验是:目标空间如果包含角度分量,最好对角度做归一化或者周期化处理,否则距离函数会在边界处产生跳变,奖励标定就会不稳定。
还有一个特别容易被忽略的坑:重标注时使用的“成功判定阈值”,必须和训练评估时使用的阈值保持一致。我在一次实验里犯过这个错——训练时用0.3的欧氏距离阈值算HER奖励,评估时却用0.1,结果训练曲线一路走高,部署后任务成功率却惨不忍睹。这属于典型的训练-评估语义错位,大家一定记住。
3. 实操过程:把一个HER+DDPG的训练流程真正跑起来
3.1 环境准备与工具选型
HER最早是从OpenAI的Fetch系列环境中验证出来的,这些环境包括FetchReach(机械臂末端到达目标)、FetchPush(把物体推到位置)、FetchPickAndPlace(捡起物体放到指定位置)和FetchSlide(把物体滑到目标点)。这些环境的共同特点是:状态空间包含当前物体位置、夹爪位置,以及一个显式目标g,目标本质上是一个三维坐标。它们天然适配HER。
环境层面,最常用的是gym中经典的FetchPickAndPlace-v1,底层依赖MuJoCo物理仿真器。MuJoCo目前已经转向免费开源授权,直接pip安装就好。如果你暂时不想折腾仿真器,也可以自己写一个二维网格世界:智能体在20x20的地图上移动,目标是某网格坐标,每一步给-1的惩罚,只有到达目标才给0。这类环境同样能验证HER的核心思想,而且训练速度快得多,上手成本低,适合先把流程跑通再换到高维仿真环境。
算法层面,我推荐先以DDPG为骨架实现HER,因为DDPG的off-policy特性与HER天然契合,结构也简单。等把HER的流程理清了,再迁移到SAC、TD3等更先进的算法也不迟。框架选PyTorch或者TensorFlow都可以,下面的代码示例按PyTorch风格写。
3.2 网络结构设计与回放池写入逻辑
有的读者可能会在搭建网络的时候犯迷糊:HER的目标g究竟怎么输入网络?常规做法是,把状态s和目标g在特征维度上拼起来,作为actor和critic的输入。举个例子,FetchPickAndPlace里s是25维,goal是3维,拼接后就是28维输入。Actor输出4维连续动作(夹爪位置和开合量)。Critic则额外接受动作向量作为输入,输出一个Q值。这里有个重要前提:所有网络的输入特征都要做归一化或标准化,不然目标坐标量纲和关节角度量纲混在一起,训练极不稳定。
回放池写入逻辑是整个HER的关键。具体流程是:一条episode跑完后,先把原始经验全部写入replay buffer,然后按future策略采样,再生成若干条重标注经验写入buffer。伪代码如下:
def her_relabel(episode, future_k=4): """ episode: list of transitions (s, a, r, s_next, g) 返回额外写入replay buffer的HER样本列表 """ her_samples = [] T = len(episode) for t in range(T): s, a, r, s_next, g = episode[t] # 从当前时间步之后的未来状态中随机抽取一个作为替代目标 future_steps = range(t + 1, T) if len(future_steps) > 0: t_prime = random.choice(list(future_steps)) g_prime = episode[t_prime][3] # 取未来某个时刻的状态 r_prime = 0.0 if distance(s_next, g_prime) < threshold else -1.0 her_samples.append((s, a, r_prime, s_next, g_prime)) # 如果想要固定比例,这里可以再做一次采样 return her_samples这里有一个细节值得展开:为什么替代目标要从s_next做距离判断而不是从s?因为经验元组里(s, a)对应的是当前决策,s_next是决策后的结果,我们用结果去判定是否达成目标,这正是“事后”视角的核心——先看实际发生了什么,再倒推哪个目标可以被视为已完成。
3.3 DDPG训练主循环与关键超参数
训练主循环分成采集和经验回放两部分。每次用当前actor在环境中跑一个episode,得到原始轨迹;把原始轨迹写入buffer;调用上面的her_relabel生成重标注样本并写入buffer;然后从buffer里随机采样一个batch,更新critic和actor。
for epoch in range(max_epochs): episode = collect_episode(env, actor) # 带探索噪声 replay_buffer.extend(episode) replay_buffer.extend(her_relabel(episode)) # HER核心 for _ in range(updates_per_epoch): batch = replay_buffer.sample(batch_size) # 计算critic的TD目标:r + gamma * Q_target(s_next, actor_target(s_next, g), g) # 更新critic,然后通过梯度上升更新actor超参数方面,我习惯的起点如下:Actor和Critic网络各两个隐藏层,每层256个神经元;学习率1e-3;奖励折扣因子gamma取0.98;目标网络软更新系数tau取0.005;探索噪声用自适应递减的高斯噪声或OUNoise;batch_size取256;每次episode后更新40步。不同环境里效果会有差异,但按这组参数起步,通常不会出现特别离谱的问题。
3.4 结果长什么样:HER带来的训练曲线差异
如果环境配置正确,你会看到非常鲜明的一张成败对比图。在相同的DDPG框架下,如果关闭HER,FetchReach这种相对简单的任务大约能在一两百个episode内逐步提升成功率;但FetchPickAndPlace这种更复杂的任务,成功率会在很长一段时间内贴着0跑,几乎看不出任何动静。打开HER之后,通常在几百个episode内成功率就会开始抬升,训练过程会明显出现“从无到有”的拐点。
我自己第一次复现的时候,都被那条曲线的反差震住了——同一个算法,只是让回放池多了一批“事后标注”的样本,学习信号就完全变了味道。这也印证了HER最核心的工程价值:它不改变网络结构、不改变目标函数,纯粹通过修改经验数据的标签,就让原本几乎学不动的任务变得可训练。
4. 实战排坑:HER训练不收敛的四个典型原因
4.1 策略完全学不动,先别怪代码,查这五件事
如果你发现训练彻底没有起色,最可能的不是网络结构写错了,而是HER根本没生效。优先级最高的检查顺序是:一,确认你用的是off-policy算法,PPO、A3C这类on-policy算法无法直接使用HER,因为经验一旦重标注,就不再是当前策略的分布;二,确认actor和critic的输入确实包含目标g,很多新手把s和g拼错维度,网络根本没看到目标;三,确认replay buffer里确实写入了重标注样本,可以在训练循环里打印her_samples的数量;四,确认奖励阈值设置合理,如果阈值定得过大,几乎所有的状态都会被判定为“成功”,学习信号反而消失;五,确认每条经验里s_next和s的顺序没写反,这种低级错误一旦出现,整个Q函数都会学歪。
4.2 训练中途突然崩溃,Q值发散怎么办
HER搭配DDPG时,一个非常常见的问题是训练前期稳步上升,突然某个episode开始Q值爆炸,随即策略迅速劣化。这通常来自DDPG本身对Q值的高估问题,HER大量使用二值奖励会让这个问题更敏感。解决办法有几个方向:把学习率从1e-3降到3e-4;给critic的更新加梯度裁剪;把DDPG换成TD3或SAC。我个人更推荐直接上TD3或SAC,因为它们的actor更新更稳定,省去折腾DDPG玄学调参的时间。
4.3 策略“飘了”:只学会简单目标,挑战不了困难目标
有些时候训练曲线也很漂亮,失败率在下降,但仔细看会发现智能体在长距离抓取任务上依然不行,只会在比较容易达到的目标附近“刷分”,这种情况通常是因为重标注比例过高。替代目标太多,相当于给智能体喂了大量“退而求其次”的成功样本,它就越学越保守。解决办法是把HER样本占比压低一点,比如从50%降到30%,同时适当增大环境里困难目标的采样频率。如果用的是k参数形式,就把k从4往下降到2试试。
4.4 离散动作空间能不能用HER?能,但要改改动
原版论文以连续控制为主要实验场景,但HER本身并不绑定连续动作。只要你的任务满足“目标作为输入”的条件,DQN同样可以和HER结合。需要注意的差异在于:连续控制里的替代目标通常直接用状态值,而离散控制下你要考虑目标空间是否能用同样的距离度量。我在一个离散的网格寻路任务里试过HER,效果同样很明显,尤其是目标点经常变化的场景,重标注能极大提升数据利用率。所以,别被“连续控制专用”的印象限制住,关键是抓住goal-conditioned这个前提。
5. 从算法回看“hindsight”:把复盘思维真正变成工程习惯
讲完了技术细节,我想把这个话题往项目实践上延伸一下。其实HER不只是算法库里的一页论文,它背后那个“后见之明”的思考方式,对做工程、做产品的人同样很有启发。我们日常做项目复盘,经常会说“如果当时目标是……就好了”,这正是活生生的hindsight。但问题在于,大多数人说完这句话就把记录丢在一边,下次遇到类似问题还是走老路。
HER给我们的提示是:不要把失败轨迹直接丢弃,而是主动给它们“换标签”。在项目管理里,一次失败的产品迭代,可能意味着“原定目标是提升留存率,但实际在付费转化上有意外突破”或者“原定目标是扩展A类用户,结果B类用户的反馈更有价值”。这时候如果坚持用原始目标评价整次迭代,得出的结论就是失败;但如果把“实际带来的结果”作为一个新目标重新审视,很多步骤和决策其实是有效的、可复用的。
我建议团队复盘的时候,可以建立一个“双目标评审”机制:先按原定目标评审结果,再按“实际产出的有价值结果”重新评审一次过程和决策。第二次评审的产出的经验片段,应该沉淀成可执行的方法库,而不是随着复盘文档一起封存。这种方法并不能替代硬性的KPI考核,但它能保持团队从失败中学习的频率,让每一次尝试都不白费——这和HER把失败轨迹重标后放进回放池,本质上是同一个道理。
回到强化学习层面,我个人实际复现HER过程中最大的体会是:数据远比算法本身更容易被低估。很多项目训练效果差,第一反应是换网络、换优化器、换探索策略,但实际上很可能只是经验回放池里躺着太多“完全无用”的失败样本。HER用一个极其简单却足够巧妙的操作——改目标、重标奖励——把死数据变成活数据。这种思路无论放在算法优化还是团队管理里,都值得反复品味。如果你最近正被强化学习训练不收敛折磨,不妨先把原版论文读一遍,再照着这篇博文的流程搭一个最小实现,跑通之后再回到你的真实任务里做迁移扩展。