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 算法流程

  1. 采集专家示范轨迹,得到状态-动作对数据集;
  2. 选择策略模型(MLP、CNN、Transformer 等);
  3. 以监督学习方式最小化动作预测损失;
  4. 将训练好的策略部署到环境中执行。

3.3 优点与局限

优点:

  • 实现简单,直接复用成熟的监督学习工具链;
  • 训练稳定,不需要设计奖励函数;
  • 数据利用率高,一次采集即可离线训练。

局限:

  • 分布偏移(Distribution Shift):训练数据来自专家分布,部署时策略一旦偏离,会进入训练中未见过的状态,误差累积导致「越走越偏」;
  • 因果混淆(Causal Confusion):模型可能学到与动作无关的虚假相关特征;
  • 多模态动作难以拟合:当同一状态下专家可能有多种合理动作时,简单的回归会输出「平均动作」,往往不可行。

4. 分布偏移问题与 DAgger

行为克隆最核心的痛点是复合误差(Compounding Error)。假设单步策略误差为 ϵ\epsilonϵ,在长度为 TTT 的轨迹上,误差可能以 O(ϵT2)O(\epsilon T^2)O(ϵT2) 的量级累积。

DAgger(Dataset Aggregation) 通过迭代式数据聚合缓解该问题:

  1. 用当前策略 πi\pi_iπi​ 在环境中执行,采集其访问到的状态;
  2. 请专家对这些状态标注「正确动作」;
  3. 将新数据加入数据集,重新训练策略;
  4. 重复上述过程直到收敛。

这样,训练分布会逐渐覆盖策略实际会访问到的状态,从而缩小训练与部署之间的分布差距。

初始示范数据

训练策略 π_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 是决定成败的关键变量。分布偏移在这里的表现非常典型:

  1. 早期几乎无差别:回合开始时 theta ≈ 0,这类状态在专家数据中占比极高,行为克隆策略的预测与专家几乎一致,前几十步看不出差距。
  2. 微小偏差被放大:由于策略是「模仿」而非「纠偏」,一旦某一步预测错了方向,杆角会朝错误方向加速倾斜,进入专家数据中很少出现的 |theta| > 0.1 区域。
  3. 迅速失败:在 |theta| 接近 0.2 rad 的临界区,策略往往仍输出与专家相反的推力,杆角在几步内越过终止阈值,回合提前结束——这就是「杆角偏离后迅速失败」的机制。
  4. 误差累积:单步误差虽小,但每步都会把状态推离专家分布,误差随步数近似二次累积,因此失败往往表现为「前面好好的,突然崩掉」。

一个可观察的验证方法是:在 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 等交互式模仿学习方法,构建更鲁棒的策略学习系统。

Logo

更多推荐