离线强化学习(一)BCQ 批量限制 Q-learning

B站 张伟楠 离线RL

动手学RL 离线RL

BCQ: Batch-Constrained Q-learning

BCQ-github

离线强化学习开篇之作 BCQ,针对离线场景外推泛化误差问题,通过生成 + 扰动的批次约束策略

BCQ 以 CVAE 为生成模型拟合批次数据分布、Actor 为扰动模型做局部动作优化、双 Q 网络实现无偏价值评估,

三者协同让策略仅访问与缓冲区 Buffer 相似的(s,a)对,有效规避分布外数据误差。

同时详解算法代码实现细节,包括 VAE、Actor、Critic 的网络设计与训练流程。

目录

[1. 离线强化学习](#1. 离线强化学习)

[2. 离线核心问题:外推泛化误差 Extrapolation Error](#2. 离线核心问题:外推泛化误差 Extrapolation Error)

[3. BCQ: Batch-Constrained Q-learning 摘要](#3. BCQ: Batch-Constrained Q-learning 摘要)

[4. 生成+扰动:只访问与数据集中相似的 (s,a) 数据](#4. 生成+扰动:只访问与数据集中相似的 (s,a) 数据)

[5. 伪代码 - 代码](#5. 伪代码 - 代码)

[1. VAE (s,a) -> a' 编码器 + 解码器](#1. VAE (s,a) -> a' 编码器 + 解码器)

[2. Actor(扰动模型)动作优化器 (s,a) -> a'](#2. Actor(扰动模型)动作优化器 (s,a) -> a')

[3. 双 Q 同 TD3 的实现](#3. 双 Q 同 TD3 的实现)

[4. agent 组件初始化 + 训练完agent 的动作选择](#4. agent 组件初始化 + 训练完agent 的动作选择)

[5. agent.train()](#5. agent.train())


1. 离线强化学习

在线训练:智能体在训练过程中可以不断和环境交互得到新的反馈数据

on-policy 数据只跑一遍; off-policy 经验回放池

离线训练:不能让没训练好的 agent 直接和环境交互(比如自动驾驶 和 医疗)

只能和 buffer 中已有的数据交互。(像有监督学习那样)

  1. Behavior Cloning / Imitation Learning 行为克隆 / 模仿学习

局限性:1. compounding error 每一步误差累积 成更大的错误 2. 效果上限 就是专家本身

  1. Model-based:不能直接和 env 交互,就搭建一个仿真环境 和仿真环境交互。

  2. Model-free:训练时 显式/隐式 设置训练策略和 buffer 数据偏差不大

2. 离线核心问题:外推泛化误差Extrapolation Error

  1. Absent Data 数据缺失(当前训练策略访问的(s,a) 是 buffer 里没有的数据)

右边的期望 基于批次数据集 B 中的经验转移。若(s,a)较少 偏差。

  1. 损失函数的权重与批次数据的出现概率相关

倘若数据集中不存在当前策略下高概率出现的 (s, a) 对,

最终的价值估计结果仍会存在较大误差。这意味着,我们仅能对一部分特定的策略进行准确评估

而在线强化学习,即使训练是离线策略的,智能体依然有机会通过与环境交互 及时采样到新的数据 ,从而修正这些误差

三组实验分别:

  1. 离线用 之前 训 DDPG 过程中收集的一批 buffer;

  2. 行为和离线一起训练 离线用行为的 buffer;

  3. 训练完成的 DDPG 作为专家 采100w步经验转移数据;

3. BCQ: Batch-Constrained Q-learning 摘要

对于那些数据采集过程成本高昂、存在风险或耗时长久的任务而言,

1. 批次强化学习 是实现强化学习规模化应用的关键前提。

由于"外推误差 " 当数据集 与当前策略对应的真实分布不相关时,算法的训练就无法成功。

  1. 无法为 选择批次数据集B 中未包含动作(s,a)的策略,学习有效的价值函数。

  2. 采用一个状态条件生成模型 来仅生成数据集中曾出现过的动作

该生成模型与 Q 网络相结合,从而筛选出与批次数据特征相近且价值最高的动作。

  1. 我们的算法为模仿学习和离策略学习提供了统一的研究视角,不仅能够基于纯专家示范数据进行学习,

还可以利用有限的非最优批次数据完成训练,且无需开展额外的探索操作。

4. 生成+扰动:只访问与数据集中相似的 (s,a) 数据

减少外推误差 只访问与数据集中相似的 (s,a) 数据

batch-constrained policy 批次约束策略:

(1) 选择的 a 接近 buffer 中已有的 a(后两者的基础)

(2) 引导到 相似数据buffer中有 的状态

(3) 最大化价值函数

表格形式:仅从 buffer 数据集中存在的动作中选择最优动作 argmax

高维/连续空间下 为了满足 buffer 约束:

在状态 s 下生成+扰动;得到几个距离 buffer 很近的 (s,a) 再通过 Q 网络 argmax

三大类组件:生成器 G;扰动器 ξ ;两个 Q 网络

相似性度量准则 similarity metric:(s,a) 对 是否与 B 相似 -> 状态条件边缘似然

在批次缓冲区B的数据集分布下,给定状态 s 时,动作 a 出现的概率

越高说明 (s,a) 与 B 越相似;低的话 就是 OOD(分布外数据)

生成模型G​(s) 的目标:学习批次缓冲区B的动作分布 ,能够基于状态 s,生成 高相似性 动作。

(使用 条件变分自编码器(CVAE))

扰动模型:提升动作多样性 限制在[−Φ,Φ]内的调整。

可在约束动作空间 的前提下,覆盖更多合理动作 ,避免为获取足够动作多样性 而对生成模型进行大量采样

最终 确定性策略为:(生成+扰动) 得到的候选动作中 -> 选 Q 最高的

扰动模型 ξ 通过确定性策略梯度算法训练,目标是最大化 Q 网络的价值估计

5. 伪代码 - 代码

continuous_BCQ/BCQ.py

VAE:(s,a) -> a 生成 符合批次数据分布的基础动作,保证动作的接近 Buffer;

Actor:对 VAE 生成的基础动作进行小幅度扰动优化 ,在合理范围内提升动作的 "价值"

Critic:双Q;准确评估状态 - 动作对的价值

1. VAE (s,a) -> a' 编码器 + 解码器

编码器 (s,a) -> latent_dim 维度的 z

python 复制代码
class VAE(nn.Module):
    def __init__(self, state_dim, action_dim, latent_dim, max_action, device):
        super().__init__()
        # 编码器网络:输入(状态+动作),输出潜在空间的均值和标准差
        self.e1 = nn.Linear(state_dim + action_dim, 750)
        self.e2 = nn.Linear(750, 750)

        self.mean = nn.Linear(750, latent_dim)
        self.log_std = nn.Linear(750, latent_dim) # 对数标准差; 取指数以确保标准差为正值

        # 解码器网络:输入(状态+潜在变量),输出生成的动作
        self.d1 = nn.Linear(state_dim + latent_dim, 750)
        self.d2 = nn.Linear(750, 750)
        self.d3 = nn.Linear(750, action_dim)

        self.max_action = max_action
        self.latent_dim = latent_dim
        self.device = device

解码器 (s,z) -> a';完整的 VAE 由编码器得到 z;

在下方 agent.select_action 中,通过N~(0,1) 采样不同的 z 得到不同的动作。

python 复制代码
    def decode(self, state, z=None):
        # 没有z 直接s->a 从标准正态分布采样z,并裁剪到[-0.5, 0.5]
        if z is None:
            z = torch.randn((state.shape[0], self.latent_dim)).to(self.device).clamp(-0.5, 0.5)

        a = F.relu(self.d1(torch.cat([state, z], 1)))
        a = F.relu(self.d2(a))
        return self.max_action * torch.tanh(self.d3(a))

forward:(s,a) -> 编码器 -> z -> (s,z) -> 解码器 -> u

python 复制代码
    def forward(self, state, action):
        z = F.relu(self.e1(torch.cat([state, action], 1)))
        z = F.relu(self.e2(z))

        mean = self.mean(z)
        log_std = self.log_std(z).clamp(-4, 15) # 截断 -> 数值稳定
        std = torch.exp(log_std)
        z = mean + std * torch.randn_like(std) # 重参数化 在潜在分布中采样潜在变量z

        u = self.decode(state, z)

        return u, mean, std

2. Actor(扰动模型)动作优化器 (s,a) -> a'

得到扰动范围内的 a;action + a 裁剪后作为最终 a'

python 复制代码
class Actor(nn.Module):
    def __init__(self, state_dim, action_dim, max_action, phi=0.05):
        super().__init__()
        self.l1 = nn.Linear(state_dim + action_dim, 400)
        self.l2 = nn.Linear(400, 300)
        self.l3 = nn.Linear(300, action_dim)

        self.max_action = max_action
        self.phi = phi # 扰动幅度系数(对应论文中的Φ),控制扰动的最大幅度

    def forward(self, state, action):
        a = F.relu(self.l1(torch.cat([state, action], 1)))
        a = F.relu(self.l2(a))
        a = self.phi * self.max_action * torch.tanh(self.l3(a)) # 扰动值 在[-phi*max_action, phi*max_action]
        return (a + action).clamp(-self.max_action, self.max_action) # 最终动作 裁剪在范围内

3. 双 Q 同 TD3 的实现

python 复制代码
class Critic(nn.Module):
    def __init__(self, state_dim, action_dim):
        super().__init__()
        # Q1 architecture
        self.l1 = nn.Linear(state_dim + action_dim, 400)
        self.l2 = nn.Linear(400, 300)
        self.l3 = nn.Linear(300, 1)

        # Q2 architecture
        self.l4 = nn.Linear(state_dim + action_dim, 400)
        self.l5 = nn.Linear(400, 300)
        self.l6 = nn.Linear(300, 1)

    def forward(self, state, action):
        q1 = F.relu(self.l1(torch.cat([state, action], 1)))
        q1 = F.relu(self.l2(q1))
        q1 = self.l3(q1)

        q2 = F.relu(self.l4(torch.cat([state, action], 1)))
        q2 = F.relu(self.l5(q2))
        q2 = self.l6(q2)
        return q1, q2

    def q1(self, state, action):
        q1 = F.relu(self.l1(torch.cat([state, action], 1)))
        q1 = F.relu(self.l2(q1))
        q1 = self.l3(q1)
        return q1

4. agent 组件初始化 + 训练完agent 的动作选择

python 复制代码
class BCQ(object):
    def __init__(self, state_dim, action_dim, max_action, device, discount=0.99, tau=0.005, lmbda=0.75, phi=0.05):
        latent_dim = action_dim * 2 # 隐变量维度

        self.actor = Actor(state_dim, action_dim, max_action, phi).to(device)
        self.actor_target = copy.deepcopy(self.actor)
        self.actor_optimizer = torch.optim.Adam(self.actor.parameters(), lr=1e-3)

        self.critic = Critic(state_dim, action_dim).to(device)
        self.critic_target = copy.deepcopy(self.critic)
        self.critic_optimizer = torch.optim.Adam(self.critic.parameters(), lr=1e-3)

        self.vae = VAE(state_dim, action_dim, latent_dim, max_action, device).to(device)
        self.vae_optimizer = torch.optim.Adam(self.vae.parameters())

        self.max_action = max_action
        self.action_dim = action_dim
        self.discount = discount
        self.tau = tau
        self.lmbda = lmbda
        self.device = device

完成训练后 使用时的动作选择:每个状态 通过 z 的采样,得到一百个动作,取 q1.argmax()

python 复制代码
    def select_action(self, state):
        with torch.no_grad():
            state = torch.FloatTensor(state.reshape(1, -1)).repeat(100, 1).to(self.device) # [100, state_dim] 的批量 state
            action = self.actor(state, self.vae.decode(state)) # 根据N~(0,1)采样的z得到100个动作,再actor扰动优化
            q1 = self.critic.q1(state, action)
            ind = q1.argmax(0) # 每个状态选择第0维(100个动作中) Q值最大的动作
        return action[ind].cpu().data.numpy().flatten()

5. agent.train()

  1. 采样批次数据训练 G 生成模型(VAE 包含 E 和 D)
  • 编码器 E 对输入的状态 - 动作对 (s,a) 编码,得到潜在变量的均值 μ 和标准差 σ。
  • 解码器 D 从潜在分布 N(μ,σ) 采样 z,并解码生成动作 ã。

损失函数由两部分组成:

  • 重建 损失 ∑(a − ã)²:确保生成的动作与原始动作尽可能相似 ,对应理论中 "生成与批次数据相似的动作" 的要求。
  • KL 散度损失 D_KL(N(μ,σ)||N(0,1)):约束潜在分布接近标准正态分布 ,防止模型过度拟合样本,提升生成动作的泛化性
python 复制代码
    def train(self, replay_buffer, iterations, batch_size=100):

        for it in range(iterations):
            # Sample replay buffer / batch
            state, action, next_state, reward, not_done = replay_buffer.sample(batch_size)

            # Variational Auto-Encoder Training
            recon, mean, std = self.vae(state, action)
            recon_loss = F.mse_loss(recon, action) # 重构损失
            KL_loss = -0.5 * (1 + torch.log(std.pow(2)) - mean.pow(2) - std.pow(2)).mean() # KL散度损失
            vae_loss = recon_loss + 0.5 * KL_loss # 总体VAE损失

            self.vae_optimizer.zero_grad()
            vae_loss.backward()
            self.vae_optimizer.step()
  1. 训练 Q;用 s' 生成动作 得到 Q(s', a') 作为 TD target

target:Clipped Double Q-learning 裁剪双 Q 学习 作为 TD target

TD3里就是 min; 这里用:min 高权重 + max 低权重。

python 复制代码
            with torch.no_grad():
                # 复制10个s'
                next_state = torch.repeat_interleave(next_state, 10, 0)

                # Compute value of perturbed actions sampled from the VAE
                target_Q1, target_Q2 = self.critic_target(next_state,
                                                          self.actor_target(next_state, self.vae.decode(next_state)))

                # 最小最大加权 Clipped Double Q-learning
                target_Q = self.lmbda * torch.min(target_Q1, target_Q2) + (1. - self.lmbda) * torch.max(target_Q1,                                                                                                        target_Q2)
                target_Q = target_Q.reshape(batch_size, -1).max(1)[0].reshape(-1, 1) # Q*
                target_Q = reward + not_done * self.discount * target_Q

            current_Q1, current_Q2 = self.critic(state, action)
            critic_loss = F.mse_loss(current_Q1, target_Q) + F.mse_loss(current_Q2, target_Q)

            self.critic_optimizer.zero_grad()
            critic_loss.backward()
            self.critic_optimizer.step()
  1. 更新 Q 和 生成模型 G;并软更新

扰动模型 ξ 通过 DDPG目标函数 训练,目标是最大化 Q 网络的价值估计(类似 TD3 只用Q1)

python 复制代码
            # 训练 actor
            actor_loss = -self.critic.q1(state, self.actor(state, self.vae.decode(state))).mean()
            self.actor_optimizer.zero_grad()
            actor_loss.backward()
            self.actor_optimizer.step()

            # 软更新
            for param, target_param in zip(self.critic.parameters(), self.critic_target.parameters()):
                target_param.data.copy_(self.tau * param.data + (1 - self.tau) * target_param.data)

            for param, target_param in zip(self.actor.parameters(), self.actor_target.parameters()):
                target_param.data.copy_(self.tau * param.data + (1 - self.tau) * target_param.data)

实验使用:DDPG 先得到 Buffer;再用 Buffer 训 BCQ.

相关推荐
副露のmagic2 小时前
深度学习基础复健
人工智能·深度学习
番茄大王sc2 小时前
2026年科研AI工具深度测评(一):文献调研与综述生成领域,维普科创助手领跑学术严谨性
人工智能·深度学习·考研·学习方法·论文笔记
代码丰2 小时前
SpringAI+RAG向量库+知识图谱+多模型路由+Docker打造SmartHR智能招聘助手
人工智能·spring·知识图谱
独处东汉3 小时前
freertos开发空气检测仪之输入子系统结构体设计
数据结构·人工智能·stm32·单片机·嵌入式硬件·算法
乐迪信息3 小时前
乐迪信息:AI防爆摄像机在船舶监控的应用
大数据·网络·人工智能·算法·无人机
风栖柳白杨3 小时前
【语音识别】soundfile使用方法
人工智能·语音识别
胡西风_foxww3 小时前
ObsidianAI_学习一个陌生知识领域_建立学习路径和知识库框架_写一本书
人工智能·笔记·学习·知识库·obsidian·notebooklm·写一本书
Hernon3 小时前
AI智能体 - 探索与发现 Clawdbot >> Moltbot
大数据·人工智能·ai智能体·ai开发框架
输出的都是我的3 小时前
科研-工具箱汇总
人工智能