☰
Q-Learning实战:从格子世界到状态离散化,手写Python代码与参数调优指南
2026/10/21 2:49:27 网站建设 项目流程

简介:Python实现Q学习强化学习的完整代码包,面向强化学习入门者与算法实践者,帮助理解基于表格的Q学习从原理到实际编码。无论是课程设计、实验报告还是自学训练,都能从这份资源中获得可直接运行的代码与对应讲解。代码以qlearningAgents.py为核心,完整呈现状态表示、动作选取、奖励反馈与Q表维护等关键环节,并通过epsilon-greedy策略协调探索与利用;压缩包同时附有README说明文档、PDF算法报告、开源许可证及临时文件,共5个文件,打包后仅852KB,结构精简,便于快速下载与研读。已有1110人学习下载,说明其质量受到一定认可。通过对照代码与报告,读者可以直观看到Q值更新公式的运算过程,掌握状态离散化与Q表操作的实现细节,并能将所学迁移到CartPole等经典环境实验中,为后续扩展深度Q网络等方法打下扎实基础。

1. 为什么我用 Python 写 Q-Learning:一个格子世界就把价值迭代讲透了

你在调试一个自制的网格世界小游戏,智能体在起点附近来回试探,死活走不到终点。换成 Q-Learning(Q 学习)之后,几行代码就让策略逐步收敛,这就是经典免模型算法的魅力。这份资源是一份可直接运行的 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.9~0.95 比较合理;如果是连续控制任务,通常要往 0.99 靠。

参数含义常见范围对训练的影响
α 学习率新信息覆盖旧信息的比例0.1~0.3太大震荡,太小收敛慢
γ 折扣因子未来奖励折现程度0.9~0.99太小短视,太大难稳定
ε 探索率执行随机动作的概率0.1~0.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.1~0.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 函数在撞墙或撞障碍时返回 done=True,智能体学到"不行动最安全",因为每次原地踏步虽然扣分,但不会死亡。 解决:只有进入终点格子才返回 done=True;撞墙和障碍应该返回原状态、给负奖励、不结束回合。这样智能体被迫移动,不会学到静止策略。

4.5 状态编号映射不一致导致 Q 表错位

现象:同一个状态在不同回合查到的 Q 值行不同,策略完全随机。 原因:有的地方用 (r, c) 表示状态,有的地方用 r*4+c 索引 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 表爆炸,训练时间直接从几分钟变成几小时。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询