1. hindsight 到底在解决什么问题
1.1 稀疏奖励让强化学习寸步难行
做强化学习的同学应该都体会过一种抓狂感:环境跑了几百万步,reward 一直是一个扁平的 0,偶尔跳出一个 -1,然后就再也没有然后了。这种问题在学术上叫稀疏奖励问题(sparse reward problem)。字面意思就是环境给你的反馈少得可怜,绝大多数时间你根本不知道自己做对了还是做错了。很多机器人操作任务、游戏通关任务、推荐系统冷启动场景,本质上都是稀疏奖励问题。
我当时第一次接触这个项目标题“hindsight”时,第一反应是:这不就是后见之明嘛。但作为一个搞过 RL 的人,我马上意识到它指的是 Hindsight Experience Replay,也就是事后经验回放,是解决稀疏奖励问题的一种经典方法。它的核心逻辑特别朴素:既然当前目标太难达成,不如换个目标——把你实际上达到的状态当成目标,然后告诉智能体“你其实完成了任务”。这是典型的用后见之明改造经验的思路。
为什么稀疏奖励让人头疼?因为强化学习的两个核心组件都依赖奖励:策略梯度要靠奖励算梯度方向,价值函数要靠奖励做回归目标。奖励信号几乎全是 0,价值函数什么都学不到,策略梯度也只能随机游走。很多项目卡在这里,不是模型不够大,不是算力不够强,而是反馈信号根本没有建立起来。
1.2 一个看得见却摸不着的失败样本
举一个最常见的机械臂抓取例子。环境里有一个桌子和一个物体,物体可能出现在任意位置,目标是让机械臂末端夹爪到达物体的位置并夹住它。这是一个稀疏奖励任务:只有末端执行器与物体中心距离小于某个阈值,且完成抓取动作,才会奖励 +1,否则都是 0。
如果物体出现在一个稍微偏一点的位置,机械臂的随机探索可能跑满了整个训练周期都没有一次成功。你翻出 replay buffer 一看,里面全是失败的轨迹。这些轨迹在传统经验回放里毫无价值,因为它们对应的目标从未达成过,奖励永远是 0,价值函数只会学会“什么都得不到”。
但仔细想一想,这些失败的轨迹真的没价值吗?并不是。机械臂虽然没抓到目标物体,但在探索过程中,它确实到达了许多其他地方。比如它伸到了桌子的左上角、靠近了桌沿、甚至一度伸到了很高处。这些实际到达的位置,是环境状态的真实样本。传统算法把这些样本扔掉了,但 hindsight 告诉你:留着它们,把目标改一下,它们就有话要说。
1.3 后见之明的核心直觉
人的学习过程里有一个很普遍的现象:第一次做一件事,完全没搞对方向,但事后回看整个过程,你会发现自己在某些节点其实做对了局部动作。比如第一次投篮,球偏到左边很远,但你的发力手型其实是对的。如果只盯着“进球”这个目标,这次投篮是彻底失败的;但如果把目标改成“让手型正确”,这次投篮就是一次成功的正样本。
HER 做的事情,就是在训练时给失败的轨迹换一个“事后看来已经完成的目标”。原来轨迹是 attempted goal 没达到,但因为轨迹的真实终点 state 是已知的,我们就把这个真实终点 state 作为“一个新目标”,然后重新计算这条轨迹的奖励。在这个新目标下,轨迹的终点就是目标本身,所以最后一步一定获得正奖励,整条轨迹就变成了一条“成功轨迹”。用这种方式,每次失败不再是失败,而是换了个角度完成的成功。
2. HER 的核心机制与技术拆解
2.1 把“失败的目标”替换成“实际到达的目标”
HER 的全称是 Hindsight Experience Replay,翻译成大白话就是“用事后眼光改造的经验回放”。它做的事情可以用一张状态流图描述,但我这里不用图,用文字也能讲清楚。假设一条轨迹是 s0, a0, s1, a1, s2,原始目标 g 是让机械臂到达位置 (1.0, 0.5)。但机械臂实际走到的终点是 (0.3, 1.2)。这条轨迹按原始目标算,奖励全 0。HER 的做法是:额外生成一条“改目标后的轨迹”,把目标变成 (0.3, 1.2),然后重新计算每个转移的奖励。
改完目标之后,奖励函数重新评估每个状态是否达到新目标。因为轨迹终点正好等于新目标,所以最后一步一定奖励 +1,前面的步骤根据与新目标的距离也可能获得更密集的中间信号。这条新轨迹会被存进 replay buffer 里,参与后续的价值函数更新和策略更新。
需要特别说明的是:存储轨迹时,总共会存储两份。一份保持原始目标,目标损失还是那句成语——办法总比困难多。真正有用的学习信号藏在“失败经验”里,换一种目标编码方式,它就是训练模型最好的燃料。
2.2 重新标注目标的几个策略
HER 论文里给出了几种选“新目标”的策略,实际项目里用得最多的是以下四类,我这里按实用程度排序。
- final:直接把轨迹的最终状态作为新目标。这是最简单、最常用、也最稳定的做法。因为轨迹终点一定可达,奖励一定成功,没有任何随机性。
- future:从轨迹终点之前的某个随机时刻状态作为新目标。相当于截取轨迹的后半段作为“成功经验”。这种方式能产生更多样化的成功轨迹,但实现稍微复杂。
- episode:从当前 episode 的随机一个状态作为新目标。比 future 更激进一点,把整条轨迹中任何一个中间状态都当作目标。
- random:从所有历史轨迹里随机挑选状态作为目标。这种方式覆盖了全状态空间,但成功率分布更稀疏,方差更大,实际效果参差不齐。
我个人的经验是:如果任务的目标是固定的单一目标,选 final 就够了;如果目标是多目标或者连续目标空间,可以考虑 future 混合 final;random 的方式别轻易用,尤其是在轨迹状态分布极其不均匀时。
2.3 为什么替换目标之后还能学
有个很自然的问题:把目标换掉,学到的策略还是“达成原始目标”的策略吗?这里其实是 HER 最巧妙的地方。在目标条件强化学习框架下,策略输入不只是状态,而是状态加目标的组合 (state, goal)。也就是说,智能体学到的是一个从“任意状态 + 任意目标”到动作的映射函数。
原始数据训练它能完成目标 g0,改过的数据训练它能完成目标 g1,合并在一起,它学会的其实是“先理解目标是什么,再朝这个目标前进”。当你真正要用的时候,给它原始目标 g0,它依然能用,因为“理解目标并接近目标”的能力已经从大量改签样本中迁移过来了。这就像你练车时练过左转、练过右转,考试时考左转入库,你自然也会,因为你学会的是“看后视镜、打方向、控制车速”这套通用操作,而不是只会某一个固定车位。
3. 实操过程与核心环节实现
3.1 基线环境与参数选择
我自己是在一个基于 MuJoCo 的机械臂抓取环境上做的实验,环境里机械臂末端要移动到一个随机目标点,并获得 +1 奖励。这个任务有两个难点:一是目标点每次 reset 都会随机重置,二是成功率初值接近 0。如果不用 HER,传统的 DDPG 训练 100 万步成功率都很难超过 5%,这个我在对比实验里验证过。
参数选择上,有几个关键项:replay buffer 容量设 100 万条转移,batch size 设 256,目标网络 soft update 系数设为 0.05,训练步数 100 万。最重要的是,每次采样一条 episode,需要同时生成原始目标版本和 hindsight 目标版本的转移,并且以 1:1 的比例放入 buffer。
注意这里的细节:HER 不是单独训练一个模型,而是把 modified 经验的样本混入原有 replay buffer 中,和原始样本一起训练。如果你只放入 modified 样本,策略会偏离原始目标太远;如果只保留原始样本,又没有解决稀疏奖励问题。1:1 是经验值,实际任务里可以根据稀疏程度调整这个比例。
3.2 融入 DDPG/SAC 的完整流程
HER 与 off-policy 算法天然合拍,因为它本质上就是在 replay buffer 里增添了一些“事后标注”的数据。我用的是 SAC,整体流程可以概括为四步:采集、改造、存储、更新。
第一步,在环境中用当前策略跑一个 episode,记录下每一步的观测、动作、奖励和 episode 结束后的最终状态。第二步,对这条 episode 中的每一条转移,额外生成一个以最终状态为目标的新转移,并重新计算奖励。第三步,把原始转移和新生成的转移都存进 replay buffer。第四步,从 replay buffer 中随机采样一个 batch,用 SAC 的公式更新价值函数和策略。
这里有一个很重要的实现细节:改目标的奖励计算必须用同一种奖励函数。比如原始奖励函数是 distance < threshold 给 +1,那么在 hindsight 目标下也要用 distance < threshold 判断是否达到新目标。如果不一致,相当于引入了不同尺度的奖励信号,会让价值函数震荡。
3.3 实操代码骨架
我给出一个最精简的伪代码骨架,去掉环境封装和网络定义,只保留 HER 部分的逻辑。这是实际跑通的版本,直接可以套到自己的环境里。
import numpy as np from collections import deque def her_modify_episode(episode, reward_func, threshold): obs, acts, goals, rews, next_obs, dones = episode # 取轨迹最终状态作为 hindsight 目标 new_goal = next_obs[-1] her_obs = [] her_next_obs = [] her_goals = [] her_rews = [] her_dones = [] for i in range(len(obs)): # 将目标替换为新目标 her_obs.append(np.concatenate([obs[i], new_goal])) her_next_obs.append(np.concatenate([next_obs[i], new_goal])) her_goals.append(new_goal) # 用同一奖励函数计算新奖励 rew = reward_func(next_obs[i], new_goal, threshold) her_rews.append(rew) her_dones.append(1.0 if rew >= 1.0 else 0.0) return (her_obs, acts, her_goals, her_rews, her_next_obs, her_dones) # 在训练循环中,每个 episode 结束后调用: # episode 是原始轨迹,包含 obs, acts, goals, rews, next_obs, dones # her_episode = her_modify_episode(episode, reward_func, threshold) # replay_buffer.push(episode) # replay_buffer.push(her_episode)这里有几个容易写错的细节。dones 的标注要特别小心,强化学习里 done 表示 episode 是否终止。在改目标后的轨迹里,由于最后一步确实达到了新目标,所以最后一个 done 应该是 1,但前面的 done 必须是 0。如果不小心把所有 done 都设成 1,价值函数会被误导成“所有状态都是终止状态”,训练直接崩掉。
还有一个细节是 state 和 goal 的拼接方式。上述代码用的是简单的 concatenate,把目标向量拼在观测后面。如果目标是一维标量,这种方式没问题;如果目标是多维结构化数据,拼接后网络输入维度会增大,需要适当增加网络容量。另外,如果你的环境状态本身分辨率很高,拼接后输入维度可能达到几百维,这时候建议用一个单独的 goal embedding 网络先压缩一下,再拼接到主网络。
最后是奖励函数的设计。HER 本身并不解决奖励函数的形状问题,它只解决“数据中缺少正样本”的问题。如果你的奖励函数本身设计成很稀疏的 0/1 形式,HER 可以把正样本从 0% 提升到一定比例,但仍然可能不够。我建议在 HER 基础上,再结合一个简单的 shaping 奖励,比如距离的负值,这样原始数据和新生成的数据都会有更密集的信号。
4. 常见问题与排查技巧实录
4.1 replay buffer 里的样本混合比例
我在调参时踩过的第一个坑就是混合比例。最开始我把 HER 生成的样本全部灌进 replay buffer,导致 buffer 里 90% 以上的样本都是“新目标版本”。训练一段时间后发现,策略确实能很好的完成任务,但是只对“已经到达过的状态”有效,对初始随机目标完全不敏感。原因很容易理解:原始目标的样本太少,策略几乎没有见过真正的目标分布。
解决方法是固定原始样本和 HER 样本的比例。我在代码里直接控制 push 进 buffer 的计数,保证 buffer 中原始样本占比至少 40%。这个比例不需要太精确,但绝对不要低于 30%。如果你发现策略对随机目标不敏感,第一步就该检查 buffer 里原始目标样本的比例。
4.2 目标空间范围太大导致方差爆炸
HER 换目标之后,新目标来自轨迹终点的状态分布。如果环境的目标空间非常大,而轨迹终点分布又很分散,那么新目标覆盖的空间就会非常广。这会导致价值函数需要拟合一个过大的目标空间,网络容量不够时训练方差变得很大。
我遇到的情况是,机械臂的目标点可以在整个 3D 空间任意取,结果 HER 生成的目标分布几乎覆盖了整个工作空间。后来我限制新目标的采样范围,只从轨迹终点附近的一个球体内采样,并且把目标空间的归一化做得更精细。做完这两件事后,训练方差明显下降。经验是:目标空间如果超过 10 维,强烈建议先做 PCA 降维或者使用自动编码器压缩。
4.3 和 HER 一起用的探索噪声
很多人以为有了 HER,探索噪声就不重要了。实际上 HER 只是让失败样本变得有信息量,但并不能帮你探索到足够多样的状态。如果你一开始的随机策略只能覆盖很小一片状态区域,那么所有 hindsight 样本也都集中在这块区域,策略最终只能学会回到这片熟悉区域。
解决方法有两种:一是加大动作噪声,在一个 episode 中让机械臂多跑一些奇怪的动作;二是引入一种简单的课程机制,前 10 万个时间步内用高熵探索,之后逐渐降低噪声。我用的是第二种,效果很直接,最终训练成功率提高了大约 20%。
4.4 什么时候不适用
我也要诚实地说几个 HER 不太适用的场景。第一个是稀疏且延迟非常长的任务,比如打一整局游戏才给一次奖励,轨迹长度上千步。HER 虽然能产生“到达新目标”的正样本,但这条轨迹里大量状态与新目标无关,改造后的奖励依然很稀疏,学习效率依然很低。
第二个是非目标条件化的任务。如果这个任务根本没有目标输入,状态就是状态,没有 goal vector,HER 的“替换目标”逻辑就没有落点。当然你也可以把某些状态维度强行解释为目标,但这就不是原版 HER 的意义了。
第三个是奖励信号存在误导性的任务。如果环境本身给了不正确的 0/1 奖励,HER 只是换个目标,但它不会纠正奖励错误本身。这种情况下,先修好奖励函数,再考虑 HER。
5. hindsight 思维在其他领域的延伸
5.1 项目复盘中的“后见之明”
“hindsight”这个词本身就有很深的价值。做项目最怕什么?不是失败,而是失败之后找不到失败的原因,下次继续踩同样的坑。很多团队做复盘,习惯性地列出“当时决策错了”“需求理解不到位”这种结论,这是典型的后见之明偏差,没有任何建设性。
但反过来,如果能把复盘变成“换一个目标重新看待数据”的过程,情况就不一样。比如一个推荐系统项目线上了两个月效果不佳,原始目标是 CTR 提升 2%,没达成。按 HER 的思路,可以把目标改成“理解这批用户到底对什么内容感兴趣”,然后回到数据里重新看,你会发现其实模型 already 在部分用户群上表现很好,只是整体平均被拉低了。这个发现本身就是值得保留的“成功轨迹”。
5.2 从失败数据中学习
做数据科学的人应该经常遇到一个困境:正样本太少,负样本太多。分类模型、异常检测模型、风控模型,都有这个问题。传统做法是过采样正样本、欠采样负样本,但信息量仍然有限。
换一个 hindsight 的视角:如果你的正样本定义得太窄,不如把“接近正样本”的样本重新标注成另一个任务的目标。比如风控场景里,“欺诈”样本很少,但“可疑交易”样本很多。你可以把“识别可疑交易”作为一个有充足样本的任务先训练好,再在这个基础上微调“识别欺诈”的分类器。这不就是 HER 里的“换一个可达成的目标”吗?先用容易达成的目标学习底层特征,再迁移到真正的目标上。
我从这个项目学到的最有价值的东西,不是某个算法的具体实现,而是这种思维模式:当目标太难达成时,不要只盯着失败本身,而要想办法从失败中重新发现已经完成的部分,把它们变成可学习的样本。这个思路无论是写强化学习算法,还是做日常工作复盘,都是通用的。
如果你手头也有一个稀疏奖励的任务,找不到突破口,我建议先用一个简单的环境把 HER 跑通,感受一下“失败样本被重新标注后带来的信息增量”。跑通之后再回到自己的任务上,心里就有谱了。这个过程比看十篇论文都管用。