离线强化学习真正落地的时候,最难受的一步往往不是网络结构怎么设计,而是模型在数据集上评估得漂漂亮亮,一换到实际环境就开始掉链子。这篇发表在 NeurIPS 上的工作,由某开源人像生成项目背后的团队联合完成,盯的就是这个链条上更靠前的一环:状态表征层面的互模拟缺陷。互模拟这套思路在离线强化学习里算是“老熟人”了,它想做的事很朴素——把那些对决策没影响的状态细节全部抹掉,让智能体在更干净的抽象空间里做规划。但问题恰恰出在这儿:抽象做得好是降维打击,做过头就是信息毁灭,而离线场景下这种毁灭是不可逆的。
我先把这篇博文能给你什么说清楚。如果你是做离线强化学习算法落地的工程师,这里会有互模拟度量从定义到代码的完整拆解,以及它在有限数据下为什么会失效的机制分析;如果你是刚接触这个方向的学生或转岗的同学,我会用大量生活化类比把“行为等价”“度量固定点”“过度聚合”这些概念讲明白,并给出可以直接跑的小规模验证流程;如果你只关心工程上怎么避坑,第 4 章和第 5 章的参数配置、训练监控指标、排查速查表可以直接拿去用。所有涉及具体实现的部分,我都会说明哪些是公开文献里的标准做法,哪些是我基于常见工程实践补的合理推断,方便你自己判断取舍。
1. 先把问题摆上桌:离线强化学习为什么需要互模拟
1.1 外推误差:所有离线算法的共同敌人
离线强化学习和在线强化学习之间最大的鸿沟,用一句话讲就是:你只能在别人录好的录像带里学开车,但考试要你上真路。数据集是固定的,策略在训练过程中会不断往数据分布之外漂移,一旦策略选出了数据集里几乎没出现过的动作,Q 网络对这个动作的价值估计就是纯猜的。更麻烦的是 Bellman 备份是个自举过程,第 t 步估错一点,第 t+1 步会拿这个错值当目标继续往下算,误差像滚雪球一样放大。这就是外推误差,也是分布偏移在价值函数上的具体表现。
业界的应对思路大致分成三派。第一派是价值保守派,代表做法是在 Q 值上加一个正则项,把数据集外动作的价值往下压,让策略不敢往那个方向走,典型的就是各种保守 Q 学习变体,用一个系数去平衡“最大化 Q”和“压低未知动作 Q”这两个目标。第二派是策略约束派,直接限制新策略和数据集里行为策略的距离,比如用 KL 散度、支持集约束或者显式的行为克隆项,好处是直观,坏处是约束太紧就学不到比数据更好的策略,太松又控制不住漂移。第三派是模型派,先学一个环境动力学模型,再用模型的不确定性做惩罚,本质上还是保守,只是保守的对象换成了转移分布。
这三派有一个共同的隐含前提:状态表征是可信的。也就是说,我们默认“当前状态长什么样”这件事是给定的、正确的,误差只可能出现在价值估计和策略更新上。但你稍微想一想就会发现这个前提很脆弱——如果两个语义完全不同的状态被编码器编码成了几乎一样的向量,那么无论你的 Q 网络多保守、策略约束多严格,下游也永远区分不开它们,学出来的策略必然在其中一个状态上做错决策。这就是表征层面的错误,而且是不可修复的错误,因为它发生在上游。
1.2 互模拟度量:用“行为等价”给状态分组
互模拟这个概念最早来自并发系统的理论,核心思想是:两个系统如果对外部观察者的刺激永远给出相同的响应,那它们就是等价的,内部实现细节不重要。搬到强化学习里,两个状态 s 和 t 是互模拟等价的,当且仅当在任意动作 a 下,它们给出的即时奖励相同,并且转移到的后继状态集合也是互模拟等价的。注意这里的关键词是“任意动作”,也就是说这种等价是关于行为的,不是关于像素的。
这个定义天然适合做状态抽象。想象你在玩一个迷宫游戏,走廊的地板是木纹还是大理石纹,对通关毫无影响,这两个状态在决策意义上是等价的。如果编码器能把这类无关差异抹掉,那么原本需要海量样本才能覆盖的状态空间,压缩之后可能只需要很小一部分样本就能覆盖,样本效率直接上来了。这也是为什么互模拟度量在提升泛化性和鲁棒性这两个方向上一直有市场。
数学上的标准形式是一个递归的固定点等式。对于状态对 (s, t),度量 d 满足 d(s,t) 等于对所有动作取最大值的“奖励差的绝对值,加上 γ 乘以两个后继分布之间的 Wasserstein 距离”。这个式子好理解:两个状态的距离,由它们自身奖励的差异和后继状态分布的差异共同决定,而后者又递归依赖更远的状态。Ferns 那批早期工作给出了这个算子的固定点存在性证明,为后来的深度实现打了地基。
工程上没人真的去解这个固定点,大家用的是深度学习版本的近似:用一个编码器把状态映射到潜空间,然后约束潜空间里的 L1 距离去拟合上面那个目标值,目标值里的后继分布距离用高斯近似下的 Wasserstein-2 闭式解来算。为什么用高斯近似?因为连续控制里的转移分布通常用对角高斯建模,两个高斯之间的 W2 距离有闭式表达,计算代价可控,不需要采样。代价是表达能力受限,多峰分布会被压成单峰,这是后面会提到的一个隐患。
1.3 这次被点名的“缺陷”,我理解是三个层面的事
标题里说“解决互模拟缺陷”,但我得坦白讲,光看标题没法确定作者具体打的是哪一只靶子。结合这类工作的常见切入角度,我判断至少有三种可能的缺陷形态,而且它们经常同时出现。
第一种是估计偏差。互模拟度量的目标值依赖奖励和动力学,这两个量在离线数据里都是经验估计出来的。数据覆盖稀疏的地方,奖励估计方差大,动力学模型更是几乎全靠猜,度量自然就歪了。第二种是过度聚合,这是我觉得最要命的一类。互模拟关系是一个等价关系,等价类一旦合并就回不去了,如果因为数据不足把两个本该分开的状态错误合并,后续所有基于这个表征的学习都会继承这个错误,而且没有任何机制能把它纠正回来,因为离线设定下你没有探索的机会。第三种是目标错位,互模拟度量本身是任务无关的,它追求的是“行为等价”这种通用性质,但你的最终目标是学一个好策略,这两者之间不一定对齐,抽象出来的维度可能对价值估计帮助不大。
我个人倾向认为这篇工作的主要贡献在于第二和第三种之间的耦合,因为单纯的第一类问题在之前的鲁棒性工作里已经被反复讨论过了,而“抽象做过头”这件事在离线设定下缺乏系统性的分析。后面的章节我会按这个理解往下拆,同时明确标注哪些是基于公开方法的合理推演。
2. 深入机制:互模拟为什么会在离线数据上失灵
2.1 度量的递归性遇上有限数据,误差会沿着状态图传播
互模拟度量的定义是递归的,这一点很优雅,但也是它在离线场景下的软肋。要准确估计状态 s 和 t 的距离,你需要先知道它们的后继状态分布,而后继状态的距离又要继续往后推。听起来像是在整个状态图上做一次 Bellman 式的迭代,事实上也确实如此——实践中就是拿一个目标编码器提供稳定的回归目标,主编码器去拟合它,跟 DQN 里目标网络的套路一模一样。
问题来了:在线设定下,迭代的每一步都有新数据进来修正估计,误差会被新鲜样本不断冲淡。离线设定下数据是死的一批,迭代只能在这批数据上反复咀嚼,一旦某个区域数据稀薄,那个区域的度量估计就会固化成错误值,然后通过递归结构向邻近状态扩散。我做过一个很直观的验证,在一条长走廊环境里只保留中间一段的转移样本,训练出来的表征会把走廊两端的状态映射到非常接近的位置,因为中间那段的数据没法支撑起足够的区分度,递归传上去距离就退化了。
这跟价值函数的外推误差是同一个病根,只是发生的位置更靠上。而且它更隐蔽,因为价值函数的错误你还能通过评估曲线看出来,表征的错误在训练日志里几乎是隐形的,除非你专门去做状态对距离的可视化。
2.2 过度聚合:把“差不多”当成了“一样”
互模拟度量在潜空间里表现为距离约束,两个状态越“行为等价”,编码距离越接近于零。这本来是个软约束,但在实践中很容易退化成硬合并,原因有两个。一个是编码器容量有限,当损失函数逼着它把大量状态对的距离压到接近零时,它最省力的解法就是让这些状态共享同一个编码,尤其当这些状态在数据里出现频率很低的时候。另一个是高斯近似的副作用,Wasserstein 距离对分布的均值差异敏感,对方差差异的惩罚相对温和,导致两个均值相近但不确定性差别巨大的状态被判为接近。
过度聚合的后果在决策上是灾难性的。举个连续控制的例子,机器人在接近障碍物时,距离障碍物 0.3 米和 0.35 米这两个状态,从纯动力学角度看差异很小,转移分布几乎重合,奖励也差不多,互模拟度量会认为它们高度等价。但控制上这两个状态对应的安全动作可能完全不同,一个是继续前进,一个是必须急停。如果编码器把它们合并了,Q 网络就只能输出一个折中的价值,策略要么过于激进撞上去,要么过于保守原地不动,两种都不对。
关键在于这个错误没法被下游修正。你可以在 Q 学习里加再大的保守惩罚,也改变不了“网络看不到差异”这个事实。这就像你把两张不同的照片压成了同一个缩略图,后面无论用多强的分类器都无法区分它们,信息在压缩那一步就已经丢了。
2.3 抽象目标与策略目标脱节,好表征不一定好用
第三个层面的问题更微妙。互模拟度量的目标是让潜空间距离精确拟合“奖励差加后继分布距离”,注意这是一个精确拟合的目标,它要求编码器保留所有影响奖励和动力学的信息,同时又要求它丢弃所有不影响的部分。这两个要求在实际优化中经常打架,因为神经网络没有能力完美地做这种“保留什么、丢弃什么”的切分。
更麻烦的是,即便互模拟度量学得很准,它学到的抽象也未必对当前任务有用。强化学习真正关心的是“哪些状态差异会导致最优动作发生变化”,而互模拟关心的是“哪些状态差异会导致奖励和动力学发生变化”,前者是后者的子集,但不相等。举个极端例子,两个状态奖励和动力学完全一致,互模拟认为它们等价,但从策略角度看如果它们处在不同的策略阶段,最优动作可能完全不同。这种“任务相关差异”在纯互模拟度量里是没有位置的。
业界的常见补救手段是在互模拟正则之外再加一个任务相关的辅助损失,比如动作预测、下一状态预测或者价值一致性约束,用多任务的方式把策略相关信息拽回表征里。代价是要调的超参变多了,几个损失之间的权重平衡直接决定成败,这也是后面实操部分要重点讲的。
2.4 一张表看清三类缺陷的差异
| 缺陷类型 | 典型现象 | 根因 | 离线下的特殊性 | 可用的缓解方向 |
|---|---|---|---|---|
| 度量估计偏差 | 潜空间距离图出现大片模糊区,邻近状态分不开 | 奖励与动力学在稀疏数据处经验估计方差大,递归传播 | 没有新数据修正,误差固化 | 降低度量学习的学习率、加目标网络软更新、对稀有状态降权 |
| 过度聚合 | 状态对距离塌缩到接近零,策略输出抖动或僵化 | 编码器容量瓶颈、高斯近似的方差惩罚偏弱、损失权重过大 | 合并不可逆,无探索机会 | 引入距离下界、改用软聚合、加对比式正则 |
| 目标错位 | 度量损失降得很低但回报不涨,甚至倒退 | 互模拟目标与任务目标不完全重合 | 离线数据无法提供任务反馈 | 增加任务相关辅助头、用价值一致性约束对齐 |
这张表我建议你贴在工位上,因为排查的时候第一件事就是判断你遇到的是哪一类,三类问题的解法方向完全不同,用错药只会越治越糟。
3. 方案怎么搭:结构选型与关键取舍
3.1 整体架构:四块拼图
我复现这类方法时习惯把系统拆成四块:编码器、度量头、价值网络、数据管线。编码器负责把原始观测压成潜向量,通常是几层卷积接全连接,或者对低维状态直接用 MLP。度量头不是独立网络,它复用编码器的输出,额外挂两个小分支分别预测奖励和后继状态的均值方差,因为互模拟目标需要这两样东西。价值网络吃的是潜向量加动作,这里有个重要的设计选择:Q 网络到底接原始状态还是接潜向量。接潜向量的好处是计算省、泛化好,坏处是表征的错误会直接传导到价值上;接原始状态的好处是价值估计不受表征误差影响,坏处是失去了抽象带来的样本效率优势。
我的建议是分阶段处理。前期用潜向量训练,让表征和价值一起收敛;后期如果发现表征明显塌缩,可以切到原始状态做微调,当作一个兜底。这个做法没有理论保证,但在我跑过的几个环境里都能把最终回报拉回来一截。
数据管线这块,离线设定下最重要的不是采样效率而是覆盖度监控。我会在训练前先统计每个状态维度的分位数分布,标出数据稀疏的区间,这些区间就是后面度量容易出问题的高危区。这个统计花不了几分钟,但能帮你在看到异常曲线时快速定位是数据问题还是算法问题。
3.2 软聚合:给距离加一个下界
针对过度聚合,最直接的思路是不让距离无限制地趋近于零。具体做法有几种,我在实践里比较过。第一种是加一个距离下界,损失函数改成“当预测距离小于阈值时施加惩罚”,相当于强制不同状态至少保持一定间隔,阈值一般设成 0.05 到 0.1 这个量级,太大表征会失去平滑性,太小起不到作用。第二种是改损失形式,把 L1 距离换成一个有饱和特性的函数,让距离压缩到一定程度后梯度自然减小。第三种是引入对比学习的思路,把同批次内明显不同的状态作为负样本推开。
我目前主要用第一种,因为它的行为最可预测,阈值和环境尺度绑定,换环境时按状态维度归一化后的尺度重新标定就行。第二种的好处是无需调阈值,但对损失曲线的解读变难了,你很难判断距离是被压住了还是本来就该那么小。第三种效果上限最高,但对批次构造很敏感,批次里如果全是相似状态,负样本就失效了。
注意:距离下界这个操作会直接削弱互模拟度量的理论性质,你不再是在拟合严格的度量,而是在拟合一个带修正的近似。如果你的论文目的是验证度量本身的性质,这条路不能走;如果是工程落地追求回报,那就无所谓。
3.3 保守强度的定标:从数据覆盖度反推
离线强化学习里那个保守系数,几乎是所有方法里最玄学的一个超参。系数太小,Q 值还是会爆,策略跑去探索数据外;系数太大,Q 值被压得过于保守,策略直接躺平只做最安全的动作。文献里常见的取值是 5 左右,但这个值在不同环境、不同数据质量下差别巨大。
我自己的定标方法是看数据覆盖度指标。先算数据集里动作分布的有效支撑范围,比如对每个状态统计其出现过的动作数量,取中位数作为覆盖度。覆盖度高的数据集(专家数据、多样性数据),保守系数可以放小,2 到 3 就够;覆盖度低的数据集(单一策略数据、窄分布数据),系数要放大到 10 甚至 20,因为外推风险高得多。
另一个实用技巧是把保守系数做成自适应的,用当前 Q 值和数据集内 Q 值的相对差距作为信号,差距大就加大惩罚,差距小就放松。这个做法的直觉是让惩罚力度动态匹配外推程度,比固定系数稳不少。实现上就是在损失里多加一个比例项,注意要做梯度截断,别让惩罚项反过来污染 Q 的学习。
3.4 目标网络的更新节奏,比你想的更重要
度量学习用的是目标网络提供回归目标,这个目标网络的软更新系数我一开始按 DQN 的习惯设成 0.005,结果发现表征训练极不稳定,损失曲线像心电图。后来把系数降到 0.001,稳定性明显改善。原因在于互模拟目标是递归定义的,目标网络更新太快等于让回归目标一直在动,编码器永远在追一个移动靶,自然收敛不了。
这里有个取舍:更新太慢,目标值滞后,表征会跟不上数据分布的变化;更新太快,训练震荡。我给的经验值是每 1000 步做一次较硬的同步,或者软更新系数设成 0.001 到 0.002 之间,具体看你的总训练步数。如果总步数只有十万级,0.002 比较合适;如果是百万级,0.0005 更稳。
4. 动手实现:从数据到训练的完整链路
4.1 数据预处理:归一化做错,后面全白搭
离线数据的归一化比在线场景更讲究,因为你没有机会通过交互去适应尺度偏差。我的做法是统计整个数据集的每个维度均值和标准差,做 z-score 归一化,但有一个例外:奖励不做归一化,或者只做温和的缩放。原因是互模拟度量里奖励差是直接参与距离计算的,你把奖励压得太狠,度量就失去了区分度,本来靠奖励差异能分开的状态会被错误合并。
动作的归一化要按数据集的实际范围来,不要用理论上的动作空间上下界,因为数据里可能根本没覆盖到边界。用实际最大最小值加上一点余量,这样能保证归一化后的动作在 [-1, 1] 之间但不会挤在中间一小段。
下面是我常用的预处理骨架:
import numpy as np def build_normalizer(states, actions, rewards, eps=1e-6): state_mean = states.mean(axis=0) state_std = states.std(axis=0) + eps # 动作按数据实际范围缩放,不用理论边界 act_min = actions.min(axis=0) act_max = actions.max(axis=0) act_scale = np.maximum((act_max - act_min) / 2.0, eps) act_center = (act_max + act_min) / 2.0 # 奖励只做温和缩放,保留区分度 reward_scale = max(np.abs(rewards).mean(), 1.0) return { "s_mean": state_mean, "s_std": state_std, "a_center": act_center, "a_scale": act_scale, "r_scale": reward_scale, } def normalize_batch(batch, norm): s = (batch["s"] - norm["s_mean"]) / norm["s_std"] s2 = (batch["s2"] - norm["s_mean"]) / norm["s_std"] a = (batch["a"] - norm["a_center"]) / norm["a_scale"] r = batch["r"] / norm["r_scale"] return s, a, r, s2, batch["done"]这段代码有两个地方的用意值得展开。状态用均值和标准差归一化是为了让编码器的输入各维度量级一致,不然某一维数值特别大就会主导梯度。奖励只除以绝对值均值,是为了把奖励控制在个位数量级,同时又保留相对关系,如果你把它也做标准化,奖励差会被压缩到零点几的量级,度量学习时这部分信号就被淹没了。
4.2 度量学习的损失实现
核心损失是三段拼起来的:潜空间距离去拟合目标值,目标是奖励差加上 γ 倍的后继分布 Wasserstein 距离。Wasserstein-2 在两个对角高斯之间有闭式解,实现如下:
import torch import torch.nn as nn import torch.nn.functional as F class BisimHead(nn.Module): def __init__(self, feat_dim, latent_dim, action_dim): super().__init__() self.reward = nn.Linear(feat_dim + action_dim, 1) self.mu = nn.Linear(feat_dim + action_dim, latent_dim) self.log_sigma = nn.Linear(feat_dim + action_dim, latent_dim) def forward(self, phi, a): x = torch.cat([phi, a], dim=-1) r = self.reward(x).squeeze(-1) mu = self.mu(x) log_sigma = torch.clamp(self.log_sigma(x), -5.0, 2.0) return r, mu, log_sigma def gaussian_w2(mu1, log_s1, mu2, log_s2): s1 = torch.exp(log_s1) s2 = torch.exp(log_s2) # 对角高斯下 W2 平方 = 均值差平方 + (sqrt(s1) - sqrt(s2))^2 之和 return (mu1 - mu2).pow(2).sum(-1) + (s1.sqrt() - s2.sqrt()).pow(2).sum(-1) def bisim_loss(phi1, phi2, a1, a2, r1, r2, head, head_target, gamma, lower_bound=0.0): # 目标用 stop-grad 的 target 头计算,保证回归目标稳定 with torch.no_grad(): r1_t, mu1_t, ls1_t = head_target(phi1, a1) r2_t, mu2_t, ls2_t = head_target(phi2, a2) target = (r1_t - r2_t).abs() + gamma * gaussian_w2( mu1_t, ls1_t, mu2_t, ls2_t).sqrt() # 预测距离 pred = (phi1 - phi2).abs().sum(-1) # 距离下界:低于阈值的部分额外惩罚,抑制过度聚合 print_penalty = F.relu(lower_bound - pred).pow(2) return (pred - target).pow(2).mean() + 0.1 * print_penalty.mean()这里有几个细节容易写错。第一,phi1和phi2必须是同一批次里不同样本的编码,不要搞混成同一状态的两次增强,互模拟约束的是不同状态之间的关系。第二,目标值必须在 no_grad 下用目标网络算,否则编码器会朝着“让目标也变小”的方向优化,直接塌缩。第三,log_sigma 一定要 clamp,不加约束的话方差会一路涨到极大或者趋近于零,前者让 W2 距离失去意义,后者导致数值不稳定。第四,距离下界只在预测值低于下界时生效,所以用 ReLU 包一层,权重我一般设 0.1,太大表征会变得过于离散。
4.3 价值网络与策略的保守更新
价值部分我用的是保守 Q 学习加策略约束的组合,这是离线场景下比较稳的配置。核心是在标准 TD 损失上加一项,把数据集外动作的 Q 值往下压:
def conservative_q_loss(q_net, target_q, s, a, r, s2, done, policy, alpha_cql=5.0, gamma=0.99): with torch.no_grad(): a2, logp = policy.sample(s2) q_next = target_q(s2, a2) - logp y = r + gamma * (1.0 - done) * q_next q_pred = q_net(s, a) td = F.mse_loss(q_pred, y) # 保守正则:压低随机采样动作的 Q,抬高策略动作的 Q a_rand = torch.empty_like(a).uniform_(-1.0, 1.0) q_rand = q_net(s, a_rand) a_pi, _ = policy.sample(s) q_pi = q_net(s, a_pi) # logsumexp 形式的近似,比直接采样更稳 cql = torch.logsumexp(q_rand, dim=0).mean() - q_pi.mean() return td + alpha_cql * cql这段代码里用均匀采样代替真正的 logsumexp,是因为连续动作空间里精确的 logsumexp 需要采样估计,均匀采样加对数平均是个廉价近似。如果你的动作维度不高,也可以多采几个点求 log-mean-exp,稳定性会更好一些,代价是每步多几次前向。
策略这头我用的是带约束的最大似然加价值最大化,约束项用数据集动作和策略动作的均方误差,或者用优势加权的行为克隆。后者的做法是先把优势算出来,只对正优势的样本加大权重,这样策略会倾向于模仿数据里那些“比平均水平好”的动作,而不是无差别地克隆全部数据。这个设计在数据质量参差的时候特别有用,纯克隆会被大量平庸样本带偏。
4.4 训练循环:指标监控比调参更重要
我把训练循环里的监控指标分成三组,每天看一次就够。
第一组是表征健康度:潜空间的平均成对距离、距离分布的方差、还有编码器输出的各维标准差。平均成对距离如果是持续下降,说明在塌缩;如果一直高位震荡,说明损失权重可能过大,编码器在硬凑目标。各维标准差的用途是查维度塌陷,某一维标准差接近零就说明这一维被废掉了,如果连续好几维都这样,那这个编码器基本没救,建议重训。
第二组是价值健康度:Q 值的均值、最大值、以及数据集内 Q 和数据集外 Q 的差距。Q 值均值缓慢上升是正常的,如果突然跳一个数量级就是爆了,这时候先降学习率,再考虑加保守系数。数据集内外 Q 差距能反映外推程度,差距过大说明策略已经跑太远了。
第三组是行为健康度:策略在数据状态上的动作分布、动作熵、还有和数据集动作的偏离度。动作熵塌到零意味着策略退化成确定性,通常是保守惩罚太强;偏离度居高不下说明约束太松。
我为这个流程配了一份参数基线,来自几个连续控制环境的实测,你可以拿来当起点:
| 组件 | 参数 | 基线值 | 调整方向 |
|---|---|---|---|
| 编码器 | 学习率 | 1e-4 | 表征不稳时降到 5e-5 |
| 编码器 | 潜空间维度 | 64 到 128 | 状态维度低时取 32 即可 |
| 度量目标 | 目标网络软更新 | 0.001 到 0.002 | 总步数越长取越小 |
| 度量目标 | 距离下界 | 0.05 到 0.1 | 归一化后状态尺度决定 |
| 价值网络 | 学习率 | 3e-4 | 常与策略共享 |
| 价值网络 | 保守系数 | 2 到 20 | 按数据覆盖度定标 |
| 价值网络 | 折扣因子 | 0.99 | 长时序任务可到 0.995 |
| 训练 | 批大小 | 256 | 显存允许可到 512 |
| 训练 | 每步梯度更新次数 | 1 | 样本效率优先可提高到 4 |
4.5 小规模验证:怎么判断改动有没有用
做完实现之后,别急着上大环境,先在一个维度很低、动力学简单的小环境里跑通。我习惯用一条 20 步以内的链式环境做冒烟测试,状态是一维位置,动作是左右两个离散化后的连续动作,奖励只有到达终点才给。这个环境的好处是所有东西都可解释,你能手动画出每个状态学到的潜向量,用肉眼判断哪些状态被正确分组了。
验证的标准有三条。第一条,链上相邻状态的距离应该接近零,因为它们的行为几乎一样;第二条,跨越拐点的状态距离应该明显大于零,因为拐点前后的最优动作相反;第三条,如果只给数据集一半的链,检出的距离应该在被截断的地方拉大,说明度量确实反映了数据支持范围。三条都过,再去跑连续控制的标准环境。
这个验证大概花半小时,但能帮你提前发现 80% 的实现 bug。我见过太多人在大环境上训了三天才发现编码器输出全是同一个值,回头查是损失函数里符号写反了。小环境上这类问题五分钟就暴露了。
5. 实战排查:那些文档里不会写的问题
5.1 Q 值在训练初期就爆炸
这是最常见的症状,表现是训练到几千步时 Q 值均值突然从个位数跳到几百。多数人的第一反应是加大保守系数,这通常有效,但会掩盖真正的原因。我排查的顺序是这样的:先看数据集的奖励分布,如果有极端大值,那 Q 爆炸可能只是奖励尺度问题,先做奖励裁剪;再看 done 标志的统计,如果 done 没被正确使用,自举会跨越终止状态,把价值一直往后传;最后看目标网络的更新频率,如果每步都硬同步,目标值震荡会导致 TD 误差巨大。
有一个特别隐蔽的坑:某些环境里 done 和 truncated 是分开返回的,数据导出的时候如果你只存了 done,截断样本会被当成正常终止处理,价值传播直接断掉。这种情况下的 Q 爆炸往往伴随回报曲线莫名其妙地下跌。解决办法是在数据里同时保留这两个标志,训练时把它们做逻辑或。
5.2 表征塌缩:损失看着在降,其实什么都没学到
表征塌缩的表现很温和,损失曲线平稳下降,看起来一切正常,但一评估就发现策略和不用编码器的版本差不多。根本原因是度量目标里的两项都在同时变小,编码器找到了一条捷径:让所有状态映射到同一个点,距离是零,目标值如果也小,那损失自然也小。
判断方法是采样一批状态对,画编码距离和目标值的散点图。健康的训练里应该是一条贴近对角线的分布;塌缩的话所有点会挤在左下角,距离和目标值都接近零。一旦确认塌缩,先检查目标网络是不是漏了 stop_grad,再检查奖励是不是被归一化得过狠,然后试着把度量损失的权重降下来,让价值损失主导一段时间再切回去。
我还遇到过一次不那么典型的情况:编码器没完全塌缩,但塌到了低维子空间里,输出分布像是被压扁在几个方向上。查了半天发现是批归一化的动量设得太大,统计量跟不上批次分布变化。改小动量之后恢复正常。这种问题在日志里看不出来,只能通过可视化发现。
5.3 离线评估和在线评估差距巨大
离线评估用的是数据集里的动作,在线评估是策略自己跑,两者的差距能反映策略对数据分布的依赖程度。差距大一般意味着策略在数据外区域做了太多决策,本质还是外推。但如果你已经加了保守项还有差距,那问题可能不在价值层,而在表征层——编码器把数据覆盖区外的状态映射到了覆盖区内,导致策略误以为自己处在熟悉区域,做出错误动作。
验证方法很直接:拿一批轨迹,统计每个状态到最近数据点的潜空间距离,看看失败回合里的状态距离是不是显著更大。如果是,说明表征的外推能力不足,可以试试在编码器训练里加一个重构损失或者一致性损失,强迫它对未见状态也产生有意义的编码,代价是表征可能变得没那么“抽象”。
5.4 排查速查表
| 现象 | 优先怀疑 | 快速验证 | 处理手段 |
|---|---|---|---|
| Q 值量级异常跳变 | 奖励尺度、done 处理、目标网络节奏 | 打印奖励分位数和 done 比例 | 裁剪奖励、修正终止标志、降低软更新系数 |
| 损失平稳但回报不涨 | 表征塌缩、目标错位 | 画潜空间距离直方图 | 检查 stop_grad、降度量权重、加任务辅助损失 |
| 策略动作几乎不变 | 保守惩罚过强、动作熵塌缩 | 统计动作方差 | 降保守系数、加熵正则 |
| 离线好在线差 | 表征外推不足 | 比对失败轨迹的潜空间距离 | 加重构或一致性损失、扩大数据覆盖 |
| 训练后期突然崩溃 | 过拟合到稀疏区、数值溢出 | 检查梯度范数和损失尖峰 | 梯度裁剪、降学习率、冻结编码器 |
提示:这张表的用法是从上往下逐行排除,不要跳行。我见过不少人直接跳到最后一行的处理手段,结果改了半天发现根因在第一行。
6. 我在反复折腾里攒下的几点体会
互模拟这条路我最深的感受是:它的价值不在于提升多少绝对回报,而在于它给了你一个可以检查的中间产物。纯端到端的强化学习是个黑盒,训崩了你只能猜;加了表征约束之后,潜空间的距离分布成了一个可以观察、可以断言的指标。我现在做任何带表征学习的离线算法,都会先把这组指标搭起来,训练过程中一旦异常,定位速度快一个量级。
另一个体会是关于“缺陷”这个词本身。很多人看到解决缺陷就以为要彻底修掉,但工程上更务实的做法是给缺陷加个护栏,让它别在最要命的地方发作。过度聚合没法根除,那就用距离下界把它限制在可接受范围;度量估计偏差没法消除,那就用目标网络和降学习率把它压慢。追求理论纯粹性和追求可用性,这是两条路,你得先想清楚自己在哪条路上。
最后分享一个我最近在用的做法。训练前先跑一遍纯行为克隆,把它的回报作为下限基准;训练完如果互模拟版本连这个下限都没超过,那基本可以判定是表征拖了后腿,直接关掉度量损失再训一遍做对照。这个 A/B 对照花不了多少算力,但能省下你几天自我怀疑的时间。