ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

从Q-learning到CQL:强化学习进阶路线与离线实战指南

从Q-learning到CQL:强化学习进阶路线与离线实战指南 1. 从Q-learning到CQL一条被低估的进阶路线很多人学强化学习的路径是这样的先看几篇科普被“智能体”“奖励”“策略”这些词绕晕然后硬着头皮啃Sutton那本砖头书看到贝尔曼方程就卡住最后转去调库跑个CartPole了事。我自己当年也是这么过来的第一次跑通DQN的时候兴奋得不行结果换个环境就崩调参调到怀疑人生。后来我才慢慢意识到问题不在于算法本身有多难而在于大多数人跳过了“表格方法”这个地基直接去盖深度强化学习的高楼。Q-learning看起来简单甚至有点“玩具”的味道但它里面藏着强化学习最核心的几个思想值函数、时序差分、探索与利用的平衡、离策略学习。这些概念在后面的DQN、SAC、CQL里会反复出现只是换了个形式。你如果没在Q-learning上把这些东西想透后面看那些论文就只能记住一堆符号理解不了为什么这么设计。这条从Q-learning到CQL的路线其实是一条非常自然的进阶路径。Q-learning解决的是小规模离散状态下的决策问题DQN用神经网络把Q表换掉让算法能处理连续状态SAC进一步引入最大熵框架让策略在连续动作空间里也能稳定学习而CQL则是把目光转向了离线场景——你只有一堆历史数据不能再和环境交互怎么把策略学好这个问题在推荐系统、自动驾驶、医疗决策这些领域非常现实因为在线试错成本太高甚至根本不允许。这篇文章适合谁看如果你已经了解强化学习的基本概念能看懂Q-learning的更新公式但对接下来的路怎么走感到迷茫那这篇内容就是为你准备的。我会从Q-learning的核心机制讲起一步步推到CQL把每个阶段的关键设计、代码实现、踩坑经验都摊开来说。不会堆砌数学公式但该有的推导和参数计算一个都不会少。读完之后你应该能自己动手实现一个CQL并且知道每一步为什么这么做。2. Q-learning一切从这张表开始2.1 Q表到底在存什么Q-learning的核心思想可以用一句话概括维护一张表记录在某个状态下采取某个动作的“长期价值”。这个价值用Q值表示Q(s, a)的意思是“在状态s下做动作a之后一直按照最优策略走能拿到的累计奖励期望”。为什么是“之后一直按照最优策略走”因为Q-learning是离策略off-policy算法它学习的target策略和实际执行的行为策略可以不一样。行为策略负责探索比如用ε-greedy随机尝试动作而target策略始终是贪心的取max Q。这个分离是Q-learning能高效利用历史数据的关键。更新公式长这样Q[s, a] Q[s, a] alpha * (reward gamma * max(Q[s_next, :]) - Q[s, a])拆开看reward gamma * max(Q[s_next, :])是时序差分目标TD targetQ[s, a]是当前估计两者之差叫TD误差。alpha是学习率控制每次更新迈多大步子gamma是折扣因子决定未来奖励的权重。我刚开始学的时候有个误区觉得gamma越接近1越好因为“要考虑长远”。实际上gamma0.99意味着100步之后的奖励只保留约36%的权重如果任务本身回合很短gamma设太高反而会让值函数估计方差变大。一般离散任务从0.9开始试连续控制任务常用0.99但这不是铁律得看具体环境的奖励稀疏程度。2.2 探索策略ε-greedy不是唯一选择ε-greedy是最简单的探索方式以ε的概率随机选动作以1-ε的概率选当前Q值最大的动作。ε通常从1.0开始衰减到0.1或0.05。但这里有个坑很多人把ε衰减设得太快导致智能体还没充分探索就锁死在次优策略上。我试过在FrozenLake这种小环境里ε从1.0线性衰减到0.01用5000步结果智能体经常掉进洞里出不来。后来改成前2000步保持ε1.0纯随机之后再慢慢衰减成功率明显提升。这说明探索阶段不能太吝啬尤其是状态空间有“陷阱”的时候。除了ε-greedy还有softmax策略按Q值的指数分布采样动作Q值高的动作被选中的概率大但低Q值动作也有机会。这种方式在动作数量不多的时候比ε-greedy更平滑但计算量稍大。实际项目中如果动作空间是离散且不大的我一般先用ε-greedy快速验证再考虑换softmax调优。2.3 学习率和折扣因子的实操选择学习率alpha控制更新幅度。设太大Q值震荡不收敛设太小学得太慢。常见做法是从0.1开始观察Q值的变化曲线如果震荡明显就降到0.05或0.01。有个经验公式是alpha取1/(访问次数)也就是每次访问状态动作对时减小学习率这在理论上能保证收敛但实际中很少这么用因为太慢。折扣因子gamma的选择和任务的时间尺度有关。如果任务回合长度平均是T步gamma可以粗略设为1 - 1/T。比如平均50步完成的任务gamma约0.98。这个估算方式不精确但能给你一个起点避免盲目试。还有一个容易被忽略的点奖励的尺度。如果奖励是0到1之间Q值也会在这个量级如果奖励是100、200这种Q值会很大学习率需要相应调小。我习惯在环境封装层把奖励归一化到[-1, 1]或[0, 1]这样超参数在不同任务之间迁移时更稳定。2.4 Q-learning的局限为什么需要函数逼近Q-learning用表格存储Q值状态和动作必须是离散且有限的。如果状态是连续值比如机器人关节角度你得先离散化但离散化的粒度很难把握太粗信息丢失太细表格爆炸。假设有10个维度每个维度离散成100个值表格大小就是100^10根本存不下。这就是函数逼近的动机用一个参数化函数Q(s, a; θ)来近似Q值输入连续状态输出每个动作的Q值。神经网络是最常用的逼近器于是就有了DQN。但函数逼近会引入新的问题训练不稳定、样本相关性高、目标值漂移。DQN用经验回放和目标网络来解决这些问题这些设计思想在后面的SAC和CQL里依然能看到影子。3. DQN把Q表换成神经网络之后3.1 经验回放为什么能稳住训练DQN最核心的贡献之一是经验回放Experience Replay。智能体把每一步的(s, a, r, s_next, done)存进一个缓冲区训练时从中随机采样一批数据。这样做有两个好处一是打破了样本之间的时间相关性让训练数据更接近独立同分布二是提高了样本利用率一条经验可以被多次使用。缓冲区大小通常设10万到100万。太小样本多样性不够太大旧数据可能过时。我一般从10万开始如果任务状态空间很大比如Atari游戏会加到100万。采样批次大小常用32或64太小梯度噪声大太大显存吃不消。这里有个细节缓冲区里的数据是用旧策略采集的而当前策略已经更新了很多次。这种“旧数据训练新策略”的做法在离策略算法里是允许的但前提是行为策略要足够探索覆盖到当前策略可能选择的动作。如果行为策略太贪心缓冲区里全是少数几个动作的数据训练出来的Q函数在其他动作上就是瞎猜。3.2 目标网络给训练一个稳定的靶子DQN的另一个关键设计是目标网络。计算TD target时不用当前网络而是用一个隔一段时间才同步一次的目标网络target reward gamma * max(target_net(s_next))为什么这么做因为如果TD target和当前Q值用同一个网络计算每次更新网络参数target也会跟着变就像在追一个移动的靶子容易发散。目标网络把靶子固定住一段时间比如每1000步同步一次训练就稳定多了。同步方式有两种硬更新和软更新。硬更新是每隔C步直接把当前网络参数复制给目标网络软更新是每次只更新一点点target_params tau * current_params (1 - tau) * target_paramstau通常取0.001到0.01。软更新更平滑但计算量稍大。我在Atari上试过硬更新C1000效果不错在连续控制任务里更倾向软更新因为动作空间大Q值变化更剧烈。3.3 Double DQN和Dueling DQN两个实用的改进DQN有个已知问题叫Q值高估。因为TD target里用了max操作而Q值估计本身有噪声max会倾向于选到被高估的动作导致Q值越来越大。Double DQN的解决办法是用当前网络选动作用目标网络算Q值best_action argmax(current_net(s_next)) target reward gamma * target_net(s_next)[best_action]这样选动作和评估动作分开高估问题明显缓解。实测在Atari的很多游戏上Double DQN的得分比原始DQN高不少而且训练曲线更稳。Dueling DQN则是从网络结构入手把Q值拆成状态价值V(s)和优势函数A(s, a)Q(s, a) V(s) A(s, a) - mean(A(s, :))这样网络能分别学习“这个状态好不好”和“这个动作比平均好多少”。在有些状态下动作选择对结果影响不大V(s)就能主导估计减少不必要的动作值波动。这个结构在动作空间较大时优势更明显。3.4 DQN的调参心得DQN对超参数比较敏感我踩过的坑包括学习率太大导致Q值爆炸缓冲区太小导致过拟合目标网络同步太频繁导致不稳定。下面这张表是我在几个常见环境上总结的起点参数可以直接拿来用参数离散控制CartPoleAtari类连续控制离散化后学习率1e-31e-45e-4缓冲区大小1e41e61e5批次大小323264目标网络更新硬更新C500硬更新C1000软更新tau0.005ε衰减1.0到0.055000步1.0到0.1100万步1.0到0.110万步折扣因子0.990.990.99这些不是最优值但能让你快速跑起来然后再根据训练曲线微调。如果Q值曲线震荡先降学习率如果学得太慢检查奖励是否太稀疏考虑加奖励塑形。4. SAC连续控制里的最大熵框架4.1 从离散到连续动作空间的挑战DQN处理的是离散动作网络输出每个动作的Q值选最大的就行。但很多任务的动作是连续的比如机械臂的关节力矩、无人车的油门和转向。你可以把连续动作离散化但维度一高就组合爆炸而且离散化会丢失精度。连续控制需要直接输出动作而不是从有限集合里选。这就引出了策略梯度方法用一个网络表示策略π(a|s)直接输出动作的分布参数比如高斯分布的均值和方差。但纯策略梯度方法样本效率低因为每次更新只能用当前策略采集的数据。SACSoft Actor-Critic把值函数方法和策略梯度方法结合起来同时引入最大熵框架在样本效率和稳定性上都表现很好。我第一次跑SAC是在PyBullet的HalfCheetah环境上同样的样本量下SAC的得分比DDPG高出一大截而且对超参数没那么挑剔。4.2 最大熵不只是探索更是鲁棒性最大熵强化学习的目标函数里多了一项熵J(π) sum(E[reward] alpha * H(π(·|s)))H是策略的熵alpha是温度参数控制探索的权重。这个设计的意思是在最大化累计奖励的同时让策略尽可能随机。为什么这样好因为随机策略在遇到环境变化或模型误差时更鲁棒而且能持续探索避免过早收敛到局部最优。alpha可以固定也可以自动调节。SAC的自动调节版本把alpha也当成一个可学习的参数目标是让策略的熵维持在一个目标值附近。这个设计很实用因为不同任务的奖励尺度不一样手动调alpha很麻烦。我一般先用自动调节如果发现策略太随机导致得分上不去再手动把alpha调小。4.3 双Q网络和软更新SAC用了两个Q网络取它们输出的最小值来计算TD targettarget reward gamma * (min(Q1(s_next, a_next), Q2(s_next, a_next)) - alpha * log_prob)取最小值是为了抑制Q值高估这个思路和Double DQN类似但更直接。两个Q网络独立初始化训练时都更新但计算target时只用较小的那个。实测下来双Q网络比单Q网络稳定很多尤其是在奖励噪声大的环境里。目标网络的更新用软更新tau通常取0.005。这个值比DQN的软更新更小因为SAC的Q网络更新更频繁需要更平滑的目标。4.4 SAC的代码骨架和关键参数下面是一个简化版的SAC核心逻辑用PyTorch风格写方便你对照理解class SAC: def __init__(self, state_dim, action_dim): self.actor GaussianPolicy(state_dim, action_dim) self.q1 QNetwork(state_dim, action_dim) self.q2 QNetwork(state_dim, action_dim) self.q1_target QNetwork(state_dim, action_dim) self.q2_target QNetwork(state_dim, action_dim) self.alpha 0.2 # 初始温度 self.gamma 0.99 self.tau 0.005 def select_action(self, state): with torch.no_grad(): mean, log_std self.actor(state) std log_std.exp() normal Normal(mean, std) action normal.rsample() # 重参数化采样 log_prob normal.log_prob(action).sum(-1) return torch.tanh(action), log_prob def update(self, batch): # 更新Q网络 with torch.no_grad(): next_action, next_log_prob self.select_action(batch.next_state) q1_next self.q1_target(batch.next_state, next_action) q2_next self.q2_target(batch.next_state, next_action) q_next torch.min(q1_next, q2_next) - self.alpha * next_log_prob target batch.reward self.gamma * (1 - batch.done) * q_next q1_loss F.mse_loss(self.q1(batch.state, batch.action), target) q2_loss F.mse_loss(self.q2(batch.state, batch.action), target) # 更新策略网络 action, log_prob self.select_action(batch.state) q1 self.q1(batch.state, action) q2 self.q2(batch.state, action) actor_loss (self.alpha * log_prob - torch.min(q1, q2)).mean() # 软更新目标网络 for param, target_param in zip(self.q1.parameters(), self.q1_target.parameters()): target_param.data.copy_(self.tau * param.data (1 - self.tau) * target_param.data)关键参数方面学习率通常设3e-4缓冲区大小1e6批次大小256。alpha初始值0.2如果自动调节目标熵设为-action_dim。这些值在MuJoCo和PyBullet的多个环境上都能跑出不错的结果。4.5 SAC的常见坑SAC虽然稳但也不是没有坑。我遇到过几个典型问题一是动作范围没做tanh压缩导致动作超出环境允许范围训练直接崩二是log_prob计算时忘了减去tanh的雅可比项策略更新方向就错了三是缓冲区采样时没有正确处理done标志把终止状态之后的Q值也算进去导致值函数估计偏高。还有一个容易忽略的点SAC对奖励尺度比较敏感。如果奖励是几百上千的量级Q值会很大梯度也大容易不稳定。我习惯在环境封装层把奖励除以一个常数或者用RunningMeanStd做归一化。这个操作看起来不起眼但对训练稳定性影响很大。5. CQL离线强化学习的保守主义5.1 离线场景为什么需要新算法前面讲的Q-learning、DQN、SAC都是在线算法智能体可以和环境交互边试错边学习。但很多现实场景不允许在线试错推荐系统不能拿用户做实验自动驾驶不能真的撞车医疗决策不能拿病人试药。你只有一批历史数据可能是人类操作记录的也可能是旧策略采集的然后要在这批数据上训练一个尽可能好的策略。离线强化学习的核心难点是分布偏移。历史数据是用某个行为策略采集的它覆盖的状态动作分布和当前要学习的策略不一样。如果直接拿离线数据跑Q-learningQ函数会在数据没覆盖到的动作上产生高估因为max操作会选到那些“看起来好但实际没试过”的动作。这种高估会被bootstrapping不断放大最终策略会跑偏。CQLConservative Q-Learning的思路很直接既然Q值会高估那我就主动压低它让Q函数在数据分布外的动作上给出保守估计。具体来说CQL在标准TD损失上加了一项正则化惩罚Q值在“当前策略选择的动作”上过高同时鼓励Q值在“数据里的动作”上保持合理。5.2 CQL的损失函数拆解CQL的完整损失由三部分组成cql_loss td_loss alpha * cql_regularizertd_loss就是标准的贝尔曼误差和DQN或SAC里的一样。cql_regularizer是CQL特有的cql_reg (logsumexp(Q(s, a_hat)) - Q(s, a_data)).mean()a_hat是从当前策略采样的动作a_data是数据里实际执行的动作。logsumexp是softmax的平滑版本它会让Q值在所有动作上不要太高但重点是压低那些“策略可能选但数据里没有”的动作。Q(s, a_data)这一项则是让数据里动作的Q值不要被压得太低保持对真实价值的估计。alpha是正则化系数控制保守程度。alpha越大Q值越保守策略越倾向于待在数据分布附近alpha太小保守不够高估问题依然存在。实践中alpha通常取1.0到5.0具体看数据质量和任务难度。5.3 CQL的实现细节和参数选择CQL可以建立在SAC或DQN的基础上。如果是连续动作通常用SAC作为基座把策略网络和Q网络保留只改损失函数。下面是一个基于SAC的CQL核心更新逻辑def cql_update(self, batch): # 标准SAC的Q网络更新 with torch.no_grad(): next_action, next_log_prob self.select_action(batch.next_state) q1_next self.q1_target(batch.next_state, next_action) q2_next self.q2_target(batch.next_state, next_action) q_next torch.min(q1_next, q2_next) - self.alpha * next_log_prob target batch.reward self.gamma * (1 - batch.done) * q_next q1_pred self.q1(batch.state, batch.action) q2_pred self.q2(batch.state, batch.action) td_loss F.mse_loss(q1_pred, target) F.mse_loss(q2_pred, target) # CQL正则项 random_actions torch.rand_like(batch.action) * 2 - 1 # 均匀采样 current_actions, current_log_prob self.select_action(batch.state) q1_random self.q1(batch.state, random_actions) q2_random self.q2(batch.state, random_actions) q1_current self.q1(batch.state, current_actions) q2_current self.q2(batch.state, current_actions) cql_reg (torch.logsumexp(q1_random, dim0).mean() - q1_pred.mean() torch.logsumexp(q2_random, dim0).mean() - q2_pred.mean()) total_loss td_loss self.cql_alpha * cql_reg这里用均匀采样的动作来近似logsumexp比直接从策略采样更稳定。cql_alpha我一般从1.0开始如果发现策略过于保守、得分上不去降到0.5如果Q值还是高估加到2.0或5.0。5.4 离线数据集的构建和质量评估CQL的效果很大程度上取决于离线数据的质量。数据通常来自以下几种来源专家演示、旧策略的探索数据、人类操作记录、或者混合数据。数据量方面简单任务几万条转移就够复杂任务可能需要百万级。评估数据质量有几个实用指标状态覆盖率、动作多样性、奖励分布。如果数据里只包含少数几个状态CQL再保守也学不出好策略。我习惯先用PCA或t-SNE把状态可视化一下看看数据覆盖了哪些区域有没有明显的空洞。还有一个重要细节数据里的动作分布会影响CQL的保守程度。如果数据是用很随机的策略采集的动作多样性高CQL的保守正则可以小一点如果数据是专家演示动作很集中CQL需要更强的保守否则策略会跑到数据外的高估区域。5.5 CQL的调参和评估CQL的超参数比SAC多一个cql_alpha调参顺序建议是先固定cql_alpha1.0调学习率和缓冲区大小让TD损失能正常下降然后观察Q值曲线如果Q值持续上升但实际回报不涨说明高估了加大cql_alpha如果Q值被压得太低策略几乎不探索减小cql_alpha。评估离线强化学习算法有个麻烦你不能在线测试只能用一个独立的评估环境或者用历史数据做离线评估。离线评估的方法包括重要性采样、模型估计等但都有偏差。我一般会留出一部分数据作为验证集计算策略在验证集上的平均Q值同时观察策略输出的动作分布是否合理。如果动作分布和数据里的动作分布差异太大说明策略跑偏了。6. 从Q-learning到CQL一条完整的实操路线6.1 环境准备和依赖安装如果你想跟着这条路线走一遍建议从gym的经典控制环境开始然后过渡到PyBullet或MuJoCo。依赖方面核心是PyTorch、gym、numpy离线部分可以用d4rl数据集。安装命令如下pip install torch gym numpy pip install d4rl # 离线数据集 pip install pybullet # 连续控制环境d4rl提供了多个离线数据集包括HalfCheetah、Hopper、Walker2d等每个环境有不同质量的数据比如random、medium、expert。建议先从medium开始数据质量适中最能体现CQL的优势。6.2 分阶段实现和验证我建议按以下顺序实现和验证第一阶段Q-learning在FrozenLake或CliffWalking上跑通确认Q表更新逻辑正确观察ε衰减对成功率的影响。第二阶段DQN在CartPole上跑通加入经验回放和目标网络对比有无目标网络的训练曲线差异。第三阶段SAC在Pendulum或HalfCheetah上跑通确认双Q网络和自动温度调节能正常工作。第四阶段CQL在d4rl的medium数据集上跑通对比CQL和普通SAC在离线数据上的表现差异。每个阶段都要记录训练曲线、Q值变化、策略熵等指标。我习惯用TensorBoard或WandB记录方便回看和对比。6.3 常见问题速查表下面这张表整理了我在实现过程中遇到的高频问题和解决方法问题现象可能原因解决方法Q值持续增大不收敛学习率太大或gamma太高降低学习率到1e-4gamma降到0.95策略完全不探索ε衰减太快或alpha太小延长探索阶段增大alpha训练初期得分就很高然后崩过拟合早期数据增大缓冲区降低学习率CQL策略过于保守cql_alpha太大减小cql_alpha到0.5CQL策略跑出数据分布cql_alpha太小增大cql_alpha到2.0SAC动作超出范围缺少tanh压缩在策略输出加tanh并修正log_prob离线评估得分远高于在线离线评估偏差用多个评估方法交叉验证6.4 一些实用的调试技巧调试强化学习代码最痛苦的是不知道哪里错了。我总结了几条经验第一先用小环境验证算法逻辑比如FrozenLake只有16个状态Q表可以打印出来看确认更新方向对不对。第二固定随机种子确保每次运行结果可复现否则调参就是碰运气。第三监控Q值的均值和最大值如果均值爆炸肯定是学习率或gamma的问题。第四可视化策略在状态空间上的动作分布能直观看出策略有没有跑偏。还有一个容易被忽略的点环境封装。gym的环境返回的done标志在时间限制截断和真正终止时都是True但两者的bootstrap处理不一样。时间限制截断时Q值应该继续bootstrap真正终止时Q值应该归零。如果没区分值函数估计会有偏差。我一般用gym的TimeLimit包装器并在训练时检查info里的TimeLimit.truncated标志。7. 这条路线之后还能往哪走从Q-learning到CQL你已经覆盖了值函数方法、深度值函数方法、最大熵方法和离线保守方法。这条线走下来强化学习的核心思想基本都摸了一遍。接下来如果还想深入有几个方向可以选。一个是基于模型的强化学习比如Dreamer、PlaNet它们学习环境的动力学模型然后在模型里做规划或策略优化。这类方法样本效率极高但模型误差会累积训练也更复杂。另一个方向是因果强化学习把因果推断的工具嵌入强化学习流程解决混淆因子和反事实推断的问题。这个方向在推荐系统和医疗决策里很有前景但理论门槛较高需要补因果推断的基础。还有一个更工程化的方向是多智能体强化学习多个智能体同时学习环境非平稳博弈论和强化学习交叉。多AGV路径规划就是一个典型场景每个AGV是一个智能体需要协调避障和任务分配。不管选哪个方向我建议先把这条从Q-learning到CQL的路线完整跑一遍代码自己写不要直接抄库。写的过程中你会遇到各种报错和调参问题解决这些问题的经验比看懂论文更值钱。我自己在实现CQL的时候光是logsumexp的维度就调了半天后来发现是动作维度没对齐。这种坑踩过一次以后就记住了。最后分享一个小技巧如果你觉得从零实现太慢可以先找一个开源实现跑通然后逐行读代码把每个模块替换成自己的版本对比结果是否一致。这样既能快速看到效果又能确保自己真正理解了每个细节。强化学习这门手艺看再多不如动手跑一遍跑一遍不如自己写一遍。
返回列表