
简介一份Python实现Q Learning强化学习的完整资源包面向正在学习机器学习或强化算法的高校学生和开发者也适合作为人工智能课程的实验参考。资源从最基础的Q表构建讲起详细演示了epsilon-greedy动作选择、奖励反馈、Q值更新等关键步骤并以gym中的CartPole环境为示例帮助读者直观理解智能体如何通过反复试错逐步获得最优策略。整个流程清晰易读适合逐行查看代码并尝试修改学习率、折扣因子等参数。压缩包共5个文件包括可运行的Python源码、README技术说明、PDF实验报告、LICENSE许可证及配套辅助文件整体大小约852KB既能直接运行观察训练效果也能结合报告中的调参分析与收敛曲线深入学习。该资源已有1110人学习下载在CSDN社区中获得较多用户关注。通过学习读者将掌握Q表的初始化与更新规则理解学习率与折扣因子对训练效果的影响并具备将Q Learning应用于简单离散状态空间问题的能力为后续学习深度强化学习奠定扎实基础。1. 一个走迷宫的小车Q Learning 到底在解决什么问题假设你有一个 5×5 的网格迷宫小车从左上角出发右下角放着一颗宝石。让小车自己尝试一开始它随机乱撞几十回合后开始避开墙壁几百回合后能找到最短路径。这个 zip 里的 Python 工程就是把“学会”这个过程完整实现并可视化出来的 Q Learning 强化学习代码包。它不教你理论推导而是给你一套能直接跑的完整代码Q 表更新、epsilon-greedy 动作选择、训练循环、奖励曲线绘制和策略渲染一应俱全。适合正在入门强化学习、需要做课程设计或者想在自定义环境中快速试跑 Q 算法的开发者和学生。你拿到的不只是算法片段而是一个能调参、能改环境、能画图的完整工程。2. 项目结构与运行准备把 zip 解压后第一件事做什么2.1 目录结构先看清每个文件是干嘛的zip 解压后是一个扁平工程常见目录结构如下ql_project/ ├── agent.py # QLearningAgent核心决策与学习类 ├── env.py # GridWorldEnv网格环境与奖励逻辑 ├── train.py # 训练主脚本跑整个学习过程 ├── plot_rewards.py # 绘制每轮奖励曲线 ├── render.py # 可视化展示策略结果 ├── config.py # 超参数配置 └── requirements.txt # 依赖列表这个结构不是随便拍的它对应强化学习里最经典的“环境-智能体-训练”三段式。env.py 负责定义状态、动作、奖励和终止条件agent.py 负责维护 Q 表并做决策train.py 把两者串起来跑出 Q 表后再用 render.py 把学习到的策略画出来。这样分层之后改环境不用动智能体改算法不用动环境二次开发时非常顺手。项目中唯一有点“工程化”味道的是 config.py。所有超参数集中在里面而不是散落在各脚本里。你调整学习率、gamma、epsilon 衰减系数时只需要改这一个文件不用翻代码。对于新手来说这个设计能避免很多改错地方的尴尬。我见过不少人直接改 train.py 里的魔法数字结果是每次换参数都要 git diff 对比一遍浪费时间。2.2 环境准备Python 3.8、numpy、matplotlib运行项目前先确认你的电脑上已经有 Python。这个工程面向 Q Learning 最朴素的实现依赖很少核心只有两个numpy 负责矩阵运算matplotlib 负责绘图。官方推荐用虚拟环境隔离依赖我一般会这么做python -m venv ql_env source ql_env/bin/activate # Windows 下用 ql_env\Scripts\activate pip install numpy matplotlib为什么用虚拟环境因为你的机器上可能已经装了不同版本的 numpy换一个项目就污染一个环境时间长了会出现各种“在 A 电脑能跑在 B 电脑报错”的玄学问题。虚拟环境把 numpy 和 matplotlib 锁在当前目录下等 zip 里的项目跑完删掉这个目录就行不会留下后患。如果你连 numpy 都没有也可以直接安装。requirements.txt 内容也很简单numpy1.20 matplotlib3.3这里没写 python 版本号但实际上 numpy 1.20 以上要求 python 3.7稳妥起见用 python 3.8 或更高。我曾经在 python 3.6 上装新版 numpy 失败后来升级到 3.8 就顺手了所以这也是一个值得记住的坑。如果你的 pip 下载缓慢可以在命令后面追加-i https://pypi.tuna.tsinghua.edu.cn/simple使用国内镜像。这不是项目要求的操作但能节省大量等待时间。注意镜像地址可能变更遇到连接失败时先检查网络。2.3 运行 demo训练、看奖励曲线、看策略渲染环境准备完成后直接跑训练脚本python train.pytrain.py 默认会运行 500 个 episode轮次每个 episode 从起点出发直到到达终点或步数超限。训练过程中终端会打印每 10 个 episode 的平均奖励帮助我们观察学习趋势。训练结束后Q 表会以 .npy 格式保存到磁盘比如 q_table.npy。接下来验证学习结果画奖励曲线python plot_rewards.py --rewards output.npy --save rewards_curve.pngplot_rewards.py 读取训练时保存的奖励序列绘制一条随 episode 变化的曲线。如果曲线整体稳定上升后保持在一个较高水平说明 Q 表在收敛。如果曲线剧烈震荡或长期低下说明参数或奖励设计有问题这时候需要回头检查 config.py。最后是策略可视化python render.py --qtable q_table.npy --episodes 3render.py 会把每步动作以箭头画在网格上同时用不同颜色表示起点、终点、墙壁和当前智能体位置。运行后你能直观看到小车是否学会了走迷宫而不是在墙角打转。可以多跑几个 episode 看策略是否稳定因为 epsilon 在测试时通常会被置为 0避免随机动作干扰判定。如果你只想快速体验可以合在一起python train.py python render.py --qtable q_table.npy这样训练完立刻渲染十几秒就能看到效果。注意 render.py 默认使用训练阶段保存的最后一张 Q 表如果你刚刚改过 config.py 而没重新训练看到的一定是旧结果。忘记重新训练就去渲染是我见过最多的误用。train.py 也支持命令行参数例如python train.py --episodes 1000 --alpha 0.2 --gamma 0.9 --epsilon 1.0 --epsilon_decay 0.995这样不用打开 config.py 也能覆盖默认值。对于做对比实验的场景这个设计很实用。我一般会把一组对比实验写成 shell 脚本循环跑不同学习率最后统一画图。需要注意的是命令行参数优先级高于 config.py两者都设定时以命令行为准。3. 核心模块拆解Q 表更新、epsilon-greedy 与训练主循环的完整链路3.1 QLearningAgent 类从状态到动作的决策核心agent.py 中的核心类是 QLearningAgent它维护一张 Q 表把每个状态对应的所有动作价值都记下来。先看初始化方法import numpy as np class QLearningAgent: def __init__(self, n_states, n_actions, learning_rate0.1, gamma0.9, epsilon1.0, epsilon_decay0.995, epsilon_min0.01): self.n_states n_states self.n_actions n_actions self.lr learning_rate self.gamma gamma self.epsilon epsilon self.epsilon_decay epsilon_decay self.epsilon_min epsilon_min self.q_table np.zeros((n_states, n_actions))n_states 是离散状态的数量n_actions 是动作数量。在这个迷宫工程里状态编码为“行号 × 每行格子数 列号”所以状态数等于格子总数动作数为 4上、下、左、右。Q 表初始化为全 0这是 Q Learning 的常见起点——它假设一开始每个动作的价值都一样。为什么不用随机初始化因为随机初始化会给不同动作带上人为偏置可能会让智能体一开始就偏好某些动作反而妨碍探索。全 0 是最无偏的起点。然后看动作选择采用 epsilon-greedy 策略def choose_action(self, state): if np.random.random() self.epsilon: return np.random.randint(self.n_actions) return int(np.argmax(self.q_table[state]))这段代码逻辑很直接以概率 epsilon 随机选一个动作作为“探索”否则选择当前 Q 值最大的动作作为“利用”。epsilon 初始设定为 1.0也就是一开始完全随机随着训练逐步降低。如果你把 epsilon 设成 0智能体永远不会探索新路径只能根据初始 Q 表全 0行动表现就是原地不动或随机走一步后保持短路。这就是探索率存在的意义。接下来是 Q 表更新的核心方法def learn(self, state, action, reward, next_state, done): predict self.q_table[state, action] if done: target reward else: target reward self.gamma * np.max(self.q_table[next_state]) self.q_table[state, action] self.lr * (target - predict) if self.epsilon self.epsilon_min: self.epsilon * self.epsilon_decay这里用的就是 Q Learning 的标准更新公式。predict 是当前估计target 是实际得到的奖励加上折现后的未来最大 Q 值。两者之差就是时间差分误差乘上学习率 lr 后更新 Q 表。注意 done 时没有未来项因为 episode 结束了下一状态不再存在。epsilon 的衰减放在 learn 方法里是很多工程的做法这样每次学习后探索率自动下降。epsilon_decay 是 0.995意味着每学一轮epsilon 乘以 0.995。从 1.0 衰减到 0.01 大约需要经历 920 轮左右正好能撑满整个训练过程。如果你把 episodes 设成 2000衰减就会在训练中途达到下限后续全是利用阶段策略变得更稳定。3.2 奖励驱动GridWorldEnv 的状态转移与终止条件env.py 定义环境包括状态转移、奖励和结束标志。常见实现如下class GridWorldEnv: def __init__(self, rows5, cols5, obstacles[(2,2)], start(0,0), goal(4,4)): self.rows rows self.cols cols self.obstacles obstacles self.start start self.goal goal self.state start def reset(self): self.state self.start return self._state_to_index(self.state) def step(self, action): row, col self.state # 动作映射0上1下2左3右 moves [(-1,0), (1,0), (0,-1), (0,1)] dr, dc moves[action] new_row, new_col row dr, col dc if 0 new_row self.rows and 0 new_col self.cols and (new_row, new_col) not in self.obstacles: self.state (new_row, new_col) else: self.state (row, col) # 撞墙或障碍原地不动 done (self.state self.goal) reward 1.0 if done else -0.1 return self._state_to_index(self.state), reward, done环境的核心是 step 方法根据动作计算新位置越界或撞击障碍物时位置不变到达终点给 1 奖励其他情况给 -0.1 的微小惩罚这是为了推动智能体尽快到达终点而不是在迷宫里闲逛。如果所有非终点奖励都是 0智能体可能学到一种“原地踏步”的最优策略因为反正不会扣分。注意这里的状态是元组 (row, col)而 agent 需要整数索引所以用 _state_to_index 做转换。0.1 的惩罚值是一个可调参数惩罚太大会让智能体不敢移动惩罚太小则探索动力不足。如果你后续想改奖励建议改这里而不是改 agent。状态索引转换的写法也很典型def _state_to_index(self, state): row, col state return row * self.cols col这个转换把二维坐标压成一维整数Q 表就能用数组下标直接索引。这里要求 cols 必须和实际地图列数一致否则索引会错位。如果你自己定义了更复杂的状态比如包含剩余步数、位置、朝向就需要设计更复杂的编码函数但核心思想不变状态必须能映射成一个唯一的整数。3.3 训练主循环一集从开始到结束的完整流程train.py 把环境与 agent 连接起来。先看主循环骨架env GridWorldEnv(rows5, cols5, obstacles[(1,3), (2,2), (3,1)]) agent QLearningAgent(n_statesenv.rows * env.cols, n_actions4) episodes 500 max_steps 100 all_rewards [] for ep in range(episodes): state env.reset() total_reward 0 for step in range(max_steps): action agent.choose_action(state) next_state, reward, done env.step(action) agent.learn(state, action, reward, next_state, done) state next_state total_reward reward if done: break all_rewards.append(total_reward) if (ep 1) % 10 0: avg_reward np.mean(all_rewards[-10:]) print(fEpisode {ep1}/{episodes}, avg reward: {avg_reward:.2f}, epsilon: {agent.epsilon:.2f}) np.save(output.npy, all_rewards) np.save(q_table.npy, agent.q_table)循环做了四件事重置环境拿到初始状态在 max_steps 内反复选择动作、执行动作、学习、更新状态记录本 episode 的总奖励每隔 10 轮打印平均奖励并保存结果。max_steps 非常重要。一个 5×5 迷宫的最短路径可能只有几格但如果没有步数上限智能体可能会在允许探索的情况下陷入无限循环向左一步再向右一步来回拉扯。加上 max_steps100 后一个 episode 最多执行 100 步超时自动结束既避免死循环也让智能体学会“尽快到达目的地”。训练结束后把 q_table 保存下来render.py 后续读取它来展示策略。如果你想中途看进度可以在循环里每隔 50 个 episode 保存一次检查点比如np.save(fq_table_{ep1}.npy, agent.q_table)。这样即使训练中断也不用从头再来。3.4 奖励曲线与策略渲染两把尺子判断模型是否学好运行完 train.py 后怎么判断 Q Learning 真的学会了第一把尺子是奖励曲线。把所有 episode 的总奖励画成折线图你会发现一开始曲线在较低区域波动因为 epsilon 很高、动作很随机随着训练推进曲线逐渐拉升并稳定在某个水平。如果最终曲线没有上升趋势可能是奖励设计问题或学习率太大导致震荡。第二把尺子是策略渲染图。render.py 根据 Q 表在每个网格上画一个箭头指向最大 Q 值对应的动作同时用颜色标出障碍物和终点。最终结果应该是一条从起点到终点的连续箭头路径。如果某些格子的箭头指向墙说明该格子的 Q 值没有训练充分可以增加 episode 数或调整 epsilon 衰减速度。4. 避坑指南Q Learning 项目跑不起来的五个常见问题4.1 奖励过于稀疏学了个寂寞现象训练几百轮后奖励曲线几乎贴着下限智能体在迷宫里乱走从未到达终点。原因环境中只有终点给 1 奖励其他所有状态都是 0。Q 表初始全 0智能体在没有正向引导时几乎无法通过随机探索碰到终点导致所有状态的价值长期为 0时间差分误差一直没有有效信号。你可能会想“只要它足够幸运碰到一次终点就够了”但迷宫状态空间越大这种幸运发生的概率指数下降500 个 episode 根本等不到。解决给非终点状态设置一个微小惩罚比如 -0.1而不是 0。这会让智能体在探索中“感受到”没到终点是不划算的从而驱动它去寻找更短的路径。同时要注意惩罚值不能太大否则会抑制探索。我一般把非终点奖励设为 -0.1 到 -0.5 之间并配合 max_steps 使用。一个更有效的方法是做“势能奖励”根据智能体距离终点的欧氏距离给予实时反馈但这是后话。4.2 忘记设置最大步数训练陷入死循环现象训练脚本运行时间极长终端打印的频率越来越低甚至某一次训练几分钟都没结束CPU 跑满但没有任何报错。原因没有给每个 episode 设置步数上限智能体可以在有随机探索的情况下在两个相邻格子之间来回移动永远到不了终点也永远不 done。Q 表在此过程中被反复更新但状态没有真正推进奖励一直累积负分程序看起来像卡死了。解决在训练主循环里加入 max_steps 参数并在循环内判断步数超过后就 break。一个经验值是 max_steps 状态数的 2~4 倍例如 5×5 的迷宫设 50~100 步足够。同时可以在 step 函数内部也做一个步数计数器从环境层面杜绝死循环。我更推荐后者因为 train.py 里忘记写 break 的情况太常见了但环境内部计数器能兜底。4.3 gamma1 不衰减Q 值越来越大现象训练后期 Q 表数值巨大甚至出现上万的数策略却乱七八糟渲染图上箭头乱指完全找不到一条通向终点的路径。原因gamma 是折扣因子控制未来奖励对当前价值的影响。gamma1 表示完全不衰减智能体对无限远的未来一视同仁。在无限步长场景下Q 值会因为自举而持续累积最终发散。尤其是存在负奖励时智能体会试图在循环中“拖延”来减少累计惩罚实际上 Q 值会因 reward 不断累加而爆炸因为每个状态的价值都在引用下一个状态的价值形成正反馈。解决把 gamma 设置在 0.9~0.99 之间让远期奖励稍微打折。这个项目里 0.9 对短期决策的迷宫已经足够。如果你换了更长期的任务可以加大到 0.99但千万不要用 1.0。我在自己的实验里试过一次 gamma1训练 300 轮后 Q 表最大值从 1 涨到 300明显发散。后来统一用 0.95曲线就稳定了。4.4 连续状态直接离散化Q 表爆炸现象把环境的连续观察值比如速度、角度直接四舍五入成整数状态输入给 agent结果 Q 表维度巨大内存直接溢出或者训练极其缓慢。原因Q Learning 是表格型算法Q 表大小 状态数 × 动作数。如果状态是一个二维连续量每个量离散成 100 个档位状态数就是 100×10010000再乘动作数很快上百万。一旦状态维数再增加内存就不够用了。比如一个四维连续状态每维 10 个档位状态数是 10^410000看起来还好但如果每维取 100 档就是 10^8Q 表占几百 GB直接 OOM。解决用更粗的粒度离散化或者用特征工程把状态压缩成少量离散值。比如迷宫这种天然离散的问题没问题如果处理连续问题建议改用线性函数逼近或者直接上 DQN。这也是 Q Learning 的天然边界不是代码 bug。如果你确实想用表格型方法最实际的做法是限制状态维度比如把连续值分成“低/中/高”三档而不是 100 档。4.5 matplotlib 渲染卡死控制台没有输出现象运行 render.py 后弹出一个窗口但无法正常显示程序像卡住了一样终端也没有任何输出只能强制关闭。原因matplotlib 的交互模式与当前环境不兼容或者 render.py 里使用了阻塞式绘图没有调用 plt.pause() 或 plt.show() 处理刷新导致 UI 线程阻塞。在无图形界面的服务器环境或者在使用某些远程桌面时这个问题特别常见。解决在绘制动态策略时使用 plt.ion() 开启交互模式每帧绘制后调用 plt.pause(0.1)。如果你只是看静态结果用 plt.savefig 保存图片到文件然后在本地打开这样就不依赖图形窗口。我会优先用保存图片的方式特别是在服务器上跑训练时图形界面经常是翻车重灾区。render.py 里如果默认弹窗你可以在命令行追加--save参数让它保存 PNG 而不是弹窗。5. 二次开发实战把迷宫换成你自己的场景5.1 自定义环境状态编码与动作空间怎么设计想把这个 Q Learning 代码用到自己的场景最关键的一步是在 env.py 里改造状态编码。原则是要把状态映射成唯一整数同时保证每个整数对应的物理意义对智能体来说可区分。常见做法是把多个维度拼成一个整数例如用 row * cols col如果你的状态是开关量可以用二进制位合并。动作空间同理有多少种候选行为就定义多少动作但要保证动作是可枚举且互斥的。改完环境后只要 QLearningAgent 不需要动train.py 里的 n_states env.rows * env.cols 需要同步修改。如果状态不是网格就按你定义的状态数传给 agent。这时你可以顺便把奖励函数从“终点 1其余 -0.1”改成你自己的逻辑比如每走一步耗时扣重一点让智能体学会时间最优。5.2 调参套路learning_rate、epsilon_decay、gamma 的推荐区间二次开发时不要一上来就盯着代码先用默认参数跑一遍再按下面的区间调参数推荐范围调参效应learning_rate0.05 ~ 0.3过小收敛慢过大会震荡gamma0.9 ~ 0.99越大越看重远期收益epsilon_decay0.98 ~ 0.999越小探索淘汰越快epsilon_min0.01 ~ 0.05最后剩下的探索噪声max_steps状态数 × 2 ~ 4防止死循环也影响路径长度我自己的习惯是先用小学习率 0.1 跑 500 轮画一条基线曲线然后把学习率改成 0.2 再跑一遍对比两条曲线选择收敛更快且更平稳的那组。千万不要同时改多个参数否则你根本不知道是哪个参数立功或翻车。从那以后我每次调参都强制走一遍“单变量实验法”改一个、跑一次、记录一次最后才做组合拳。希望帮到你。本文还有配套的精品资源点击获取