# 模仿学习

> 来源：https://dqn.cn/advanced/imitation-learning ｜ 作者：DQN.CN 编辑部 ｜ 发布：2026-10-29 ｜ 更新：2026-10-30 ｜ 许可：CC BY-NC-SA 4.0（文字）/ MIT（代码）

> **要点速览**
>
> - 模仿学习从专家示范中学习策略，不需要显式的奖励函数。
> - 行为克隆（BC）把模仿当作监督学习，简单高效，但会因复合误差而在偏离专家分布的状态上表现失常。
> - DAgger 让学习者自己执行策略、请专家标注访问到的状态，迭代聚合数据，以缓解分布偏移。
> - 逆强化学习（IRL）先从示范中推断奖励函数再做强化学习；GAIL 用判别器区分专家与学习者，绕开显式奖励。
> - 本站 CartPole 实测（3 个种子、只有 25 个专家状态—动作对）：GAIL 用奖励 −log(1−D) 时 3 个种子都达到 500 分（同数据的 BC 为 42.0、494.0、53.6）；换成 log D − log(1−D) 后 3 个种子只有 9.2、37.7、9.2，说明奖励的符号偏置会显著影响结果。

模仿学习（Imitation Learning）从**专家示范**中学习策略，适用于奖励难以设计、但容易获得示范数据的场景，例如自动驾驶、机器人操作和游戏 AI。最简单的**行为克隆**（Behavior Cloning，BC）把问题当作监督学习，但会受到复合误差（分布偏移）的影响；**DAgger** 通过交互式地请专家标注学生访问到的状态来缓解这一问题；**逆强化学习**（IRL）尝试从示范中恢复奖励函数；**GAIL** 则用对抗训练直接匹配专家的状态—动作分布。模仿学习也是大语言模型训练的重要组成部分：监督微调（SFT）本质上就是对人类示范的行为克隆。

## 动机：为什么要模仿

强化学习需要一个奖励函数，但很多任务的奖励很难写清楚：“开车开得好”应该如何量化？安全、舒适、效率、遵守交规之间如何权衡？与其设计奖励，不如直接让专家（人类司机）示范。

模仿学习的设定：给定专家示范数据 $\mathcal{D}^* = \{(s_i, a_i^*)\}$，来自专家策略 $\pi^*$，学习一个策略 $\pi_\theta$ 使其行为接近专家。通常**没有奖励信号**。

## 行为克隆

### 监督学习视角

行为克隆直接把状态当作输入、专家动作当作标签，用监督学习拟合：

$$
\theta^* = \arg\min_\theta\ \mathbb{E}_{(s, a^*) \sim \mathcal{D}^*}\left[\ell\big(\pi_\theta(s), a^*\big)\right]
$$

离散动作用交叉熵（等价于最大化 $\log \pi_\theta(a^* \mid s)$），连续动作用均方误差或高斯负对数似然。BC 实现简单、不需要与环境交互、不需要奖励。早在 1989 年，ALVINN（Pomerleau）就用神经网络从人类驾驶数据中学会了沿道路行驶。

### 复合误差问题

监督学习假设训练和测试数据**同分布**，但 BC 违反了这一点：

- 训练时的状态来自**专家**的轨迹分布 $d_{\pi^*}$；
- 测试时的状态来自**学生自己**的轨迹分布 $d_{\pi_\theta}$。

学生一旦犯了一个小错误，就会进入专家从未到过的状态，在那里它更容易犯错，错误越积越多。Ross & Bagnell（2010）证明：若学生在专家状态分布上每步的错误率为 $\epsilon$，时域为 $T$，则 BC 的总代价可能比专家差 $O(\epsilon T^2)$——误差随时域**二次**增长，而不是监督学习中的线性增长。

## DAgger

**DAgger**（Dataset Aggregation，Ross, Gordon & Bagnell, 2011）的思路很直接：既然问题出在“训练数据中没有学生会访问到的状态”，那就**让学生自己去跑，然后请专家标注**这些状态：

```text
用专家示范训练初始策略 π_1（即 BC）
对 i = 1, 2, ..., N：
    用 π_i 与环境交互，收集访问到的状态 {s}
    请专家为这些状态标注正确动作 a* = π*(s)
    把 {(s, a*)} 加入数据集 D（聚合所有轮次的数据）
    在 D 上重新训练得到 π_{i+1}
```

DAgger 把问题转化为在线学习（no-regret learning），理论上可以把误差界降到 $O(\epsilon T)$。代价是需要一个可以**随时查询**的专家——对人类专家来说，在学生控制的状态下标注动作可能很困难（例如人类司机很难在“车已经偏离车道”的录像上给出正确的转向角）。

## 代码：CartPole 上的 BC 与 DAgger

我们用一个手写的“专家”规则 $a^* = \mathbb{1}[\theta + 0.5\dot\theta > 0]$（实测 20 个回合都能拿满 500 分），它可以随时被查询，正好适合演示 DAgger。为了让复合误差的影响显现出来，我们**刻意只给很少的数据**：BC 只用专家前 10 步的状态；DAgger 每轮让学生跑 10 步、请专家标注后加入数据集，共 5 轮。所有结果是 5 个随机种子的平均。

```python
"""模仿学习：行为克隆（BC）与 DAgger，CartPole-v1。
专家是一个手写规则 a = 1[θ + 0.5·θ̇ > 0]（实测 20 个回合均达到满分 500）。"""
import gymnasium as gym
import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F

env = gym.make("CartPole-v1")

def expert(o):
    return int(o[2] + 0.5 * o[3] > 0)

def rollout(policy_fn, seed, max_steps=500):
    """用 policy_fn 与环境交互，返回访问到的状态和回报。"""
    o, _ = env.reset(seed=seed); states, G = [], 0.0
    for _ in range(max_steps):
        states.append(o)
        o, r, te, tr, _ = env.step(policy_fn(o)); G += r
        if te or tr:
            break
    return states, G

def fit(X, y, steps=2000):
    pi = nn.Sequential(nn.Linear(4, 64), nn.Tanh(), nn.Linear(64, 2))
    opt = torch.optim.Adam(pi.parameters(), lr=1e-2)
    X, y = torch.as_tensor(np.array(X), dtype=torch.float32), torch.as_tensor(y)
    for _ in range(steps):
        loss = F.cross_entropy(pi(X), y)
        opt.zero_grad(); loss.backward(); opt.step()
    return lambda o: int(pi(torch.as_tensor(o, dtype=torch.float32)).argmax())

def evaluate(policy_fn, n=20):
    return np.mean([rollout(policy_fn, 10_000 + i)[1] for i in range(n)])

N_DEMO = 10      # 每轮只收集 10 个状态（刻意很少，以便观察复合误差）
N_ROUNDS = 5

def run(seed):
    torch.manual_seed(seed); np.random.seed(seed)
    # ---------- 行为克隆：只用专家自己访问到的状态 ----------
    demo_states, _ = rollout(expert, seed=seed, max_steps=N_DEMO)
    X, y = list(demo_states), [expert(s) for s in demo_states]
    policy = fit(X, y)
    scores = [evaluate(policy)]
    # ---------- DAgger：学生策略采样状态 → 专家标注 → 聚合数据 → 重新训练 ----------
    for it in range(N_ROUNDS):
        states, _ = rollout(policy, seed=seed * 100 + it, max_steps=N_DEMO)
        X += states
        y += [expert(s) for s in states]
        policy = fit(X, y)
        scores.append(evaluate(policy))
    return scores

results = np.array([run(seed) for seed in range(5)])     # 5 个随机种子取平均
print(f"BC（只用专家数据，{N_DEMO} 个样本）     平均评估回报 {results[:, 0].mean():6.1f}")
for it in range(1, N_ROUNDS + 1):
    print(f"DAgger 第{it}轮（共 {N_DEMO * (it + 1):3d} 个样本） 平均评估回报 {results[:, it].mean():6.1f}")
print("对照：BC 直接使用同样数量（60 个）的专家样本：", end=" ")
bc60 = []
for seed in range(5):
    torch.manual_seed(seed)
    st, _ = rollout(expert, seed=seed, max_steps=N_DEMO * (N_ROUNDS + 1))
    bc60.append(evaluate(fit(st, [expert(s) for s in st])))
print(f"平均评估回报 {np.mean(bc60):.1f}")
```
实际运行输出：

```text
BC（只用专家数据，10 个样本）     平均评估回报   64.0
DAgger 第1轮（共  20 个样本） 平均评估回报  123.0
DAgger 第2轮（共  30 个样本） 平均评估回报  200.2
DAgger 第3轮（共  40 个样本） 平均评估回报  205.7
DAgger 第4轮（共  50 个样本） 平均评估回报  178.5
DAgger 第5轮（共  60 个样本） 平均评估回报  310.1
对照：BC 直接使用同样数量（60 个）的专家样本： 平均评估回报 464.2
```

**如实解读**：

- 只用 10 个专家样本的 BC 平均只能坚持 64 步；随着 DAgger 不断加入学生访问到的状态，评估回报总体上升到 310（中间有波动，样本很少时方差很大）。
- **但在这个玩具实验中，DAgger 并没有胜过“直接多给 BC 一些专家数据”**：用同样 60 个专家样本训练的 BC 达到了 464。原因是这里的专家是一个简单的线性规则，任务也很简单：专家自己轨迹上的状态已经足以让一个小网络学到这条线性决策边界；而 DAgger 早期轮次加入的是学生“快要倒下”时的状态，并且每轮只跑 10 步，覆盖有限。
- DAgger 的理论优势在于**学生的状态分布与专家显著不同**、而这些状态又无法从专家示范中获得的情形（例如自动驾驶中偏离车道后的恢复动作）。在 CartPole 这种简单任务上我们没能观察到这一优势，这是一个诚实的负面结果。

## 逆强化学习（IRL）

模仿学习的另一条路线是：专家的行为是在优化某个**未知的奖励函数**，那就先从示范中把这个奖励恢复出来，再用强化学习求最优策略。这就是**逆强化学习**（Ng & Russell, 2000）。

好处是奖励比策略更“简洁、可迁移”：环境动力学变了，恢复出的奖励依然适用。难点在于问题本身是**病态的**——很多奖励函数都能解释同一组示范（例如 $r \equiv 0$ 让所有策略都最优）。

- **学徒学习**（Abbeel & Ng, 2004）：假设奖励是特征的线性组合，匹配专家的特征期望；
- **最大熵 IRL**（Ziebart et al., 2008）：在所有能匹配专家特征期望的轨迹分布中，选熵最大的那个，即 $p(\tau) \propto \exp(r_\psi(\tau))$，有效地解决了歧义问题。

IRL 的经典算法需要在内循环中反复求解 RL 问题，计算代价高。

## 生成对抗模仿学习（GAIL）

**GAIL**（Ho & Ermon, 2016）证明了“最大熵 IRL + RL”可以等价地写成一个**占用度量匹配**问题，并用 GAN 的方式求解：

- **判别器** $D_\omega(s, a)$：区分状态—动作对来自专家还是学生；
- **生成器**（学生策略 $\pi_\theta$）：用强化学习（例如 TRPO / PPO）最大化“骗过判别器”的奖励，例如 $r(s, a) = -\log(1 - D_\omega(s, a))$。

$$
\min_\theta \max_\omega\ \mathbb{E}_{\pi^*}\left[\log D_\omega(s, a)\right] + \mathbb{E}_{\pi_\theta}\left[\log\left(1 - D_\omega(s, a)\right)\right] - \lambda \mathcal{H}(\pi_\theta)
$$

GAIL 不显式恢复奖励，直接得到策略；由于学生在自己的状态分布上接受训练，它也缓解了复合误差。缺点是需要与环境交互，且对抗训练不太稳定。后续的 AIRL（Fu et al., 2018）在 GAIL 框架下恢复出可迁移的奖励函数。

### 代码：CartPole 上的 GAIL

我们沿用上面的线性规则专家，并刻意只给极少的示范：专家跑 1 条完整轨迹（500 步），每隔 20 步取 1 个状态—动作对，共 **25 对**（这种子采样方式与 GAIL 原论文的做法类似）。学生策略用 [PPO](https://dqn.cn/advanced/ppo) 训练（4 个并行环境，共约 20 万步），**看不到环境奖励**，只能看到判别器给出的奖励；判别器每轮用 5 个小批量区分专家与学生的 $(s, a)$。为了说明奖励形式的影响，代码比较两种常见写法：

- `softplus`：$r = -\log(1 - D(s, a))$（GAIL 原论文的形式），恒为正；
- `logit`：$r = \log D - \log(1 - D)$（AIRL 中使用的形式），没有固定符号。

```python
"""GAIL（生成对抗模仿学习）：CartPole-v1。
专家与 bc_dagger_cartpole.py 相同：手写规则 a = 1[θ + 0.5·θ̇ > 0]。
策略只能看到 GAIL 奖励 r = -log(1 - D(s, a))，看不到环境奖励；D 判别 (s, a) 是否来自专家。
注意：该奖励恒为正，在 CartPole 这种“活得越久越好”的任务里自带“生存偏置”。
为此再提供一个无符号偏置的对照奖励 r = log D - log(1 - D) = logit(D)（AIRL 形式），见教程正文。
用法：python gail_cartpole.py [种子] [softplus|logit]；DQN_QUICK=1 只跑很短的版本。"""
import os
import sys

import gymnasium as gym
import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.distributions import Categorical

QUICK = os.environ.get("DQN_QUICK") == "1"
torch.set_num_threads(1)
N_ENVS, N_STEPS = 4, 256
TOTAL_STEPS = 20_480 if QUICK else 200_704
GAMMA, LAM, LR, CLIP, EPOCHS, N_MB, ENT = 0.99, 0.95, 3e-4, 0.2, 10, 4, 0.01
N_DEMO_TRAJ, SUBSAMPLE = 1, 20            # 只用 1 条专家轨迹，每 20 步取 1 个 (s, a)：共 25 对

def expert(o):
    return int(o[2] + 0.5 * o[3] > 0)

def layer(i, o, std=np.sqrt(2)):
    l = nn.Linear(i, o); nn.init.orthogonal_(l.weight, std); nn.init.zeros_(l.bias); return l

def evaluate(act_fn, n=20):
    env, scores = gym.make("CartPole-v1"), []
    for i in range(n):
        o, _ = env.reset(seed=10_000 + i); done, G = False, 0.0
        while not done:
            o, r, te, tr, _ = env.step(act_fn(o)); G += r; done = te or tr
        scores.append(G)
    return float(np.mean(scores))

def demos(seed):
    env = gym.make("CartPole-v1"); S, A = [], []
    for k in range(N_DEMO_TRAJ):
        o, _ = env.reset(seed=seed * 100 + k); done, t = False, 0
        while not done:
            a = expert(o)
            if t % SUBSAMPLE == 0:
                S.append(o); A.append(a)
            o, _, te, tr, _ = env.step(a); done = te or tr; t += 1
    return torch.as_tensor(np.array(S), dtype=torch.float32), torch.as_tensor(A)

def bc(S, A, seed, steps=2000):
    torch.manual_seed(seed)
    pi = nn.Sequential(nn.Linear(4, 64), nn.Tanh(), nn.Linear(64, 2))
    opt = torch.optim.Adam(pi.parameters(), lr=1e-2)
    for _ in range(steps):
        loss = F.cross_entropy(pi(S), A); opt.zero_grad(); loss.backward(); opt.step()
    return lambda o: int(pi(torch.as_tensor(o, dtype=torch.float32)).argmax())

def sa(s, a):
    return torch.cat([s, F.one_hot(a.long(), 2).float()], -1)

def gail(S_exp, A_exp, seed, reward="softplus"):
    torch.manual_seed(seed); np.random.seed(seed)
    envs = gym.vector.SyncVectorEnv([lambda: gym.make("CartPole-v1") for _ in range(N_ENVS)],
                                    autoreset_mode=gym.vector.AutoresetMode.SAME_STEP)
    actor = nn.Sequential(layer(4, 64), nn.Tanh(), layer(64, 64), nn.Tanh(), layer(64, 2, 0.01))
    critic = nn.Sequential(layer(4, 64), nn.Tanh(), layer(64, 64), nn.Tanh(), layer(64, 1, 1.0))
    disc = nn.Sequential(nn.Linear(6, 64), nn.Tanh(), nn.Linear(64, 64), nn.Tanh(), nn.Linear(64, 1))
    params = list(actor.parameters()) + list(critic.parameters())
    opt = torch.optim.Adam(params, lr=LR, eps=1e-5)
    d_opt = torch.optim.Adam(disc.parameters(), lr=3e-4)
    exp_sa = sa(S_exp, A_exp)

    obs = torch.as_tensor(envs.reset(seed=seed)[0], dtype=torch.float32)
    O = torch.zeros(N_STEPS, N_ENVS, 4); A, LP, V, D = (torch.zeros(N_STEPS, N_ENVS) for _ in range(4))
    FINAL = torch.zeros(N_STEPS, N_ENVS, 4); TR = torch.zeros(N_STEPS, N_ENVS)
    env_returns, ep_ret = [], np.zeros(N_ENVS)
    n_iters, log = TOTAL_STEPS // (N_ENVS * N_STEPS), []
    for it in range(1, n_iters + 1):
        for t in range(N_STEPS):
            with torch.no_grad():
                d = Categorical(logits=actor(obs)); a = d.sample(); V[t] = critic(obs).squeeze(-1)
            o2, r, te, tr, info = envs.step(a.numpy())
            O[t], A[t], LP[t] = obs, a, d.log_prob(a)
            D[t] = torch.as_tensor(te | tr, dtype=torch.float32); TR[t] = torch.as_tensor(tr, dtype=torch.float32)
            if tr.any():
                FINAL[t] = torch.as_tensor(np.stack([info["final_obs"][i] if tr[i] else o2[i] for i in range(N_ENVS)]), dtype=torch.float32)
            ep_ret += r                                         # 环境回报只用于记录，不参与训练
            for i in np.flatnonzero(te | tr):
                env_returns.append(ep_ret[i]); ep_ret[i] = 0
            obs = torch.as_tensor(o2, dtype=torch.float32)

        # ---------- 1) 训练判别器：专家 (s,a) → 1，策略 (s,a) → 0 ----------
        pol_sa = sa(O.reshape(-1, 4), A.reshape(-1))
        for _ in range(5):
            j = torch.randint(len(pol_sa), (256,)); k = torch.randint(len(exp_sa), (256,))
            logits_p, logits_e = disc(pol_sa[j]), disc(exp_sa[k])
            d_loss = F.binary_cross_entropy_with_logits(logits_e, torch.ones_like(logits_e)) + \
                     F.binary_cross_entropy_with_logits(logits_p, torch.zeros_like(logits_p))
            d_opt.zero_grad(); d_loss.backward(); d_opt.step()

        # ---------- 2) GAIL 奖励 r = -log(1 - D) = softplus(logit) ----------
        with torch.no_grad():
            logit = disc(pol_sa).reshape(N_STEPS, N_ENVS)
            R = F.softplus(logit) if reward == "softplus" else logit
            R = R + GAMMA * critic(FINAL).squeeze(-1) * TR   # 时间截断：自举
            next_v = critic(obs).squeeze(-1); adv = torch.zeros_like(R); last = 0
            for t in reversed(range(N_STEPS)):
                nv = next_v if t == N_STEPS - 1 else V[t + 1]
                delta = R[t] + GAMMA * nv * (1 - D[t]) - V[t]
                last = delta + GAMMA * LAM * (1 - D[t]) * last; adv[t] = last
            ret = adv + V

        # ---------- 3) PPO 更新 ----------
        b = [x.reshape(N_STEPS * N_ENVS, *x.shape[2:]) for x in (O, A, LP, adv, ret)]
        for _ in range(EPOCHS):
            for j in torch.randperm(N_STEPS * N_ENVS).chunk(N_MB):
                s, a_, lp, ad, rt = (x[j] for x in b)
                ad = (ad - ad.mean()) / (ad.std() + 1e-8)
                d = Categorical(logits=actor(s)); ratio = (d.log_prob(a_) - lp).exp()
                pg = -torch.min(ratio * ad, ratio.clamp(1 - CLIP, 1 + CLIP) * ad).mean()
                loss = pg + 0.5 * ((critic(s).squeeze(-1) - rt) ** 2).mean() - ENT * d.entropy().mean()
                opt.zero_grad(); loss.backward(); nn.utils.clip_grad_norm_(params, 0.5); opt.step()
        if it % (5 if QUICK else 49) == 0:
            score = evaluate(lambda o: int(actor(torch.as_tensor(o, dtype=torch.float32)).argmax()))
            log.append((it * N_ENVS * N_STEPS, score))
            print(f"  GAIL[{reward}] 种子{seed} 步数 {it * N_ENVS * N_STEPS:6d} 最近20回合环境回报 {np.mean(env_returns[-20:]):6.1f}  贪心评估 {score:6.1f}", flush=True)
    return log

if __name__ == "__main__":
    seeds = [int(sys.argv[1])] if len(sys.argv) > 1 else ([0] if QUICK else [0, 1, 2])
    rewards = [sys.argv[2]] if len(sys.argv) > 2 else ["softplus", "logit"]
    for sd in seeds:
        S, A = demos(sd)
        print(f"种子{sd}: 专家数据 {len(S)} 个 (s, a)；BC 贪心评估 {evaluate(bc(S, A, sd)):.1f}")
        for rw in rewards:
            log = gail(S, A, sd, rw)
            print(f"种子{sd}: GAIL[{rw}] 最终贪心评估 {log[-1][1]:.1f}")
```
实际运行结果（贪心策略在 20 个回合上的平均回报，满分 500）：

| 种子 | BC（同样 25 对数据） | GAIL，$r=-\log(1-D)$ | GAIL，$r=\log D-\log(1-D)$ |
| --- | --- | --- | --- |
| 0 | 42.0 | 500.0 | 9.2 |
| 1 | 494.0 | 500.0 | 37.7 |
| 2 | 53.6 | 500.0 | 9.2 |

如实解读：

- **只有 25 对示范时，BC 很不稳定**：3 个种子分别为 42.0、494.0、53.6，取决于这 25 个状态是否恰好让网络学到正确的决策边界。
- **GAIL（$-\log(1-D)$）3 个种子都达到满分**，学习过程中第 5 万步左右评估回报就已超过 400。它能在自己访问到的状态上得到训练信号，不受“示范状态太少”的限制。
- **但要警惕“生存偏置”**：$-\log(1-D)$ 恒为正，每多活一步就多拿一份正奖励，而 CartPole 的环境奖励恰好就是“每活一步 +1”。也就是说，即使判别器什么都没学到，这个奖励形式本身也会推动策略不倒下。因此这里的满分**不能完全归功于模仿**。
- **换成没有固定符号的 $\log D - \log(1-D)$ 后结果完全不同**：训练初期判别器很容易认出学生，奖励大多为负，策略于是学会尽快结束回合（种子 0 和 2 平均约 9 步就倒下），3 个种子分别只有 9.2、37.7、9.2。
- 结论：GAIL 的奖励形式与任务的终止条件会相互作用（Kostrikov 等，2019 系统分析了这一问题，并提出用“吸收状态”消除偏置）。评估模仿学习算法时，最好在“提前终止对智能体有利”和“提前终止对智能体不利”的任务上都做检验。

## 模仿学习与强化学习的结合

- **BC 预训练 + RL 微调**：AlphaGo 先用人类棋谱做监督学习，再用自博弈强化学习提升；大语言模型先做 SFT 再做 [RLHF](https://dqn.cn/advanced/rlhf-llm)，是同样的范式。
- **示范加入回放缓冲区**：DQfD（Hester et al., 2018）把示范数据放入 DQN 的回放缓冲区，并加入监督的大间隔损失。
- **离线 RL**：有奖励的示范数据可以用 [离线强化学习](https://dqn.cn/advanced/offline-rl) 方法学习，有可能超过示范者。
- **扩散策略、Transformer 策略**：近年机器人领域的 BC 大量使用扩散模型（Diffusion Policy）和大规模 Transformer，来建模示范中多模态的动作分布。

## 实践建议与常见误区

- **数据质量胜过数量**：不一致、带噪声的示范会严重影响 BC。
- **多模态动作**：专家在同一状态下可能有多种合理动作（向左或向右绕过障碍），用均方误差回归会得到“平均”的错误动作。可以使用混合高斯、离散化或扩散模型。
- **误区：BC 训练误差低就说明策略好**：BC 的问题在于测试时的分布偏移，训练集上的准确率不能反映闭环表现，一定要在环境中评估。
- **误区：DAgger 总是更好**：如我们的实验所示，在专家轨迹覆盖充分的简单任务上，多给专家数据可能更划算。

## 小结

- 行为克隆 = 监督学习，简单有效，但会受复合误差影响（误差随时域二次增长）。
- DAgger 让学生采样、专家标注，缓解分布偏移，但需要可交互查询的专家。
- 逆强化学习从示范中恢复奖励；GAIL 用对抗训练直接匹配专家的占用度量；奖励形式的符号会与终止条件相互作用，带来生存或自毁偏置。
- 模仿学习常与强化学习结合：先模仿、再强化。

## 练习题

1. 把 `N_DEMO` 改为 50，BC 和 DAgger 的差距如何变化？
2. 给专家动作加入 20% 的标注噪声（随机翻转），BC 的表现如何下降？
3. 实现 DAgger 的混合策略版本：第 $i$ 轮以概率 $\beta_i$ 执行专家动作、以 $1 - \beta_i$ 执行学生动作（论文中的 $\beta_i$ 衰减策略）。
4. 设计一个专家在某些状态下有两种同样好的动作的环境，比较交叉熵 BC 与均方误差 BC 的表现。
5. 阅读 GAIL 论文，解释为什么判别器的输出可以作为奖励。
6. 在 `gail_cartpole.py` 中加入“吸收状态”处理（回合终止后再接一个奖励由判别器决定的吸收状态），重新比较两种奖励形式。

## 参考文献

- Pomerleau, D. A. (1989). ALVINN: An Autonomous Land Vehicle in a Neural Network. *NeurIPS*.
- Ng, A. Y., & Russell, S. (2000). Algorithms for Inverse Reinforcement Learning. *ICML*.
- Abbeel, P., & Ng, A. Y. (2004). Apprenticeship Learning via Inverse Reinforcement Learning. *ICML*.
- Ziebart, B. D., Maas, A., Bagnell, J. A., & Dey, A. K. (2008). Maximum Entropy Inverse Reinforcement Learning. *AAAI*.
- Ross, S., & Bagnell, J. A. (2010). Efficient Reductions for Imitation Learning. *AISTATS*.
- Ross, S., Gordon, G., & Bagnell, J. A. (2011). A Reduction of Imitation Learning and Structured Prediction to No-Regret Online Learning. *AISTATS*. [arXiv:1011.0686](https://arxiv.org/abs/1011.0686)
- Ho, J., & Ermon, S. (2016). Generative Adversarial Imitation Learning. *NeurIPS*. [arXiv:1606.03476](https://arxiv.org/abs/1606.03476)
- Fu, J., Luo, K., & Levine, S. (2018). Learning Robust Rewards with Adversarial Inverse Reinforcement Learning. *ICLR*. [arXiv:1710.11248](https://arxiv.org/abs/1710.11248)
- Kostrikov, I., Agrawal, K. K., Dwibedi, D., Levine, S., & Tompson, J. (2019). Discriminator-Actor-Critic: Addressing Sample Inefficiency and Reward Bias in Adversarial Imitation Learning. *ICLR*. [arXiv:1809.02925](https://arxiv.org/abs/1809.02925)
- Hester, T. et al. (2018). Deep Q-learning from Demonstrations. *AAAI*. [arXiv:1704.03732](https://arxiv.org/abs/1704.03732)
- Chi, C. et al. (2023). Diffusion Policy: Visuomotor Policy Learning via Action Diffusion. *RSS*. [arXiv:2303.04137](https://arxiv.org/abs/2303.04137)

## 下一步

上一节：[多智能体强化学习](https://dqn.cn/advanced/multi-agent)　｜　继续阅读：[RLHF 与大模型中的 RL](https://dqn.cn/advanced/rlhf-llm)

## 常见问题速查（FAQ）

**问：行为克隆为什么会失败？**

答：训练数据只覆盖专家访问的状态，学习者一旦犯小错就会进入没见过的状态，错误不断累积（复合误差），误差随时间步数可能平方级增长。

**问：DAgger 一定比行为克隆好吗？**

答：理论上 DAgger 能缓解分布偏移，但需要专家可以随时查询。本站 CartPole 实验中，在相同专家标注数量下，DAgger（平均 310.1）反而不如直接用专家数据的 BC（464.2），这与实验设置有关。

**问：GAIL 的奖励应该用 −log(1−D) 还是 log D？**

答：两者的符号不同：−log(1−D) 恒为正，会鼓励“活得更久”，在 CartPole 这类存活即得分的任务中自带有利的偏置；log D 恒为负，会鼓励尽早结束回合；log D − log(1−D) 没有固定符号。本站实验中前者 3 个种子都达到 500 分，后者大多在 10 步左右就结束回合。实际使用时要结合任务的终止条件选择，或用吸收状态等方法消除偏置。

**问：模仿学习和离线强化学习怎么选？**

答：数据来自可靠专家且没有奖励时用模仿学习；数据质量参差、带有奖励信息时可以考虑离线强化学习。

## 如何引用本文

DQN.CN 编辑部. 模仿学习[EB/OL]. DQN.CN, 2026-10-29（更新于 2026-10-30）. https://dqn.cn/advanced/imitation-learning.
