FEATURED · 精选文章

用Pygame可视化Q-Learning训练:网格世界强化学习实战

发布时间 / 2026/9/14 3:19:44
来源 / 创域科博编辑部
栏目 / 资讯中心
用Pygame可视化Q-Learning训练:网格世界强化学习实战 简介这份资源是一个基于Pygame的Q-Learning可视化项目适合想通过图形化方式理解强化学习核心概念的Python学习者与算法初学者。项目在5x5网格中训练蓝色智能体避开障碍物2,2并抵达右下角绿色目标点通过“上下左右”动作与环境交互体验撞墙-10、到达目标50、普通移动-1的奖励机制。代码完整实现ε-greedy动作选择与Q表更新逻辑运行后能看到智能体从随机游走逐步走向最优策略的完整过程。资源包内共3个文件包括1个可直接运行的Python脚本、1份简洁的README说明文档和1个训练效果演示GIF压缩包仅46KB轻量易用。该资源已有1040人学习下载适合自学或作为教学演示帮助初学者将Q-learning的抽象公式映射到可感知的交互过程快速掌握奖励设计、探索率与收敛效果之间的关系。1. 为什么用 Pygame 看 Q-Learning 训练Q-Learning 的公式只有一行但跑完训练后屏幕上只剩一堆浮点数。问题在于你把训练后的 Q 表打印出来却看不出智能体是怎么“变会”的。用 Pygame 把每个格子的 Q 值大小、智能体当前位置、每一步的探索程度以及回放出来的移动路径连起来一眼就能看出策略何时稳定、哪里还在摇晃。这篇博客要写出这样一套完整代码在 5x5 网格世界里用 Python 实现 Q-Learning 算法用 Pygame 实现热力图、智能体动画和定期回放。适合刚接触强化学习、想确认公式实际效果的人也适合写过 Q-Learning 但调试时看不到内部状态的人。不需要深度学习经验只需要基础 Python 语法和 pygame 画矩形、画圆的熟练度。2. 网格世界的 Q 表、奖励设计与单步训练代码2.1 为什么把状态和动作压进一张三维表Q-Learning 是典型的 model-free 算法它不需要环境的转移概率只需要智能体反复试错最后在 Q 表里积累“在这个状态做这个动作有多好”的估计值。网格世界是最小可运行环境状态就是坐标动作就是上下左右Q 表本质是一个5 x 5 x 4的数组。import numpy as np GRID_H, GRID_W 5, 5 ACTIONS [(-1, 0), (1, 0), (0, -1), (0, 1)] # 上、下、左、右 GOAL (GRID_H - 1, GRID_W - 1) START (0, 0) Q np.zeros((GRID_H, GRID_W, len(ACTIONS)))这里状态用(row, col)表示第一维是行第二维是列。动作集用位移向量表示(-1, 0)是向上移动一行(0, 1)是向右移动一列。Q[r, c, a]就表示在格子(r, c)执行动作a的长期回报估计。用 numpy 数组而不是 Python 三维列表是为了后面写np.argmax(Q[r, c])和np.max(Q[r, c])时不需要手动套循环。这个选择在网格变大后依然是高效的因为更新的核心操作是数组切片和聚合。2.2 奖励函数三层奖励比单层更容易收敛奖励设计是这个小 demo 里最敏感的部分。我常用的做法是分成三种情况而不是只在到达目标时给一个正奖励情况奖励值作用到达目标点1.0给智能体一个明确的正向反馈并结束当前回合越界或撞墙-0.1惩罚无效动作让智能体学会避开边界正常移动一步-0.01时间惩罚让最短路成为最优策略如果只设“到达目标 10其他 0”智能体完全没有动机走捷径它可以在网格里闲逛几百步才到达目标回合结束不了训练效率很低。-0.01这个很小的负值不影响目标格的吸引力但足够把“绕圈”和“直奔”区分开。越界惩罚取-0.1而不是更大是因为越界动作在 Q-Learning 里会被当作“留在原地”处理惩罚太大会让智能体完全不敢接近边界反而限制搜索空间这个数值在实际调试中很容易被忽略。2.3 单步更新代码注意 done 状态不能继续看未来核心迭代公式是Q[s][a] α * (r γ * max(Q[s]) - Q[s][a])其中max(Q[s])是下一步状态的最优价值估计。注意如果下一步就是目标点回合已经结束不能再把max(Q[s])加进来否则目标格子的价值会被高估。def choose_action(state, epsilon): r, c state if np.random.random() epsilon: return np.random.randint(len(ACTIONS)) return int(np.argmax(Q[r, c])) def step(state, action_id): r, c state dr, dc ACTIONS[action_id] nr, nc r dr, c dc if not (0 nr GRID_H and 0 nc GRID_W): return state, -0.1, False if (nr, nc) GOAL: return (nr, nc), 1.0, True return (nr, nc), -0.01, False alpha 0.5 gamma 0.9 epsilon 1.0 eps_min 0.01 eps_decay 0.995 EPISODES 800 for episode in range(EPISODES): state START while True: action choose_action(state, epsilon) next_state, reward, done step(state, action) r, c state nr, nc next_state if done: target reward else: target reward gamma * float(np.max(Q[nr, nc])) Q[r, c, action] alpha * (target - Q[r, c, action]) state next_state if done: break epsilon max(eps_min, epsilon * eps_decay)choose_action实现了 epsilon-贪心策略随机数小于epsilon时完全随机探索否则选择当前 Q 值最大的动作。step负责模拟环境越界会返回原地并给负奖励目标是doneTrue。更新时用done分支把未来价值截断这是 Q-Learning 代码里最常见也最容易漏掉的细节。epsilon在每个回合结束后按指数方式衰减从 1.0 逐渐降到 0.01让训练从“大量探索”平滑过渡到“利用已有知识”。3. 用 Pygame 把训练过程画出来热力图与回放3.1 用 Q 值给每个格子着色的热力底图Q 表是三维的把它压成二维可视化最简单的方式是取每个格子所有动作里的最大值max(Q[r, c])再映射成灰度。这样做的含义很直观格子越亮说明从这个格子出发能拿到越高的长期回报。靠近目标点的格子应该越来越亮起点附近的格子一开始很暗训练中后期才开始变亮。import pygame CELL 80 MARGIN 20 INFO_H 60 SCREEN_W GRID_W * CELL MARGIN * 2 SCREEN_H GRID_H * CELL MARGIN * 2 INFO_H pygame.init() screen pygame.display.set_mode((SCREEN_W, SCREEN_H)) pygame.display.set_caption(Q-Learning GridWorld) font pygame.font.SysFont(microsoftyahei, 18) def draw_grid(Q, agent_pos, episode, epsilon, step): screen.fill((20, 20, 20)) for r in range(GRID_H): for c in range(GRID_W): rect pygame.Rect(MARGIN c * CELL, MARGIN r * CELL, CELL, CELL) if (r, c) GOAL: base_color (220, 90, 70) else: v float(np.max(Q[r, c])) g int(min(220, max(40, 40 v * 160))) base_color (g, g, g) pygame.draw.rect(screen, base_color, rect) pygame.draw.rect(screen, (70, 70, 70), rect, 1) px, py agent_pos center (MARGIN py * CELL CELL // 2, MARGIN px * CELL CELL // 2) pygame.draw.circle(screen, (80, 220, 130), center, CELL // 3) status font.render( fepisode{episode} epsilon{epsilon:.3f} step{step}, True, (230, 230, 230) ) screen.blit(status, (MARGIN, SCREEN_H - INFO_H 15))这里坐标转换是关键逻辑坐标(r, c)转屏幕坐标时要交换行列。行r对应屏幕纵坐标列c对应横坐标所以格子在屏幕上的 x 是MARGIN c * CELLy 是MARGIN r * CELL。智能体圆点的中心要加上CELL // 2让圆落在格子正中间。灰度映射用了40 v * 160意味着 Q 值在 0 到 1 附近变化时格子亮度从暗灰逐渐变到接近白色低于 0 的 Q 值会被压到下限 40不会出现黑色看不清边界的情况。3.2 回放模式训练归训练渲染归渲染一个常见的错误是在训练循环里每走一步都立刻渲染导致帧率被 Q 值更新牵着走训练速度慢得离谱。我一般把两者分开训练循环加速跑每隔固定回合数切到回放模式让当前 Q 表对应的贪心策略自己走出来。REPLAY_INTERVAL 20 def replay(Q, clock, episode, epsilon): state START step_count 0 while True: for event in pygame.event.get(): if event.type pygame.QUIT: pygame.quit() return False draw_grid(Q, state, episode, epsilon, step_count) pygame.display.flip() clock.tick(10) action int(np.argmax(Q[state[0], state[1]])) state, _, done step(state, action) step_count 1 if done or step_count GRID_H * GRID_W * 3: break return True回放模式下不再是 epsilon-贪心而是直接用np.argmax选最优动作这样才能看到当前策略的真实效果。clock.tick(10)把每步间隔控制在 0.1 秒左右移动过程肉眼可跟踪。设定最大步数为GRID_H * GRID_W * 3是防止策略太差时无限循环下去因为如果step因为越界而返回原地回合既不会结束也不会推进没有这个上限回放会卡住。3.3 主循环训练、回放、事件处理怎么拼在一起主循环的骨架是先处理 Pygame 事件再跑一个完整的训练回合每隔REPLAY_INTERVAL回合进入一次回放。事件处理放在每个回合开始时而不是每个训练步里窗口拖动、关闭这些操作仍然能响应又不会因为频繁查询事件拖慢训练。clock pygame.time.Clock() for episode in range(EPISODES): for event in pygame.event.get(): if event.type pygame.QUIT: pygame.quit() raise SystemExit state START while True: action choose_action(state, epsilon) next_state, reward, done step(state, action) r, c state nr, nc next_state if done: target reward else: target reward gamma * float(np.max(Q[nr, nc])) Q[r, c, action] alpha * (target - Q[r, c, action]) state next_state if done: break epsilon max(eps_min, epsilon * eps_decay) if episode % REPLAY_INTERVAL 0: if not replay(Q, clock, episode, epsilon): break运行这段代码会看到两种画面交替出现训练阶段窗口安静地停留在上一帧回放阶段智能体开始移动。每 20 个回合回放一次路径会从早期绕远路逐渐变成直奔目标。如果某个阶段回放时智能体频繁撞墙又弹回原地说明策略在那个区域仍不稳定需要继续训练或者调整参数。这个交替节奏本身就是最好的训练进度指示器。4. 超参数怎么调从探索到收敛的可视化信号4.1 五个超参数的作用方向算法只有一行公式可调参数却有五个。网格世界小参数摆动的容忍度比真实环境高但这不代表可以随便设。下表是按影响力排序的核心参数参数Demo 常用值调大的后果调小的后果alpha学习率0.5收敛快但震荡明显Q 值来回跳收敛慢但曲线更平滑gamma折扣因子0.9更看重远期目标路径更全局最优只看眼前几步容易被局部奖励带偏epsilon初始探索率1.0前期彻底随机覆盖更多状态开局就贪心可能锁死局部策略eps_decay衰减系数0.995探索期长训练回合数要更多过早转为利用策略固化太早EPISODES总回合数800训练更充分适合更大网格可能还没收敛就停掉eps_decay的值直接决定探索期长度。想让epsilon从 1.0 在 N 个回合内衰减到eps_min应该用公式eps_decay (eps_min) ** (1 / N)比如目标是 800 回合内从 1.0 降到 0.02eps_decay就是0.02 ** (1/800) ≈ 0.9951。手动想当然地填 0.99会导致前 200 回合探索率还很高训练结束时epsilon还停在 0.13 左右没有完全收敛。把衰减值和回合数绑定是控制探索节奏最直接的方法。4.2 从回放路径判断探索和利用是否失衡回放是观察参数问题的最好窗口。早期回放里智能体走得歪歪扭扭是正常的因为 Q 值还没有区分度任何动作都在被探索。到了第 200 回合左右回放路径应该明显出现从起点到目标的稳定通道偶尔绕一下。如果 400 回合以后路径还在大幅绕圈先看状态栏里的epsilon数值还大于 0.3 就说明衰减太慢训练量不够已经降到 0.1 以下还绕圈则说明利用阶段卡住了需要检查奖励函数。提示把RENDER_EVERY改成 1即每个回合都回放一次能逐回合看到路径的演化过程。代价是训练速度变慢但 5x5 网格规模下完全可接受适合第一次运行这套 demo 时用。不过每回合都回放会让探索阶段的画面非常杂乱连续几十个回合都是随机乱走。折中的做法是前 100 回合每 5 回合回放一次之后每 20 回合一次。这样既能看清早期探索形态又不会在中间阶段浪费时间。4.3 可视化失败案例热力图怎么暴露问题热力图是最容易暴露训练异常的层。正常收敛时目标点周围一圈格子先变亮然后像水波一样向外扩散起点附近最后变亮。出现下面三种情况基本都能从画面上直接定位问题所有格子始终接近黑色alpha太低或者奖励值量级太小。把-0.01的步数惩罚改成-0.1灰度映射的乘数提高到 200 左右通常就能看到明显变化。某个非目标格子特别亮这个格子可能被奖励设计绑定了比如越界惩罚设置过大智能体宁可堵在一个角落也不愿意走出去。检查这个格子的动作分布会发现某个方向的动作 Q 值异常高。热力图离散且互相不连通奖励只在目标点有正信号中间没有梯度。网格世界小这种问题不明显换成 10x10 网格后会在调试中频繁遇到解决办法是缩小步数惩罚的绝对值让价值从目标点向四周平滑扩散。每次改动参数后只关心两个视觉信号回放路径是否变短热力图是否连续。其他指标在这个规模下都不如这两条直观。5. 收敛验证与新目标迁移策略箭头和增量训练5.1 在热力图上叠加策略箭头热力图能看出“值的高低”但看不出“下一步往哪走”。策略箭头正好补上这一层在每个格子中心画一条短线方向指向该格子的最优动作。箭头和热力图叠加在一起就同时展示了价值分布和决策结果。def draw_policy_arrows(Q): for r in range(GRID_H): for c in range(GRID_W): if (r, c) GOAL: continue action int(np.argmax(Q[r, c])) dr, dc ACTIONS[action] sx MARGIN c * CELL CELL // 2 sy MARGIN r * CELL CELL // 2 ex sx dc * CELL // 3 ey sy dr * CELL // 3 pygame.draw.line(screen, (230, 200, 60), (sx, sy), (ex, ey), 4) pygame.draw.circle(screen, (230, 200, 60), (ex, ey), 4)网格规模只有 5x5 时箭头会组成一条从左上角俯冲到右下角的清晰路径。如果某个格子的箭头方向在训练后期还在左右摇摆甚至两个相邻回放之间指向不同说明这个格子的 Q 值方差过大。此时最有效的验证方式是看目标点周围四个格子的箭头是否全部指向目标这比任何收敛指标都直接。5.2 换一个目标点增量训练比从零训练快在哪网格世界的目标点不是固定的。用鼠标点击任意格子设为新目标然后观察 Q 表如何适应是理解 Q-Learning 迁移能力最有意思的小实验。new_goal None for event in pygame.event.get(): if event.type pygame.MOUSEBUTTONDOWN: mx, my event.pos grid_c (mx - MARGIN) // CELL grid_r (my - MARGIN) // CELL if 0 grid_r GRID_H and 0 grid_c GRID_W: new_goal (grid_r, grid_c) if new_goal is not None: GOAL new_goal alpha 0.1 epsilon 0.05 for episode in range(200): state START while True: action choose_action(state, epsilon) next_state, reward, done step(state, action) r, c state nr, nc next_state if done: target reward else: target reward gamma * float(np.max(Q[nr, nc])) Q[r, c, action] alpha * (target - Q[r, c, action]) state next_state if done: break这段代码把学习率降到了 0.1探索率降到 0.05保留了旧目标训练出的 Q 表。结果是离新目标近的格子几回合内就转向远端的格子继续沿用旧策略然后逐渐修正。相比完全清零 Q 表这个增量过程在 Pygame 里看起来像是路径被整体“拽”向新目标而不是重新学习一遍。把旧 Q 表留下让探索率回到 0.3两百回合内你会看到新路径从旧策略里平滑地长出来。本文还有配套的精品资源点击获取
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻