
1. 为什么我要从零手写一遍Q-Learning很多人第一次接触强化学习都是从Q-Learning开始的。它足够简单简单到用一张Excel表格就能手工推演但它又足够深刻深刻到几乎包含了强化学习所有核心概念的雏形——状态、动作、奖励、策略、价值函数、探索与利用的权衡。我见过太多人上来就跑深度强化学习的代码结果连贝尔曼方程在干什么都说不清楚调参全靠玄学。所以这篇文章我想把Q-Learning这件事从头到尾讲透不是那种“贴一段代码然后说它能跑”的教程而是把每一步为什么这么做、参数怎么定、坑在哪里全部摊开来讲。这篇文章适合谁看如果你已经会Python基础语法懂一点numpy想真正搞明白强化学习到底在学什么那这篇就是写给你的。如果你已经用过Q-Learning但总觉得哪里没吃透比如epsilon到底该设多少、学习率大了小了会怎样、为什么有时候Q值会爆炸那这篇也能帮你把那些模糊的地方补上。我会用一个经典的网格世界GridWorld作为贯穿案例从环境搭建、算法实现、参数调优到问题排查一步步走完。所有代码都是可以直接复制运行的没有隐藏依赖不需要GPU一台普通笔记本就能跑。先说清楚Q-Learning的定位。它属于无模型Model-Free、异策略Off-Policy、时序差分Temporal Difference, TD的强化学习算法。这三个标签每一个都有实际含义无模型意味着我们不需要知道环境的转移概率和奖励函数直接通过交互来学异策略意味着行为策略用来探索的策略和目标策略被评估和改进的策略可以不同时序差分意味着我们不需要等到回合结束才更新而是每一步都可以用下一步的估计值来更新当前值。理解了这三个标签你就理解了Q-Learning的设计哲学。2. Q-Learning的核心原理拆解2.1 从“值”的角度理解强化学习强化学习的目标说白了就是让智能体在某个环境里学会一套动作策略使得长期累积奖励最大化。这里的关键词是“长期”。有时候为了长远利益你得放弃眼前的小利这正是强化学习比监督学习难的地方——奖励是延迟的你做了一个动作可能过几十步才知道这个动作到底好不好。Q-Learning解决这个问题的思路是维护一张表记录在某个状态下做某个动作的“价值”。这个价值用Q值表示Q(s, a)的含义是“在状态s下执行动作a之后按照最优策略继续走下去能拿到的累积奖励的期望”。注意这里的“按照最优策略继续走”这是Q-Learning最精妙的地方——它在更新时用的是下一状态的最大Q值而不是实际执行的那个动作的Q值。这就是异策略的体现。用数学语言写出来就是Q(s, a) ← Q(s, a) α [r γ · max Q(s, a) - Q(s, a)]这个公式是整个算法的灵魂。α是学习率控制新信息覆盖旧信息的比例γ是折扣因子控制未来奖励的权重r是即时奖励max Q(s, a)是对下一状态所有可能动作中最大Q值的估计。中括号里的部分叫TD误差TD Error衡量的是“我原来的估计”和“我现在的估计”之间的差距。2.2 为什么用max而不是实际动作的Q值这个问题值得单独拿出来说因为它是Q-Learning和SARSA的分水岭。SARSA的更新用的是实际执行的下一个动作的Q值而Q-Learning用的是最大Q值。区别在哪假设智能体在一个悬崖边上最优策略是沿着悬崖边走因为路径最短但偶尔探索时会掉下去。SARSA因为用的是实际动作的Q值它会“知道”自己有时候会掉下去所以学到的策略会离悬崖远一点更保守。Q-Learning因为用的是max它假设下一步一定选最优动作所以学到的策略就是贴着悬崖走更激进。这不是谁好谁坏的问题而是适用场景不同。如果你在训练一个真实机器人掉下去就摔坏了那SARSA的保守策略更安全如果你在模拟环境里训练摔了可以重来那Q-Learning的激进策略能更快找到最优解。理解这一点你在实际项目中选算法时就不会盲目。2.3 探索与利用的平衡策略Q-Learning有一个绕不开的问题如果智能体一直选当前Q值最大的动作那它可能永远发现不了更好的动作。这就是探索Exploration和利用Exploitation的矛盾。最常用的解决方案是ε-贪婪策略以ε的概率随机选一个动作探索以1-ε的概率选当前Q值最大的动作利用。ε通常从一个较大的值开始比如1.0完全随机然后逐渐衰减到一个较小的值比如0.01几乎完全利用。衰减的方式可以是线性的、指数的或者按回合数分段衰减。我个人的经验是ε的衰减速度比初始值更重要。衰减太快智能体还没探索够就开始利用容易陷入局部最优衰减太慢训练时间会拉得很长。一个比较稳妥的做法是让ε在前60%的回合里从1.0线性降到0.1然后在剩余回合里从0.1缓慢降到0.01。当然具体数值要根据环境复杂度来调。3. 用GridWorld把算法跑起来3.1 环境设计一个5x5的网格世界为了让代码足够直观我用一个5x5的网格世界作为环境。规则很简单智能体从左上角出发目标是到达右下角。每走一步奖励-1到达目标奖励10。网格里有一些障碍物撞上去奖励-5并且原地不动。动作空间是上下左右四个方向。为什么选这个环境因为它足够小小到你可以手工验证每一步的Q值更新是否正确同时它又包含了强化学习的核心挑战——延迟奖励、障碍物惩罚、路径规划。你把这个环境跑通了换成更复杂的环境比如FrozenLake、Taxi只是改一下状态转移逻辑的事。环境的状态可以用一个整数表示比如state row * 5 col。动作可以用0,1,2,3表示上下左右。奖励函数和转移函数都写死在环境类里。这里有一个细节到达目标状态后回合结束不再有后续状态。这个终止条件在代码里要处理好否则Q值更新会出错。3.2 Q表的初始化与更新逻辑Q表就是一个二维数组行数是状态数25列数是动作数4。初始化通常全设为0也可以设成一个小的随机值。我倾向于全设0因为这样更容易复现结果而且对于小规模问题从0开始收敛速度已经够快了。更新逻辑就是前面那个公式的直接翻译。每一步智能体根据ε-贪婪策略选一个动作执行后得到奖励和下一状态然后计算TD误差并更新Q表。如果下一状态是终止状态那么max Q(s, a)直接设为0因为终止状态没有后续动作。这里有一个容易踩的坑Q表的索引顺序。如果你用numpy数组Q[state, action]和Q[action, state]是完全不同的。我建议统一用Q[state, action]因为这样在取max的时候可以直接用np.max(Q[next_state, :])代码更简洁。另外状态和动作的映射关系要写清楚注释否则过两天回来看代码自己都忘了。3.3 完整代码实现与逐行注释import numpy as np import random class GridWorld: def __init__(self, size5): self.size size self.n_states size * size self.n_actions 4 # 0:上, 1:下, 2:左, 3:右 self.start_state 0 self.goal_state size * size - 1 # 障碍物位置用状态编号表示 self.obstacles [6, 7, 12, 17, 18] self.max_steps 100 def reset(self): self.state self.start_state self.steps 0 return self.state def step(self, action): self.steps 1 row self.state // self.size col self.state % self.size if action 0: row max(0, row - 1) elif action 1: row min(self.size - 1, row 1) elif action 2: col max(0, col - 1) elif action 3: col min(self.size - 1, col 1) next_state row * self.size col if next_state in self.obstacles: next_state self.state reward -5 elif next_state self.goal_state: reward 10 else: reward -1 done (next_state self.goal_state) or (self.steps self.max_steps) self.state next_state return next_state, reward, done环境类写好了接下来是Q-Learning的主体。我把它写成一个函数方便调参。def q_learning(env, n_episodes1000, alpha0.1, gamma0.99, epsilon_start1.0, epsilon_end0.01, decay_ratio0.6): Q np.zeros((env.n_states, env.n_actions)) rewards_history [] decay_episodes int(n_episodes * decay_ratio) for episode in range(n_episodes): state env.reset() total_reward 0 done False # epsilon线性衰减 if episode decay_episodes: epsilon epsilon_start - (epsilon_start - epsilon_end) * (episode / decay_episodes) else: epsilon epsilon_end while not done: # epsilon-贪婪策略选择动作 if random.random() epsilon: action random.randint(0, env.n_actions - 1) else: action np.argmax(Q[state, :]) next_state, reward, done env.step(action) total_reward reward # Q值更新 if done: td_target reward else: td_target reward gamma * np.max(Q[next_state, :]) Q[state, action] alpha * (td_target - Q[state, action]) state next_state rewards_history.append(total_reward) return Q, rewards_history这段代码里有两个地方值得展开说。第一epsilon的衰减我用了线性方式并且把衰减过程限制在前60%的回合里后40%保持最小epsilon。这样做的好处是前期探索充分后期策略稳定。第二终止状态的TD目标直接等于奖励没有加gamma乘以max Q因为终止状态没有后续动作。这个细节如果处理错了Q值会在目标状态附近出现异常。3.4 训练过程与结果验证跑1000个回合每100个回合打印一次平均奖励。你会看到平均奖励从最初的-100左右因为随机走经常撞障碍物逐渐上升到-10以内接近最优路径。最优路径大概是8步左右每步-1加上终点10总奖励大约是2。如果训练正常最后几百个回合的平均奖励应该稳定在0到3之间。验证策略是否最优的方法很简单把epsilon设为0从起点开始跑一个回合打印出路径。如果路径是沿着最短路线避开障碍物到达终点说明Q表已经收敛。你也可以把Q表打印出来看看每个状态下四个动作的Q值最大的那个是否指向合理的方向。4. 参数调优与常见问题排查4.1 学习率α和折扣因子γ怎么选学习率α控制每次更新的步长。α太大Q值会震荡甚至发散α太小收敛速度慢得让人抓狂。对于GridWorld这种确定性环境α0.1是一个比较稳妥的起点。如果你发现训练曲线波动很大试着降到0.05如果收敛太慢可以提到0.2但一般不建议超过0.3。折扣因子γ控制未来奖励的权重。γ0.99意味着智能体相当“有远见”愿意为了10步后的奖励放弃眼前的利益。γ0.9则更短视。对于GridWorldγ0.99是合适的因为最优路径大概8步折扣后的未来奖励仍然有足够的权重。如果γ太小比如0.5智能体可能学不会绕开障碍物因为绕路的代价在折扣后变得微不足道。有一个经验公式可以参考γ的取值应该使得有效视野大约1/(1-γ)步覆盖到目标所需的最大步数。GridWorld最大步数100但最优路径8步所以1/(1-0.99)100足够覆盖。如果环境更大比如20x20最优路径可能40步那γ0.99仍然够用但训练回合数要增加。4.2 Q值爆炸与震荡的排查思路Q值爆炸是Q-Learning最常见的问题之一。表现是Q值越来越大最终变成inf或nan。原因通常有三个学习率太大、奖励尺度太大、或者更新公式写错了。排查步骤我一般是这样先检查更新公式确认td_target的计算没有多加或少加gamma然后把学习率降到0.01试试如果Q值不再爆炸说明是学习率的问题如果还不行检查奖励函数把奖励缩放到[-1, 1]区间再试。奖励缩放是一个很实用的技巧尤其是当你的环境奖励有正有负且量级差异大时。Q值震荡的表现是训练曲线上下波动没有明显的收敛趋势。这通常是因为epsilon衰减太快智能体在探索不足的情况下就锁定了某个次优策略。解决办法是放慢epsilon衰减或者增加训练回合数。另外如果环境本身有随机性比如动作有概率失败Q值震荡是正常的这时候要看平均奖励的趋势而不是单个回合的奖励。4.3 训练不收敛的五个典型原因问题现象可能原因排查方法解决方案平均奖励一直上不去epsilon太大或衰减太慢打印每回合的epsilon值加快衰减或降低epsilon_end策略在几个状态间循环障碍物惩罚不够或奖励设计不合理检查奖励函数增加障碍物惩罚或调整目标奖励Q值全为0或变化极小学习率太小或奖励太小打印Q表统计量提高学习率或放大奖励到达目标后Q值异常终止状态处理错误检查done时的TD目标终止状态不加gamma*max训练后期性能下降过拟合或epsilon太小对比不同epsilon下的表现保持最小epsilon在0.01以上这张表是我在实际项目中总结出来的基本上覆盖了90%的Q-Learning问题。遇到问题的时候按表排查比盲目调参高效得多。4.4 收敛速度优化的三个实用技巧第一个技巧是乐观初始化。把Q表初始化为一个较小的正值比如0.1而不是0。这样智能体在早期会倾向于尝试那些还没访问过的状态-动作对因为它们的Q值“看起来”比已经访问过的被更新后可能变成负值更高。这本质上是一种内置的探索机制能显著加快早期探索速度。第二个技巧是经验回放。虽然Q-Learning是异策略算法理论上可以用经验回放但在表格型Q-Learning里用得不多。不过如果你的环境状态数很多可以考虑把最近的经验存下来每次更新时随机抽一批来更新。这样做的好处是打破经验之间的相关性让Q值更新更稳定。代价是需要更多内存和计算。第三个技巧是动态调整学习率。训练初期用大学习率快速探索训练后期用小学习率精细调整。实现方式可以是α α_0 / (1 episode / K)其中K是一个常数。这样α会随着回合数增加而逐渐减小收敛更稳定。我在几个项目里试过相比固定学习率动态学习率能让最终策略的稳定性提升不少。5. 从表格到神经网络Q-Learning的扩展思路5.1 什么时候表格不够用表格型Q-Learning有一个硬性限制状态数必须是有限的而且不能太大。如果状态是连续的比如机器人的关节角度或者状态数是指数级的比如围棋的棋盘状态表格就存不下了。这时候就需要用函数逼近最常用的就是神经网络也就是DQNDeep Q-Network。从表格到神经网络的过渡核心变化是不再维护一张Q表而是训练一个神经网络输入状态输出每个动作的Q值。更新的时候损失函数是TD误差的平方用梯度下降来更新网络参数。听起来简单但实际做起来有几个坑样本相关性、目标网络、经验回放这些在表格型Q-Learning里都不是问题但在DQN里必须处理。5.2 经验回放与目标网络的作用经验回放解决的是样本相关性问题。在强化学习里连续的经验是高度相关的这一步和下一步的状态很相似如果直接用这些样本来训练神经网络会导致网络在局部样本上过拟合训练不稳定。解决办法是把经验存到一个缓冲区里每次训练时随机抽一批这样样本之间就近似独立了。目标网络解决的是目标值不稳定的问题。在表格型Q-Learning里TD目标用的是当前的Q表更新后Q表变了目标也变了但因为是表格变化是局部的、可控的。在神经网络里目标值依赖于网络参数网络参数一变目标值也跟着变容易导致训练震荡。解决办法是维护两个网络一个在线网络用来选动作和更新一个目标网络用来计算TD目标目标网络的参数定期从在线网络复制过来这样目标值在一段时间内是稳定的。这两个技巧在表格型Q-Learning里不是必须的但在DQN里是标配。如果你打算从表格过渡到神经网络建议先把这两个概念搞清楚再动手写代码。5.3 表格型Q-Learning的适用边界虽然DQN很强大但表格型Q-Learning并没有过时。在很多实际问题里状态空间是离散且有限的比如库存管理、推荐系统的粗排、简单的游戏AI表格型Q-Learning完全够用而且训练速度快、可解释性强、调参简单。我个人的建议是能用表格就用表格实在不行再上神经网络。不要为了用新技术而用新技术解决问题才是目的。表格型Q-Learning的适用边界大概是状态数在10^6以内动作数在100以内环境是离散的或者可以合理离散化。超过这个范围表格的内存和训练时间就会成为瓶颈。另外如果状态之间的泛化很重要比如相似状态应该有相似Q值表格型Q-Learning需要额外的泛化机制比如状态聚合而神经网络天然具备泛化能力。6. 几个我踩过的坑和实操心得第一个坑是状态编码不一致。有一次我在环境里用(row, col)元组表示状态在Q表里用整数索引结果在更新的时候忘了转换导致Q表更新到了错误的位置。训练了半天策略完全不对。后来我养成了一个习惯在环境类里统一用整数表示状态所有对外接口都返回整数这样就不会出现编码不一致的问题。第二个坑是奖励设计不合理。我一开始把障碍物惩罚设成-1和目标奖励10相比太小了智能体宁愿撞障碍物也不愿意绕路。后来把障碍物惩罚改成-5智能体才学会避开。奖励设计是强化学习里最玄学的部分没有固定公式只能多试。我的经验是惩罚要足够大让智能体“疼”到记住但也不能太大否则智能体会变得过于保守不敢探索。第三个坑是epsilon衰减太快。有一次我设了decay_ratio0.2结果智能体在前200个回合就完成了探索后面800个回合都在利用一个次优策略。训练曲线看起来收敛了但实际策略不是最优的。后来我把decay_ratio调到0.6问题解决。这个参数没有理论最优值只能根据环境复杂度和训练预算来试。第四个坑是忘了处理终止状态。在GridWorld里到达目标后回合结束但我的代码里没有正确处理done时的TD目标导致目标状态的Q值被反复更新越来越大。后来加上if done: td_target reward的判断问题解决。这个坑很隐蔽因为训练曲线可能看起来正常但策略在目标附近会表现异常。最后分享一个调试技巧把Q表打印出来看。对于小规模问题直接把Q表打印成矩阵每个状态一行每个动作一列看看最大值是否指向合理的方向。如果某个状态的Q值全是0或者全是负数说明这个状态很少被访问到可能需要调整探索策略。这个技巧虽然原始但非常有效比看训练曲线直观得多。另外如果你发现训练结果每次都不一样不要慌这是正常的。Q-Learning的探索过程有随机性不同的随机种子会得到不同的训练轨迹。判断算法是否正常要看多次运行的平均表现而不是单次结果。我一般会跑5次取平均奖励曲线如果5次的趋势一致说明算法是稳定的。