离线强化学习(一)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.

相关推荐
Funny_AI_LAB2 分钟前
重构 Coding Agent:当 LLM 没有 KV Cache 时,上下文工程该怎么做?
人工智能·经验分享·语言模型·重构
GISMagic6 分钟前
AI 时代的软件工程与架构能力提升路线
人工智能·架构·软件工程
中伟视界8 分钟前
化工检修作业新规明日施行,边缘AI如何守好现场?
人工智能
OpenCSG11 分钟前
OpenCSG Agentic-27B 正式开源:27B Dense 模型,Agent 执行能力实现全面跃升
人工智能·开源·opencsg
Elastic 中国社区官方博客18 分钟前
使用 Lucene 搜索你的 Bean —— Elasticsearch
大数据·开发语言·人工智能·elasticsearch·搜索引擎·全文检索·lucene
雪兽软件19 分钟前
AI爆改学术研究?
人工智能·学术研究
合调于形27 分钟前
Rengong zhzzneng 《人工智能》词条汉语拼音字母标调拼写实测案例
人工智能·自然语言处理·人机交互·语音识别·学习方法
cg501729 分钟前
大模型 SFT(监督微调)该怎么做?(实践版)
人工智能·深度学习·机器学习
Qt云程序员35 分钟前
选型参考:光刃RPA 与常见 RPA / 自动化方案的对比
人工智能·自动化·rpa
neocheng_52237 分钟前
品牌、内容和效果营销,受AI影响为什么完全不同?
人工智能