1. 为什么我要把强化学习从Q-learning重新学一遍
做机器学习这行的人大概都有个共同的毛病:工具用顺手了就不想回头看底层。我最早接触强化学习是在一个推荐策略的小项目里,当时直接上了DQN,调了几天参数跑通了,奖励曲线看着也还行,就觉得自己“会了”。后来遇到离线数据场景——手里只有一批历史日志,没法跟环境实时交互——才发现DQN那一套根本用不上,于是被迫回头补课,从Q-learning的表格法一路啃到CQL(Conservative Q-Learning)。这个过程踩了不少坑,也纠正了很多之前似是而非的理解。
这篇内容就是把我重新学习这条路径的笔记和实操心得整理出来。核心关键词是强化学习、Q-learning、DQN、离线强化学习、CQL、分布偏移。适合两类人看:一类是刚入门强化学习、被各种算法名字绕晕的新手;另一类是有一定基础、但在离线场景下不知道怎么落地的从业者。我会尽量用大白话把每个算法“为什么长这样”讲清楚,而不是只丢公式。
先说结论性的判断:Q-learning是理解一切值函数方法的根,DQN是把表格变成神经网络的工程化桥梁,而CQL解决的是“不能和环境交互时怎么学”这个现实问题。这三者是一条清晰的演进线,理解了这条线,再看别的算法就不会觉得是天上掉下来的。下面我按自己的学习顺序,一层层拆开讲。
2. Q-learning的表格本质:一张会自我更新的记账表
2.1 用一个具体场景理解Q表到底在记什么
很多人第一次看Q-learning的公式就懵了,其实它的本质特别朴素。想象你在一栋陌生的楼里找出口,每走到一个路口(状态),你有几个方向可以选(动作),每选一次会得到一点反馈(奖励)。Q-learning要做的事情,就是维护一张表,记录“在哪个路口往哪个方向走,长期来看能拿到多少好处”。
这张表就是Q表,行是状态,列是动作,格子里填的是Q值——在某个状态下采取某个动作的长期期望回报。一开始表全是零,什么都不懂。每走一步,就用实际拿到的奖励加上对下一步最优价值的估计,去修正当前格子的值。这个修正过程就是那个著名的更新公式:
# Q-learning 核心更新(伪代码) # Q(s,a) <- Q(s,a) + alpha * [r + gamma * max(Q(s',:)) - Q(s,a)] q_current = Q[state][action] q_target = reward + gamma * max(Q[next_state]) Q[state][action] = q_current + alpha * (q_target - q_current)这里的alpha是学习率,控制每次修正多少;gamma是折扣因子,控制未来奖励的权重。我第一次真正理解这个公式,是在纸上手动算了一个三状态的小例子之后——那种“原来就是不断用新估计去逼近旧估计”的感觉,比看十遍推导都管用。
2.2 学习率和折扣因子:两个最容易被随手填错的参数
新手最容易犯的错,就是alpha和gamma随手填0.1和0.9,然后发现要么学不动要么震荡。我踩过的坑是:在一个奖励稀疏的环境里,gamma设成0.9,结果智能体永远看不到远处的奖励,因为折扣衰减太快,远处的信号传到起点时已经接近于零了。
经验上,alpha的选择要看状态空间的稳定性。如果环境随机性大,alpha要小一点(比如0.01到0.1),让更新平滑;如果环境确定性强,alpha可以大一些加快收敛。gamma则取决于任务的时间跨度——任务越长、奖励越靠后,gamma越要接近1。但gamma太接近1也有问题,会让Q值估计的方差变大,收敛变慢。
提示:判断
gamma是否合理的一个土办法,是看gamma的若干次方衰减到0.1需要多少步。如果这个步数远小于任务实际需要的步数,说明折扣太狠了。
2.3 探索与利用:epsilon-greedy不是万能药
Q-learning必须解决一个矛盾:是选当前已知最好的动作(利用),还是试试没走过的路(探索)。最常用的是epsilon-greedy,以epsilon的概率随机选,否则选当前最优。我一开始固定epsilon=0.1,结果训练后期还在乱走,收敛很差。
后来改成衰减策略:前期epsilon大(比如1.0)鼓励探索,随着训练逐步降到0.01左右。这个衰减曲线怎么设计,其实挺讲究。降太快,很多状态没探索到就固化了;降太慢,收敛遥遥无期。我一般用指数衰减,配合一个最小下限,实测下来比线性衰减更稳。
还有一个细节:在状态空间巨大的问题里,很多状态可能只被访问过一两次,这些状态的Q值估计极不可靠。这时候可以考虑给访问次数少的状态更高的探索优先级,这就是后来各种计数探索方法的动机。表格法时代这些还能手动处理,到了神经网络时代就变成了更复杂的探索策略。
3. 从表格到神经网络:DQN到底解决了什么工程难题
3.1 状态爆炸:表格法撞墙的那一刻
Q-learning的表格法有个致命限制:状态必须是离散且有限的。一旦状态是连续的(比如图像、传感器读数),或者状态组合爆炸(比如围棋的棋盘),表格就存不下了。我最早意识到这个问题,是在一个用传感器数据做控制的小项目里——传感器有十几个维度,每个维度离散成10档,状态数就是10的十几次方,表根本建不起来。
自然的想法是:用一个函数来近似Q值,输入状态(和动作),输出Q值。神经网络就是最好的函数近似器。但直接把Q-learning套上神经网络,会非常不稳定,甚至发散。为什么?因为神经网络训练假设样本是独立同分布的,而强化学习的样本是高度相关的时间序列,而且目标值(Q-target)本身还在不断变化,相当于“追着一个移动的靶子打”。
3.2 经验回放和目标网络:两个让训练稳定的关键设计
DQN的两个核心贡献,恰好对应上面两个不稳定因素。第一个是经验回放(Experience Replay):把交互产生的(状态, 动作, 奖励, 下一状态)存进一个缓冲区,训练时从中随机采样。这样打破了样本的时间相关性,让数据接近独立同分布。第二个是目标网络(Target Network):用一个结构相同但参数更新很慢的网络来计算Q-target,避免“自己追自己”导致的震荡。
# DQN 训练循环的关键片段(伪代码) # 1. 与环境交互,存入回放缓冲区 buffer.push(state, action, reward, next_state, done) # 2. 从缓冲区随机采样一个batch batch = buffer.sample(batch_size) # 3. 用目标网络计算Q-target q_next = target_network(next_states).max(dim=1) q_target = rewards + gamma * q_next * (1 - dones) # 4. 用在线网络计算当前Q值,算损失,反向传播 q_current = online_network(states).gather(actions) loss = mse_loss(q_current, q_target.detach()) loss.backward() optimizer.step() # 5. 每隔若干步,把在线网络的参数同步给目标网络 if step % sync_interval == 0: target_network.load_state_dict(online_network.state_dict())我实测下来,目标网络的同步间隔是个敏感参数。同步太频繁,等于没有目标网络,训练不稳;同步太慢,目标网络太滞后,学习效率低。常见的做法是软更新(每次只更新一点点)或者硬更新(每隔固定步数全量同步),具体选哪个要看任务。
3.3 过估计问题:DQN的一个隐藏缺陷
DQN还有一个容易被忽略的问题:Q值过估计。因为Q-target里用的是max操作,而估计误差有正有负,取最大值会系统性地偏向正误差,导致Q值被高估。这个问题在动作空间大、奖励噪声大的场景下尤其明显。
我第一次遇到这个问题,是发现训练出来的策略在仿真里表现很好,一到真实环境就拉胯——因为Q值虚高,策略对某些动作过于乐观。后来用了Double DQN的思路:用在线网络选动作,用目标网络评估这个动作的价值,把选择和评估解耦,过估计就缓解了很多。这个改动很小,但效果立竿见影,属于性价比极高的技巧。
4. 离线强化学习的真正难点:不能试错时怎么办
4.1 分布偏移:离线场景下最致命的暗礁
前面讲的Q-learning和DQN,都有一个隐含前提:智能体可以和环境交互,边试边学。但现实里很多场景根本不允许试错——比如医疗决策、金融交易、工业控制,你不可能让一个没训练好的策略去真实环境里乱来。这时候只能用一批已经收集好的历史数据来训练,这就是离线强化学习。
离线场景的核心难题叫分布偏移(Distribution Shift)。简单说:历史数据是由某个旧策略(行为策略)产生的,它覆盖的状态和动作分布,和当前正在学习的策略想要探索的分布不一样。当学习到的策略想选一个历史数据里从没出现过的动作时,Q函数对这个动作的估计完全是外推出来的,可能错得离谱。而Q-learning的max操作偏偏喜欢挑这些被高估的、没见过的动作,于是策略越学越偏,最后崩掉。
我踩过的最典型的坑:用一个离线数据集训练DQN,训练损失看着一直在降,但评估时策略表现极差。排查了很久才发现,是Q值在数据分布之外被严重高估,策略被“骗”到了那些虚假的高价值区域。
4.2 保守思想:给Q值加一个“别太乐观”的惩罚
解决分布偏移的思路,核心是“保守”——对数据分布之外的动作,别让Q值估计得太高。CQL(Conservative Q-Learning)就是沿着这个思路设计的。它的做法是在标准Q-learning损失之外,加一个正则项:压低当前策略倾向选择的动作的Q值,同时抬高历史数据里出现过的动作的Q值。
用大白话说:CQL在告诉Q函数,“你没见过的动作,别给太高分;数据里见过的动作,可以给合理的高分”。这样策略就不会被外推出来的虚假高价值吸引,学出来的策略更贴近数据能支撑的范围。
# CQL 损失的核心结构(伪代码,简化版) # 标准TD损失 td_loss = mse_loss(q_current, q_target) # CQL正则项:压低策略动作的Q值,抬高数据动作的Q值 q_random = online_network(states, random_actions) # 策略可能选的动作 q_data = online_network(states, data_actions) # 数据里实际的动作 cql_loss = (logsumexp(q_random) - q_data).mean() # 总损失 total_loss = td_loss + alpha * cql_loss这里的alpha是保守程度系数,控制正则项的相对权重。alpha越大越保守,策略越贴近数据;alpha太小则退化成普通离线Q-learning,分布偏移问题又会冒出来。这个参数的调节是CQL实操中最花时间的部分。
4.3 CQL的保守系数怎么调:我的实测经验
alpha的调节没有万能公式,但有几个经验可以参考。首先,看数据质量:如果历史数据覆盖的状态动作空间比较广、质量高,alpha可以小一点,让策略有更多学习空间;如果数据窄且噪声大,alpha要大一些,宁可保守也别跑偏。
其次,看评估指标的变化。我一般会同时盯两个量:一个是训练损失,一个是离线评估的策略回报(用历史数据做重要性采样估计)。如果训练损失降但评估回报不升反降,多半是alpha太小,Q值过估计了;如果评估回报一直上不去、策略几乎不更新,可能是alpha太大,保守过头了。
还有一个实用技巧:CQL的alpha可以随训练动态调整。前期数据利用不充分时用小一点的alpha,后期策略稳定了再加大保守力度。这个调度策略在几个项目里都帮我省了不少调参时间。
5. 一条可复现的学习与实操路径
5.1 环境搭建与最小可跑示例
如果你也想走一遍这条路,我建议按“表格Q-learning → DQN → CQL”的顺序,每个阶段都亲手跑通一个最小例子。环境用经典的CartPole就够,它状态简单、奖励清晰,适合验证算法是否正确。
# 依赖安装(以常见科学计算栈为例) pip install numpy gymnasium torch matplotlib先写一个纯表格的Q-learning解CartPole——虽然CartPole状态是连续的,但你可以手动离散化,比如把每个维度分成若干档。这一步的目的是把更新公式、探索策略、折扣因子这些基础概念用手感记住。跑通之后,你会对“Q表怎么收敛”有直观认识。
然后换成DQN,用神经网络替换Q表。这一步的重点是理解经验回放和目标网络的作用——你可以做个对比实验:去掉目标网络,看训练曲线怎么抖;去掉经验回放,看收敛怎么变慢。这种“破坏性实验”比看论文印象深得多。
最后上CQL,用DQN跑出来的数据或者随机策略产生的数据作为离线数据集,对比普通离线DQN和CQL的表现。你会亲眼看到普通离线DQN怎么被分布偏移坑掉,而CQL怎么稳住。
5.2 每个阶段该重点盯什么指标
不同阶段关注的指标不一样,这点很多人没意识到。表格Q-learning阶段,重点看Q表是否收敛、策略是否稳定,可以打印几个关键状态的Q值变化。DQN阶段,重点看训练损失、平均回报、Q值量级——Q值如果爆炸式增长,多半是过估计或者学习率太大。CQL阶段,重点看离线评估回报和保守项的大小,判断保守程度是否合适。
我习惯用一个简单的表格记录每次实验的关键参数和结果,方便回溯:
| 阶段 | 关键参数 | 观察指标 | 常见异常 |
|---|---|---|---|
| 表格Q-learning | alpha, gamma, epsilon | Q值收敛性、策略稳定性 | 不收敛、震荡 |
| DQN | 学习率、回放缓冲区大小、目标网络同步间隔 | 训练损失、平均回报、Q值量级 | Q值爆炸、训练不稳 |
| CQL | 保守系数alpha、数据质量 | 离线评估回报、保守项大小 | 过保守、策略不更新 |
这张表是我踩了无数坑之后总结的,每次开新实验先照着填一遍,能省很多瞎调的时间。
5.3 几个让我印象深刻的踩坑记录
第一个坑:在DQN里忘了对Q-target做detach,导致梯度回传到了目标网络,训练直接崩。这个错误很隐蔽,因为代码能跑,只是结果不对。后来我养成了习惯,凡是计算target的地方都显式加detach。
第二个坑:离线数据集是用随机策略收集的,覆盖太窄,CQL怎么调都学不出好策略。这让我明白一个道理——离线强化学习的效果,很大程度上取决于数据质量,算法再好也救不了烂数据。后来我学会了先分析数据集的覆盖范围,再决定用多保守的策略。
第三个坑:CQL的保守系数一开始设得太大,策略几乎不更新,评估回报一直卡在行为策略的水平。调小之后才慢慢有提升。这提醒我,保守是为了防止跑偏,不是为了不学习,度很重要。
6. 这套方法在实际项目里的边界与取舍
6.1 什么时候该用CQL,什么时候别硬上
CQL不是万能的。如果你的场景可以和环境交互,那老老实实用在线方法(DQN、PPO之类)就行,没必要上离线方法,因为离线方法天生受限于数据质量。只有当交互成本极高、或者交互有风险时,离线强化学习才有价值。
另外,CQL对数据质量的要求不低。如果历史数据覆盖的状态动作空间太窄,或者数据里充满了次优甚至错误的决策,CQL也只能在数据支撑的范围内学,学不出超越数据的策略。我一般会先做数据覆盖度分析,如果发现关键状态几乎没数据,就会先考虑补数据,而不是硬调算法。
6.2 从CQL再往前走:还有哪些值得关注的方向
CQL之后,离线强化学习还有很多演进方向,比如基于扩散模型的策略、基于Transformer的序列建模方法等。但我的建议是,别急着追新,先把Q-learning到CQL这条线吃透。因为后面那些方法,本质上还是在解决“分布偏移”和“保守估计”这两个核心问题,只是用了不同的数学工具。基础打牢了,看新论文会快很多。
我个人在实际项目里的体会是:算法选型永远要服务于数据和场景。离线强化学习看起来高大上,但如果你的数据量小、覆盖窄,可能一个简单的行为克隆(直接模仿历史动作)效果反而更稳。CQL的价值在于,当数据足够支撑、又需要超越行为策略时,它能给你一个相对可靠的框架。理解它的边界,比会调它的参数更重要。
最后分享一个小技巧:每次实验前,先花十分钟想清楚“我的数据能支撑什么样的策略”,再决定用多保守的算法。这个习惯帮我避免了很多次“算法调了半天,其实是数据不行”的无效劳动。