news 2026/10/29 20:14:24

Q-Learning表格法实战:从贝尔曼方程到ε-greedy收敛

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Q-Learning表格法实战:从贝尔曼方程到ε-greedy收敛

简介:面向强化学习入门者与需要动手复现经典算法的开发者,这份资源用 Python 完整实现 Q Learning,重点解决在无法预知全部状态与动作空间时如何构建并更新 Q 表、如何用 epsilon-greedy 平衡探索与利用的问题。压缩包共 5 个文件,以 py 脚本为核心,辅以 md 说明、pdf 报告及 license 等配套文件,整体约 852KB,体量轻便,便于直接阅读与运行。已有 1111 人学习下载,说明其在同类入门材料中具有一定参考价值。代码围绕 Q 函数更新公式展开,涵盖 Q 表初始化、动作选择、奖励回传与学习率、折扣因子调节等关键环节,并涉及用 gym 的 CartPole 环境做验证的思路,读者可据此理解状态表示与离散化处理,快速搭建自己的实验流程并对照报告梳理算法细节。

1. Q-Learning 的“玄学收敛”:为什么一张表就能学会走路

很多人第一次跑 Q-Learning,最困惑的不是公式,而是那种“玄学”感:明明只维护了一张表,没有神经网络、没有梯度、没有反向传播,智能体却在几百个回合后突然像开了窍,路径越走越短。更反直觉的是,这张表初始时全是零,它对世界一无所知,却能在一次次撞墙、掉坑、绕远路之后,把“在哪个格子往哪走”这件事记下来。这背后没有魔法,只有一个极其朴素的信念:一个状态动作的价值,等于即时奖励加上“之后能拿到的最好价值”的折扣。整份资源就是围绕这个信念,用纯 Python 把表格、探索、更新、收敛这条链路完整跑通。搞懂它,你就搞懂了几乎所有强化学习算法的祖先——后面那些深度 Q 网络、策略梯度,无非是把这张表换成了函数逼近。而“为什么一张表能 work”这个问题,恰恰是理解强化学习底层机制最好的入口。

2. 贝尔曼方程落地:Q 表到底在存什么

2.1 从“价值”这个词说起

Q-Learning 里最容易被含糊带过的概念就是 Q 值。很多人背得出更新公式,却说不清 Q(s,a) 到底代表什么。我一般会这么理解:它是对“在状态 s 下先执行动作 a,然后一直按最优策略走到结束,累计能拿到的折扣奖励”的估计。注意两个关键词——先执行 a,之后按最优走。这意味着 Q 值不是对某条固定轨迹的打分,而是对“这个起手式好不好”的打分。折扣因子 γ 决定了智能体有多短视:γ 接近 0,它只在乎下一步的即时奖励;γ 接近 1,它愿意为长远收益等待。这个定义直接来自贝尔曼最优方程:

Q*(s,a) = r + γ · max Q*(s', a')

左边是当前状态动作的真实最优价值,右边是即时奖励加上下一状态里最好动作的价值打折。这个等式之所以成立,是因为“最优”具有递归结构:如果你从 s' 开始已经走的是最优路线,那么从 s 出发的最优路线就是“走一步到 s',然后接上那条最优路线”。Q-Learning 做的事情,本质上就是用采样到的 (s, a, r, s') 四元组,一次次把等式右边算出来,去逼近左边的估计值。表里存的不是“正确答案”,而是这个等式在当前经验下的一个不断修正的近似。

2.2 Q 表的数据结构选择

既然是表格法,第一个工程决策就是表怎么存。状态和动作都是离散的,最自然的实现是用嵌套字典:外层键是状态,内层键是动作,值是浮点 Q 值。相比用二维数组,字典的好处是状态不必是连续整数,可以是元组坐标、字符串编码,甚至任意可哈希对象,这让同一份代码能直接套到网格世界、迷宫、简单博弈上。代价是查表有哈希开销,状态空间大时会吃内存——这也正是后来必须转向函数逼近的根本原因。

# Q 表:state -> {action: q_value} # 用 defaultdict 省去“状态首次出现要手动建内层字典”的样板代码 from collections import defaultdict class QTable: def __init__(self, actions, alpha=0.1, gamma=0.9): self.q = defaultdict(lambda: {a: 0.0 for a in actions}) self.actions = actions self.alpha = alpha # 学习率:新经验覆盖旧估计的幅度 self.gamma = gamma # 折扣因子:未来奖励的衰减权重 def value(self, state, action): return self.q[state][action] def best(self, state): # 返回 (最优动作, 最优Q值),用于更新时的 max 项和决策时的贪心选择 items = self.q[state].items() return max(items, key=lambda kv: kv[1])

这段代码里有两个细节值得说。第一,defaultdict的工厂函数在状态第一次被访问时自动生成全零的动作字典,这保证了“未见过状态”的 Q 值默认为 0,符合“一无所知”的初始假设。第二,best方法同时返回动作和值,是因为更新公式需要的是 max 的数值,而决策需要的是 argmax 的动作,两者复用同一段逻辑能避免不一致。学习率 α 控制每次更新往目标值靠多少:α=1 表示完全用新经验覆盖,α 很小表示保守地慢慢挪。实践中 α 取 0.1 到 0.5 之间比较常见,太大容易震荡,太小收敛慢得让人怀疑人生。

2.3 更新公式的时序拆解

把更新公式拆成时序,能看清它为什么是“离线”的。所谓 off-policy,指的是更新时用的 max 项来自“假设下一步选最优动作”,而不是智能体实际选的那个动作。也就是说,哪怕智能体这一步因为探索随机走了一个烂动作,它在更新当前 Q 值时,仍然按“下一步会走最好”来估算未来收益。这个设计让 Q-Learning 可以边探索边学最优策略,行为策略和目标策略解耦。代价是它在某些随机环境下会高估 Q 值——因为 max 操作对噪声正偏,这也是后来 Double Q-Learning 要修的问题。常见的实现思路是:先算目标值target = r + gamma * max_q_next,再算误差td_error = target - q_current,最后q_current += alpha * td_error。这个 td_error 就是时序差分误差,整个强化学习的更新几乎都围绕它转。

3. 探索与利用的拉锯:ε-greedy 为什么不能一路贪心

3.1 纯贪心的死局

如果智能体从一开始就按当前 Q 值最大的动作走,会发生什么?初始 Q 全是 0,所有动作并列最大,它可能永远只走第一个动作,撞墙撞到天荒地老,永远发现不了旁边那条能拿奖励的路。这就是纯贪心的死局:没有探索,就没有新经验;没有新经验,Q 值永远不更新;Q 值不更新,策略永远不变。这个循环一旦锁死,再多的回合数也是白跑。血泪经验是:很多人调参时把探索率设得太低,看到 loss 不降就以为是学习率问题,其实是智能体压根没去过关键状态。

3.2 ε-greedy 的机制与衰减策略

ε-greedy 用一个小概率 ε 强制随机选动作,剩下 1-ε 的概率走贪心。它的精妙之处在于简单到不需要任何额外状态,却能保证每个动作都有被尝试的机会。但 ε 怎么设是有讲究的:固定 ε 会让策略永远带随机性,学到的 Q 值对应的策略和实际执行策略不一致;ε 衰减则让智能体早期多探索、后期多利用,逐渐收敛到贪心策略。常见的实现思路是线性衰减或指数衰减,比如从 1.0 降到 0.05。

import random def epsilon_greedy(qtable, state, epsilon): # 以 epsilon 概率随机探索,否则走当前最优 if random.random() < epsilon: return random.choice(qtable.actions) # 探索:打破贪心死局 action, _ = qtable.best(state) return action def decay_epsilon(episode, total_episodes, start=1.0, end=0.05): # 线性衰减:前期大胆试错,后期稳定利用 ratio = min(episode / (total_episodes * 0.8), 1.0) return start + (end - start) * ratio

这里有个容易翻车的点:衰减曲线的前 80% 回合用来降 ε,后 20% 保持最低探索率。如果一路衰减到 0,智能体在后期完全贪心,一旦早期学到的 Q 值有偏差,就再也没有机会纠正。保留一个小的 ε 下限,相当于给策略留了后悔药。另一个细节是random.choice应该在所有动作上均匀采样,而不是只在“看起来还行”的动作里选——探索的意义恰恰在于尝试那些当前评估很差的动作,因为评估本身可能就是错的。

3.3 探索不足与探索过度的症状对照

调参时怎么判断 ε 设得对不对?看两条曲线:回合总奖励和每回合步数。探索不足的典型症状是总奖励长期卡在一个较低平台,步数居高不下,智能体反复走同一条死路;探索过度的症状是奖励波动极大,偶尔能拿到高分但稳定不下来,因为每次都在随机乱走。健康的曲线应该是奖励逐步抬升、步数逐步下降,中间允许有小幅回撤。如果跑了上千回合还是原地踏步,先别怀疑算法,把 ε 下限调高、把衰减放慢,往往立刻见效。

4. 一个回合怎么跑:训练循环的源码级拆解

4.1 环境交互的骨架

Q-Learning 的训练循环本质是一个双层循环:外层是回合(episode),内层是单回合内的步步交互,直到到达终止状态或触发步数上限。每一步做四件事:选动作、执行动作拿反馈、用四元组更新 Q 表、转移到下一状态。这个骨架简单到可以手写,但每个环节都有坑。

def train(env, qtable, episodes=1000, max_steps=200): rewards_history = [] for ep in range(episodes): state = env.reset() # 每回合重置到起点 epsilon = decay_epsilon(ep, episodes) total_reward = 0.0 for step in range(max_steps): action = epsilon_greedy(qtable, state, epsilon) next_state, reward, done = env.step(action) # 核心更新:目标值 = 即时奖励 + 折扣后的下一状态最优价值 _, max_q_next = qtable.best(next_state) target = reward + qtable.gamma * max_q_next td_error = target - qtable.value(state, action) qtable.q[state][action] += qtable.alpha * td_error state = next_state total_reward += reward if done: break # 到达终止状态,本回合结束 rewards_history.append(total_reward) return rewards_history

这段代码里最值得盯的是done的处理。终止状态的 Q 值不应该再往后传播未来收益,因为游戏已经结束了。上面写法里,终止状态的best会返回 0(因为没被更新过),所以target = reward + gamma * 0 = reward,恰好等价于“终止状态无未来”。但如果环境在终止后还允许访问 next_state 并给它赋了非零 Q 值,就会引入错误的自举。稳妥的做法是在done为真时直接把 target 设为 reward,不乘 gamma。这个细节在简单网格里可能看不出差别,换到有多个终止状态的环境就会暴露。

4.2 学习率与折扣因子的耦合

α 和 γ 不是独立的。γ 越大,未来奖励的权重越高,目标值的方差也越大,此时 α 应该相应调小,否则每次更新都被高方差的远期估计带偏。反过来,γ 很小的时候目标值主要由即时奖励决定,方差小,α 可以大一些加快收敛。一个实用的起点是 γ=0.9、α=0.1,然后根据奖励曲线的抖动程度微调。如果曲线剧烈震荡,先降 α;如果智能体明显短视、不肯为远处的奖励绕路,先升 γ。这两个参数调反了,会出现“越调越乱”的假象。

4.3 收敛判据怎么看

表格法不像神经网络有 loss 可以盯,判断收敛主要看策略是否稳定。一个可操作的判据是:连续若干回合的最优动作序列不再变化,且回合奖励的滑动平均进入平台期。更严格一点,可以监控 Q 表的最大变化量,当所有状态的 max |td_error| 都小于某个阈值时,说明贝尔曼方程已经基本被满足。但要注意,Q 值收敛不等于策略最优——在存在多个等价最优路径的环境里,Q 值可能还在缓慢调整而策略早已稳定。所以实践中最靠谱的还是看奖励曲线加人工抽查几条轨迹。

5. 避坑与排查:那些让 Q 表学不会的常见问题

5.1 现象:奖励一直是 0,Q 表纹丝不动

原因通常是奖励设计有问题。如果只有到达终点才给 +1,其余全是 0,那么在前若干回合智能体根本到不了终点,所有 Q 值更新都是 0,表永远是零,策略永远是随机。解决方法是加中间引导奖励,比如每走一步给一个小的负值(鼓励走近路),或者对靠近终点的状态给塑形奖励。但塑形要克制,给太多会让智能体学会“刷步数”而不是真正到达目标。

5.2 现象:训练后期表现突然崩掉

这多半是 ε 衰减到 0 之后,策略完全贪心,而早期 Q 值里的偏差再也没机会被纠正。也可能是学习率在后期仍然偏大,导致 Q 值在最优值附近来回跳。解决办法是保留 ε 下限(比如 0.05),并在后期把 α 调小,让更新更稳。我一般会在训练后段把 α 乘一个衰减系数,效果比固定 α 好不少。

5.3 现象:同一份代码换个环境就不收敛

常见原因是状态表示不一致。比如网格坐标有时是 (row, col),有时是 row*width+col,如果 reset 和 step 返回的编码方式不同,Q 表会把同一个物理状态当成两个不同状态,学到的经验被割裂。排查方法是打印前几个回合的 state 和 next_state,确认编码统一、可哈希、且终止状态能被正确识别。另一个隐蔽原因是动作集合的顺序在不同地方不一致,导致 argmax 选出的动作和实际执行的动作对不上。

5.4 现象:内存暴涨或运行越来越慢

字典实现的 Q 表在状态空间大时会持续膨胀,尤其是状态里混入了连续值或时间戳。如果发现内存曲线一路上扬,先检查状态是否被无意中做成了无限集合。解决思路是离散化状态、限制状态编码的粒度,或者干脆换成数组实现并预分配。表格法适合的是状态数可控的离散问题,硬套到高维连续环境上,慢和爆内存是必然的,这时候该考虑的是函数逼近而不是继续调参。

6. 表格法的边界:什么时候该放下这张表

Q-Learning 用一张表能 work,前提是状态动作对可以被枚举。这个前提在网格世界、简单卡牌、小型博弈里成立,但一旦状态维度上去,表的规模是指数爆炸的。假设每个维度离散成 100 个取值,5 个维度就是 100 亿个状态,字典再省也存不下。这不是实现问题,是表格法的方法论边界:它用“记住每一个具体情形”来替代“归纳规律”,而现实问题需要的是后者。理解这一点,才能明白为什么深度 Q 网络要用神经网络去逼近 Q 函数——不是表格不好,是表格的存储假设撑不住了。

但反过来,表格法恰恰是验证你对强化学习理解是否到位的试金石。因为表里的每个数字你都能打印出来看,每次更新你都能手算核对,贝尔曼方程有没有被正确实现、探索策略有没有生效、奖励设计有没有漏洞,全都暴露在明面上。我一般建议先用表格法在一个小环境里把整条链路跑通、把曲线调顺,再去碰函数逼近。跳过这一步直接上神经网络,出了问题你根本分不清是算法错、超参错还是环境错,黑匣子里全是玄学。

这份资源的价值也在这里:它把 Q 表、ε-greedy、时序差分更新、训练循环这几块用纯 Python 串成一个可运行、可打印、可手算核对的完整实现。没有框架封装,没有隐藏的状态管理,每一行逻辑都摊开在你面前。想真正搞懂 Q-Learning 为什么能收敛、更新公式里每个符号在代码里对应哪一步,拿这份代码跑一遍、改改参数、打印 Q 表看它怎么变,比读十篇公式推导都管用。需要的话,按标题里的文件名去取这份完整代码,从最小的网格环境开始,把上面这些坑一个个踩过去,你对强化学习底层机制的理解会扎实得多。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/29 20:11:58

基于Spring Boot的TMS运输管理系统:单据链、状态机与运费核算

简介&#xff1a;面向物流运输企业与Java后台开发学习者的TMS运输管理系统压缩包&#xff0c;围绕订单管理、路线规划与优化、车辆司机调度、成本控制、货物追踪、报表分析及ERP/WMS接口集成等场景&#xff0c;提供一套可参考、可扩展的管理系统实现思路&#xff0c;适合需要理…

作者头像 李华
网站建设 2026/10/29 20:11:58

JavaWeb教务管理系统CRUD全链路与事务权限避坑

简介&#xff1a;这是一套面向JavaWeb初学者的教务管理系统项目源码&#xff0c;采用MVC设计模式与DAO模式&#xff0c;结合Servlet、JSP、Filter及MySQL数据库实现&#xff0c;运行于Windows下的Tomcat 7与JDK 1.8环境&#xff0c;开发工具为Eclipse与Navicat。系统按登录、学…

作者头像 李华
网站建设 2026/10/29 20:11:46

彩影ARP防火墙6.02安装配置全指南:Win7/2008R2下驱动级ARP防护实战

简介&#xff1a;彩影ARP防火墙6.02简体中文单机版是一款面向局域网终端用户&#xff08;尤其适用于32位Windows系统&#xff09;的专业级ARP防护工具&#xff0c;专为解决ARP欺骗、IP冲突、内网中毒传播及DoS攻击等常见局域网安全问题而设计。资源包为RAR压缩格式&#xff0c;…

作者头像 李华
网站建设 2026/10/29 20:09:29

食物图像识别落地:数据增强、迁移学习与ONNX量化部署

简介&#xff1a;围绕卷积神经网络实现食物图像识别的完整实践项目&#xff0c;面向具备 Python 与深度学习基础、希望打通数据预处理到模型评估全流程的学习者与开发者。项目以 TensorFlow/Keras 构建 CNN&#xff0c;覆盖卷积、池化、全连接与激活函数等核心结构&#xff0c;…

作者头像 李华
网站建设 2026/10/29 20:09:06

C#自绘工控仪表控件:圆表直表旋钮开关与指示灯实现

简介&#xff1a;面向C#工控界面开发者的仪表控件示例包&#xff0c;聚焦圆表、直表&#xff08;测量标尺&#xff09;、旋钮、开关与指示灯等常见仿真控件的实现&#xff0c;适合需要为组态软件或设备监控界面补充可视化反馈的初、中级开发者参考。压缩包共18个文件、约1.15MB…

作者头像 李华
网站建设 2026/10/29 20:08:36

网页变化监控实战指南:用Ponytail把竞品盯梢变成自动化流程

做电商运营的朋友前两天跟我抱怨&#xff0c;说每天上班第一件事就是挨个刷新十几个竞品页面&#xff0c;看价格变没变、库存还剩多少&#xff0c;一上午就这么没了。我听完直接给他推荐了Ponytail——一个专门做网页变化监控的浏览器插件。这篇文章不写官方说明书&#xff0c;…

作者头像 李华