Q-Learning实战:从格子世界到状态离散化,手写Python代码与参数调优指南 简介Python实现Q学习强化学习的完整代码包面向强化学习入门者与算法实践者帮助理解基于表格的Q学习从原理到实际编码。无论是课程设计、实验报告还是自学训练都能从这份资源中获得可直接运行的代码与对应讲解。代码以qlearningAgents.py为核心完整呈现状态表示、动作选取、奖励反馈与Q表维护等关键环节并通过epsilon-greedy策略协调探索与利用压缩包同时附有README说明文档、PDF算法报告、开源许可证及临时文件共5个文件打包后仅852KB结构精简便于快速下载与研读。已有1110人学习下载说明其质量受到一定认可。通过对照代码与报告读者可以直观看到Q值更新公式的运算过程掌握状态离散化与Q表操作的实现细节并能将所学迁移到CartPole等经典环境实验中为后续扩展深度Q网络等方法打下扎实基础。1. 为什么我用 Python 写 Q-Learning一个格子世界就把价值迭代讲透了你在调试一个自制的网格世界小游戏智能体在起点附近来回试探死活走不到终点。换成 Q-LearningQ 学习之后几行代码就让策略逐步收敛这就是经典免模型算法的魅力。这份资源是一份可直接运行的 Python 实现完整代码包含从环境定义、Q 表更新到训练闭环的全部逻辑。我拆完这份代码最大的感受是Q-Learning 原理听起来很简单真正落地时学习率、奖励设计、探索衰减这些参数全是坑。适合刚接触强化学习、想用最小成本跑通一个 Demo 的从业者也适合需要一份可改动基线代码的算法调参玩家。2. 从状态价值到动作价值先看明白 Q 表的更新逻辑再写代码2.1 奖励折扣与 Q 值三个关键参数决定学得快还是学偏Q-Learning 的核心更新公式只有一行Q(s,a) ← Q(s,a) α [ r γ max_a Q(s,a) - Q(s,a) ]但这一行里藏着三个直接影响收敛行为的参数。α 是学习率控制新样本对旧经验的覆盖程度γ 是折扣因子决定智能体是看重眼前奖励还是长远收益r 是即时奖励由环境设计者定义。这三者组合起来决定了 Q 表是稳定增长还是反复震荡。我在跑这个 Demo 时习惯先做一组参数小实验固定 α0.2看 γ 从 0.8 提到 0.99 时策略有什么变化。γ 太小智能体会选择尽快拿分的路径忽略远处的终点γ 太大前期探索到的好奖励会被过度放大导致 Q 值更新迟迟稳定不下来。对格子世界这种回合较短的任务γ 取 0.90.95 比较合理如果是连续控制任务通常要往 0.99 靠。参数含义常见范围对训练的影响α 学习率新信息覆盖旧信息的比例0.10.3太大震荡太小收敛慢γ 折扣因子未来奖励折现程度0.90.99太小短视太大难稳定ε 探索率执行随机动作的概率0.10.3 起步太大浪费太小易局部最优要注意Q-Learning 属于时间差分TD方法它不需要等一个回合结束就能更新 Q 值。每一步拿到 (s, a, r, s) 四元组后立刻用下一状态的最大 Q 值作为目标值的一部分。这也是它比蒙特卡洛方法训练更快的原因。但正因为是单步更新如果 α 设得太大单次噪声样本就会把 Q 值带偏这就是后面避坑章要展开的问题。2.2 决策策略为什么用 ε-greedy 而不是每次贪心在训练过程中智能体不能总是选择当前 Q 值最大的动作否则它只会重复第一次碰巧获得高回报的动作永远没机会发现更好的路径。这里最常用的是 ε-greedy 策略以概率 ε 随机探索以概率 1-ε 利用当前最优动作。import random def epsilon_greedy(q_table, state, epsilon): if random.random() epsilon: action random.randint(0, q_table.shape[1] - 1) else: action int(q_table[state].argmax()) return action这段代码先产生一个 [0,1) 的随机数如果小于 epsilon就随机挑一个动作否则选当前状态下 Q 值最大的动作。参数说明q_table 是二维数组行是状态编号列是动作编号state 是当前状态索引epsilon 是探索率。逻辑上epsilon 越大动作越随机状态空间被覆盖的概率就越高。我一般会在训练初期把 epsilon 设为 0.3然后随时间衰减到 0.01形成先探索、后利用的节奏。这里有个容易被忽略的点如果把 epsilon 设成 0每步都走当前看起来最优的动作Q 表在碰到第一个正向奖励之前永远不会更新智能体就困在原地。所以完全贪心在 Q-Learning 训练期是不可取的只有在评估阶段才把 epsilon 强制置 0。2.3 环境建模用字典模拟格子世界比写类更贴近算法本身很多初学者一上来就写一个 Environment 类增加抽象层级反而把核心逻辑盖住了。对于 Q-Learning 这类表格型方法环境本质上就是一个状态转移函数。我更喜欢直接用函数表达def step(state, action, grid): r, c state moves {0: (-1, 0), 1: (1, 0), 2: (0, -1), 3: (0, 1)} dr, dc moves[action] nr, nc r dr, c dc if nr 0 or nr len(grid) or nc 0 or nc len(grid[0]): return state, -1, False if grid[nr][nc] 1: return state, -1, False if grid[nr][nc] 2: return (nr, nc), 10, True return (nr, nc), -1, False这个 step 函数接收当前状态、动作和二维网格返回下一状态、奖励和是否结束。参数说明grid 中 0 表示可走1 表示障碍2 表示终点动作 0 上、1 下、2 左、3 右。如果撞墙或撞障碍状态不变奖励 -1不结束如果走到终点状态更新奖励 10回合结束普通行走奖励 -1目的是让智能体用最少的步数到达终点。之所以用函数而不是类是因为 Q-Learning 只需要给定状态和动作问环境要下一个状态和奖励这一件事。少一层封装调试时更容易把 Q 表更新逻辑和环境逻辑分开。这份资源里就是这种轻量实现训练循环直接引用 step 函数不用实例化对象对新手友好。3. 手写 Q-Learning 训练循环从随机游走到收敛的完整代码3.1 初始化 Q 表与超参数学习率、折扣因子的取值边界训练开始前先把 Q 表、超参数和状态空间定好。对 4×4 网格去掉障碍后可用状态数小于 16动作数固定是 4。import numpy as np n_states 16 n_actions 4 q_table np.zeros((n_states, n_actions)) alpha 0.2 gamma 0.95 epsilon 0.3 epsilon_min 0.01 decay_rate 0.995参数说明alpha 是学习率这里取 0.2属于中间值gamma 是折扣因子0.95 适合回合较短的任务epsilon 初始 0.3让前几十回合有足够随机性epsilon_min 保证后期不会完全停止探索decay_rate 是一个回合结束后用于缩放 epsilon 的系数。需要提醒的是Q 表为什么要初始化为零因为表格型 Q-Learning 不需要先验把所有动作价值设为 0 是最中性、最不容易引入偏置的做法。如果你对某个动作有先验偏好可以给对应位置设一个小正数。状态编号这一步也容易出错。常见做法是把 (r, c) 坐标转成一维编号state_id r * 4 c。这个映射规则要全程一致训练循环里每次拿到新坐标都要转一次否则 Q 表查不到正确的行。3.2 训练主循环epsilon 衰减与回合停止条件训练循环是这份代码的核心负责把 step 函数返回的四元组喂给 Q 表更新公式。for episode in range(1000): state (0, 0) state_id state[0] * 4 state[1] done False step_count 0 while not done: action epsilon_greedy(q_table, state_id, epsilon) next_state, reward, done step(state, action, grid) next_state_id next_state[0] * 4 next_state[1] best_next_q q_table[next_state_id].max() q_table[state_id][action] alpha * ( reward gamma * best_next_q - q_table[state_id][action] ) state next_state state_id next_state_id step_count 1 if step_count 100: done True epsilon max(epsilon_min, epsilon * decay_rate)这段逻辑里每个回合从起点 (0,0) 出发用 epsilon_greedy 选动作然后调用 step 得到下一步状态和奖励。关键更新公式中括号里的部分就是时间差分误差奖励 下一状态最大 Q 值 - 当前 Q 值。乘以 alpha 后加到旧 Q 值上。这里有个容易混淆的点我们用到的 max 是下一状态下所有动作的 Q 值最大值而不是实际执行动作的 Q 值这正是 Q-Learning 作为离策略算法的标志。参数说明step_count 是回合内最大步数限制防止智能体在不可达区域死循环epsilon 衰减放在每个回合结束后而不是每步结束后这样更符合一个回合的探索结束再调整全局探索水平的直觉。decay_rate 取 0.995 意味着 1000 个回合后 epsilon 仍然有约 0.01 的下限不会完全消失。如果你发现智能体后期还在随机乱走就把 epsilon_min 调到 0.005 或直接置 0。3.3 训练后评估怎么判断策略真的收敛了训练结束只是拿到了一个 Q 表能不能用还需要验证。这一步必须把探索关掉直接用 argmax 选动作。success 0 total_reward 0 for _ in range(100): state (0, 0) state_id state[0] * 4 state[1] done False while not done: action int(q_table[state_id].argmax()) next_state, reward, done step(state, action, grid) next_state_id next_state[0] * 4 next_state[1] total_reward reward state next_state state_id next_state_id if state (2, 3): success 1 print(success rate:, success / 100) print(average reward:, total_reward / 100)这段代码用纯贪心策略跑 100 次统计到达终点的比例。注意 state 的终止条件必须和 step 函数里定义的终点一致否则评估结果会失真。我一般看到成功率稳定在 95% 以上才会开始调超参数如果只有 50% 左右先检查奖励设计和 epsilon 衰减曲线而不是盲目增大回合数。另外average reward 是负数很正常因为每步都有 -1 的移动惩罚只要它不再持续下降说明策略趋于稳定。4. 避坑指南Q-Learning 实现里常被忽略的四个翻车点4.1 学习率过高导致 Q 值震荡现象训练后期 Q 表还在大幅跳动智能体某一回合能到终点下一回合又迷路。 原因alpha 设成 0.8 甚至 0.9单次样本的奖励覆盖了之前积累的经验Q 值被噪声样本反复拉拽。 解决把 alpha 调整到 0.10.3 区间。我一般先设 0.2如果发现收敛慢再降到 0.1。也可以让学习率随回合数衰减但表格型问题通常没必要。4.2 奖励稀疏导致前期无信号现象训练了 500 回合Q 表几乎全是零智能体行为完全随机。 原因环境只在终点给 10其余状态奖励 0。对于状态空间较大的情况随机探索碰到终点的概率极低Q 表从未接收正向反馈。 解决给中间过程加形状奖励比如每次移动给 -1终点给 10这样智能体至少能学到少走弯路。注意形状奖励不能反转最优策略最大步数惩罚不能大于终点奖励否则智能体会停在原地不动。4.3 epsilon 衰减太快导致局部最优现象智能体走出一条路径但明显绕远看起来懒散。 原因epsilon 从 0.5 在 10 个回合内降到 0.01探索窗口过短智能体还没见过最短路径就开始利用。 解决把衰减率调整到 0.995 以上并设置 epsilon_min 0.05。我会在训练初期记录 epsilon 曲线确保前 300 个回合内至少有一半时间处于探索状态。4.4 把边界和障碍设为 done 导致智能体原地摆烂现象评估成功率很高但智能体实际停在离终点很远的地方不动。 原因step 函数在撞墙或撞障碍时返回 doneTrue智能体学到不行动最安全因为每次原地踏步虽然扣分但不会死亡。 解决只有进入终点格子才返回 doneTrue撞墙和障碍应该返回原状态、给负奖励、不结束回合。这样智能体被迫移动不会学到静止策略。4.5 状态编号映射不一致导致 Q 表错位现象同一个状态在不同回合查到的 Q 值行不同策略完全随机。 原因有的地方用 (r, c) 表示状态有的地方用 r*4c 索引 Q 表两个映射混用。 解决统一用一维编号作为 Q 表的行索引在环境 step 函数入口状态用坐标元组训练循环内部只操作编号。可以在代码开头写一个 zfill 式的小函数负责双向转换确保所有模块走同一套映射。5. 从格子世界到真实场景状态离散化的技巧与验证格子世界是离散状态真实世界的状态往往是连续的。把连续量分桶时桶的数量和方法直接影响 Q 表大小与学习效率。我常用的分桶函数是def discretize(value, min_val, max_val, bins): if value min_val: return 0 if value max_val: return bins - 1 ratio (value - min_val) / (max_val - min_val) return int(ratio * bins)参数说明value 是连续量min_val 和 max_val 是边界bins 是区间数量。比如把速度分成 20 个区间边界设成 -2 和 2这个函数会将任何超出边界的值截断到端点避免索引越界。要注意的是分桶粒度越细Q 表越大每个状态被访问到的次数越少。我习惯先粗分观察策略是否稳定再逐步加密。验证阶段除了看成功率还要做稳定性检查。固定随机种子跑 5 次完整训练记录每次收敛后的平均回报。如果 5 次结果波动超过 10%说明参数或状态离散化还不稳。我会把每次训练的 Q 表存成 numpy 文件比较不同种子下 Q 表元素的最大差值差值较大说明智能体的经验覆盖不均匀。从那以后我每次跑新的 Q-Learning 环境都强制走一遍状态边界检查 → 离散化分区 → 小步调参的流程再开始训练。尤其状态离散化我吃过一次亏把速度分成了 200 份结果 Q 表爆炸训练时间直接从几分钟变成几小时。希望帮到你。本文还有配套的精品资源点击获取