强化学习(七):模仿学习与逆强化学习

从专家示范中学习:行为克隆为何在长任务上必败、DAgger 如何把误差从二次降到线性、最大熵 IRL 如何反推奖励、GAIL/AIRL 如何用对抗训练匹配专家占用度。配可运行 PyTorch 代码、方法选择阶梯,以及七张高质量配图。

之前章节里所有算法的前提,都是已经有一个奖励函数。但在工程实践中,写出那个奖励函数本身往往才是最难的一步。“像一个有经验的老司机一样开车”、“像一个裁缝一样把衬衫叠整齐”、“像一个资深编辑一样把这篇文档总结一下”——这类任务你能轻易演示,却很难一段话写清楚

模仿学习把这个直觉做到了极致:不再优化人为设计的标量奖励,而是直接从专家示范 $\mathcal{D} = \{(s_t, a_t)\}$ 学策略。本章按"一根阶梯"的视角依次讲四个经典方法——行为克隆、DAgger、最大熵 IRL、GAIL/AIRL。每登上一阶,都是放松上一阶的某个假设、然后用新的结构买回来。

你将学到什么

  • 行为克隆(BC):把模仿当成监督学习,它在短任务上为什么够用、在长任务上为什么必败。
  • DAgger:通过交互式重新打标签,把 BC 的二次误差降到线性,背后的 no-regret 定理是怎么一回事。
  • 最大熵 IRL:从行为反推一个可解释的奖励,使其最优策略复现示范。
  • GAIL 与 AIRL:用对抗训练,端到端地匹配专家的占用度量(occupancy measure)。
  • 方法选择:根据是否能在线询问专家、是否能与环境交互、是否需要迁移,给出明确选择规则。

前置知识第 5 部分 及 PPO(第 6 部分 )。代码段假设熟悉 PyTorch 基本用法。


1. 问题设定

给定专家示范

$$ \mathcal{D} = \{(s_1, a_1), (s_2, a_2), \ldots, (s_N, a_N)\}, $$

要学出一个策略 $\pi_\theta$,行为接近某个未知的专家策略 $\pi^*$。我们看不到 $\pi^*$ 本身,只能看到它的样本;也没有奖励信号。有时候我们还能在新状态上主动询问专家(这就是 DAgger 的设定);更多时候不能。

维度强化学习模仿学习
监督信号奖励 $r(s, a)$专家数据集 $\mathcal{D}$
是否需要交互必须探索可以纯离线(BC)
优化目标$\mathbb{E}[\sum \gamma^t r_t]$匹配专家行为分布
典型失败模式奖励作弊、探索失败分布偏移、模式塌缩
典型应用游戏、机器人、RLHF自动驾驶、手术、语言风格

下面这五种方法在这些维度上不同地权衡。整张"阶梯图"先放在这里,后文按阶展开:

模仿学习方法阶梯


2. 行为克隆(Behavioral Cloning)

最简单的模仿算法,也是工业界部署得最多的算法:把 $\mathcal{D}$ 当成一个监督学习数据集,最小化

$$ \mathcal{L}(\theta) \;=\; \mathbb{E}_{(s,a)\sim\mathcal{D}}\big[ \ell\big(\pi_\theta(s),\, a\big) \big], $$

其中 $\ell$ 在离散动作下是交叉熵、在连续动作下是 MSE 或负对数似然。这个思路最早可追溯到 1989 年的 ALVINN(Pomerleau, 1989)——一个全连接网络从摄像头图像直接学到方向盘角度,让一辆面包车自主行驶。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
import torch
import torch.nn as nn
import numpy as np
from torch.utils.data import DataLoader, TensorDataset


class BehavioralCloning:
    """监督式模仿:把策略输出与专家动作之间的损失最小化。"""

    def __init__(self, state_dim, action_dim, hidden_dims=(256, 256),
                 lr=1e-3, continuous=False, dropout=0.1):
        self.continuous = continuous
        layers, prev = [], state_dim
        for h in hidden_dims:
            layers += [nn.Linear(prev, h), nn.ReLU(), nn.Dropout(dropout)]
            prev = h
        if continuous:
            layers += [nn.Linear(prev, action_dim), nn.Tanh()]
            self.criterion = nn.MSELoss()
        else:
            layers += [nn.Linear(prev, action_dim)]
            self.criterion = nn.CrossEntropyLoss()
        self.policy = nn.Sequential(*layers)
        self.optim = torch.optim.Adam(self.policy.parameters(), lr=lr)
        self.state_mean = self.state_std = None

    def fit(self, states, actions, epochs=100, batch_size=64, val_frac=0.1):
        # 标准化状态——BC 是个小监督模型,未归一化的特征会主导损失。
        self.state_mean = states.mean(0)
        self.state_std = states.std(0) + 1e-8
        S = (states - self.state_mean) / self.state_std

        n_val = int(len(S) * val_frac)
        idx = np.random.permutation(len(S))
        S_tr, S_val = S[idx[n_val:]], S[idx[:n_val]]
        A_tr, A_val = actions[idx[n_val:]], actions[idx[:n_val]]

        S_tr = torch.as_tensor(S_tr, dtype=torch.float32)
        S_val = torch.as_tensor(S_val, dtype=torch.float32)
        if self.continuous:
            A_tr = torch.as_tensor(A_tr, dtype=torch.float32)
            A_val = torch.as_tensor(A_val, dtype=torch.float32)
        else:
            A_tr = torch.as_tensor(A_tr, dtype=torch.long)
            A_val = torch.as_tensor(A_val, dtype=torch.long)

        loader = DataLoader(TensorDataset(S_tr, A_tr),
                            batch_size=batch_size, shuffle=True)
        best, best_state = float("inf"), None
        for epoch in range(epochs):
            self.policy.train()
            for s, a in loader:
                loss = self.criterion(self.policy(s), a)
                self.optim.zero_grad(); loss.backward(); self.optim.step()
            self.policy.eval()
            with torch.no_grad():
                v = self.criterion(self.policy(S_val), A_val).item()
            if v < best:
                best, best_state = v, {k: t.clone() for k, t in
                                       self.policy.state_dict().items()}
        self.policy.load_state_dict(best_state)

    def act(self, state):
        s = (state - self.state_mean) / self.state_std
        s = torch.as_tensor(s, dtype=torch.float32).unsqueeze(0)
        self.policy.eval()
        with torch.no_grad():
            out = self.policy(s)
        return out.squeeze().numpy() if self.continuous else out.argmax(-1).item()

三个看似不起眼、其实决定成败的工程细节:

  1. 必须做输入标准化。 BC 网络很小,未归一化的特征会让损失被某几维主导,在罕见状态上输出过度自信的动作。
  2. 必须用验证集早停。 训得过久会把专家自己的噪声学进去,反而放大下一节要讲的分布偏移问题。
  3. 动作表示要选对。 连续控制下,输出高斯分布参数(用 NLL 损失)几乎总是优于"Tanh + MSE"——尤其当专家行为是多模态的时候。

2.1 BC 为什么在长任务上必败

BC 训练时看到的状态来自专家分布 $d_{\pi^*}$,部署时却走在自己策略的状态分布 $d_{\pi_\theta}$ 上。因为 $\pi_\theta$ 不完美,这两个分布会逐步分开。

BC 与 DAgger 的状态分布对比

经典误差界把这个级联效应说得非常清楚。如果 $\pi_\theta$ 在专家分布上的单步期望误差是 $\varepsilon$,那么在长度 $T$ 的轨迹上,最坏情况下总误差是

$$ J(\pi^*) - J(\pi_\theta) \;\le\; \mathcal{O}\!\left( \varepsilon \, T^2 \right), $$

关于 $T$ 二次方(Ross & Bagnell, 2010)。听起来温和,实际杀伤力极大:哪怕单步 99% 准确,跑 200 步也基本会偏离——一旦走到专家从没见过的状态,模型就完全没有训练信号,错误自此一发不可收拾。

把训好的策略放回环境跑,能很直观地看到这个级联:

专家轨迹与学习者轨迹对比

前期 BC 学习者还能贴在专家走廊上;过了中段出现一个小偏差,策略进入分布外状态,然后慢慢漂入危险区。每一步的误差曲线也讲了同一个故事:

BC 的误差累积


3. DAgger:数据集聚合

DAgger(Ross, Gordon & Bagnell, 2011)的核心思想极其朴素:到学习者真正走到的状态上去问专家。每一轮迭代,都把当前策略走出来的 $(s, a^*)$ 加进训练集,然后整体重训。

算法。

  1. 用初始的专家示范训出 $\pi_1$。
  2. 对 $i = 1, \ldots, N$:
    • 用 $\pi_i$ 和专家按调度 $\beta_i \to 0$ 的混合策略采样状态。
    • 在每个访问到的状态上向专家询问正确动作 $a^*$。
    • 聚合:$\mathcal{D} \leftarrow \mathcal{D} \cup \{(s, a^*)\}$。
    • 在整个 $\mathcal{D}$ 上重训出 $\pi_{i+1}$。

No-regret 保证。 把这一连串策略看成一个在线学习者,对手是这些访问分布所诱导的损失序列;用 no-regret 算法可以证明

$$ J(\pi^*) - J(\pi_{\hat{i}}) \;\le\; \mathcal{O}\!\left( \varepsilon \, T \right) + \mathcal{O}\!\left( \tfrac{T \sqrt{\log N}}{\sqrt{N}} \right), $$

也就是对 $T$ 线性。直观读法:每多一轮迭代,都在把 BC 丢掉的"恢复信息"重新拿回来。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class DAgger:
    """DAgger = BC + 在学习者访问的状态上反复请专家标注。"""

    def __init__(self, state_dim, action_dim, **bc_kwargs):
        self.bc = BehavioralCloning(state_dim, action_dim, **bc_kwargs)
        self.S, self.A = [], []

    def train(self, env, expert, n_iters=10,
              n_init=50, n_per_iter=20):
        # 第 0 轮:纯专家轨迹做 warm start。
        s0, a0 = self._collect(env, expert, n_init, beta=1.0)
        self.S += s0; self.A += a0
        self.bc.fit(np.array(self.S), np.array(self.A))

        for i in range(1, n_iters + 1):
            beta = max(0.0, 1.0 - i / n_iters)   # 专家比例从 1 衰减到 0
            s, a = self._collect(env, expert, n_per_iter, beta=beta)
            self.S += s; self.A += a
            self.bc.fit(np.array(self.S), np.array(self.A))

    def _collect(self, env, expert, n_episodes, beta):
        states, actions = [], []
        for _ in range(n_episodes):
            s, done = env.reset(), False
            while not done:
                # 行动用混合策略;标签始终来自专家。
                a_play = expert(s) if np.random.rand() < beta else self.bc.act(s)
                a_label = expert(s)
                states.append(s); actions.append(a_label)
                s, _, done, _ = env.step(a_play)
        return states, actions

DAgger 在哪些场景适用? 它要求专家能在训练时被实时询问。这在以下场合是现实的:

  • 专家本身是个算法(MPC 控制器、基于搜索的规划器、最优控制器);
  • 专家是更强的模型,你想做蒸馏(teacher-student RL、模型蒸馏、语言模型自蒸馏);
  • 人类专家在线,愿意按批次打标签。

不适用的典型场景,就是只能拿到一份静态日志——比如一份录制好的驾驶数据。这种纯离线设定,要跳到 GAIL。


4. 逆强化学习(IRL)

BC 和 DAgger 都在模仿动作。IRL 问得更深一层:专家为什么这么做?它假设专家在(近似)最大化某个未知的奖励 $r^*$,从示范中反推出一个候选 $\hat r$,然后在 $\hat r$ 上跑标准 RL。

绕这一圈值得吗?两个理由:

  • 可解释性。 $\hat r$ 直接告诉你专家在乎什么——可以审计、可以改。
  • 迁移性。 奖励函数对动力学、形态、初始状态分布的变化具有鲁棒性,而行为级别的模型一改环境就废。

逆强化学习从行为反推奖励

4.1 最大熵 IRL

单纯"匹配专家价值"的目标是病态的——很多奖励都能解释同一段行为。最大熵 IRL(Ziebart et al., 2008)用一句话打破歧义:恢复出来的策略要在最大化 $r$ 的同时熵最大。形式上,专家在轨迹 $\tau$ 上的分布取 Boltzmann 形式:

$$ p_\theta(\tau) \;\propto\; \exp\!\left( \sum_t r_\theta(s_t, a_t) \right). $$

最大化示范的对数似然,能得到一个非常优雅的梯度:

$$ \nabla_\theta \mathcal{L}(\theta) \;=\; \mathbb{E}_{\tau \sim \pi^*}\!\left[\nabla_\theta r_\theta(\tau)\right] \;-\; \mathbb{E}_{\tau \sim \pi_\theta}\!\left[\nabla_\theta r_\theta(\tau)\right]. $$

把它读成一个对比更新:专家走过的地方把奖励抬高,自己走过的地方把奖励压低。收敛时两个期望相等,策略就复现了专家的占用度。

代价在第二个期望上。计算 $\mathbb{E}_{\pi_\theta}$ 需要在每次更新奖励之后重新解一遍 RL——这个嵌套循环把传统 IRL 限制在小规模网格世界里。Guided cost learning(Finn, Levine & Abbeel, 2016)用重要性采样的轨迹替代这一内部 RL 求解,把最大熵 IRL 拓展到了连续控制。

4.2 奖励的不可识别性

即便有了最大熵正则,$\hat r$ 也只能恢复到一族整形不变(shaping invariance):加上一个势能差 $\Phi(s') - \Phi(s)$ 不会改变最优策略,却会改变 $r$ 本身。所以 IRL 恢复出来的奖励,是状态间一个有用的"排序",而不是绝对尺度。下一节的 AIRL 显式把整形项分离出来。


5. 对抗式模仿:GAIL 与 AIRL

IRL 的内层 RL 求解太贵。GAIL(Ho & Ermon, 2016)的洞察是:做模仿其实并不需要 $r$——我们只需要让策略的状态-动作占用度和专家匹配上。GAIL 因此把"先恢复奖励、再重解 RL"换成一个对抗博弈

判别器 $D_\phi(s, a)$ 努力区分专家样本和策略样本;策略努力骗过判别器。Minimax 目标:

$$ \min_\theta \max_\phi ;; \mathbb{E}{(s,a)\sim \pi^*}!\left[\log D\phi(s, a)\right]

  • \mathbb{E}{(s,a)\sim \pi\theta}!\left[\log\big(1 - D_\phi(s, a)\big)\right]
  • \lambda H(\pi_\theta). $$

熵项 $\lambda H(\pi_\theta)$ 用来稳住生成器、防止模式塌缩。鞍点处,$\pi_\theta$ 的占用度等于 $\pi^*$ 的——这正是 BC 想做却做不到的事,因为 BC 的"监督分布"和"部署分布"不一致。

GAIL 判别器-生成器架构

实践中,判别器的得分直接当作每步奖励,喂给 PPO 来更新策略:

$$ \hat r(s, a) \;=\; -\log\!\big(1 - D_\phi(s, a)\big) \quad\text{或}\quad \hat r(s, a) \;=\; -\log D_\phi(s, a). $$

文献里两种形式都有。前者是 GAIL 原论文用的;后者在训练早期方差更小、更稳。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class GAIL:
    """对抗式模仿:判别器输出充当策略的每步奖励。"""

    def __init__(self, state_dim, action_dim, hidden_dim=256, continuous=False):
        self.continuous = continuous
        in_dim = state_dim + action_dim
        self.discriminator = nn.Sequential(
            nn.Linear(in_dim, hidden_dim), nn.Tanh(),
            nn.Linear(hidden_dim, hidden_dim), nn.Tanh(),
            nn.Linear(hidden_dim, 1),
        )
        self.disc_optim = torch.optim.Adam(self.discriminator.parameters(),
                                           lr=3e-4)
        # 策略 / 价值网络:任何标准 PPO 实现都行(此处省略)。

    def _sa(self, s, a):
        if not self.continuous:
            a = torch.nn.functional.one_hot(a.long(), self.action_dim).float()
        return torch.cat([s, a], dim=-1)

    def reward(self, states, actions):
        """喂给策略学习器的每步模仿奖励。"""
        with torch.no_grad():
            logits = self.discriminator(self._sa(states, actions))
            d = torch.sigmoid(logits)
            r = -torch.log(1.0 - d + 1e-8)   # 原版 GAIL 的奖励形式
        return r.squeeze(-1)

    def update_discriminator(self, expert_sa, policy_sa):
        # 二元交叉熵:专家=0,策略=1。
        e_logits = self.discriminator(expert_sa)
        p_logits = self.discriminator(policy_sa)
        bce = torch.nn.functional.binary_cross_entropy_with_logits
        loss = bce(e_logits, torch.zeros_like(e_logits)) + \
               bce(p_logits, torch.ones_like(p_logits))
        self.disc_optim.zero_grad(); loss.backward(); self.disc_optim.step()
        return loss.item()

调参要点。 实际跑 GAIL 时,三种失败模式最常见:

  • 判别器赢得太快。 策略侧的奖励梯度直接消失。对策:裁剪判别器 logits、用 spectral normalization、或加 Lipschitz 约束(WGAIL)。
  • 奖励信号塌缩。 当 $D \to 0$,奖励整体退化成接近零。对策:对奖励做 batch 内的滑动归一化(running mean / std)。
  • 策略模式塌缩。 把熵奖励 $\lambda$ 调大,或者直接把生成器换成最大熵 actor(SAC 风格)。

5.1 “匹配占用度"到底意味着什么?

GAIL 不是在最小化某种动作预测损失,而是在最小化专家占用度 $\rho_{\pi^*}(s, a)$ 与学习者占用度 $\rho_{\pi_\theta}(s, a)$ 的 Jensen-Shannon 散度。这是个比 BC 严格强得多的目标——因为它意识到了部署分布的存在。代价:训练时必须能与环境交互(用来采 $\rho_{\pi_\theta}$),所以原版 GAIL 不适用于纯离线场景。

5.2 AIRL:把奖励从整形里剥出来

GAIL 学到的判别器擅长模仿,但不是一个可复用的奖励。AIRL(Fu, Luo & Levine, 2018)把判别器重写成

$$ D_\phi(s, a, s') \;=\; \frac{\exp\big(f_\phi(s, a, s')\big)}{\exp\big(f_\phi(s, a, s')\big) + \pi_\theta(a \mid s)}, \qquad f_\phi(s, a, s') \;=\; r_\psi(s) + \gamma \Phi_\xi(s') - \Phi_\xi(s), $$

其中 $r_\psi$ 是只依赖状态的奖励,整形项被 $\Phi_\xi$ 吸收。这样训出来的 $\hat r = r_\psi$ 在动力学发生改变时仍然可用——这是 AIRL 论文最具冲击力的实证结果。


6. 样本效率:模仿 vs RL

模仿学习最有力的工程论据是样本效率:几千条专家示范,往往能省掉数千万步的环境交互——尤其在高维控制任务上。

样本效率对比:模仿 vs RL

左图的模式在机器人基准上非常稳定:BC 在专家水平之下饱和(它无法超越训练数据)、DAgger 在见到恢复状态后稳步上升、GAIL 最终能匹配专家、纯 RL 要花长得多的时间才能追上。右图把"代价"摆得很直白——吃专家数据多的方法,环境步数就少;反之亦然。

这也是为什么**“模仿预训练 + RL 微调”**已经成了现代系统的标准配方——AlphaStar、机器人操作、InstructGPT 全是先模仿再 RL。


7. 方法选择

方法需要在线询问专家?需要环境交互?样本效率可解释奖励?典型场景
BC高(离线)短任务、有大量日志
DAgger中-高算法/模型专家可被询问
最大熵 IRL是(内层)小状态空间、需要迁移
GAIL高维连续控制
AIRL是(状态奖励)想跨任务迁移奖励

一条简短的决策规则:

  • 静态示范日志、短时序任务 → 先上 BC。一定要做标准化和早停;想要不确定性的话用集成。
  • 训练时可以在线询问专家DAgger。这个"线性误差"保证是你能买到的最便宜的提升。
  • 需要理解专家在乎什么、或者要把它迁移到另一个环境 → 小问题用最大熵 IRL,连续控制用 AIRL
  • 高维连续控制 + 静态数据 + 可与环境交互GAIL。但要为对抗训练的不稳定性留预算。

8. 常见问题

模仿学习能超越专家吗? 纯模仿不能——最优的模仿者就是复刻专家。标准做法是把模仿当作初始化:从 BC/GAIL 策略出发,用你能写出的奖励(或 RLHF 的偏好奖励)去做 RL 微调。当今大规模系统几乎都在用这套两段式配方。

专家示范有噪声、甚至次优,怎么办? 三层防线:(i)按估计的质量分(回报、人类偏好)给样本加权;(ii)把交叉熵换成鲁棒损失(Huber、log-cosh)来折损离群点;(iii)在示范上跑离线 RL——IQL、CQL 这类方法天生能处理次优轨迹。

专家是多模态的——同一状态有多种合理动作,怎么办? 标准 MSE-BC 会把多模态平均掉,输出一个危险的"中间动作”(“一半示范向左、一半向右躲障碍,BC 学到的是直撞过去”)。换成混合密度网络(MDN)、离散化动作策略、条件 VAE,或者扩散策略(diffusion policy)。Chi 等人 2023 的 diffusion policy 在多模态操作任务上目前是最强的。

多收点数据能不能解决 BC 的分布偏移? 能减轻,但不能根除。更多数据让 $\varepsilon$ 变小,却不会改变 $T^2$ 这个指数。一旦时序长到策略会偏离 $\mathcal{D}$ 的支撑,就必须用 DAgger 风格的重打标签、GAIL 风格的占用匹配,或者保守的离线 RL(CQL、IQL,显式惩罚分布外动作)。

RLHF 算不算模仿学习? 部分算。RLHF 的 SFT 阶段就是 BC(模仿人类写的示范);偏好建模阶段更接近 IRL 的一种变体——从对比中学奖励模型,再用 PPO 优化它。完整流水线在第 12 部分 详述。


参考文献

  • Pomerleau, D. (1989). ALVINN: An Autonomous Land Vehicle in a Neural Network. NIPS.
  • 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
  • Ziebart, B., Maas, A., Bagnell, J. A., & Dey, A. (2008). Maximum Entropy Inverse Reinforcement Learning. AAAI.
  • Finn, C., Levine, S., & Abbeel, P. (2016). Guided Cost Learning. ICML. arXiv:1603.00448
  • Ho, J., & Ermon, S. (2016). Generative Adversarial Imitation Learning. NeurIPS. arXiv:1606.03476
  • Fu, J., Luo, K., & Levine, S. (2018). Learning Robust Rewards with Adversarial Inverse Reinforcement Learning. ICLR. arXiv:1710.11248
  • Chi, C., et al. (2023). Diffusion Policy: Visuomotor Policy Learning via Action Diffusion. RSS. arXiv:2303.04137

系列导航

部分主题
1基础与核心概念
2Q-Learning 与深度 Q 网络
3Policy Gradient 与 Actor-Critic
4探索策略与好奇心驱动学习
5Model-Based 强化学习与世界模型
6PPO 与 TRPO
7模仿学习与逆强化学习(本文)

Liked this piece?

Follow on GitHub for the next one — usually one a week.

GitHub