ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

HER事后经验回放原理与实现:用hindsight破解稀疏奖励

HER事后经验回放原理与实现:用hindsight破解稀疏奖励 最近这段时间技术社区里总能看到同一个英文词hindsight。直译是“后见之明”说难听点就是“事后诸葛亮”。这个词放到强化学习领域指代的是一个分量很重的算法——Hindsight Experience Replay通常缩写为HER中文一般叫“事后经验回放”。我这次项目的标题干脆就叫hindsight本质上是把这个经典算法的原理、实现、实验从头到尾完整跑了一遍。如果你正被稀疏奖励问题折磨或者准备做目标条件化的机器人操控任务这篇文章可以给你一套能直接上手的路径。下面按我实际操作的顺序来讲。1. 项目源头稀疏奖励让我一次次撞墙1.1 什么叫稀疏奖励它难在哪先说清楚我在做这个项目之前遇到的问题。拿机械臂抓取来说状态空间是关节角度、物体位置这些高维连续量目标是“把物体推到某个指定位置”奖励函数通常只在目标达成的瞬间给出一个正信号其余时间全是0或者-1。试想一下一个随机初始化的策略要在几十维空间里做连续控制还要在一个episode结束时恰好把物体放到指定位置成功的概率有多低。绝大多数情况下整个episode跑完拿到的奖励一模一样critic根本分不清哪一步更好policy gradient也没有有效信号可用。这就是稀疏奖励问题的本质不是没有奖励而是有奖励的样本太少训练信号几乎被无效样本淹没。这种现象在真实工程里非常普遍不只是机械臂。游戏里“赢了才给分”是稀疏奖励推荐系统里“用户完成特定转化才给正样本”也是稀疏奖励。任务目标越复杂、动作空间越大碰巧成功一次的概率就越低算法能学到的正向反馈就越少于是陷入一个恶性循环没成功就学不到学不到就更难成功。1.2 常见解决思路为什么容易翻车面对稀疏奖励最直接的想法是给奖励塑形把奖励改成“物体离目标越近奖励越大”也就是dense reward。这个方法做简单任务很有效但遇到复杂任务就麻烦了。第一距离函数选欧氏距离还是别的度量判断阈值设多少这些超参数都很难拍板第二塑形后的奖励会引入一种“钻空子”的行为算法可能找到一条不断缩短距离、但根本不解实际问题的路径。我自己就在之前的项目里见过不少这种“预想外的成功”最后只能怀疑奖励设计本身有问题。另一个常见思路是课程学习先学简单目标再逐步逼近复杂目标。听起来很美但课程难度怎么划分、每个阶段怎么切换又是一堆要调的旋钮。如果不做自动化课程生成人工去排序大量任务目标光维护成本就够喝一壶的。我当时在这个项目上把两套思路都试过最后还是决定走另一条路事后重定义目标也就是HER。事后视角的关键在于运行失败的经验并不是没有信息量只是我们一直用错误的目标去解读它。2. 核心思路拆解事后重标记这个操作到底好在哪2.1 目标条件化强化学习的基本设定在讲HER之前必须把目标条件化的框架说清楚。在目标条件化强化学习里策略输入多了一个目标g形式是π(s, g) - a每个episode会先采样一个目标gagent再去尝试达成它。状态可以拆成两个角色当前观测s以及“目前实际达到的目标”achieved goal。以Fetch环境为例desired goal是“物体要到的坐标”achieved goal是“物体现在实际所在的坐标”。奖励函数可以统一写成r R(s, g)具体实现时一般用距离阈值判断如果当前状态与目标之间的距离小于阈值奖励为0否则为-1。这个写法的好处是评价agent表现的核心变成“当前状态离目标有多近”目标本身只是一个额外的输入变量。这样一来同一个策略可以在不同的目标之间共享经验而不是每个目标各训一次模型。如果你没接触过这个设定可以把它理解成让模型同时学会“听懂指令”和“执行动作”。目标条件化把任务的多样性变成了模型输入的一部分而不是每个任务单独训练一套策略。HER的整个操作就建立在这个框架上因为重标记的本质是修改输入里的目标字段。2.2 HER最关键的步骤goal relabelingHER的核心操作只有一句话如果这条轨迹没有达成原始目标那就把它实际达成的状态当作另一个目标重新计算这条轨迹的奖励作为有效经验存下来。这句话看着简单实际效果非常惊人。举个例子。机械臂推物体原始目标是(1.0, 0)结果物体最后停在(0.5, 0.2)。从原始目标看这是一次失败奖励从头到尾全是-1。但把目标换成(0.5, 0.2)之后这条轨迹立刻变成一次“成功示范”终点对应奖励0前面的每一步也都可以被解读成“正在朝这个位置努力”。于是一条本来毫无价值的失败轨迹被直接转化成了带有正向反馈的有效样本。打个比方一个人扔飞镖本来想扔靶心结果扎在了右上角。站在事后视角他完全可以说自己练的是“右上角九环”这个目标而且这次练习是成功的。注意这里说的是“重新定义目标”而不是“修改轨迹”。轨迹本身不变动作不变只是把目标替换掉再根据新目标重算奖励。这个操作完全发生在经验回放阶段不影响在线采样的策略。2.3 四种重标记策略怎么选具体实现时重标记的目标从哪来直接影响训练效果。常见的有四种final用整条episode的最终状态future从当前时间步之后随机采一个状态episode从整个episode里随机采一个状态random从所有见过的状态里随机采一个。论文结论和我的实验都指向同一个方向future效果最好。原因很合理用future时重标记的目标一定出现在当前transition之后意味着agent在真实时间流里确实见过自己从当前状态走向那个状态。这比random凭空指定一个离自己十万八千里的目标靠谱得多。random策略的问题在于很多随机采到的目标离当前状态太远重标记后的transition虽然奖励变了但状态和动作之间的因果关系并没有被真正利用起来。我在小规模对比实验里得到的主观感受是重标记策略训练早期收敛速度FetchPush最终成功率相对感受final慢中等future快高episode中等较高random很慢低当然单次实验的数字不代表绝对结论但future在大多数任务上确实值得优先尝试。未来状态是agent真正经历过的状态用它当目标轨迹里的因果关系才站得住。3. 实操过程我的一版完整实现3.1 环境与算法选型项目标题叫hindsight环境我选的是OpenAI Gym里经典的Fetch系列具体跑了FetchReach和FetchPush。前者是机械臂末端点到达目标点相对简单后者是机械臂把桌上的物体推到指定位置难度上了一个台阶。算法选择上HER需要一个off-policy基础算法我用了DDPG。也有人用TD3或者SAC效果可能更好但DDPG参数少配合HER已经足够说明问题。选off-policy的原因非常直接HER的精髓在于“离线重新标注”样本必须等整条episode跑完才能生成新的transition这天然依赖一个能反复采样历史经验的基础算法。DDPG这类算法会把经验存进replay buffer多次采样、多次更新重标记后的样本才能最大化利用。如果用PPO这类on-policy算法每条经验只用一次重标记的价值就被稀释掉了。3.2 核心代码逻辑从存储到重标记我直接放一段核心逻辑这是我最开始实现时的版本结构和论文描述基本一致。# 假设已经收集完一个episode: # states, actions, rewards, next_states # achieved_goals: 每个状态对应的achieved goal连续数组 # desired_goal: 本episode的原始目标 def compute_relabel_reward(achieved_goal_next, new_goal, threshold0.05): # 注意这里必须用 执行动作后的状态 去判断是否达成“新目标” dist np.linalg.norm(achieved_goal_next - new_goal) return 0.0 if dist threshold else -1.0 episode_size len(states) transitions [] for t in range(episode_size): transitions.append({ state: states[t], action: actions[t], reward: rewards[t], next_state: next_states[t], achieved_goal: achieved_goals[t], desired_goal: desired_goal, }) K 4 # 每条transition额外生成K条重标记样本 for t in range(episode_size): for _ in range(K): # 从t之后的时刻中随机选一个状态作为新目标 future_idx np.random.randint(t 1, episode_size) new_goal achieved_goals[future_idx] # 重标记奖励必须基于执行动作后的状态来判断 achieved_goal_next achieved_goals[t 1] if t 1 episode_size else achieved_goals[-1] new_reward compute_relabel_reward(achieved_goal_next, new_goal) transitions.append({ state: states[t], action: actions[t], reward: new_reward, next_state: next_states[t], achieved_goal: achieved_goals[t], desired_goal: new_goal, })这里有一个我一开始特别容易写错的点重标记后的reward必须用执行动作之后的状态去判断也就是achieved_goals[t 1]或next_state对应的achieved goal而不是当前状态的achieved goal。如果搞混了agent会以为“自己现在站的位置就等于下一步要到达的位置”奖励全部变成0训练直接失去意义。K是每条原始transition额外生成的重标记样本数论文默认是4。我一开始也用的4这个值太小提升不明显太大离线样本比例失衡、训练变慢。从我的经验看在2到8之间调整比较合理特殊情况再单独试。3.3 训练循环里容易忽略的细节训练循环本身不复杂但有几个细节值得专门说。第一个是episode边界。因为future策略需要知道当前transition在episode中的位置我是在完整收集episode之后才生成重标记样本的。这样保证future索引只在当前episode内部取样。如果把所有transition直接丢进一个无边界的大buffer重标记时很可能取到另一个episode的状态逻辑就乱了。第二个是actor和critic的输入一致性。relabel后的desired_goal变了actor和critic的输入都要换成新目标不能只改奖励。我见过有人只替换了rewardcritic输入还保留原始目标训练出来的结果奇奇怪怪后面排查了很久才发现是输入不一致。第三个是replay buffer的混合采样。我把原始transition和重标记后的transition混在一起放进同一个buffer采样时完全随机。训练初期这没问题但等到agent已经基本学会任务之后大量hindsight样本反而会拖慢收敛因为很多重标记目标已经不值得额外关注了。到后期可以把K调到1甚至直接关闭重标记收敛速度会更快。4. 实验结果从0到收敛的完整过程4.1 评估指标与记录方式我评估的标准很简单每隔固定episode数保存一次模型在同样的初始状态下重新跑一批测试episode统计成功率。FetchPush的成功率定义是物体最终位置与目标点距离小于5厘米。除了成功率我也记录了平均距离、每个episode的累计奖励、critic loss这些指标但最能说明问题的还是成功率曲线。这里有一个测试细节测试时用的目标分布要和训练时一致不然曲线会很假。我一开始训练时随机采样目标测试时却用了固定目标集导致指标上下波动极大后来统一成相同分布才稳定下来。4.2 和不用HER的对比为了验证HER的效果我在同一个DDPG实现下关闭了重标记逻辑只保留普通回放。差距非常明显。在FetchPush任务上普通DDPG训练了约800个episode成功率几乎没有超过5%开启HER之后大概三四百个episode开始成功率明显抬升最终稳定在85%上下。这不是调参造成的差别而是信息的复用方式彻底变了。普通回放里90%以上的transition奖励都是-1critic学到的东西非常有限。HER把这些失败样本中的一部分重新变成信息量很大的样本训练信号一下子厚实了。这其实也解释了为什么HER和off-policy算法是绝配一个负责大量收集失败经验一个负责从失败经验中重新挖出成功信号。4.3 几个值得琢磨的现象有几个现象我留意了很久。第一个是HER对dense reward的加成反而小。我在FetchReach这种距离信息本身就很丰富的任务上开启HER收敛速度提升几乎看不出来甚至因为重标记样本的干扰慢了一点点。这说明HER的核心价值是把稀疏信号变稠密如果任务本身的信息量已经足够它的意义就会下降。第二个是在复杂任务上future策略里目标在轨迹中的位置会影响最终精度。如果总是从轨迹后半段采样目标agent学到的偏向于“接近目标”而不是“准确到达”。我后来在训练后期让目标尽量靠近episode末尾的状态最终精度确实好了一些。第三个是重标记样本占比过高时会不稳定。当额外样本在总buffer里的占比超过80%训练开始出现波动这个现象在TD3上比DDPG更明显。虽然HER论文没有强制规定混合比例但实际操作里还是需要观察一下额外样本对经验分布的影响。5. 踩坑实录与问题排查5.1 重标记目标越界了怎么办第一个坑很常见。在Fetch环境里物体位置有固定的空间范围future采样的目标来自真实状态本身是合法的。真正的坑出在自定义环境上如果你人为定义的目标范围比状态空间窄重标记后模型会学到一批目标范围外的东西策略执行时就会产生奇怪路径。解决办法很直接重标记前把目标clip到合法范围或者确保目标采样空间和状态空间完全一致。我有个项目就是因为这个问题训练曲线突然跳高又很快崩掉排查了好几天。5.2 future策略不能跨episode采样第二个坑是episode边界。如果直接把所有transition揉进一个大buffer不记录episode信息future策略就可能从一条transition的时间点“往后”采样到一个实际属于另一个episode的状态。这在逻辑上是灾难性的agent会学到“我这个动作之后莫名其妙到达了一个不相关的目标”因果关系完全断裂。我的做法是收集完当前episode后立刻生成重标记样本再统一入buffer。这个顺序不能颠倒。5.3 别把稀疏和稠密奖励混着用第三个坑是奖励函数设计。我一开始为了“稳妥”用了稀疏奖励加一点距离惩罚的混合奖励结果训练表现比纯稀疏奖励差。原因是HER的重标记逻辑在稀疏奖励下非常干净替换目标后尾部样本必然变成成功样本中间样本也容易正向化。混合奖励之后重标记判断变得模糊同一个transition在原始目标和重标记目标下的奖励差别不大反而引入了大量噪音。所以我的最终实现里还是用回了纯稀疏奖励只保留距离阈值判断。常见问题原因处理方案重标记目标超出状态范围目标空间与状态空间不一致重标记前clip目标到合法区间future采样跨episodebuffer未记录episode边界收集完整episode后再生成重标记样本混合奖励训练失常重标记对奖励判断不再明确优先使用纯稀疏奖励后期收敛慢额外重标记样本过多后期降低K或关闭重标记最终精度不足future策略总采样轨迹后段目标让目标尽量贴近episode末尾状态5.4 怎么判断重标记后的样本是否合法最后分享一个检查方法。我在怀疑训练数据有问题时会写几条断言来校验重标记样本的合法性。第一new_goal必须在状态空间的合法范围内第二如果用的是future策略new_goal对应的时刻必须严格晚于当前transition所在的时刻第三重标记后的reward要和new_goal配套也就是根据执行动作后的状态重新计算而不是从原始reward里顺手抄过来。这三条检查看着不起眼但每次改动环境或重写buffer逻辑时都能帮我挡住大量隐性bug。有过一次惨痛教训之后我把这些检查写进了项目的测试文件里改动任何一环都会自动跑一遍。6. 项目之外hindsight这个词本身就够写一篇复盘6.1 从算法到日常复盘后见之明怎么用跑完这个项目我一直觉得hindsight这个词很有意思。在算法层面它的含义是“失败之后重新解释目标把失败变成可用经验”。放到我们平时做项目复盘其实也有相似之处。真正有价值的事后复盘不是反复批判自己“当时怎么就没看到”而是把当时的决策过程和后来的结果重新对齐找出在哪些前提下那条路其实是合理的哪些目标换一个角度就成了正确目标。不过这里有一个需要警惕的点人类的后见之明往往是“事后诸葛亮偏差”也就是知道结果之后高估自己事前预测的准确性。比如一个项目失败了你可能会下意识觉得“我早就知道这方向不对”但真实记录里可能根本没有这个判断。所以复盘时最好写决策日志把当时的目标、假设、可选方案都记下来再在事后对照。不要用结果倒推记忆那是不准确的。HER算法的价值恰恰在于它用目标重标记替代了主观记忆让失败经验在客观计算下重新变得可用。6.2 我的一点实际体会我做这个项目最大的感受是很多被认为“经典”的算法只有亲手复现并且踩过坑之后才能建立起真正的体感。HER看起来只有短短几行重标记代码但里面的时间信息、目标空间、奖励一致性每一个细节都在决定成败。如果你也打算复现建议从FetchReach这种简单任务开始先把重标记和回放的逻辑跑通再碰FetchPush和更复杂的操控任务。别急着调参先理解清楚为什么future策略优于final为什么重标记需要和off-policy算法搭配之后再调整K和奖励阈值一定事半功倍。我在实际训练中还有一个体会一个算法叫hindsight并不代表它可以替代你对问题的理解。它只是提供了一种把失败重新解读为经验的方式。你自己在项目里的记录、复盘、验证习惯才是真正决定一个项目能不能沉淀出东西的关键。
返回列表