11.1 行为克隆与交互式模仿学习
本节导读
核心内容
- 理解行为克隆(BC)如何把专家示范转化为监督学习问题——这是最简单也是最常用的模仿学习方法
- 看到行为克隆的致命缺陷:分布偏移——训练时见专家状态,部署时一旦犯错就再也回不来
- 理解 DAgger 如何通过"让策略跑、让专家标"的交互式流程解决分布偏移问题
- 对比 BC、DAgger、GAIL 三条路线的数据需求和适用场景,理解为什么需要从"模仿动作"进化到"推断奖励"
前面几章我们学了很多强化学习算法:DQN、PPO、SAC……它们都有一个共同的前提——环境提供奖励函数。智能体每走一步,环境告诉它这一步得多少分,然后算法努力让总分最大。
但是——让我们停下来想一下。现实中很多任务,你能找到专家示范,但你写不出奖励函数。比如:
- 自动驾驶:人类驾驶员开了几百万公里,有大量录像,但你没法用几行代码定义"什么是好的驾驶"
- 机器人操作:工人熟练地组装零件,但你很难精确写出每个关节应该怎么动
- 大语言模型:高质量的问答对随处可见,但"好回答"的奖励函数根本没法手写
这些场景有一个共同点:专家会做,但你说不清楚他为什么这么做。如果我们不设计奖励,直接让智能体"抄作业"——看专家在什么状态下做什么动作,然后学着做——行不行?
这就是模仿学习(Imitation Learning)要解决的问题。第 11 章沿着三步展开:先用行为克隆与 DAgger 直接学习专家动作,再用逆强化学习与 GAIL 从示范推断奖励,最后进入 MAML、RL²、PEARL 与上下文强化学习,研究策略如何快速适应新任务。本节先回答四个问题:行为克隆怎样训练,误差为什么会累积,DAgger 怎样收集错误状态,以及几种路线各自需要什么数据。
1. 把专家示范写成监督学习
回忆第 6 章的策略梯度方法,它们都假设环境提供 reward。但很多真实任务中我们只有专家示范——人类驾驶员的轨迹、熟练工人的操作记录、高质量问答对。模仿学习直接从示范学策略,跳过奖励函数的设计。
你可能会问:"没有奖励怎么学?"答案很简单:把它变成监督学习。
1.1 行为克隆的训练目标
最直接的方法是把专家数据写成监督学习样本:状态 s 是输入,专家动作 a 是标签。策略给专家动作的概率越高,损失越小:
LBC(θ)=−E(s,a)∼Dexpert[logπθ(a∣s)]
让我们把这个式子拆开看:
- Dexpert={(si,ai)}i=1N:这是专家示范数据集,里面有 N 对"状态-动作"——专家在状态 si 时做了动作 ai
- πθ(a∣s):这是我们的策略网络,它在状态 s 下选择动作 a 的概率
- logπθ(a∣s):专家动作的对数概率——这个值越大,说明策略越"同意"专家的选择
- 负号:把"提高专家动作概率"变成最小化问题——我们最小化负对数似然,等价于最大化专家动作的概率
让我们用具体数字算一下这个损失。专家数据里有一条样本:车正好在车道中央(状态 s),专家的动作是"向左轻打方向"(动作 a)。两个策略在这条样本上的输出是:
- 策略一给这个动作的概率 π1(a∣s)=0.7,损失是 −log0.7≈0.36
- 策略二给这个动作的概率 π2(a∣s)=0.95,损失是 −log0.95≈0.05
策略二更"同意"专家,损失就更小。行为克隆的训练就是把数据集里所有样本的这种损失最小化——和手写数字分类里的交叉熵训练是同一个操作。
离散动作通常使用交叉熵损失(就是分类问题),连续动作可以改用均方误差或高斯分布的负对数似然。LLM 的监督微调(SFT)也使用同样的条件似然目标,只是"动作"变成了下一个 token——本质上就是行为克隆。
def behavior_cloning_step(policy_net, expert_batch):
states, actions = expert_batch
log_probs = policy_net.log_prob(states, actions)
loss = -log_probs.mean() # 负对数似然
optimizer.zero_grad()
loss.backward()
optimizer.step()
return loss.item()这看起来太简单了——收集专家数据,做监督学习,就完了?如果真这么简单,强化学习早就被监督学习取代了。让我们停下来想一下:行为克隆在什么情况下会出问题?
2. 为什么行为克隆会累积错误
BC 训练时看到的是专家访问的状态分布 dexpert(s),部署时访问的却是当前策略产生的 dπθ(s)。一次小错误可能把智能体带到训练集中没有出现的状态,后续动作就更容易继续出错。
让我们用一个具体例子感受一下。假设你在学开车,教练只在"正确的驾驶状态"下教你:车在车道中间、速度合适、前车距离正常。你在这些状态下学得很好,转向、油门、刹车都很到位。
但是——万一你某一下方向盘打多了,车偏向了车道边缘,进入了一个训练时从来没见过的状态。你不知道该怎么办,可能会慌乱中再打一把方向,结果更偏;然后下一个状态更陌生,错得更离谱……这就是误差累积,也叫分布偏移(distribution shift)。
我们用数字算一下。设策略在专家访问过的状态上错误率只有 ϵ=0.01——99% 的时候都和专家一致。单独看每一步,这个错误率低得可以忽略;但任务是连续执行 T 步的,全程不出错的概率是 (1−ϵ)T,它随步数急剧衰减:
| 任务长度 T | 10 步 | 50 步 | 100 步 | 1000 步 |
|---|---|---|---|---|
| 全程无错的概率 | 0.90 | 0.61 | 0.37 | 0.00004 |
一个 100 步的任务,三局里有两局会在中途犯至少一次错。而且这还是乐观估计——它假设偏离专家轨迹后错误率仍然是 1%;实际上一旦偏离,状态变成了训练集中没有的,错误率会急剧上升。DAgger 论文(Ross et al. 2011)把累计任务代价写成 O(T2ϵ) 量级:
E[t=0∑T1[πθ(st)=π∗(st)]]≤O(T2ϵ)
这个式子告诉我们什么?
- T 是任务长度(步数),ϵ 是监督学习误差
- T2 表示错误会滚雪球:早期错误不仅影响当前一步,还会改变后面许多步看到的状态
- 任务越长,只在专家状态上训练的代价越明显——1000 步的任务,代价可能是 1 步任务的 100 万倍
这就是为什么行为克隆在短任务、简单任务上能工作(比如 Atari 游戏的简单关卡),但在长决策任务(自动驾驶、机器人操作)上经常失败。
加餐:Ross 等人的 DAgger 理论界
BC 的误差界是 O(T2ϵ),这来自于"协变量偏移"(covariate shift)的累积——每次遇到一个分布外的状态,策略就可能犯一个新错误,这些错误又会把后续状态推得更远。Ross 等人证明,如果能在训练过程中不断让专家标注当前策略访问的状态,这个界可以改进到 O(Tϵ)——线性而不是平方。
3. 用 DAgger 收集策略真正访问的状态
既然问题出在"训练时没见过策略自己犯错的状态",那解决方案就很直接了:让策略跑,跑到它会犯错的状态,然后请专家告诉它在这些状态下应该怎么做。这就是 DAgger(Dataset Aggregation,数据集聚合)的核心思想。
Dataset Aggregation 直接补充策略会访问、而专家数据没有覆盖的状态。当前策略先执行任务,专家再为这些状态提供正确动作。我们来看代码:
def dagger(env, expert, policy_net, n_iterations=20, n_traj_per_iter=50):
dataset = []
for it in range(n_iterations):
# 1. 用当前策略 rollout(注意:不是用专家!)
trajectories = []
for _ in range(n_traj_per_iter):
s = env.reset()
traj = []
done = False
while not done:
# β 混合:早期多用专家保证安全,后期多用策略
beta = max(0.0, 1.0 - it / 10)
if np.random.rand() < beta:
a = expert(s)
else:
a = policy_net.act(s)
s_next, r, done, _ = env.step(a)
traj.append((s, a))
s = s_next
trajectories.append(traj)
# 2. 关键:对策略访问到的状态(包括失败状态)请专家重新标注
for traj in trajectories:
for s, _ in traj:
a_expert = expert(s)
dataset.append((s, a_expert))
# 3. 用扩展后的数据集重训策略
train_bc(policy_net, dataset)让我们拆解 DAgger 的三个关键步骤:
第一步:跑策略。用当前策略(还不完美的那个)去环境里交互,收集它实际访问到的轨迹。注意代码里有一个 β 混合策略——早期迭代多用专家(避免一开始就乱撞出危险),后期逐渐多用策略(让它暴露自己的错误)。
第二步:专家标。这是 DAgger 最核心也最"昂贵"的一步:不管轨迹里的动作是专家选的还是策略选的,对轨迹里出现的每一个状态 s,都请专家给出"在这个状态下正确的动作应该是什么"。哪怕策略在这个状态下犯了傻,专家也要告诉它正确答案。
第三步:重训练。把这些新标注的"策略犯错状态"加进数据集,重新做行为克隆。这样数据集里就既有专家正常访问的状态,也有策略容易犯错的状态了。
让我们跟着训练走一遍。最初几轮,β 接近 1,车基本由专家驾驶,收集到的都是"车道中间、车速正常"的状态,标注毫无压力。几轮之后,β 降到一半,车有一半时间由还不熟练的策略控制,开始出现"压线""车速过快"的状态——这些状态专家示范里从来没有,现在专家针对它们给出正确动作,数据集补上了这些容易出事的位置。训练后期 β 归零,策略独立驾驶,仍会犯错,但每个犯错状态都会被标注、入库,下一轮策略在这些位置就学乖了。数据集就这样一轮轮滚大,逐渐覆盖的正是策略自己的分布 dπθ。
在无遗憾在线学习等条件下,累计代价可以从 BC 的 O(T2ϵ) 改善到 O(Tϵ) 量级——从平方变成线性!
但是——你可能已经想到了一个问题:代价是训练期间必须反复调用专家。
4. 比较 BC、DAgger 与 GAIL
让我们把三种方法放在一起对比:
| 方法 | 训练数据来源 | 是否解决分布偏移 | 需要专家在线标注 |
|---|---|---|---|
| BC | 仅离线专家数据 | ❌ | ❌ |
| DAgger | 专家 + 策略访问的状态 | ✅ | ✅(关键限制) |
| GAIL | 专家 + 策略 rollout | ✅(隐式) | ❌(只需状态-动作对) |
DAgger 的工程瓶颈是需要专家在线交互。人类驾驶员很难实时为策略访问到的异常状态标注正确动作——你不可能让教练坐在旁边,每次车偏了都立刻告诉你方向盘该打多少度;更不可能让他为你标注成千上万次失败轨迹。
这就推动我们思考下一个问题:能不能不让专家实时标注?能不能只从专家现成的轨迹里,不仅学"做什么动作",还能反推出"为什么这么做"——也就是奖励函数?这就是下一节逆强化学习与 GAIL 要解决的问题。
本节总结
行为克隆(BC)是最朴素的模仿学习——把专家轨迹当作监督数据训练策略。它简单、计算成本低,和大语言模型的 SFT 本质是一回事。但它有分布漂移问题:训练时只在专家状态分布上学习,部署时一旦偏离就再也回不来,误差会按 O(T2ϵ) 累积。DAgger 通过让专家纠正 agent 的实际轨迹解决这个问题,把误差界改进到 O(Tϵ),但代价是需要专家在线反复标注。
下一节 11.2 逆强化学习与 GAIL 不再直接模仿动作,而是从专家行为反推奖励函数——这就是逆强化学习(IRL)。