资讯动态

深度Q-learning实战:从函数逼近到训练稳定性的核心解析

发布时间:2026/9/28 8:18:15 来源:尧图企业网站定制
1. 从“查表”到“函数逼近”Q-learning到底在解决什么问题如果你正在啃CS285深度强化学习课程的第三讲大概率已经在前两讲里被MDP、策略梯度、值函数这些概念轮番轰炸过一遍了。到了Q-learning这一讲很多人第一反应是“这不就是个查表更新吗有什么难的”然后打开作业一看发现要处理的是连续状态空间、高维观测、稀疏奖励瞬间就懵了。我当初也是这个状态所以这篇内容就是把我自己从“看懂公式”到“跑通代码”之间踩过的坑、想明白的逻辑完整地梳理一遍。Q-learning的核心思想其实一句话就能说清楚不去学策略而是去学一个“动作价值函数”Q(s,a)它告诉你“在状态s下做动作a之后一直按最优方式走能拿到多少累计回报”。一旦你有了足够准的Q函数策略就是现成的——在每个状态选Q值最大的那个动作就行。这个思路和策略梯度是两条完全不同的路线策略梯度直接参数化策略π(a|s)用采样估计梯度然后往上爬Q-learning则是先解决“值”的问题再从中导出“策略”。两者各有各的麻烦但Q-learning在某些场景下样本效率更高因为它可以复用旧数据这一点在CS285的作业里体现得非常明显。那为什么这一讲要叫“Q-learning”而不是“值迭代”或者“动态规划”因为真正的难点不在于Q-learning的更新公式本身而在于当状态空间大到无法枚举时你怎么表示和训练这个Q函数。表格型Q-learning只能处理离散、有限、小规模的状态动作空间比如FrozenLake那种几十个格子的问题。但CS285的目标是让机器人、让智能体在连续控制任务里工作状态是浮点数向量动作也是连续的你不可能为每个(s,a)对存一个数字。所以这一讲真正要解决的是如何用函数逼近器神经网络来参数化Q(s,a)并且让训练过程稳定收敛。这里就引出了Q-learning在深度强化学习里的两个经典难题。第一个是**“移动目标”问题**你用一个网络Q_θ去估计目标值但目标值本身又依赖于Q_θ相当于你在追一个自己也在跑的靶子。第二个是**“分布偏移”问题**Q-learning是off-policy的它可以用旧策略采集的数据来更新当前策略但旧数据覆盖的状态动作分布和当前策略关心的分布可能差很远导致某些(s,a)的Q值被高估或者根本没被训练到。这两个问题在CS285的后续课程里会分别用目标网络target network和双重Q-learningdouble Q-learning等技巧来缓解但在第三讲这个阶段你需要先理解它们为什么会成为问题。我个人的经验是学这一讲的时候不要一上来就纠结代码细节先把“Q-learning的更新目标是什么”“为什么可以用off-policy数据”“函数逼近引入后为什么不稳定”这三个问题想清楚。想清楚之后再看作业里的网络结构、损失函数、目标值计算方式你会发现每一行代码都有它的道理。下面我就按这个逻辑把Q-learning在CS285里的核心内容拆开来讲。2. 时序差分更新Q-learning的“自举”逻辑与目标值构造2.1 从蒙特卡洛到时序差分为什么不用等回合结束在理解Q-learning的更新之前先回忆一下蒙特卡洛方法是怎么估计Q值的。假设你跑完一个完整的回合得到一条轨迹(s0,a0,r0,s1,a1,r1,...,sT)那么对于轨迹里出现的每个(s_t,a_t)你可以用从t时刻开始的累计折扣回报G_t来作为它的Q值估计。这个方法很直观但有个致命问题你必须等回合结束才能更新。如果回合很长或者任务根本没有终止状态比如持续控制的机器人蒙特卡洛方法就没法用了。时序差分Temporal DifferenceTD方法换了个思路我不等完整回报我用“当前奖励加上下一状态的价值估计”来构造目标值。具体到Q-learning更新目标是y r γ · max_{a} Q(s, a)这个式子就是Q-learning的灵魂。它说的是你在状态s做了动作a环境给了你奖励r然后你到了状态s。那么“在s做a的价值”应该等于“即时奖励r”加上“在s选最优动作的折扣价值”。注意这里用的是max不是当前策略实际会选的动作这是Q-learning区别于SARSA的关键——Q-learning学的是最优Q函数不管当前行为策略是什么。我第一次看到这个式子的时候心里有个疑问Q(s,a)本身也是估计出来的用它来构造目标值不会越估越偏吗这就是所谓的“自举”bootstrapping。答案是会偏但如果更新步长合适、并且你反复迭代最终会收敛到真实Q值。这背后的数学保证来自随机逼近理论但在实际操作中自举带来的偏差和方差权衡才是你需要关心的。蒙特卡洛是无偏但高方差TD是有偏但低方差Q-learning选择了后者换来了可以在线更新、可以处理无终止任务的能力。2.2 表格型Q-learning的完整更新流程在状态和动作都离散且数量有限的情况下Q-learning可以用一张表来表示。表的行是状态列是动作每个格子存一个Q值。更新规则非常直接# 表格型Q-learning的核心更新 # Q_table[s, a] 是当前估计 # alpha 是学习率gamma 是折扣因子 td_target reward gamma * np.max(Q_table[next_state]) td_error td_target - Q_table[state, action] Q_table[state, action] alpha * td_error这个流程里有两个细节值得展开。第一探索策略。如果你每次都选Q值最大的动作那很多(s,a)对永远得不到更新Q表会收敛到一个次优解。所以通常用ε-greedy以ε的概率随机选动作以1-ε的概率选当前最优动作。ε一般从1.0逐渐衰减到0.1或0.05。第二学习率α。表格型情况下α可以设得比较大比如0.1因为每个格子独立更新不会互相干扰。但在函数逼近里α就不能乱设了这个后面会讲。还有一个容易忽略的点Q-learning是off-policy的。这意味着你用来采集数据的策略行为策略和你想评估的策略目标策略可以不一样。比如你可以用ε-greedy采集数据但更新时用max相当于在学贪婪策略的Q值。这个性质在深度Q-learning里非常重要因为它允许你用经验回放池replay buffer里的旧数据来训练大大提高了样本效率。但off-policy也带来了分布偏移问题这是后面要重点讨论的。2.3 目标值里的max操作Q-learning与SARSA的分水岭很多人分不清Q-learning和SARSA其实就差在一个符号上。SARSA的更新目标是y r γ · Q(s, a)其中a是行为策略在s实际选的动作。而Q-learning用的是max_{a} Q(s,a)。这个差别看起来很小但含义完全不同。SARSA学的是“当前策略的价值”所以它是on-policy的Q-learning学的是“最优策略的价值”所以它是off-policy的。在CS285的作业里如果你用SARSA的更新方式去训练然后测试时用贪婪策略你会发现性能比Q-learning差一截。原因在于SARSA把探索带来的随机性也算进了价值里而Q-learning直接假设下一步会选最优动作不受探索噪声影响。当然Q-learning的max操作也有代价它容易导致Q值高估。因为max是对估计值取的如果估计有噪声max会偏向正噪声大的动作导致Q值系统性偏高。这个问题在深度Q-learning里会被放大后面会详细说。我自己的理解是Q-learning的max操作是一种“乐观估计”它假设你在未来总是能选到最好的动作。这种乐观在表格型情况下问题不大因为每个格子的估计会随着更新逐渐准确。但在函数逼近里一个(s,a)的Q值更新会影响到附近(s,a)的估计高估就会传播开来。所以CS285后续会讲Double Q-learning和Clipped Double Q-learning本质上都是在解决这个max带来的高估问题。3. 函数逼近登场为什么神经网络成了Q-learning的标配3.1 连续状态空间的表示困境假设你的状态是一个7维的关节角度向量每个维度是连续实数。你不可能为每个可能的向量存一个Q值因为状态空间是无限的。这时候你需要一个函数逼近器输入状态和动作输出Q值。最简单的选择是线性函数逼近Q(s,a) w^T φ(s,a)其中φ是手工设计的特征。但手工设计特征在复杂任务里几乎不可行所以大家转向了神经网络。神经网络作为函数逼近器有几个好处它可以自动学习特征表示可以处理高维输入比如图像可以通过梯度下降端到端训练。但坏消息是神经网络和Q-learning的结合并不天然稳定。原因在于Q-learning的更新目标依赖于网络自己的输出而神经网络的训练又假设数据分布是固定的。这两者放在一起就形成了一个“移动目标”的反馈回路很容易发散。我在跑CS285作业的时候第一次用两层MLP去拟合Q函数结果训练损失忽上忽下最后Q值直接爆炸。后来才明白问题出在目标值计算上每次更新时我用当前网络算y r γ max Q(s,a)然后拿这个y去回归Q(s,a)。但下一次更新时网络参数变了同一个(s,a)的Q值也变了y就变了。这相当于我在追一个不断移动的目标而且目标移动的方向还取决于我自己的更新方向很容易形成正反馈。3.2 目标网络给“移动靶”拍一张快照解决移动目标问题的标准做法是引入目标网络target network。具体来说你维护两个网络一个是在线网络Q_θ用来选动作和计算当前Q值另一个是目标网络Q_θ用来计算更新目标y r γ max Q_θ(s,a)。目标网络的参数不是每一步都更新而是每隔C步从在线网络复制一次或者用软更新Polyak averaging慢慢跟随。这个技巧背后的逻辑是让目标值在一段时间内保持相对稳定这样在线网络就有了一个固定的回归目标训练会稳定很多。你可以把目标网络想象成“给移动靶拍一张快照”然后你对着快照练枪法练一段时间后再拍一张新快照。虽然快照是滞后的但总比对着一个乱跑的靶子打要靠谱。在CS285的作业里目标网络的更新周期C是一个需要调的超参数。C太小目标网络跟得太紧稳定性提升有限C太大目标网络太滞后Q值会偏离真实值。我试过C1相当于没有目标网络、C100、C1000发现C100左右在大多数任务里比较平衡。当然软更新τ0.005通常比硬更新更平滑但实现起来稍微麻烦一点。3.3 经验回放让off-policy真正发挥作用Q-learning是off-policy的这意味着你可以用旧策略采集的数据来更新当前策略。经验回放池replay buffer就是把这个性质利用到极致的工具你把每一步的(s,a,r,s,done)存进一个大的循环队列训练时从里面随机采样一批数据来更新网络。这样做有几个好处第一打破了数据之间的时间相关性让梯度估计更稳定第二提高了样本效率因为一条数据可以被多次使用第三让训练过程更接近监督学习因为你可以从固定分布里采样。但经验回放也有坑。如果回放池里的数据大部分是旧策略采集的而当前策略已经变得很不一样那么这些数据对当前Q函数的更新可能是有害的。比如你早期用随机策略采集了一堆数据里面有很多“撞墙”的(s,a)对这些对的Q值应该很低。但如果你现在策略已经学会了避开墙回放池里这些旧数据仍然会被采样到网络需要花容量去记住这些旧状态的低Q值可能会影响对新状态的学习。这个问题在CS285的作业里可能不明显但在实际项目里通常需要控制回放池的大小或者用优先经验回放prioritized replay来缓解。我自己的习惯是回放池大小设成100万条左右batch size设成256或512。如果任务比较简单回放池可以小一点比如10万这样旧数据被覆盖得快分布偏移问题轻一些。如果任务很复杂回放池大一点能保留更多多样性但训练会慢。这个权衡需要根据具体任务来调。4. 深度Q-learning的训练稳定性从损失函数到梯度裁剪4.1 损失函数的选择MSE还是Huber深度Q-learning的损失函数通常是均方误差MSEL(θ) E_{(s,a,r,s)~D} [ (r γ max_{a} Q_θ(s,a) - Q_θ(s,a))^2 ]这个损失对θ求梯度然后做梯度下降。但MSE对异常值很敏感如果某个(s,a)的TD误差特别大比如因为奖励尺度问题或者目标网络滞后梯度会被这个样本主导导致网络参数剧烈变化。这时候可以用Huber损失代替MSE当TD误差小于阈值δ时用平方大于δ时用线性。这样异常值的梯度被截断训练更稳定。我在CS285作业里试过MSE和Huber发现Huber在奖励稀疏的任务里明显更稳。比如在某个任务里大部分奖励是0偶尔有1或-1MSE会让网络对那几个非零奖励的样本过度反应而Huber把梯度限制住了网络学得更平滑。当然Huber的δ也需要调一般设1.0左右。4.2 梯度裁剪与奖励缩放两个容易被忽略的细节梯度裁剪是深度强化学习里的常规操作但在Q-learning里尤其重要。因为TD误差可能很大反向传播后梯度范数可能爆炸。通常的做法是计算所有参数的梯度范数如果超过某个阈值比如10或5就按比例缩放。这个操作在PyTorch里一行代码就能搞定torch.nn.utils.clip_grad_norm_(q_network.parameters(), max_norm10.0)奖励缩放是另一个容易被忽略的点。如果你的奖励范围是[-100, 100]而Q值初始接近0那么TD目标可能很大网络需要很长时间才能拟合。通常的做法是把奖励除以一个常数比如最大可能奖励或者用运行均值做标准化。但要注意奖励缩放会改变最优策略吗如果缩放是线性的且对所有奖励一视同仁最优策略不变。但如果奖励有正有负缩放后相对比例不变策略也不变。所以奖励缩放是安全的而且能显著加速训练。我踩过的一个坑是在某个任务里奖励是每一步-1直到到达目标给100。如果不缩放Q值会累积到-1000左右网络输出层需要很大的权重才能表示这个范围训练很慢。把奖励除以100后Q值范围变成[-10, 1]网络很快就拟合了。所以我的建议是在训练前先看一眼奖励的尺度和范围如果绝对值超过10就考虑缩放。4.3 目标网络更新频率与软更新的取舍前面提到了目标网络的硬更新和软更新。硬更新是每隔C步把在线网络参数直接复制给目标网络软更新是每一步都做θ ← τθ (1-τ)θ。硬更新的优点是实现简单目标值在一段时间内完全固定缺点是更新时会有突变可能导致训练损失突然跳一下。软更新的优点是平滑目标值逐渐变化训练更稳定缺点是需要调τ而且τ太小的话目标网络跟得太慢Q值会滞后。我个人的经验是如果任务比较简单硬更新就够了C设100到1000之间。如果任务复杂或者训练不稳定软更新更靠谱τ设0.005或0.001。在CS285的作业里两种方式都可以跑通但软更新通常需要更长的训练时间才能看到效果。另外目标网络的初始参数应该和在线网络完全一致否则一开始目标值就是错的训练会走弯路。还有一个细节目标网络只在计算目标值时使用不参与梯度计算。也就是说计算y r γ max Q_θ(s,a)时要把Q_θ的梯度关掉用torch.no_grad()或者detach()。这个如果忘了梯度会传到目标网络导致目标网络也被更新就失去了“固定目标”的意义。我当初就犯过这个错误训练损失一直不降排查了半天才发现是目标网络的梯度没关。5. 过估计问题与Double Q-learningmax操作的代价5.1 为什么max会导致Q值高估Q-learning的目标值里有max_{a} Q(s,a)。如果Q(s,a)的估计是无偏的但带有噪声那么max操作会偏向噪声为正的动作。举个例子假设真实Q值是[1.0, 1.0]但估计值是[1.1, 0.9]和[0.9, 1.1]两种情况max分别是1.1和1.1平均是1.1比真实值1.0高。这就是所谓“最大化偏差”maximization bias。在表格型Q-learning里这个问题不严重因为每个格子的估计会随着更新逐渐收敛到真实值噪声会减小。但在函数逼近里一个(s,a)的Q值更新会影响附近(s,a)的估计高估会传播。更糟糕的是如果某个动作的Q值被高估了贪婪策略会选这个动作然后这个动作的Q值又被进一步高估形成正反馈。最终Q值可能发散策略也变得次优。我在CS285作业里观察过这个现象训练初期Q值缓慢上升然后突然加速上升最后爆炸。对应的策略表现是先变好然后突然变差。这就是过估计的典型症状。解决这个问题的方法有几种Double Q-learning、Clipped Double Q-learning、以及后来的TD3和SAC里的各种技巧。CS285第三讲主要讲Double Q-learning的基本思想。5.2 Double Q-learning的拆解用两个网络解耦动作选择与价值评估Double Q-learning的核心思想是把“选动作”和“评估动作”分开。具体来说你维护两个Q网络Q_A和Q_B每次更新时随机选一个网络来选动作用另一个网络来评估这个动作的价值。比如用Q_A选动作a* argmax Q_A(s,a)然后用Q_B计算Q_B(s,a*)作为目标值。这样即使Q_A高估了某个动作Q_B的评估也不一定高估因为两个网络的噪声是独立的。在深度Q-learning里这个思想被简化成用在线网络选动作用目标网络评估。也就是说目标值变成y r γ Q_θ(s, argmax_{a} Q_θ(s,a))注意这里argmax用的是在线网络Q_θ而Q值用的是目标网络Q_θ。因为在线网络和目标网络的参数不同它们的噪声也不完全相关所以能一定程度上缓解过估计。这个技巧在CS285的作业里很容易实现只需要把目标值计算改一行# 原来的Q-learning目标 next_q target_network(next_state).max(dim1)[0] # Double Q-learning目标 next_actions online_network(next_state).argmax(dim1, keepdimTrue) next_q target_network(next_state).gather(1, next_actions).squeeze()我实测下来这个改动在大多数任务里都能让Q值更接近真实值策略表现也更稳定。但要注意Double Q-learning并不能完全消除过估计只是减轻了。如果任务本身奖励噪声很大或者网络容量太小过估计仍然可能发生。5.3 过估计的检测与调参经验怎么判断你的Q-learning有没有过估计最直接的方法是记录训练过程中Q值的最大值和平均值。如果Q值持续上升且没有收敛趋势或者Q值远大于理论上限比如折扣回报的最大可能值那大概率是过估计了。另一个方法是比较训练回报和Q值如果Q值很高但实际回报很低说明Q值虚高。调参方面除了用Double Q-learning还可以降低学习率让更新更平滑、增大目标网络更新周期让目标更稳定、用Huber损失限制异常梯度、以及减小网络容量减少过拟合噪声。我试过在某个任务里把学习率从1e-3降到1e-4过估计明显减轻但训练变慢了。所以这是一个权衡需要根据任务来调。还有一个经验如果任务奖励是稀疏的过估计问题会更严重因为大部分TD目标都是0网络容易对少数非零奖励过度反应。这时候可以用n-step回报或者优先经验回放来缓解。n-step回报是把目标值改成r_t γr_{t1} ... γ^n max Q(s_{tn},a)这样目标值里包含了更多真实奖励减少了对Q估计的依赖。优先经验回放则是让TD误差大的样本被更频繁地采样加快这些样本的学习。6. CS285作业里的Q-learning实现从伪代码到可运行代码6.1 网络结构设计与输入输出维度CS285的Q-learning作业通常要求你实现一个MLP作为Q函数。输入是状态可能还有动作如果是连续动作的话输出是每个离散动作的Q值。对于离散动作空间网络输出维度等于动作数对于连续动作空间网络需要同时输入状态和动作输出一个标量Q值。我建议的网络结构是两层隐藏层每层256或512个单元激活函数用ReLU。对于大多数MuJoCo任务这个容量足够了。如果任务观测是高维图像那就需要CNN但CS285第三讲通常不涉及图像输入。输出层不要加激活函数因为Q值可以是任意实数。一个容易忽略的点是输入标准化。如果状态向量的不同维度尺度差异很大比如位置是0.01量级速度是10量级网络训练会很慢。通常的做法是用运行均值方差对状态做标准化或者至少把状态缩放到[-1,1]附近。我在作业里加了一个简单的RunningMeanStd类训练速度提升很明显。6.2 训练循环的骨架与关键代码段下面是一个简化的训练循环骨架涵盖了Q-learning的核心步骤for step in range(total_steps): # 1. 用ε-greedy选动作 if random.random() epsilon: action env.action_space.sample() else: with torch.no_grad(): q_values online_network(torch.FloatTensor(state)) action q_values.argmax().item() # 2. 执行动作存回放池 next_state, reward, done, _ env.step(action) replay_buffer.push(state, action, reward, next_state, done) state next_state if not done else env.reset() # 3. 从回放池采样一批数据 batch replay_buffer.sample(batch_size) # 4. 计算目标值 with torch.no_grad(): next_actions online_network(batch.next_states).argmax(dim1, keepdimTrue) next_q target_network(batch.next_states).gather(1, next_actions) targets batch.rewards gamma * (1 - batch.dones) * next_q # 5. 计算损失并更新 current_q online_network(batch.states).gather(1, batch.actions) loss F.mse_loss(current_q, targets) optimizer.zero_grad() loss.backward() torch.nn.utils.clip_grad_norm_(online_network.parameters(), 10.0) optimizer.step() # 6. 定期更新目标网络 if step % target_update_freq 0: target_network.load_state_dict(online_network.state_dict())这个骨架里每一步都有讲究。比如第4步里的(1 - batch.dones)是为了在终止状态时把未来回报截断否则网络会以为终止状态之后还有价值。这个细节如果漏了Q值会在终止状态附近发散。第5步的gather操作是按动作索引取Q值因为网络输出是所有动作的Q值你只需要当前动作的那个。6.3 超参数设置与调试顺序CS285作业里通常会给一组默认超参数但如果你想自己调我建议按以下顺序超参数推荐范围调试优先级说明学习率1e-4 ~ 1e-3高太大导致发散太小导致收敛慢回放池大小1e5 ~ 1e6中太小分布偏移严重太大训练慢batch size64 ~ 512中太小梯度噪声大太大内存不够目标网络更新频率100 ~ 1000高太频繁不稳定太慢Q值滞后折扣因子γ0.99低通常固定除非任务视野很短ε衰减1.0 → 0.05中衰减太快探索不足太慢收敛慢隐藏层大小256 ~ 512低太小欠拟合太大过拟合调试时先固定其他参数只调学习率和目标网络更新频率。如果训练损失震荡先降学习率如果Q值发散先增大目标网络更新频率。这两个调好之后再调回放池和batch size。ε衰减策略可以用线性衰减或者指数衰减我一般用线性衰减从1.0降到0.05持续总步数的10%左右。还有一个调试技巧在训练初期打印几个batch的TD误差和Q值范围。如果TD误差一开始就很大比如超过100说明奖励尺度或者初始化有问题。如果Q值范围很快变得很大比如超过1000说明过估计或者学习率太大。这些早期信号能帮你快速定位问题。7. 从Q-learning到后续课程这一讲留下的伏笔学完CS285第三讲你手里就有了一个能跑通离散动作空间Q-learning的完整框架。但这个框架有几个明显的局限后续课程会逐一解决。第一个局限是连续动作空间Q-learning的max操作在连续动作上没法直接做因为你需要对动作求argmax而神经网络输出的Q(s,a)对a是非凸的没有解析解。后续会讲DDPG和SAC它们用actor网络来近似argmax或者用重参数化技巧来采样动作。第二个局限是策略随机性Q-learning导出的是确定性贪婪策略但在很多任务里随机策略更好因为它能探索、能处理多模态的最优行为。后续会讲Soft Q-learning和SAC它们在Q值里加入熵项让策略保持随机性。第三个局限是样本效率虽然Q-learning比策略梯度样本效率高但在复杂任务里仍然需要百万级的环境交互。后续会讲基于模型的强化学习用学到的环境模型来生成虚拟数据进一步提高样本效率。我自己的体会是Q-learning这一讲是整个CS285里承上启下的关键。它上承MDP和动态规划下启深度Q-learning、DDPG、SAC等一系列算法。如果你这一讲没吃透后面看SAC的论文会觉得云里雾里因为SAC的Q网络训练目标里包含了Q-learning的TD更新、目标网络、Double Q-learning、熵正则化等一堆东西。反过来如果你把Q-learning的更新逻辑、目标网络、过估计问题都想清楚了后面那些算法只是在这个基础上加加减减。最后分享一个我踩过的坑不要一上来就追求SOTA性能。CS285的作业通常有多个任务从简单的CartPole到复杂的Humanoid。我当初在CartPole上跑通之后直接去跑Humanoid结果训练了几天都没收敛后来才发现是网络容量不够、回放池太小、奖励没缩放。正确的做法是先在简单任务上把超参数调稳然后逐步增加任务难度每次只改一个变量。这样你才能知道哪个参数对哪个任务重要而不是面对一个黑箱瞎调。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑