模仿学习(Imitation Learning / Behavior Cloning)从原理到实践
1. 引言:为什么需要模仿学习
在强化学习(Reinforcement Learning, RL)中,智能体需要通过与环境反复交互、试错来学习策略,并依赖精心设计的奖励函数来引导行为。然而在很多真实场景中,奖励函数难以定义,试错成本也极高——例如自动驾驶、机器人操作、医疗决策等,一次错误可能带来不可逆的后果。
模仿学习(Imitation Learning, IL) 提供了一条更直接的路径:与其让智能体从零开始试错,不如让它模仿专家(Expert)的示范行为。专家可以是人类操作员,也可以是已有的高性能控制器。模仿学习的核心思想是:给定一批「状态-动作」示范数据,学习一个策略,使其在相似状态下做出与专家相近的动作。
本文将从行为克隆(Behavior Cloning)出发,系统讲解模仿学习的原理、常见问题、改进方法,并给出可运行的代码示例。
2. 模仿学习的整体框架
模仿学习通常可以抽象为如下流程:
按照是否需要与环境交互,模仿学习大致可分为两类:
- 纯离线模仿:只使用专家示范数据训练,典型代表是行为克隆(Behavior Cloning, BC)。
- 交互式模仿:在训练过程中允许与环境交互,典型代表是逆强化学习(Inverse RL)、DAgger、GAIL 等。
本文重点讨论最基础也最常用的行为克隆。
3. 行为克隆(Behavior Cloning)原理
3.1 核心思想
行为克隆把模仿学习直接建模为一个监督学习问题:
- 输入:状态 sss(或观测 ooo)
- 输出:动作 aaa
- 数据:专家示范轨迹 {(si,ai)}i=1N\{(s_i, a_i)\}_{i=1}^{N}{(si,ai)}i=1N
目标是学习一个参数化策略 πθ(a∣s)\pi_\theta(a \mid s)πθ(a∣s),使得在示范数据上的损失最小:
L(θ)=E(s,a)∼Dexpert[ℓ(πθ(s),a)] \mathcal{L}(\theta) = \mathbb{E}_{(s,a) \sim \mathcal{D}_{\text{expert}}} \left[ \ell\big(\pi_\theta(s), a\big) \right] L(θ)=E(s,a)∼Dexpert[ℓ(πθ(s),a)]
其中 ℓ\ellℓ 可以是均方误差(连续动作)或交叉熵(离散动作)。
3.2 算法流程
- 采集专家示范轨迹,得到状态-动作对数据集;
- 选择策略模型(MLP、CNN、Transformer 等);
- 以监督学习方式最小化动作预测损失;
- 将训练好的策略部署到环境中执行。
3.3 优点与局限
优点:
- 实现简单,直接复用成熟的监督学习工具链;
- 训练稳定,不需要设计奖励函数;
- 数据利用率高,一次采集即可离线训练。
局限:
- 分布偏移(Distribution Shift):训练数据来自专家分布,部署时策略一旦偏离,会进入训练中未见过的状态,误差累积导致「越走越偏」;
- 因果混淆(Causal Confusion):模型可能学到与动作无关的虚假相关特征;
- 多模态动作难以拟合:当同一状态下专家可能有多种合理动作时,简单的回归会输出「平均动作」,往往不可行。
4. 分布偏移问题与 DAgger
行为克隆最核心的痛点是复合误差(Compounding Error)。假设单步策略误差为 ϵ\epsilonϵ,在长度为 TTT 的轨迹上,误差可能以 O(ϵT2)O(\epsilon T^2)O(ϵT2) 的量级累积。
DAgger(Dataset Aggregation) 通过迭代式数据聚合缓解该问题:
- 用当前策略 πi\pi_iπi 在环境中执行,采集其访问到的状态;
- 请专家对这些状态标注「正确动作」;
- 将新数据加入数据集,重新训练策略;
- 重复上述过程直到收敛。
这样,训练分布会逐渐覆盖策略实际会访问到的状态,从而缩小训练与部署之间的分布差距。
4.1 可运行示例:CartPole 上的 DAgger
下面给出一个完整可运行的 DAgger 实现,沿用第 5 节的 CartPole 环境与规则专家。核心循环是:用当前策略采集状态 → 用规则专家标注动作 → 聚合数据集 → 重新训练策略,重复若干轮。
import gymnasium as gym
import numpy as np
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import TensorDataset, DataLoader
# 固定随机种子,保证结果可复现
np.random.seed(0)
torch.manual_seed(0)
# 规则专家:杆角向右偏(theta > 0)则向右推,否则向左推
def expert_policy(obs):
x, x_dot, theta, theta_dot = obs
return 1 if theta > 0 else 0
# 策略网络:输入 4 维状态,输出 2 个动作的 logits
class PolicyNet(nn.Module):
def __init__(self, obs_dim=4, act_dim=2):
super().__init__()
self.net = nn.Sequential(
nn.Linear(obs_dim, 64),
nn.ReLU(),
nn.Linear(64, 64),
nn.ReLU(),
nn.Linear(64, act_dim),
)
def forward(self, x):
return self.net(x)
# 用当前策略在环境中执行,采集它实际访问到的状态
# 注意:这里只收集状态,动作稍后由专家重新标注
def rollout_states(policy, env, num_episodes=10):
policy.eval()
states = []
with torch.no_grad():
for _ in range(num_episodes):
obs, _ = env.reset()
done = False
while not done:
states.append(obs)
obs_t = torch.from_numpy(obs.astype(np.float32)).unsqueeze(0)
action = int(torch.argmax(policy(obs_t), dim=1).item())
obs, _, terminated, truncated, _ = env.step(action)
done = terminated or truncated
return np.array(states, dtype=np.float32)
# 用规则专家为采集到的状态标注「正确动作」
def label_by_expert(states):
return np.array([expert_policy(s) for s in states], dtype=np.int64)
# 在聚合后的数据集上重新训练策略
def train_policy(policy, states, actions, epochs=20, lr=1e-3, batch_size=256):
optimizer = optim.Adam(policy.parameters(), lr=lr)
criterion = nn.CrossEntropyLoss()
dataset = TensorDataset(torch.from_numpy(states), torch.from_numpy(actions))
loader = DataLoader(dataset, batch_size=batch_size, shuffle=True)
policy.train()
for epoch in range(epochs):
total_loss = 0.0
for s, a in loader:
logits = policy(s)
loss = criterion(logits, a)
optimizer.zero_grad()
loss.backward()
optimizer.step()
total_loss += loss.item() * s.size(0)
print(f" Epoch {epoch + 1:02d} | Loss: {total_loss / len(dataset):.4f}")
# 评估策略的平均回报
def evaluate(policy, env, num_episodes=10):
policy.eval()
returns = []
with torch.no_grad():
for _ in range(num_episodes):
obs, _ = env.reset()
done = False
ep_return = 0.0
while not done:
obs_t = torch.from_numpy(obs.astype(np.float32)).unsqueeze(0)
action = int(torch.argmax(policy(obs_t), dim=1).item())
obs, reward, terminated, truncated, _ = env.step(action)
ep_return += reward
done = terminated or truncated
returns.append(ep_return)
return np.mean(returns), np.std(returns)
env = gym.make("CartPole-v1")
# 第 0 轮:先用专家示范数据做一次行为克隆,得到初始策略 π_0
init_states, _ = [], None
for _ in range(20):
obs, _ = env.reset()
done = False
while not done:
init_states.append(obs)
obs, _, terminated, truncated, _ = env.step(expert_policy(obs))
done = terminated or truncated
init_states = np.array(init_states, dtype=np.float32)
init_actions = label_by_expert(init_states)
policy = PolicyNet()
print("初始行为克隆训练:")
train_policy(policy, init_states, init_actions, epochs=20)
# 聚合数据集,初始为专家示范数据
agg_states = init_states
agg_actions = init_actions
# DAgger 主循环:迭代式数据聚合
num_dagger_iters = 5
for it in range(1, num_dagger_iters + 1):
print(f"\n=== DAgger 第 {it} 轮 ===")
# 1. 用当前策略 π_i 在环境中执行,采集它访问到的状态
new_states = rollout_states(policy, env, num_episodes=10)
# 2. 请专家对这些状态标注正确动作
new_actions = label_by_expert(new_states)
# 3. 将新数据聚合进数据集
agg_states = np.concatenate([agg_states, new_states], axis=0)
agg_actions = np.concatenate([agg_actions, new_actions], axis=0)
print(f"聚合后数据规模: {agg_states.shape[0]} 条状态-动作对")
# 4. 在聚合数据集上重新训练策略
train_policy(policy, agg_states, agg_actions, epochs=10)
# 每轮结束后评估当前策略表现
mean_ret, std_ret = evaluate(policy, env)
print(f"第 {it} 轮策略平均回报: {mean_ret:.2f} ± {std_ret:.2f}")
几点说明:
- 只采集状态、由专家标注动作:这是 DAgger 与行为克隆的关键区别。策略自己跑出来的状态正是部署时会遇到的分布,专家在这些状态上给出正确动作,相当于把训练分布「拉回」到策略的真实访问分布。
- 数据集只增不减:每轮把新数据
concatenate进agg_states/agg_actions,保留历史数据可避免灾难性遗忘。 - 每轮训练轮数可减少:由于数据在累积,单轮
epochs设为 10 左右即可,整体训练成本可控。 - 预期效果:随着迭代进行,策略访问到的状态越来越接近专家轨迹,平均回报通常能稳定上升到 450 以上,明显优于纯行为克隆。
5. 代码示例:CartPole 上的行为克隆
下面用一个经典控制任务 CartPole 演示行为克隆。我们先用一个训练好的专家策略(这里用规则控制器模拟专家)采集示范数据,再用监督学习训练一个神经网络策略。
import gymnasium as gym
import numpy as np
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import TensorDataset, DataLoader
# 1. 定义专家策略(简单规则:根据角度和角速度决定动作)
def expert_policy(obs):
x, x_dot, theta, theta_dot = obs
# 角度向右偏则向右推,否则向左推
return 1 if theta > 0 else 0
# 2. 采集专家示范数据
def collect_expert_data(env, num_episodes=50):
states, actions = [], []
for _ in range(num_episodes):
obs, _ = env.reset()
done = False
while not done:
action = expert_policy(obs)
states.append(obs)
actions.append(action)
obs, _, terminated, truncated, _ = env.step(action)
done = terminated or truncated
return np.array(states, dtype=np.float32), np.array(actions, dtype=np.int64)
env = gym.make("CartPole-v1")
states, actions = collect_expert_data(env, num_episodes=50)
print("示范数据规模:", states.shape, actions.shape)
# 3. 定义策略网络
class PolicyNet(nn.Module):
def __init__(self, obs_dim, act_dim):
super().__init__()
self.net = nn.Sequential(
nn.Linear(obs_dim, 64),
nn.ReLU(),
nn.Linear(64, 64),
nn.ReLU(),
nn.Linear(64, act_dim),
)
def forward(self, x):
return self.net(x)
policy = PolicyNet(obs_dim=4, act_dim=2)
optimizer = optim.Adam(policy.parameters(), lr=1e-3)
criterion = nn.CrossEntropyLoss()
# 4. 监督学习训练
dataset = TensorDataset(torch.from_numpy(states), torch.from_numpy(actions))
loader = DataLoader(dataset, batch_size=256, shuffle=True)
for epoch in range(20):
total_loss = 0.0
for s, a in loader:
logits = policy(s)
loss = criterion(logits, a)
optimizer.zero_grad()
loss.backward()
optimizer.step()
total_loss += loss.item() * s.size(0)
print(f"Epoch {epoch + 1:02d} | Loss: {total_loss / len(dataset):.4f}")
# 5. 评估学到的策略
def evaluate(policy, env, num_episodes=10):
policy.eval()
returns = []
with torch.no_grad():
for _ in range(num_episodes):
obs, _ = env.reset()
done = False
ep_return = 0.0
while not done:
obs_t = torch.from_numpy(obs.astype(np.float32)).unsqueeze(0)
action = int(torch.argmax(policy(obs_t), dim=1).item())
obs, reward, terminated, truncated, _ = env.step(action)
ep_return += reward
done = terminated or truncated
returns.append(ep_return)
return np.mean(returns), np.std(returns)
mean_ret, std_ret = evaluate(policy, env)
print(f"行为克隆策略平均回报: {mean_ret:.2f} ± {std_ret:.2f}")
运行后通常可以看到,行为克隆策略在 CartPole 上能取得不错的平均回报。但由于分布偏移,其表现往往略低于专家本身,这正是行为克隆的典型特征。
5.1 结果分析:专家与行为克隆的差距
先看两组策略的预期表现(CartPole-v1 单回合上限为 500 步,每步奖励 1,因此回报即存活步数):
| 策略 | 预期平均回报 | 典型表现 |
|---|---|---|
规则专家(theta > 0 则右推) | 约 400 ~ 500 | 多数回合能撑到 500 步上限,杆角始终维持在 ±0.2 rad 内 |
| 行为克隆(50 条轨迹 / 20 epoch) | 约 200 ~ 450,方差较大 | 部分回合接近专家,部分回合在 100 ~ 200 步提前失败 |
也就是说,行为克隆在训练分布内(杆角较小、状态平稳)几乎能完美复现专家动作,但一旦进入训练中罕见的极端状态,表现就会明显掉档。这正是第 4 节所说的复合误差在 CartPole 上的直观体现。
5.2 分布偏移在 CartPole 上的具体表现
CartPole 的状态只有 4 维 (x, x_dot, theta, theta_dot),其中杆角 theta 是决定成败的关键变量。分布偏移在这里的表现非常典型:
- 早期几乎无差别:回合开始时
theta ≈ 0,这类状态在专家数据中占比极高,行为克隆策略的预测与专家几乎一致,前几十步看不出差距。 - 微小偏差被放大:由于策略是「模仿」而非「纠偏」,一旦某一步预测错了方向,杆角会朝错误方向加速倾斜,进入专家数据中很少出现的
|theta| > 0.1区域。 - 迅速失败:在
|theta|接近 0.2 rad 的临界区,策略往往仍输出与专家相反的推力,杆角在几步内越过终止阈值,回合提前结束——这就是「杆角偏离后迅速失败」的机制。 - 误差累积:单步误差虽小,但每步都会把状态推离专家分布,误差随步数近似二次累积,因此失败往往表现为「前面好好的,突然崩掉」。
一个可观察的验证方法是:在 evaluate 中额外记录每回合的终止原因(terminated 为杆角/位置越界,truncated 为达到 500 步上限),统计 terminated 的比例。行为克隆策略的 terminated 比例通常显著高于专家。
5.3 可复现的调参建议
如果想稳定复现并改善上述结果,可以按下面的顺序调整超参数:
- 示范轨迹条数
num_episodes:默认 50 条约产生 1 万~2 万条状态-动作对。建议从 20 起步观察欠拟合,再逐步加到 100~200。数据量增加对缓解分布偏移有直接帮助,但收益会递减;关键是覆盖更多样的初始状态(可通过env.reset(seed=...)使用不同随机种子采集)。 - 训练轮数
epoch:默认 20 轮。可先看 loss 曲线,若 loss 仍在明显下降说明欠拟合,可加到 50 轮;若 loss 已降到 0.01 以下但评估回报不升反降,说明开始过拟合专家数据,应减少轮数或增加数据。 - 学习率
lr:默认1e-3。若 loss 震荡不收敛,降到3e-4或1e-4;若 20 轮后 loss 几乎不动,可提高到3e-3试一次。配合Adam一般1e-3已足够稳定。 - 批量大小
batch_size:默认 256。数据量较小时可降到 64,让每个 epoch 有更多梯度更新步数。 - 固定随机种子:在采集数据、初始化网络、
DataLoader(shuffle=True)前分别设置np.random.seed与torch.manual_seed,保证结果可复现。
一个推荐的稳定配置是:num_episodes=100、epoch=30、lr=1e-3、batch_size=256,通常能把行为克隆的平均回报稳定在 400 以上,同时仍能观察到偶发的提前失败——这恰好是理解分布偏移的最佳教学样例。
6. 基于 AFSIM 的智能体训练:模仿学习落地实践
前面几节用 CartPole 讲清了行为克隆与 DAgger 的原理,但真实工程中更常见的场景是:在 AFSIM(Advanced Framework for Simulation, Integration, and Modeling)这类作战仿真框架里训练智能体。AFSIM 提供了高保真的平台运动、传感器、武器与指挥控制模型,天然适合作为智能体的「环境」,而模仿学习恰好能绕开「奖励函数难设计」的痛点——直接向人类指挥员或已有的规则脚本学习。
6.1 为什么在 AFSIM 上做模仿学习
- 奖励难定义:空战/对抗任务的胜负往往稀疏且延迟,手工设计稠密奖励容易引入偏差;而人类操作员或现有战术脚本本身就是「专家示范」。
- 仿真成本可控:AFSIM 可离线批量跑仿真,采集示范轨迹比真实飞行/实装演练便宜得多。
- 可复现:固定随机种子与想定参数后,同一套示范数据可反复用于训练与对比。
6.2 状态、动作与专家的映射
把 AFSIM 的仿真接口包装成类似 Gym 的 env,是接入模仿学习的关键一步:
| 要素 | AFSIM 中的来源 | 建议处理 |
|---|---|---|
状态 s | 本机位置/速度/姿态、目标相对方位与距离、雷达跟踪状态、剩余弹药与燃油 | 归一化到 [-1, 1],剔除绝对坐标等与决策无关的量 |
动作 a | 航向/高度/油门指令、雷达开关、武器发射 | 离散化为有限动作集,或对连续量做分箱 |
| 专家 | 人类操作员回放,或已有的规则/脚本控制器 | 统一记录为 (s, a) 序列,注意时间对齐 |
| 回报 | 仿真结束时的毁伤、生存、任务完成度 | 仅用于评估,训练阶段不需要 |
一个典型的采集循环如下(伪代码,接口按实际 AFSIM 封装调整):
# 将 AFSIM 想定包装为 Gym 风格环境
env = AfsimEnv(scenario="air_combat_1v1", seed=0)
def collect_expert_data(env, num_episodes=50):
states, actions = [], []
for _ in range(num_episodes):
obs = env.reset()
done = False
while not done:
# 专家:人类回放或规则脚本给出的动作
action = env.expert_action()
states.append(obs)
actions.append(action)
obs, reward, done, info = env.step(action)
return np.array(states, dtype=np.float32), np.array(actions, dtype=np.int64)
拿到 (states, actions) 后,训练流程与第 5 节完全一致:一个 MLP 策略网络 + 交叉熵损失即可。区别只在于状态维度更高、动作空间更结构化,需要针对不同动作头(航向、雷达、武器)分别设计输出层。
6.3 在 AFSIM 上落地时的注意事项
- 分布偏移更严重:空战状态空间远大于 CartPole,行为克隆策略一旦偏离专家轨迹,很容易进入「从未见过」的态势。建议直接上 DAgger:让策略在 AFSIM 里自己飞,由专家(或规则脚本)对访问到的状态重新标注动作。
- 仿真接口的确定性:AFSIM 仿真中若含随机因素(传感器噪声、初始位置扰动),务必固定种子,否则同一策略两次评估结果差异很大,难以判断改进是否有效。
- 样本效率:一次 AFSIM 仿真可能耗时数秒到数分钟,示范数据宝贵。可先用少量想定做行为克隆验证流程,再逐步扩展到多机、多场景。
- 安全边界:训练阶段让策略自由探索可能产生大量无效甚至异常指令,建议在仿真侧加动作合法性校验,避免无效仿真浪费算力。
6.4 一个最小可用的训练闭环
# 1. 采集专家示范(人类回放或规则脚本)
states, actions = collect_expert_data(env, num_episodes=50)
# 2. 行为克隆得到初始策略 π_0
policy = PolicyNet(obs_dim=states.shape[1], act_dim=NUM_ACTIONS)
train_policy(policy, states, actions, epochs=30)
# 3. DAgger 迭代:策略自己飞,专家纠正
agg_s, agg_a = states, actions
for it in range(5):
new_s = rollout_states(policy, env, num_episodes=10) # 策略访问到的状态
new_a = env.label_by_expert(new_s) # 专家标注
agg_s = np.concatenate([agg_s, new_s], axis=0)
agg_a = np.concatenate([agg_a, new_a], axis=0)
train_policy(policy, agg_s, agg_a, epochs=10)
print(f"第 {it+1} 轮评估回报: {evaluate(policy, env)}")
这套闭环与第 4.1 节的 CartPole DAgger 在结构上完全同构,只是把 gym.make("CartPole-v1") 换成了 AFSIM 想定环境。先在小想定上跑通,再迁移到复杂对抗场景,是工程上最稳妥的路径。
6. 改进方向与实践建议
针对行为克隆的不足,实践中可以考虑以下改进:
- 数据增强与多样化:采集更多样化的专家数据,覆盖更广的状态分布;
- DAgger 类交互式数据聚合:让策略自己跑,再由专家纠正;
- 多模态策略建模:使用混合密度网络(MDN)、扩散策略(Diffusion Policy)等建模多峰动作分布;
- 时序建模:使用 RNN / Transformer 利用历史观测,缓解部分可观测问题;
- 正则化与鲁棒性训练:加入噪声、对抗扰动,提升策略在分布外的稳定性;
- 结合强化学习:先用行为克隆预训练,再用 RL 微调(如 RLHF 中的 SFT 阶段)。
7. 总结
模仿学习为智能体提供了一条「向专家学习」的高效路径,而行为克隆是其中最简单、最常用的方法。它把模仿问题转化为监督学习,实现门槛低、训练稳定,但也面临分布偏移、因果混淆、多模态动作等挑战。
理解行为克隆的局限,并配合 DAgger、多模态建模、RL 微调等手段,才能让模仿学习在真实任务中发挥更大价值。后续可以进一步探索逆强化学习与 GAIL 等交互式模仿学习方法,构建更鲁棒的策略学习系统。
更多推荐


所有评论(0)