强化学习入门:DQN核心概念、CNN架构与实战调参指南
1. 从“打游戏”到“做决策”强化学习到底在解决什么问题很多人第一次接触强化学习脑子里浮现的都是AlphaGo下围棋、机器人后空翻这类画面觉得离自己很远。但如果你换个角度想——一个智能体在环境里不断试错根据得到的奖励调整自己的行为最终学会一套能拿到最高累计奖励的策略——这本质上和打游戏刷副本、训练宠物、甚至教小孩过马路没有区别。强化学习要解决的核心问题就一句话在不知道标准答案的情况下通过不断尝试和反馈找到最优的行动序列。这和监督学习有本质区别。监督学习是你告诉模型“这张图是猫”模型学的是输入到输出的映射而强化学习里没人告诉你每一步该怎么做你只知道最后赢了还是输了中间哪些操作是关键、哪些是败笔全靠模型自己去悟。这个“悟”的过程就是强化学习的魅力所在也是它比监督学习难训得多的根本原因。DQNDeep Q-Network是强化学习里一个里程碑式的工作。它把深度神经网络和Q-Learning结合起来让智能体可以直接从原始像素输入中学会玩Atari游戏而且在很多游戏上超过了人类水平。这个思路放到今天依然有很强的参考价值——当你面对一个状态空间巨大、无法用表格穷举的决策问题时DQN提供了一套可落地的方案。这篇文章适合谁看如果你已经了解神经网络的基本概念知道卷积是干什么的想搞明白强化学习里那几个绕不开的核心概念并且想知道DQN到底是怎么把这些概念串起来跑通的那接下来的内容就是为你准备的。我会尽量用生活化的类比把概念讲透同时给出可以直接参考的实现思路和参数设置。2. 强化学习的五个核心概念用“训练小狗”来理解2.1 智能体与环境谁在学跟谁学智能体就是那个正在学习的“大脑”环境是它交互的外部世界。放到游戏场景里智能体是玩家环境是游戏本身。智能体在每一步观察环境的状态选择一个动作环境根据这个动作给出奖励并转移到下一个状态。这个循环就是强化学习的基本交互单元。这里有个容易混淆的点环境不仅包括游戏画面还包括游戏内部的逻辑规则。比如在《超级马里奥》里环境不只是当前这一帧的图像还包括马里奥的位置、速度、敌人位置、关卡进度等所有影响决策的信息。实际实现时你需要明确哪些信息是智能体可以观察到的哪些是隐藏的。这个边界划清楚了后面的状态设计才不会跑偏。2.2 状态、动作、奖励决策的三要素状态是智能体对当前环境的观察。在DQN玩Atari的经典设定里状态就是连续四帧的游戏画面堆叠在一起。为什么要堆四帧因为单帧画面无法表达运动信息——球往哪个方向飞、敌人是在靠近还是远离这些都需要通过帧间的变化来判断。四帧是一个经验值既能捕捉到足够的动态信息又不至于让输入维度爆炸。动作是智能体可以执行的操作集合。在Atari游戏里动作空间通常是离散的比如上下左右、开火、不做任何操作等几个到十几个选项。动作空间的设计直接影响学习难度动作太多探索成本高动作太少可能无法完成任务。你需要根据具体问题来权衡。奖励是环境给智能体的反馈信号是一个标量。奖励设计是强化学习里最考验功力的环节。奖励太稀疏智能体很难学到东西奖励太密集又可能引导智能体学会“刷分”而不是真正完成任务。比如在《蒙特祖玛的复仇》这个经典难题里奖励极其稀疏智能体可能探索几百万步都拿不到一分导致学习极其困难。实际项目中奖励塑形往往需要反复迭代。2.3 策略与价值函数两种思考方式策略是智能体的行为准则它定义了在每个状态下应该选择什么动作。策略可以是确定性的——看到某个状态就固定选某个动作也可以是随机性的——以一定概率选择不同动作。随机策略在探索阶段很重要后面讲探索与利用时会展开。价值函数是评估状态或状态-动作对好坏的工具。状态价值函数V(s)表示从状态s出发按照某个策略走下去预期能拿到的累计奖励。动作价值函数Q(s,a)表示在状态s下先执行动作a然后再按照策略走下去的预期累计奖励。DQN学的就是Q函数——它不直接学策略而是学每个动作值多少钱然后选最值钱的那个动作来执行。这两者的关系可以这样理解策略是“怎么做”价值函数是“这么做值多少”。你可以直接学策略策略梯度方法也可以先学价值再间接导出策略DQN这类值方法。DQN选择后者的原因是值函数的学习信号更稳定尤其是在离散动作空间下实现起来更直接。2.4 探索与利用什么时候该冒险这是强化学习里最经典的权衡。利用是选当前已知最好的动作探索是尝试那些还没试过或者试得少的动作。只利用不探索可能永远困在局部最优只探索不利用学到的经验无法转化为稳定收益。最常用的探索策略是ε-greedy以ε的概率随机选动作以1-ε的概率选当前Q值最大的动作。ε通常从一个较大的值比如1.0完全随机开始随着训练逐步衰减到一个较小的值比如0.1或0.01。衰减的速度很关键衰减太快探索不充分衰减太慢训练前期浪费太多时间在随机动作上。我个人的经验是ε的衰减曲线最好和训练步数挂钩而不是和回合数挂钩因为不同回合的长度差异可能很大。另外在训练后期可以保留一个很小的ε比如0.01到0.05让智能体保持一定的探索能力避免策略僵化。2.5 折扣因子未来的钱现在值多少折扣因子γgamma是一个0到1之间的数用来衡量未来奖励的当前价值。γ越接近1智能体越有远见γ越接近0智能体越短视。在Atari游戏里γ通常设为0.99这意味着大约100步之后的奖励在决策时还有约37%的权重。γ的选择和任务的步数尺度有关。如果任务通常几十步就结束γ可以设小一点比如0.9如果任务需要几百步才能拿到关键奖励γ就得设大比如0.99甚至0.995。但γ太大也有问题训练信号传播得慢收敛速度会下降。实际调参时可以先根据任务的平均回合长度估算一个合理的γ再在训练中微调。3. 为什么DQN要用CNN从像素到决策的桥梁3.1 原始像素输入的挑战Atari游戏的画面是210×160的彩色图像如果直接拉平成一个向量输入全连接网络维度会超过10万。更麻烦的是相邻像素之间的空间关系被完全破坏了——全连接网络不知道哪个像素和哪个像素挨着它只能靠大量参数去硬记这些关系效率极低。CNN卷积神经网络天生就是处理图像数据的。卷积核在图像上滑动提取局部特征然后通过池化或步长卷积逐步降低空间分辨率最终得到一组高层特征。这个过程保留了空间结构信息而且参数共享让模型参数量大幅减少。在DQN里CNN负责把原始像素压缩成一个紧凑的特征向量后面的全连接层再基于这个特征向量预测每个动作的Q值。3.2 DQN的CNN架构设计细节经典的DQN架构来自2015年Nature论文是这样的输入是连续四帧的84×84灰度图像堆叠成一个4×84×84的张量。第一层卷积用32个8×8的卷积核步长4第二层用64个4×4的卷积核步长2第三层用64个3×3的卷积核步长1。这三层卷积之后接一个512维的全连接层最后输出每个动作的Q值。为什么第一层用8×8的大卷积核因为输入分辨率是84×84用大卷积核可以快速降低空间维度同时捕捉大范围的结构信息。后面的层用更小的卷积核是为了在更精细的尺度上提取特征。这个设计思路和图像分类网络如AlexNet有相似之处但DQN没有用池化层而是用步长卷积来降采样。原因之一是池化会丢失精确的位置信息而游戏里物体的精确位置对决策很重要。另一个细节是输入用了四帧堆叠。单帧84×84灰度图只能看到当前画面四帧堆叠后网络可以通过帧间差异感知运动方向和速度。这个设计在Atari游戏上被验证非常有效后来很多强化学习工作都沿用了这个做法。3.3 特征提取与Q值输出的衔接CNN提取的特征最终要映射到每个动作的Q值上。在经典DQN里卷积层之后是一个512维的全连接层然后再接一个输出层输出维度等于动作数量。每个输出节点对应一个动作的Q值。这里有个设计选择是让网络输出所有动作的Q值共享特征还是为每个动作单独建一个网络DQN选择了前者因为共享特征可以让不同动作之间共享学到的知识训练效率更高。而且在实际操作中一个网络比多个网络更容易管理和训练。需要注意的是输出层通常不加激活函数因为Q值可以是任意实数不需要限制范围。损失函数用的是均方误差但只对实际执行的那个动作的Q值计算损失其他动作的Q值不参与当前样本的梯度更新。这个细节在实现时很容易搞错后面讲实现时会再强调。4. DQN的两大创新经验回放与目标网络4.1 经验回放为什么要打破时间相关性在强化学习里连续的经验样本之间是高度相关的——这一步的状态和上一步的状态只差了一点点动作也往往有连续性。如果直接用这些样本来训练神经网络模型会倾向于记住最近的经验导致训练不稳定甚至发散。经验回放的做法是把智能体的经验状态、动作、奖励、下一状态、是否结束存到一个缓冲区里训练时从缓冲区里随机采样一批数据。这样采样出来的样本之间近似独立符合神经网络训练对数据独立同分布的基本假设。同时一条经验可以被多次使用提高了数据利用率。缓冲区的容量是一个需要调的超参数。经典DQN用了100万条经验的缓冲区。太小了样本多样性不够太大了旧的经验可能已经过时因为策略在变反而拖累训练。实际项目中我一般从10万到50万开始试根据任务复杂度和训练稳定性调整。4.2 目标网络给训练一个稳定的靶子DQN的另一个关键设计是目标网络。在计算TD目标即Q值的更新目标时如果直接用当前网络来算下一状态的最大Q值那么每次更新网络参数目标值也跟着变就像在追一个移动的靶子训练很难稳定。目标网络的思路是复制一份当前网络的参数固定住不训练专门用来计算TD目标。每隔一定步数比如1000步或10000步把当前网络的参数复制给目标网络。这样在一段时间内TD目标是相对稳定的当前网络可以朝着这个目标稳步更新。目标网络的更新频率是个关键参数。更新太频繁稳定性收益不明显更新太慢目标网络的信息过时训练效率下降。经典DQN用的是每10000步更新一次但在实际项目中这个值需要根据任务调整。我通常会在训练初期用较小的更新间隔比如1000步让目标网络跟得紧一些后期再拉长间隔。4.3 损失函数与梯度更新只对执行过的动作算账DQN的损失函数看起来很简单TD目标减去当前Q值的平方。但实现时有一个关键细节只对实际执行的那个动作计算损失。也就是说如果智能体在某个状态下选了动作2那么只有动作2对应的Q值参与梯度更新其他动作的Q值不参与。为什么这样设计因为其他动作的Q值在当前样本中没有被实际执行也就没有真实的奖励反馈来验证它们的好坏。如果强行让它们也参与更新相当于用猜测来训练猜测容易引入噪声。这个细节在代码实现时通常通过gather操作来选取对应动作的Q值然后计算MSE损失。梯度更新时还需要注意梯度裁剪。强化学习的奖励信号可能波动很大导致梯度爆炸。经典DQN把梯度裁剪到[-1, 1]之间这是一个简单但有效的稳定训练的手段。5. 从零实现DQN关键步骤与参数设置5.1 环境封装与状态预处理如果你用的是现成的强化学习环境库环境封装这一步通常已经帮你做好了。但如果你想自己从头搭或者想理解每一步在干什么下面这些预处理步骤值得关注。首先是灰度化和降采样。Atari的原始画面是210×160的彩色图直接处理计算量太大。经典做法是转成灰度图然后缩放到84×84。灰度化减少了通道数降采样减少了空间维度两者结合把输入数据量降到了原来的几十分之一。然后是帧堆叠。维护一个长度为4的队列每次新帧进来就把最旧的帧挤出去。这样网络每次看到的是最近四帧的画面可以感知运动信息。实现时要注意在回合开始时需要用零帧或者重复第一帧来填充队列保证输入维度始终一致。奖励裁剪也是常见操作。Atari游戏的得分可能是几十、几百甚至几千直接用作奖励会让Q值范围过大训练不稳定。经典DQN把奖励裁剪到[-1, 1]之间只保留正负号信息。这个做法在有些游戏上会丢失信息但在大多数Atari游戏上效果不错。如果你的任务奖励范围本身就很合理可以不做裁剪。5.2 网络搭建与初始化用PyTorch搭建DQN网络很直接。卷积层用nn.Conv2d全连接层用nn.Linear激活函数用ReLU。初始化方面卷积层和全连接层的权重可以用Kaiming初始化或者正交初始化偏置初始化为0。输出层的权重初始化要小一些避免初始Q值过大导致训练初期的不稳定。下面是一个参考实现import torch import torch.nn as nn class DQN(nn.Module): def __init__(self, input_channels, num_actions): super(DQN, self).__init__() self.conv1 nn.Conv2d(input_channels, 32, kernel_size8, stride4) self.conv2 nn.Conv2d(32, 64, kernel_size4, stride2) self.conv3 nn.Conv2d(64, 64, kernel_size3, stride1) self.fc1 nn.Linear(64 * 7 * 7, 512) self.fc2 nn.Linear(512, num_actions) self.relu nn.ReLU() def forward(self, x): x self.relu(self.conv1(x)) x self.relu(self.conv2(x)) x self.relu(self.conv3(x)) x x.view(x.size(0), -1) x self.relu(self.fc1(x)) return self.fc2(x)输入通道数是4四帧堆叠动作数根据具体游戏定。卷积层之后的特征图大小是64×7×7这是根据输入84×84和卷积参数算出来的。你可以用公式 (输入尺寸 - 卷积核尺寸 2×填充) / 步长 1 逐层计算验证。5.3 训练循环与关键参数训练循环的核心逻辑是智能体与环境交互收集经验存入回放缓冲区从缓冲区采样一批数据计算损失更新网络定期更新目标网络。关键参数包括参数经典值说明回放缓冲区大小100万根据内存和任务复杂度调整批次大小32越大越稳定但显存占用高折扣因子γ0.99根据任务步数尺度调整ε初始值1.0完全随机探索ε最终值0.1保留少量探索ε衰减步数100万与总训练步数相关目标网络更新间隔10000步可尝试1000到10000学习率0.00025Adam优化器常用值梯度裁剪[-1, 1]防止梯度爆炸训练时还有一个细节在回合结束时下一状态的价值应该为0因为已经没有后续奖励了。实现时用一个done标志位来控制如果done为TrueTD目标就只等于当前奖励不加折扣后的下一状态价值。5.4 训练监控与调参经验训练DQN时最直观的监控指标是每个回合的总奖励和回合长度。奖励上升、回合长度增加通常说明智能体在进步。但这两个指标波动很大最好用滑动平均来看趋势。损失值也是一个监控指标但要注意DQN的损失值不一定会持续下降它可能在某个范围内波动。损失突然飙升往往意味着训练出了问题比如学习率太大、目标网络更新太慢、或者奖励尺度不合理。我踩过的一个坑是训练初期奖励一直不涨以为是网络结构有问题后来发现是ε衰减太快智能体还没来得及探索就陷入了局部最优。把ε衰减步数从10万拉到100万之后训练曲线明显改善。另一个坑是回放缓冲区太小导致样本相关性太高训练震荡严重。把缓冲区从10万加到100万后训练稳定了很多。6. 常见问题与排查技巧实录6.1 训练不收敛或发散怎么办这是DQN最常见的问题。排查思路可以按以下顺序来先检查奖励尺度。如果奖励范围是几百到几千而学习率还是默认的0.00025梯度更新可能会过大。可以尝试奖励裁剪或者调小学习率。再检查目标网络更新频率。如果更新太频繁目标网络和当前网络几乎同步就失去了稳定训练的作用。可以尝试增大更新间隔比如从1000步改成10000步。然后检查回放缓冲区。缓冲区太小会导致样本相关性高训练不稳定。可以尝试增大缓冲区或者检查采样策略是否真的是均匀随机。最后检查网络初始化。如果输出层的初始Q值太大训练初期会很不稳定。可以尝试把输出层权重初始化得更小一些。6.2 Q值过高估计怎么发现和缓解DQN有一个已知问题Q值会被系统性地高估。原因是TD目标里用了max操作而max操作会倾向于选择被高估的动作。这个偏差在训练中会累积导致智能体过于乐观。发现这个问题的方法是记录训练过程中Q值的平均值如果它持续增长到远大于实际回合奖励的水平就说明可能存在高估。缓解方法包括使用Double DQN用当前网络选动作用目标网络算价值、Dueling DQN把Q值分解为状态价值和优势函数等。这些改进在原始DQN之后被提出实现起来也不复杂建议在实际项目中直接采用。6.3 训练速度太慢的优化思路DQN训练慢是出了名的。在Atari游戏上经典DQN需要几千万帧才能达到人类水平单卡训练可能要几天甚至几周。优化思路有几个方向一是减小网络规模。如果任务没那么复杂可以把卷积层的通道数减半或者去掉一层卷积。二是减小输入分辨率。84×84已经比较小了但如果任务允许可以进一步降到64×64甚至42×42。三是用更高效的采样策略比如优先经验回放Prioritized Experience Replay让重要的经验被更频繁地采样。四是混合精度训练用float16代替float32可以显著减少显存占用和计算时间。6.4 常见问题速查表问题现象可能原因排查方向奖励不涨ε衰减太快、学习率太小、奖励太稀疏调大ε衰减步数、调大学习率、检查奖励设计训练震荡缓冲区太小、批次太小、目标网络更新太频繁增大缓冲区、增大批次、拉长更新间隔Q值爆炸学习率太大、奖励尺度太大、缺少梯度裁剪调小学习率、奖励裁剪、加梯度裁剪显存不足网络太大、批次太大、缓冲区太大减小网络、减小批次、用更小的缓冲区训练后期性能下降过拟合、ε太小、目标网络过时增大ε、检查目标网络更新、早停7. 从DQN出发后续可以怎么扩展DQN是一个起点不是终点。在实际项目中你可能会遇到离散动作空间之外的场景比如机器人控制里的连续动作。这时候DQN就不适用了需要转向DDPG、TD3、SAC这类支持连续动作的算法。它们的核心思想类似——用网络近似价值函数或策略——但在具体实现上有不少差异。另一个方向是改进探索策略。ε-greedy虽然简单但在复杂任务上效率不高。可以尝试基于不确定性的探索如Bootstrapped DQN、基于计数的新颖性探索、或者基于内在奖励的探索。这些方法在奖励稀疏的任务上往往有奇效。如果你想把DQN用到自己的业务问题上最关键的一步是定义好状态、动作和奖励。状态要包含足够的信息让智能体做决策动作要覆盖所有可能的操作奖励要能准确反映任务目标。这三者定义好了后面的网络结构和超参数调优才有意义。定义不好再好的算法也跑不出好结果。我个人在实际操作中的体会是强化学习项目里80%的时间花在环境设计和调试上只有20%花在算法本身上。把环境搞清楚把奖励设计合理比盲目换算法有效得多。