ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

模仿学习算法包实战:BC、DAgger与GAIL训练调参与排错

模仿学习算法包实战:BC、DAgger与GAIL训练调参与排错

简介:面向深度强化学习与模仿学习方向的学习者,这份代码包围绕从专家轨迹中学习策略这一主题,覆盖 PPO、AIRL、BC、DRIL、FAIRL、GAIL、GMMIL、PUGAIL、RED 等算法,并提供仅状态模仿、吸收状态指示器与 R1 梯度正则化等实验选项,适合算法复现、课程项目与基线对比。压缩包共 27 个文件、约 999KB,其中 6 个 Python 脚本承担训练、评估与环境交互逻辑,17 张 PNG 汇总各算法训练与测试回报曲线,2 个 Markdown 文档说明使用方法,另附专家轨迹数据文件,目录紧凑、模块边界清楚。已有 1068 人学习,读者可从中获得可运行的模仿学习实验框架、专家轨迹加载与策略优化示例、不同算法的结果对照图,以及排查训练不稳定与回报波动的参考思路,并据此理解生成对抗式模仿与行为克隆的差异。

1. 奖励函数写不出来时:imitation-learning 模仿学习算法包能接住哪类任务

控制类项目最常见的卡点不在模型大小,而在奖励函数:机械臂要的是平滑轨迹,写个距离惩罚训练出来全是抖动;策略梯度在稀疏奖励下几乎不更新,调两周不如人遥操作十分钟。imitation-learning 这套 Python 实现的模仿学习算法包走的是另一条路——把专家演示当监督信号,先用行为克隆(BC)把策略立起来,再用 DAgger 补上分布偏移,最后用 GAIL 在环境里做对抗式精调。它面向的是已经有脚本专家或遥操作数据、但奖励写不出来或写不准的 deep-reinforcement-learning 场景;判断自己适不适合,标准很朴素:你能把一条条轨迹存成(obs, action, next_obs, done)的数组,并且能让策略真正跑进环境里,这套东西就能用。

2. 三种范式的代码骨架:BC 打底、DAgger 补偏移、GAIL 做对抗精调

模仿学习算法包最容易让人误判的地方,是把三个算法当成三选一的替代品。实际工程里它们是阶梯:BC 决定你有没有一个能跑的策略,DAgger 决定这个策略在离线和在线之间能不能接住,GAIL 决定最终性能能不能逼近专家。这一章按数据流顺序把三块骨架拆开,代码用最常见的组织方式写,仓库里的类名函数名可能不一样,但张量形状和损失形式是对得上的。

2.1 轨迹容器与数据协议:先把专家数据对齐成 (obs, action, next_obs, done)

新手翻车十有八九不在算法,在数据协议。专家数据是离线采的,策略数据是在线采的,如果两边字段顺序、dtype、done的表示方式不一致,后面所有 loss 曲线都没有参考价值。我一般先写一个两种数据源共用的环形缓冲,把协议钉死。

import numpy as np class TrajectoryBuffer: """环形缓冲:专家轨迹和策略 rollout 共用同一套字段协议""" def __init__(self, obs_dim, act_dim, capacity=200_000): self.capacity = capacity self.obs = np.zeros((capacity, obs_dim), dtype=np.float32) self.act = np.zeros((capacity, act_dim), dtype=np.float32) self.rew = np.zeros((capacity, 1), dtype=np.float32) self.next_obs = np.zeros((capacity, obs_dim), dtype=np.float32) self.done = np.zeros((capacity, 1), dtype=np.float32) # 用 0/1,不用 bool self.ptr, self.size = 0, 0 def add(self, obs, act, rew, next_obs, done): i = self.ptr self.obs[i], self.act[i] = obs, act self.rew[i], self.next_obs[i], self.done[i] = rew, next_obs, float(done) self.ptr = (self.ptr + 1) % self.capacity self.size = min(self.size + 1, self.capacity) def sample(self, batch_size, rng=None): rng = rng or np.random idx = rng.randint(0, self.size, size=batch_size) # 有放回采样,够用且实现简单 return (self.obs[idx], self.act[idx], self.rew[idx], self.next_obs[idx], self.done[idx]) def save(self, path): # 键名固定成这五个,评估脚本和可视化脚本都按这套键读 np.savez_compressed(path, obs=self.obs[:self.size], act=self.act[:self.size], rew=self.rew[:self.size], next_obs=self.next_obs[:self.size], done=self.done[:self.size])

几个参数值得说清。dtype=np.float32不是随手写的,PyTorch 默认张量就是 float32,用 float64 存一遍再转一遍,几十万条样本会白白多占一倍内存,而且转换发生在DataLoader的 worker 里,很容易变成隐性瓶颈。done存成 0/1 浮点而不是布尔,是为了和rew一起拼成(N, 5)的数组做批量处理时类型不打架。capacity给 20 万条是经验值:连续动作任务单条轨迹通常几百到几千步,20 万条大致相当于几百条轨迹,覆盖 GAIL 一轮训练足够,再大就只是拖慢采样。

图像观测要额外注意一点:如果环境返回的是(H, W, C)的 uint8 帧,在add之前就要定好是展平还是保留三维。展平省事,但卷积策略网络就用不上了;保留三维则要在obs_dim上写(C, H, W)并做通道前置。这个决定一旦写进 npz,后面改起来要重采数据,属于典型的后悔药没处买。

2.2 行为克隆:三层 MLP 加 MSE 就够,但输出头决定成败

BC 本质是监督学习,obs → action的回归或分类。网络不需要深,两层 256 的 tanh MLP 在绝大多数连续控制任务上已经够用;真正决定成败的是输出头跟动作空间的匹配方式。

import torch import torch.nn as nn class PolicyNet(nn.Module): def __init__(self, obs_dim, act_dim, hidden=256): super().__init__() self.body = nn.Sequential( nn.Linear(obs_dim, hidden), nn.Tanh(), nn.Linear(hidden, hidden), nn.Tanh(), ) self.head = nn.Linear(hidden, act_dim) # 确定性策略,直接回归动作均值 def forward(self, obs): return self.head(self.body(obs)) def bc_loss(policy, obs, act, discrete=False): out = policy(obs) if discrete: # 离散动作:交叉熵,act 是 long 型类别索引 return nn.functional.cross_entropy(out, act.long()) # 连续动作:MSE 回归,act 需与 out 同量纲 return nn.functional.mse_loss(out, act)

nn.Tanh()而不是 ReLU,是因为连续控制的观测和动作经常带负值,tanh 的对称性让初始阶段的梯度分布更均匀;如果观测是图像,把body换成几层 stride=2 的卷积再接展平即可,输出头不动。连续动作用 MSE 意味着你在拟合条件均值,这在高斯策略假设下等价于只学均值不学方差,好处是稳定,坏处是遇到多峰专家数据(同一个状态专家有时左拐有时右拐)会学出一个谁都不像的中间动作。常见做法是改成高斯头,多输出一维log_std并用-log_prob当损失,代价是训练更容易发散,方差要 clamp 到[-5, 2]这种范围。

动作量纲是另一个高频坑。环境action_space是[-1, 1]的话,专家数据如果来自力矩控制(比如[-20, 20]),直接回归出来的策略一进环境就会被 clip 成常量,现象是 BC loss 掉得很快但 rollout 一动不动。落数据前统一按(a - low) / (high - low) * 2 - 1归一化到[-1, 1],评估时再反归一化,这一步写在采集脚本里比写在训练脚本里安全。

2.3 DAgger 的在线聚合:把「策略会走到哪」交给专家重新打标

BC 的理论误差随轨迹长度平方增长,直觉解释就是:策略在专家没去过的状态上没学过,一旦偏离就再也回不来。DAgger 的思路很直接——让当前策略自己去跑,跑到哪些状态就请专家对这些状态重新打标签,把新样本并进数据集重训。代码骨架不长,但循环里三个顺序不能乱。

def dagger_loop(env, expert, policy, dataset, iters=10, beta0=0.5, steps_per_iter=2000): for it in range(iters): # 1) 用当前策略在环境里采样,收集它真实访问到的状态 states = [] obs, _ = env.reset() for _ in range(steps_per_iter): beta = beta0 ** it # 专家介入概率指数衰减 if np.random.rand() < beta: act = expert.act(obs) # 专家接管,把轨迹拉回可行域 else: with torch.no_grad(): act = policy(torch.as_tensor(obs, dtype=torch.float32)).numpy() states.append(obs) obs, _, term, trunc, _ = env.step(act) if term or trunc: obs, _ = env.reset() # 2) 请专家对这些“策略自己走出来的状态”打标签 states = np.asarray(states, dtype=np.float32) labels = expert.label(states) # 支持批量查询的实现,比逐帧快一个量级 # 3) 聚合后重训,注意是在全量数据集上重训而不是只训新样本 dataset.extend(states, labels) train_bc(policy, dataset, epochs=5)

beta0控制专家介入强度,0.5意味着第一轮一半动作由专家给,之后每轮减半。有人直接取beta = max(0, 1 - it / iters)线性衰减,差别不大,但指数衰减在前期更依赖专家,适合专家本身很贵的场景。steps_per_iter=2000是每轮新采的步数,太小聚合效果不明显,太大则单轮训练时间压不住,2000 到 5000 之间比较常见。

这里有个实现细节容易被忽略:expert.label(states)必须支持批量查询。如果专家是规则函数,逐帧调用两百次和一次批量调用在 Python 里的开销差几十倍;如果专家是人,那 DAgger 就不适合跑十轮,得改成两三轮加大量增广。另外,train_bc每轮只训 5 个 epoch 是有意的——DAgger 靠的是数据分布持续扩张,不是单轮把 loss 压到最低,训太多轮会让策略过早过拟合到旧状态分布上。

2.4 GAIL 的判别器与奖励整形:奖励从哪来

GAIL 解决的是 BC 误差累积的另一种思路:不学动作,学一个「像专家」的判别器,用判别器输出反向构造奖励,再拿这个奖励跑标准策略梯度。判别器输入是(obs, action)拼接,输出一个 logit,专家样本标 1、策略样本标 0。

class Discriminator(nn.Module): def __init__(self, obs_dim, act_dim, hidden=128): super().__init__() self.net = nn.Sequential( nn.Linear(obs_dim + act_dim, hidden), nn.Tanh(), nn.Linear(hidden, hidden), nn.Tanh(), nn.Linear(hidden, 1), ) def forward(self, obs, act): return self.net(torch.cat([obs, act], dim=-1)).squeeze(-1) # 输出 logit def disc_loss(disc, expert_obs, expert_act, policy_obs, policy_act): # 标准二分类交叉熵:专家 1,策略 0 pos = disc(expert_obs, expert_act) neg = disc(policy_obs, policy_act) return (nn.functional.binary_cross_entropy_with_logits(pos, torch.ones_like(pos)) + nn.functional.binary_cross_entropy_with_logits(neg, torch.zeros_like(neg))) def gail_reward(disc, obs, act, clip=10.0): # r = -log(1 - D(s,a)),D 是判别器给出的“像专家”概率 prob = torch.sigmoid(disc(obs, act)) r = -torch.log(1.0 - prob + 1e-8) return torch.clamp(r, -clip, clip)

判别器网络刻意比策略网络小(128 vs 256)且只有两层,是为了不让它太快把策略和专家完全分开。奖励取-log(1 - D)而不是直接用D,是因为前者在D接近 0 时给的正奖励更平滑,策略在早期不至于所有样本奖励都是 0.001 这个量级,梯度全被吞掉。clip=10.0是给奖励设上限,防止个别样本把优势函数撑爆。

判别器的训练和策略的训练是交替的:每一轮先用当前策略采一批 rollout,更新判别器若干次,再用更新后的判别器算奖励、更新策略。这个「若干次」就是下一章要单独说的判别器步频,是 GAIL 里最敏感的超参之一。

3. 从零跑通一次训练:依赖、专家数据、配置和日志的四段命令链

拿到一个算法包,第一件事不是读完全部源码,是把它跑出第一条非零奖励曲线。这一步跑通了,后面所有调参才有意义;跑不通,读源码也读不出所以然。下面这条链路按「装环境 → 采数据 → 训 BC → 训 GAIL → 评估」的顺序排,脚本名以仓库里的实际文件为准,我按常见命名写,对不上就按功能找对应入口。

3.1 依赖装完之后,先验环境接口对不对

# 1) 建独立解释器环境,避免和系统里的 torch 版本打架 python -m venv .venv source .venv/bin/activate # Windows 下换成 .venv\Scripts\activate # 2) 装依赖:requirements.txt 里通常锁 numpy / torch / 环境接口库 / pyyaml / tensorboard pip install -r requirements.txt # 3) 自检:把算法注册表和已安装的环境列出来,确认 BC/DAgger/GAIL 三个入口都在 python -m <pkg>.registry --list

第三步别省。算法包的入口常挂在注册表或getattr动态导入上,包名写错、模块没被__init__.py导出,报错会出现在训练中段而不是启动时,排查成本翻倍。如果注册表能列出三个算法名,且环境接口库能make出你的环境,依赖这一关就算过了。

提示:环境接口库的版本差异主要体现在reset和step的返回值上。老接口reset()只返回obs,step()返回四元组;新接口返回(obs, info)和五元组。包里的 rollout 代码只认其中一种,版本不对会报「too many values to unpack」这类看似莫名其妙的错。改法很简单:在采集脚本里加一层适配,或者在环境外面包一个 wrapper,不要在算法内部到处打补丁。

3.2 采集专家轨迹:脚本参数与落盘格式

# 采集专家轨迹。--env 换成自己的环境 id,--episodes 决定演示条数 python scripts/collect_demos.py \ --env YourEnv-v0 \ --episodes 60 \ --policy expert_rule_based \ --noise 0.02 \ --out data/expert_60ep.npz

--episodes 60不是拍脑袋的数,下一章会讲怎么估;第一次跑通先给 20 到 30 条,确认文件能存下来、字段对得上,再补量。--noise 0.02是给专家动作加的小幅高斯噪声,目的是让专家数据覆盖一点状态邻域,BC 学出来的策略不会在专家轨迹两侧立刻崩掉。噪声太大等于污染标签,0.01 到 0.05 之间按动作量纲调,动作在[-1, 1]的话 0.02 合适。

落盘之后先做个形状检查,比直接开训省事得多:

import numpy as np d = np.load("data/expert_60ep.npz") print({k: (d[k].shape, d[k].dtype) for k in d.files}) # 期望看到 obs (N, obs_dim) float32, act (N, act_dim) float32, done 里 1 的个数等于轨迹条数

done为 1 的样本数应该正好等于--episodes,多一个少一个都说明采集循环里对terminated和truncated的处理有问题。超时截断算不算done,在不同算法里处理不一样:BC 不在意,GAIL 在意——超时被当成终止会让价值函数学到「到时间就没价值」,正确做法是用truncated做 bootstrap 而不是当终止。

3.3 配置文件:一次训练里真正要动的十几个键

algo: gail seed: 0 env_id: YourEnv-v0 demo_path: data/expert_60ep.npz policy: hidden: [256, 256] activation: tanh train: total_steps: 1_000_000 batch_size: 256 lr_policy: 3.0e-4 lr_disc: 1.0e-3 disc_updates: 3 # 每轮策略更新前,判别器更新几次 gamma: 0.99 gae_lambda: 0.95 ent_coef: 0.01 gail: reward_clip: 10.0 disc_hidden: 128

total_steps: 1_000_000是环境交互步数而不是梯度步数,这个口径一定要跟日志对上,否则你会以为训了很久其实只采了几万步。ent_coef: 0.01是熵正则,GAIL 里不建议设太大——奖励本身是从判别器来的,熵太高会让策略主动往判别器没见过的区域跑,判别器给的奖励全是噪声,训练曲线表现为先涨后崩。

3.4 训练与评估:日志里该盯哪几条曲线

# 训 BC 先确认数据管线没问题 python scripts/train_bc.py --config configs/bc.yaml --demo data/expert_60ep.npz # 训 GAIL,从 BC 权重热启动 python scripts/train_gail.py --config configs/gail.yaml --init runs/bc/policy.pt # 评估:20 条 episode,同时看均值和最差分位数 python scripts/evaluate.py --ckpt runs/gail/policy.pt --episodes 20 --percentile 25

日志里至少四条曲线要看:episode_return、disc_accuracy、policy_entropy、value_loss。disc_accuracy稳定在 0.5 到 0.75 之间是健康区间,说明判别器还分不太清;一旦冲到 0.95 以上并横住,策略这边基本拿不到有效梯度,奖励曲线会变成一条缓慢下滑的直线。policy_entropy应该缓慢下降而不是断崖式掉到 0,掉太快说明策略过早确定化,GAIL 阶段就废了。

评估只看均值是自欺欺人。模仿学习的失败是尾部事件——20 条里 18 条接近专家、2 条完全崩,均值可能还不错,但上线就是那 2 条在出问题。所以评估脚本里同时打 25 分位数,--percentile 25就是干这个的。

4. 超参不是玄学:专家轨迹量、判别器步频与折扣因子的联动关系

模仿学习算法的超参比普通监督学习少,但相互耦合更强。改一个数往往要连带改另一个,不然就是从「不收敛」跳到「过拟合」再跳回来。这一章把三个最影响结果的量单独拎出来,给的都是范围和判断依据,不是绝对最优值。

4.1 专家轨迹数量与状态维度:先算清覆盖度再谈算法

BC 的误差上界大致是O(T²ε),T是轨迹长度,ε是单步监督误差。这个式子说明两件事:轨迹越长,同样的单步误差放大得越厉害;想降低误差,要么减小ε(更好的网络/更多数据),要么打断误差累积(DAgger)。

实操里的估算方式是按状态空间覆盖率,不是按条数。经验区间:

任务类型状态维度建议专家轨迹条数备注
低维连续控制10 以内20 到 50每条几百步,够 BC 出可用策略
中维(带关节角、速度)10 到 5050 到 200建议配 DAgger,纯 BC 容易偏
图像输入高维200 以上必须做数据增广,否则过拟合背景
多模态专家(人遥操作)任意200 以上高斯头或多峰建模,MSE 会学出平均动作

表里的数字是起点不是终点。判断数据够不够,看 BC 的验证集 loss 和 rollout 表现是否背离:验证 loss 很低但 rollout 一开始就偏,说明数据覆盖不够,加数据或者上 DAgger;验证 loss 本身就高,说明网络容量或者输入特征有问题,加数据没用。

4.2 判别器步频与学习率:GAIL 最容易失衡的地方

disc_updates和lr_disc是一对联动参数。判别器更新太频繁或学习率太高,会迅速把策略和专家分开,奖励饱和,策略梯度消失;更新太少,判别器跟不上策略变化,奖励信号滞后,训练表现为奖励曲线剧烈震荡。

我一般这样起手:lr_disc = 1e-3、lr_policy = 3e-4,判别器学习率比策略高 3 倍左右,disc_updates = 1先跑 20 万步看disc_accuracy。如果它一直在 0.5 附近上不去,说明判别器太弱,把disc_updates提到 3 或者把disc_hidden从 128 加到 256;如果它三步之内冲到 0.9 以上,反过来做:lr_disc降到 3e-4,或者给判别器加梯度惩罚。

另一个常被忽略的旋钮是reward_clip。判别器输出经过-log(1-D)之后理论上无界,D → 1时奖励会冲到几十甚至上百,优势估计直接被个别样本主导。reward_clip: 10.0相当于给奖励设了个硬上限,代价是损失一部分区分度,但换来的稳定性在调试阶段更值钱。

4.3 折扣因子与 GAE:把信用分配对上专家轨迹长度

gamma = 0.99意味着有效视野大约 100 步,0.995大约 200 步,0.999大约 1000 步。这个数要和专家轨迹的实际长度匹配:如果专家轨迹平均 300 步收敛到目标,gamma给 0.99 会让价值函数看不到后半段,策略学到的是「快速动一下」,而不是「完整完成任务」。

gae_lambda控制偏差和方差的折中,0.95 是通用起点。GAIL 的奖励来自判别器,噪声比环境原生奖励大,这种情况下把lambda降到 0.9 甚至 0.85 能明显减小优势估计的方差,代价是偏差变大、收敛变慢。判断依据是value_loss的波动幅度:如果它在两个数量级之间来回跳,先降lambda再考虑降学习率。

注意:gamma改大之后,total_steps也要跟着加。视野从 100 步拉到 300 步,同样学到「完整任务」需要的数据量大致按比例增加,只改gamma不改步数,最常见的现象是训练到一半曲线开始缓慢下滑,看起来像过拟合,其实是步数不够。

4.4 参数速查表

参数作用起步值什么时候改
lr_policy策略网络步长3e-4曲线震荡就减半,20 万步不动就翻倍
lr_disc判别器步长1e-3判别器准确率过高就降,过低就升
disc_updates每轮判别器更新次数1 到 3先动这个再动学习率,影响更平滑
gamma折扣因子0.99按专家轨迹长度的 1/3 到 1/2 反推
gae_lambda优势估计折中0.95value_loss抖动大就降到 0.9
ent_coef熵正则0.01策略过早确定化就加大,探索过头就减小
reward_clip判别器奖励上限10.0奖励方差爆炸时降到 5.0
batch_size批量大小256显存够就加到 512,梯度更稳

5. 五类翻车现场排查:现象、成因和能直接改的那一行

算法包的 issue 区里重复率最高的不是「怎么实现」,而是「我训出来不对」。下面五条按出现频率排,每条都按现象、成因、解决三层写,能直接对应到代码里改哪一行。

5.1 BC 训练 loss 很低,rollout 三步就飘

现象:训练集 MSE 降到 1e-3 量级,进环境跑,前三步还行,第五步动作开始抖,第十步直接撞墙。

成因:典型的分布偏移。专家数据只覆盖专家走过的状态,策略在专家状态上学得很准,一旦偏出去几厘米,进入没见过的状态,输出就是网络外推的随机值,误差按T²放大。

解决:上 DAgger,哪怕只跑两三轮也有效;暂时不想引入在线专家的话,退一步做状态增广——对专家观测加小幅高斯噪声、对图像做随机裁剪和亮度扰动,让策略在专家轨迹邻域内有定义。噪声幅度按观测标准差的 5% 到 10% 给,给大了等于教策略在错误状态上输出正确动作,反而更糟。

5.2 GAIL 判别器准确率三步冲到 0.98,奖励曲线变平

现象:训练前几千步disc_accuracy迅速逼近 1.0,gail_reward均值掉到接近 0 并横住,策略性能不再变化。

成因:判别器容量或学习率相对策略太强,几乎瞬间就把两类样本线性分开。奖励函数变成「策略样本恒为 -10,专家样本恒为 +10」,策略拿到的梯度全是常数,等于没有奖励。

解决:先把disc_updates从 3 降到 1,再把lr_disc降到 3e-4;还不够就给判别器加梯度惩罚(WGAN-GP 那一套),或者把disc_hidden从 256 砍到 128。判别器网络比策略网络小一到两档,是 GAIL 里比较稳的配置习惯。

5.3 观测归一化统计量随训练漂移,训练中期突然崩

现象:前 30 万步曲线正常上涨,之后突然掉下去再也不回来,重启同 seed 也不一定复现。

成因:观测归一化用了滑动统计量,而策略探索到的状态分布随训练不断扩张,mean/std跟着漂,导致同一个观测在不同时刻被归一化成不同值。价值网络学的是旧分布,输入一变就全乱。

解决:归一化统计量只用专家数据算一次,然后冻结,写进 checkpoint 一起保存。评估和训练必须用同一份mean/std,这一点在加载模型时最容易漏——只加载了权重没加载统计量,现象是「训练好好的模型评估时表现极差」。

5.4 专家动作和环境动作空间量纲不一致

现象:BC loss 正常下降,但 rollout 里动作像是被截断,永远贴着[-1, 1]的边界走,机器人只朝一个方向缓慢移动。

成因:专家数据来自力矩控制,动作范围是[-20, 20],环境action_space是[-1, 1],回归出来的策略输出被环境 clip,等于把 95% 的信息丢了。

解决:采集阶段就把专家动作按(a - low) / (high - low) * 2 - 1归一化,落盘的 npz 里存归一化后的值,同时在文件里存一份action_low/action_high。评估脚本反归一化后再env.step。这条建议写进采集脚本的注释里,不然下一个人接手还要再踩一次。

5.5 固定 seed 但两次训练结果差很多

现象:seed: 0写死在配置里,同样的数据同样的超参,两次跑出来的最终回报差 30% 以上。

成因:三处非确定性没关掉——环境内部的随机数生成器没设种子、PyTorch 的 cuDNN 自动调优、DataLoader 多 worker 的采样顺序。少关一处,复现性就没了。

解决:环境reset(seed=cfg.seed)、torch.backends.cudnn.deterministic = True、torch.backends.cudnn.benchmark = False、DataLoader(..., worker_init_fn=...)里给每个 worker 派生种子。调试阶段干脆num_workers=0,慢一点但结果可比。这套开关关掉之后性能可能降几个点,属于为了可复现付的必要成本。

6. 再往前一步:BC 预训练接 GAIL 微调,加一个误差分箱的验证习惯

从零训 GAIL 的探索成本很高,尤其是动作维度大于 10 的时候,判别器早期的奖励几乎全是噪声。更省事的做法是先用 BC 把策略热启动到一个「能跑但不精细」的水平,再切 GAIL 微调。切换时有两个细节要改:策略学习率降到原来的三分之一到五分之一(BC 权重已经很接近解,大步长会把它们打散),判别器从零开始训、学习率保持原值,因为判别器面对的是一个已经比较像专家的策略,需要足够强的区分能力才能提供有效梯度。

验证环节我习惯加一个误差分箱,比只看总回报有用得多。思路是拿一批专家观测喂给策略,算每个样本的动作误差,再按专家动作模长或者状态某个关键维度分箱,看误差集中在哪一段。

import numpy as np import torch @torch.no_grad() def divergence_by_bin(policy, expert_obs, expert_act, n_bins=5): """按专家动作模长分箱,统计策略与专家的动作 MSE,定位误差集中区""" obs_t = torch.as_tensor(expert_obs, dtype=torch.float32) pred = policy(obs_t).numpy() err = ((pred - expert_act) ** 2).mean(axis=1) # 每个样本一个标量误差 key = np.linalg.norm(expert_act, axis=1) # 用动作模长当分箱依据 edges = np.quantile(key, np.linspace(0, 1, n_bins + 1)) idx = np.clip(np.digitize(key, edges[1:-1]), 0, n_bins - 1) return {i: round(float(err[idx == i].mean()), 5) for i in range(n_bins) if (idx == i).any()}

n_bins=5按分位数切,保证每箱样本量接近,避免某一箱只有几个样本让均值失真。输出里如果误差集中在最高的那一箱,说明策略在「大幅度动作」上学得差,多半是专家数据里大幅动作样本少,补采时要有意识地多采这类片段;如果集中在最低那一箱,通常是噪声问题——小幅动作的信噪比低,专家自己抖一下就被当成标签学走了,这时候该做的是对专家动作做一点平滑再训。

配合评估脚本的 25 分位数一起看,基本能定位八成问题:分箱误差均匀但最差分位数很差,是分布偏移;分箱误差本身就偏某一箱,是数据覆盖问题;两者都正常但环境回报上不去,那要回头看奖励整形,而不是继续调策略。

从那以后我每次跑 GAIL 之前,都会强制先跑一遍 BC 的分箱误差,确认失败状态集中在哪一段,再决定是补数据还是直接上对抗训练——省下的不是训练时间,是那种「训了两天不知道错在哪」的消耗。希望帮到你。

本文还有配套的精品资源,点击获取

返回列表