1. 从Q表到神经网络为什么非得用DQN不可很多人学强化学习第一个跑通的算法大概率是Q-Learning。它逻辑简单、代码量少一张Q表就能让智能体在格子世界里学会走路。但当你把状态空间稍微放大一点比如从16个格子变成连续的状态输入Q表就彻底崩了——你不可能为每一个可能的状态都开一行存储空间因为状态是无穷的。这就是DQNDeep Q-Network要解决的核心问题用神经网络替代Q表把“查表”变成“函数拟合”。Q-Learning维护的是Q(s, a)的离散表格而DQN维护的是一个参数为θ的神经网络输入状态s输出每个动作a对应的Q值。这样一来即使遇到训练时从未见过的状态网络也能基于学到的特征给出一个合理的估计。我第一次从Q-Learning切换到DQN的时候最直观的感受就是“自由度大了但坑也多了”。Q表时代收敛慢顶多是迭代次数不够到了DQN网络不收敛的原因可能来自十几个方向——学习率、网络结构、经验回放、目标网络、奖励尺度任何一个环节出问题loss曲线都能给你表演一场“心电图”。所以这篇文章不打算只给你贴一段代码就完事。我想把DQN从动机到落地拆开讲清楚尤其是那些在论文里一笔带过、但在实际编码中会让你卡半天的细节。适合已经了解Q-Learning基本概念、想进一步掌握深度强化学习实战的读者。如果你连Q-Learning的更新公式都还没搞明白建议先补一下基础再来看这篇。2. 拆解DQN的三大核心机制2.1 经验回放为什么不能边跑边学Q-Learning是on-policy的在线学习方式智能体走一步、更新一次数据用完就扔。但神经网络训练有一个基本假设训练数据要近似独立同分布。如果智能体按时间顺序连续采样相邻的(s, a, r, s)之间存在强相关性直接拿这种序列去训练网络会导致梯度更新方向剧烈震荡网络很难稳定下来。经验回放Experience Replay的思路很朴素搞一个固定大小的缓冲区把智能体每一步的转移元组(s, a, r, s, done)存进去训练的时候从缓冲区里随机抽一批mini-batch出来。这样每次抽到的样本来自不同的时间点、不同的轨迹相关性被大幅削弱训练稳定性显著提升。实际操作中缓冲区大小是一个需要调的超参数。太小了样本多样性不够容易过拟合最近的经验太大了早期那些“菜鸟阶段”的垃圾数据会长期污染训练集。我一般从10000到50000开始试具体看任务复杂度。另外还有一个容易忽略的点缓冲区里的样本不能无限期保留。当缓冲区满了之后新数据会覆盖最老的数据这个覆盖策略本身也是一种“遗忘机制”让网络更关注近期的经验。2.2 目标网络给训练加一个“锚”DQN最容易被忽视、但最不能省的设计就是目标网络Target Network。在朴素的做法里计算TD目标时用的是当前网络自己输出的Q值TD目标 r γ · max Q(s, a; θ)而当前网络的参数θ在每一步都在更新这意味着你用来计算目标的那把“尺子”本身也在不停晃动。网络在追一个不断移动的目标训练很容易发散。目标网络的解决方案是复制一份和当前网络结构完全相同的网络参数记为θ⁻。计算TD目标时用θ⁻而当前网络θ只负责拟合这个目标。每隔C步把θ的参数硬拷贝给θ⁻。这样一来在一段时间内TD目标是相对固定的网络有了一个稳定的学习方向。这里有个经验性的细节目标网络的同步频率C不能太频繁也不能太稀疏。太频繁了目标网络和当前网络几乎一样等于没加太稀疏了目标网络落后太多TD目标过时严重学习效率下降。实践中C取几百到几千步比较常见也有用软更新每次只更新一点点的变体效果通常更平滑。2.3 ε-贪婪策略探索与利用的平衡术强化学习永远绕不开探索与利用的权衡。DQN用的是ε-贪婪策略以ε的概率随机选动作探索以1-ε的概率选当前Q值最大的动作利用。ε通常从一个较大的值比如1.0开始随着训练逐步衰减到一个较小的值比如0.01或0.05。这个衰减策略的设计很有讲究。如果ε衰减太快智能体还没来得及充分探索环境就陷入了局部最优如果衰减太慢训练后期还在大量随机动作策略无法收敛到最优。我一般用线性衰减或者指数衰减衰减步数设为总训练步数的30%到50%左右。还有一个容易被忽略的点ε的最小值不要设成0。即使训练后期保留一点点随机性有助于智能体应对环境中的微小变化也能防止策略完全僵化。0.01到0.05是一个比较安全的区间。3. 网络结构与损失函数的设计取舍3.1 输入输出层的维度怎么定DQN的网络结构没有固定模板但输入输出层的维度是由问题本身决定的。输入维度等于状态的特征数输出维度等于可选动作的数量。比如经典的CartPole任务状态是4维小车位置、速度、杆角度、角速度动作是2个左推、右推那么网络输入就是4输出就是2。中间隐藏层的设计就比较自由了。对于低维状态输入两到三层全连接层、每层64到256个神经元通常够用。对于图像输入比如Atari游戏就需要上卷积层来提取空间特征。我见过不少人一上来就堆很深的网络结果训练慢不说还容易过拟合。强化学习的网络不是越深越好够用就行因为训练信号本身噪声就大网络容量太大反而学不到稳定的策略。激活函数方面隐藏层用ReLU是默认选择输出层不加激活函数因为Q值可以是任意实数。这一点和分类任务不同不要习惯性地在输出层加softmax。3.2 损失函数MSE还是HuberDQN的损失函数衡量的是当前Q值和TD目标之间的差距。最直接的选择是均方误差MSEL E[(r γ · max Q(s, a; θ⁻) - Q(s, a; θ))²]但MSE对异常值非常敏感。在强化学习中奖励信号偶尔会出现极端值比如游戏里突然吃到一个大奖励这些异常样本会产生很大的梯度把网络参数带偏。Huber损失是一个更鲁棒的选择当误差较小时表现为MSE误差较大时表现为线性从而限制了异常样本的梯度幅度。我的经验是如果奖励信号比较平滑MSE够用如果奖励稀疏且存在极端值优先用Huber。这个切换成本很低但在训练稳定性上的差异可能非常明显。3.3 优化器的选择与学习率设置Adam是DQN中最常用的优化器它对学习率不那么敏感收敛速度也快。学习率一般从1e-4到1e-3之间试我通常从5e-4开始。如果loss震荡厉害就往下调如果收敛太慢就往上调。这里有一个和监督学习不同的地方DQN的loss不一定要降到很低。因为TD目标本身在变化loss反映的是当前网络和目标网络的差距而不是绝对的预测误差。有时候loss看着不降但智能体的平均回报在稳步上升这说明策略其实在变好。所以评估DQN训练效果看累计回报比看loss曲线更靠谱。4. 训练流程的完整实现与关键代码4.1 整体训练循环的骨架先把训练循环的骨架搭出来后面再逐个填充细节。整个流程大致是这样的for episode in range(max_episodes): state env.reset() total_reward 0 for step in range(max_steps): # 1. 用ε-贪婪策略选动作 action select_action(state, epsilon) # 2. 执行动作观察结果 next_state, reward, done, _ env.step(action) # 3. 存入经验回放缓冲区 replay_buffer.push(state, action, reward, next_state, done) # 4. 从缓冲区采样并训练 if len(replay_buffer) batch_size: batch replay_buffer.sample(batch_size) loss compute_loss(batch) optimizer.zero_grad() loss.backward() optimizer.step() # 5. 定期同步目标网络 if total_steps % target_update_freq 0: target_net.load_state_dict(policy_net.state_dict()) state next_state total_reward reward if done: break # 6. 衰减epsilon epsilon max(epsilon_min, epsilon * epsilon_decay)这个骨架看起来简单但每一步都有坑。下面挑几个关键环节展开说。4.2 动作选择与epsilon衰减的实现细节动作选择的实现看似简单但有一个容易犯的错误在计算Q值时忘记切换网络的评估模式。PyTorch里如果用Dropout或BatchNorm训练模式和评估模式的输出是不一样的。选动作时应该用policy_net.eval()训练时再切回policy_net.train()。这个细节不注意会导致动作选择带有随机性影响策略的稳定性。epsilon衰减我一般用指数衰减epsilon max(epsilon_min, epsilon * decay_rate)decay_rate通常取0.995到0.999之间。如果环境比较简单也可以直接用线性衰减每步减一个固定值。关键是衰减的节奏要和训练总步数匹配不要出现训练还没结束epsilon就已经降到最低的情况。4.3 经验回放缓冲区的实现要点缓冲区用collections.deque就能实现设置一个maxlen满了自动覆盖最老的数据。采样的时候用random.sample随机抽取。存储的数据格式建议用numpy数组而不是Python列表这样在批量转换成张量的时候效率更高。有一个性能上的小技巧把状态存储为float32而不是默认的float64。神经网络的输入通常是float32如果存储时用float64每次采样后还要做类型转换白白浪费计算资源。在状态维度较高的时候这个差异会很明显。另外done标志要正确存储。计算TD目标时如果doneTrue说明这是终止状态后续没有奖励了TD目标就只是r本身不需要加γ·max Q(s, a)。这个逻辑写错了智能体会学到“死亡后还有未来奖励”的错误认知。4.4 目标网络同步的时机与方式硬更新就是每隔C步把policy_net的参数直接拷贝给target_netif step % target_update_freq 0: target_net.load_state_dict(policy_net.state_dict())软更新则是每次只更新一点点for target_param, policy_param in zip(target_net.parameters(), policy_net.parameters()): target_param.data.copy_(tau * policy_param.data (1 - tau) * target_param.data)tau通常取0.001到0.01。软更新的好处是目标网络的变化非常平滑不会出现硬更新时TD目标突然跳变的情况。我个人更倾向于软更新虽然多了一点计算量但训练曲线通常更漂亮。5. 那些论文里不会写的踩坑实录5.1 奖励尺度没调好网络直接摆烂这是我踩过最深的坑之一。有一次做一个自定义环境奖励值设成了几百甚至上千的量级。结果训练的时候loss直接爆炸网络输出全是NaN。排查了半天才发现奖励值太大会导致TD目标过大梯度爆炸。解决方案很简单把奖励归一化到[-1, 1]或者[0, 1]的区间。如果奖励的分布范围很广可以用running mean和std做标准化。这个操作在监督学习里很常见但在强化学习中容易被忽略因为大家更关注算法本身而不是数据预处理。5.2 状态表示没做好智能体学了个寂寞另一个常见的坑是状态表示。比如做股票交易模拟如果把原始价格直接作为状态数值范围可能从几块到几百块网络很难学到有效的特征。更好的做法是用收益率、技术指标等归一化后的特征作为状态输入。还有一个更隐蔽的问题状态中包含了冗余或无关信息。比如状态里同时有“位置”和“速度”但速度其实可以通过位置差分算出来两个特征高度相关会增加网络的训练难度。特征工程在强化学习里同样重要不要指望网络能自动筛选出有用信息。5.3 训练不收敛时的排查顺序当DQN训练不收敛时不要盲目调参。我一般按以下顺序排查排查项检查内容常见问题奖励信号奖励是否归一化是否存在极端值奖励尺度过大导致梯度爆炸状态表示特征是否归一化是否有冗余输入范围差异大导致训练慢经验回放缓冲区大小是否合理太小导致相关性高太大导致陈旧数据多目标网络同步频率是否合适太频繁等于没有太稀疏导致目标过时学习率是否过大或过小过大震荡过小收敛慢网络结构层数和神经元数是否合理太深容易过拟合太浅拟合能力不足这个排查顺序的原则是先检查数据和奖励再检查算法超参数最后检查网络结构。因为前两者的影响通常更大而且修复成本更低。5.4 评估策略的常见误区训练过程中怎么评估智能体的表现也有讲究。一个常见的错误是用训练时的epsilon去评估。训练时epsilon可能还有0.1意味着10%的动作是随机的这时候评估出来的回报会偏低。正确的做法是评估时把epsilon设为0或者一个很小的值用纯贪婪策略跑几个episode取平均回报。另一个误区是只看最高回报。强化学习的训练波动很大偶尔跑出一个高回报不代表策略稳定。应该看最近N个episode的平均回报以及回报的方差。如果方差很大说明策略还不够稳定需要继续训练或者调整超参数。6. 从DQN出发的改进方向6.1 Double DQN解决Q值高估问题标准DQN在计算TD目标时用同一个网络既选动作又评估动作这会导致Q值被系统性地高估。Double DQN的思路是把这两个步骤分开用policy_net选动作用target_net评估这个动作的Q值。改动很小但效果通常很显著。如果你发现训练出来的Q值大得离谱比如实际回报最多100但Q值算出来好几百大概率就是高估问题试试Double DQN。6.2 Dueling DQN分离状态价值和动作优势Dueling DQN把Q值拆成两部分状态价值V(s)和动作优势A(s, a)。网络有两个分支最后合并输出Q值。这样做的好处是在某些状态下不管选什么动作差别都不大网络可以专注于学习状态本身的价值而不必为每个动作单独估计。这个改进在动作空间较大、且很多动作效果相近的任务中特别有效。6.3 优先经验回放让重要的样本多出现几次标准经验回放是均匀采样每个样本被抽到的概率相同。但有些样本的TD误差很大说明网络在这些样本上预测得不准应该多学几次。优先经验回放就是按TD误差的大小给样本分配采样概率误差大的样本被抽到的概率更高。实现上需要用SumTree这样的数据结构来高效采样代码量会增加不少。但如果你的任务中“关键经验”很稀疏比如只有在特定条件下才能获得高奖励优先经验回放能显著加速收敛。6.4 从DQN到更广阔的深度强化学习DQN是深度强化学习的入门算法但它的局限也很明显只能处理离散动作空间。如果动作是连续的比如机器人的关节角度DQN就无能为力了。这时候需要转向DDPG、TD3、SAC等专门处理连续动作的算法。不过把DQN吃透之后再学这些算法会轻松很多因为它们共享很多核心思想——经验回放、目标网络、探索策略只是具体的实现方式不同。所以我的建议是不要急着追新算法先把DQN调通、调稳把那些坑都踩一遍后面的路会好走很多。我在实际项目中的体会是DQN的训练稳定性比理论性能更重要。一个收敛稳定的简单DQN往往比一个理论先进但训练困难的算法更实用。尤其是在工程落地场景中可复现、可调试比刷榜重要得多。
阅读完成 · 觉得有帮助?