Q Learning强化学习Python实现:从零跑通到调参避坑

发布时间:2026/10/29 11:24:43
Q Learning强化学习Python实现:从零跑通到调参避坑 简介面向强化学习入门者与需要动手复现经典算法的开发者这份Python实现Q Learning的完整代码包围绕Q表构建、epsilon-greedy动作选择与Q函数更新公式展开帮助读者在无法预知全部状态动作空间的环境中理解智能体如何通过交互逐步优化策略并可在gym倒立摆等经典环境中直接验证。压缩包共5个文件约852KB包含qlearningAgents.py核心实现脚本、README.md使用说明、LICENSE授权文件、报告.pdf原理与实验讲解以及一个临时文件代码与文档配套便于边读边跑。目前已有1111人学习下载适合作为课程作业、算法练手或项目原型的参考底稿读者可从中获得Q表字典结构设计、学习率与折扣因子调节、探索与利用平衡处理以及按episode训练收敛的完整实现思路。1. 从一份 Q Learning 源码说起为什么我建议你先跑通再谈算法去年带一个刚转方向的同学做小游戏 AI他啃了三天公式Q 表更新那行还是写反了。后来我让他先别推导直接把一份 Q Learning 的 Python 实现跑起来看 Q 表怎么从全零一点点长出数值第二天他自己就把公式改对了。这份python实现Q Learning强化学习(完整代码).zip就是干这个用的它把 Q Learning 从环境定义、Q 表初始化、动作选择、奖励回传、迭代收敛到结果可视化串成一条能直接执行的链路适合两类人——刚接触强化学习、想看到「智能体真的在学」的新手以及需要一份干净基线代码、准备往自己业务场景里套的开发者。它不依赖重型框架纯 Python 加少量数值计算和绘图库就能跑拿它当强化学习的第一个可运行项目比读十篇推导都实在。2. 项目定位与技术选型为什么用纯 Python 手写 Q 表而不是上框架2.1 这个项目到底解决什么问题Q Learning 是强化学习里最经典的离线策略时序差分算法核心就一张 Q 表行是状态列是动作格子里存「在状态 s 下做动作 a 的长期期望回报」。这份源码把抽象公式落成了一个具体可跑的闭环——智能体在环境里试错拿到奖励用贝尔曼方程更新 Q 值反复迭代直到策略稳定。它解决的不是「训练一个工业级智能体」而是「让你亲眼看到 Q 表怎么收敛」这件事。从工程角度看它覆盖了 Q Learning 落地的全部关键环节环境建模状态、动作、奖励、终止条件、Q 表数据结构、ε-greedy 探索策略、学习率与折扣因子、训练循环、策略提取与结果展示。这些环节在真实项目里一个都不能少只是规模不同。所以把它当教学基线改起来心里有数。2.2 技术栈与依赖取舍项目走的是极简路线常见依赖组合如下依赖用途说明Python 3.x运行环境3.8 以上都能跑语法没用新特性numpyQ 表与数值运算用 ndarray 存 Q 表索引和批量操作都方便matplotlib训练曲线与策略可视化画每轮奖励、Q 表热力图random / collections探索与经验记录标准库无需额外安装为什么不上 Gym、不上 PyTorch我的判断是这个项目的价值在于「透明」。用框架封装环境你看到的是env.step()返回的元组看不到状态怎么定义、奖励怎么设计手写 Q 表每一行更新逻辑都摊在眼前改一个参数立刻能看到 Q 值变化。对于要二次开发的人这种透明比性能重要得多。等你把这份代码吃透再迁到 Gym 环境或深度 Q 网络迁移成本很低因为概念链路是通的。提示如果你的机器上 numpy 和 matplotlib 都没装先建虚拟环境再装别往系统 Python 里塞后面改依赖不至于互相打架。2.3 目录结构长什么样压缩包解开后典型结构大致是这样不同整理方式文件名可能略有差异但职责划分一致qlearning_project/ ├── main.py # 入口组装环境、智能体、训练循环 ├── agent.py # Q Learning 智能体Q 表、选动作、更新 ├── env.py # 环境状态转移、奖励、终止判断 ├── train.py # 训练与评估流程 ├── config.py # 超参数集中管理 └── utils.py # 绘图、日志、结果保存这种拆法的好处是职责边界清楚环境只管「世界怎么变」智能体只管「怎么学、怎么选」训练脚本只管「跑多少轮、怎么记录」。二次开发时你改奖励函数只动env.py调探索策略只动agent.py不会牵一发动全身。如果拿到的是单文件版本也建议先按这个边界拆开再改否则几百行堆在一起调参调到怀疑人生。3. 核心模块拆解Q 表、ε-greedy 与贝尔曼更新怎么落地3.1 环境建模状态、动作、奖励三件套Q Learning 能不能学出东西八成取决于环境定义得干不干净。这份代码里环境通常是一个网格或离散状态空间核心接口就两个reset()返回初始状态step(action)返回(next_state, reward, done)。下面是一段典型实现我按常见写法补了注释class GridEnv: def __init__(self, size5): self.size size self.start (0, 0) self.goal (size - 1, size - 1) self.state self.start def reset(self): # 每轮训练开始回到起点保证 episode 独立 self.state self.start return self.state def step(self, action): # action: 0上 1下 2左 3右 moves {0: (-1, 0), 1: (1, 0), 2: (0, -1), 3: (0, 1)} dr, dc moves[action] r, c self.state nr min(max(r dr, 0), self.size - 1) # 边界裁剪撞墙原地不动 nc min(max(c dc, 0), self.size - 1) self.state (nr, nc) if self.state self.goal: return self.state, 10.0, True # 到达目标给正奖励并终止 return self.state, -0.1, False # 每走一步小惩罚逼它找最短路径这段代码里有两个设计点值得说。第一边界用min/max裁剪而不是报错撞墙等价于原地不动避免训练中途抛异常。第二每步给-0.1的小惩罚这是网格类任务里很常见的做法——没有步数惩罚智能体可能绕远路也不心疼收敛出来的策略不是最短路径。奖励设计是 Q Learning 里最容易翻车的地方奖励给得太稀疏只有终点有分前期几乎学不动给得太密又容易诱导出奇怪行为。我一般先用「终点大正奖励 每步小负奖励」这组基线跑通再调。3.2 Q 表初始化与动作选择ε-greedy 的取舍Q 表本质是一个二维数组行数等于状态数列数等于动作数。状态如果是元组需要先映射成整数索引常见做法是用字典或线性编号。动作选择用 ε-greedy以 ε 概率随机探索以 1-ε 概率选当前 Q 值最大的动作。import numpy as np import random class QAgent: def __init__(self, n_states, n_actions, lr0.1, gamma0.9, epsilon1.0, epsilon_min0.05, epsilon_decay0.995): self.q_table np.zeros((n_states, n_actions)) # 全零初始化 self.lr lr self.gamma gamma self.epsilon epsilon self.epsilon_min epsilon_min self.epsilon_decay epsilon_decay self.n_actions n_actions def choose_action(self, state_idx): if random.random() self.epsilon: return random.randint(0, self.n_actions - 1) # 探索 return int(np.argmax(self.q_table[state_idx])) # 利用参数含义逐个说清lr是学习率控制新经验覆盖旧 Q 值的幅度太大震荡、太小收敛慢0.1 是稳妥起点gamma是折扣因子决定未来奖励的权重越接近 1 越看重长期回报0.9 适合大多数离散任务epsilon从 1.0 开始意味着前期纯随机探索epsilon_decay每轮乘一次逐步衰减到epsilon_min保证后期以利用为主但不完全锁死。这里有个血泪经验ε 衰减别设太快前期探索不够Q 表会卡在局部最优也别设太慢训练后期还在乱走曲线一直抖。3.3 贝尔曼更新一行公式背后的四个参数Q Learning 的灵魂是这行更新def update(self, s, a, r, s_next, done): # 目标值即时奖励 折扣后的下一状态最大 Q 值 best_next 0.0 if done else np.max(self.q_table[s_next]) td_target r self.gamma * best_next td_error td_target - self.q_table[s, a] self.q_table[s, a] self.lr * td_error # 朝目标值走一步逐项拆best_next取下一状态所有动作里的最大 Q 值这是 Q Learning 作为离线策略的关键——它用「最优动作」的估值来更新而不是用实际执行的动作done为真时下一状态没有未来best_next置零否则终止状态会被错误估值td_target是当前这步应该逼近的目标td_error是目标与现状的差距最后乘学习率做增量更新。很多人第一次写会把max写成实际动作的 Q 值那就变成 SARSA 了两者在悬崖行走类任务里策略差别很明显这是概念题也是工程题。3.4 训练主循环把上面三块拼起来训练循环负责把环境、智能体、更新逻辑串成 episode 序列同时记录每轮总奖励用于观察收敛def train(env, agent, episodes500, max_steps200): rewards_history [] for ep in range(episodes): state env.reset() s_idx state_to_index(state, env.size) total_reward 0.0 for _ in range(max_steps): a agent.choose_action(s_idx) next_state, r, done env.step(a) ns_idx state_to_index(next_state, env.size) agent.update(s_idx, a, r, ns_idx, done) s_idx ns_idx total_reward r if done: break agent.epsilon max(agent.epsilon_min, agent.epsilon * agent.epsilon_decay) rewards_history.append(total_reward) return rewards_historymax_steps是单轮步数上限防止智能体在没学会之前无限打转state_to_index把二维坐标压成一维索引r * size c是最简单的映射每轮结束衰减 ε注意衰减放在 episode 外层而不是每步否则探索窗口太短。跑完把rewards_history画出来前几十轮奖励很低甚至为负中段开始抬升后段趋于平稳这条曲线就是「学起来了」的直接证据。如果曲线一直趴着不动先查奖励设计和 ε 衰减再查状态索引有没有映射错——索引错位是新手最常见的黑匣子问题Q 表在更新但更新的是错误的行。4. 跑起来与调参从零到收敛的完整操作路径4.1 环境准备与首次运行拿到压缩包后按这个顺序走基本不会卡住# 1. 解压后进入项目目录 cd qlearning_project # 2. 建虚拟环境可选但推荐 python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate # 3. 安装依赖 pip install numpy matplotlib # 4. 运行入口 python main.py首次运行建议先把episodes调小到 50 左右确认流程能走通、图能画出来再放大到 500 以上看收敛。如果入口脚本名不是main.py看目录里哪个文件带if __name__ __main__那就是入口。运行后终端一般会打印每若干轮的奖励和当前 ε图窗弹出奖励曲线没有图窗的环境比如远程机器就把绘图改成保存文件plt.savefig(reward.png)一行的事。4.2 超参数怎么调一张对照表调参别瞎试按影响面从大到小排参数典型值调大后的效果调小后的效果episodes500~2000更充分收敛耗时增加可能没学完就停lr0.1学得快但易震荡稳但慢可能欠拟合gamma0.9更重视长期回报短视只看眼前奖励epsilon_decay0.995探索更久过早收敛到局部最优max_steps200允许更长路径复杂环境可能走不到终点我的习惯是先固定lr0.1, gamma0.9只动episodes和epsilon_decay看曲线形状曲线能抬起来但抖再降lr抬不起来先加episodes再考虑调奖励。一次只改一个参数改完记录结果否则出了问题根本不知道是谁的锅。4.3 怎么判断「真的学会了」别只看最后几轮奖励高就下结论那可能是运气。三个验证动作第一把 ε 强制设为 0 跑一轮评估看智能体是否稳定走到终点第二打印最终 Q 表检查终点附近状态的 Q 值是否明显高于其他区域第三换几个随机起点测试策略的泛化性。这三步做完你才对这份代码的收敛质量有底。评估和训练要分开写训练时带探索评估时纯利用混在一起看数字会失真。5. 避坑与排查那些让 Q 表「学了但没完全学」的坑5.1 现象奖励曲线一直不涨原因通常是奖励太稀疏或 ε 衰减过快。只有终点有正奖励、其余全零时随机探索很难碰到终点Q 表长期全零。解决加每步小负奖励制造梯度信号或把epsilon_decay调到 0.999 延长探索期必要时先用小网格3x3验证逻辑再放大。5.2 现象Q 值越来越大直至溢出原因多是done判断漏了终止状态仍在累加未来回报形成正反馈。解决检查update里done为真时best_next是否置零以及环境step到达终点是否真的返回True。这个坑很隐蔽因为前期数值小看不出来跑几百轮才炸。5.3 现象训练很快「收敛」但策略很蠢原因一般是状态索引映射错误多个状态映射到同一行Q 表被污染。解决打印state_to_index的映射表确认每个状态唯一网格环境下用r * size c并检查行列是否写反。索引错位属于典型的静默失败不报错但结果全错。5.4 现象每次运行结果差异巨大原因是没有固定随机种子探索路径不可复现。解决在入口处设random.seed(42)和np.random.seed(42)调参阶段尤其需要否则你无法判断性能变化是参数起作用还是运气。复现性不是洁癖是调参的前提。5.5 现象图窗不弹或中文乱码原因分别是无图形界面环境和 matplotlib 默认字体不含中文。解决改用savefig保存图片中文标签要么换英文字段要么显式指定系统里存在的中文字体。这类问题不影响算法但会挡住你观察结果别在这上面耗太久。6. 二次开发把这份基线改成你自己的任务想把这份代码用到自己的场景改造顺序我建议按「环境 → 奖励 → 状态表示 → 算法」推进每一步都能独立验证。先换环境把网格换成你的业务状态机只要保持reset/step接口不变智能体和训练循环一行不用动这是当初拆模块的红利。再改奖励业务目标是什么奖励就往那个方向设计但记住奖励要能区分「好」和「更好」只有二值奖励的系统很难学出精细策略。状态表示是第二个改造重点。网格状态是离散小空间Q 表扛得住一旦状态维度上去比如带多个连续特征Q 表会爆炸这时要么做状态离散化分桶要么转向函数逼近线性近似或深度 Q 网络。分桶是最省事的过渡方案def discretize(value, bins): # 把连续值落到桶索引bins 是升序边界列表 for i, b in enumerate(bins): if value b: return i return len(bins)bins的划分直接决定学习效果切太细状态爆炸、切太粗信息丢失我一般先按业务分位数切 5~10 桶跑基线。算法层面如果发现 Q Learning 在随机环境里震荡明显可以换成期望 SARSA 或加经验回放但别一上来就上深度网络——离散小任务用 Q 表又快又稳杀鸡用牛刀只会增加调试面。验证改造是否成功我固定走三步固定种子跑三次看方差、关掉探索跑评估看成功率、把 Q 表或策略可视化出来人工抽查几个状态。这套流程帮我拦下过好几次「指标好看但策略荒谬」的假收敛。从那以后我每次改奖励函数或状态表示都强制先跑小规模对照实验再放大不再凭感觉直接上全量训练。希望这份拆解帮到你把代码跑起来剩下的边改边看就顺了。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询