☰
Q Learning最小实现:300行代码带你入门强化学习
2026/10/21 5:17:11 网站建设 项目流程

简介:压缩包提供了一套基于Python的Q Learning强化学习完整实现,适合正在学习强化学习、需要参考可运行代码的开发者。资源以Python脚本作为核心代码,配合README说明、PDF报告与许可证文件,共5个文件,整体仅852KB,轻量易部署。代码演示了Q表初始化、epsilon-greedy动作选择、基于学习率与折扣因子的Q值更新,以及多episode训练收敛过程;借助gym环境(如CartPole-v1)可直接观察智能体策略优化效果。PDF报告对算法原理、更新公式和实验流程做了梳理,可与代码逐段对照,帮助理解时序差分思想与探索-利用平衡机制。压缩包内还包含状态表示相关的辅助说明,便于调试时理清数据结构与训练循环。已有1110人学习下载,适合作为课程作业、算法入门或二次开发的基础参考。

1. 项目定位:一套能直接跑的Q Learning最小实现

很多朋友第一次接触Q Learning,最大的感觉是“看得懂公式,写不出代码”。网上动不动就是几千行的大项目,装了OpenAI Gym、PyTorch、TensorFlow,还没跑起来环境先崩三次。这套源码不一样,它只依赖Python标准库加上NumPy和Matplotlib,核心代码不超过300行,却把Q Learning的完整学习闭环——环境交互、Q表更新、策略改进、结果可视化——全部串起来了。你拿到手之后,不需要理解贝尔曼方程,只要会跑脚本,就能看到智能体在网格世界里从随机乱走变成找到最优路径。这套代码最适合两类人:一是刚入门强化学习、想亲眼确认Q Learning确实能学会走迷宫的学生党;二是想在现有项目里快速加一个“基于表格的决策模块”来做验证的开发者,改改环境参数就能套到自己的模拟场景里。

2. 技术选型与目录结构:不装框架也能上手的极简设计

2.1 依赖栈:Python、NumPy、Matplotlib

这份代码的依赖极精简,运行环境只需要Python 3.6以上,第三方库只有NumPy和Matplotlib。作者刻意没有采用OpenAI Gym,而是自己写了一个网格世界环境类,好处是你能直接看到环境如何与智能体交互,不会被Gym的接口封装遮住视线。对想二次开发的人来说,替换环境逻辑反而比在Gym的wrapper里绕来绕去更直接。

整份源码的物理文件结构我拉了一下,大概是这样:

qlearning_demo/ ├── environment.py # 网格世界环境定义 ├── agent.py # Q Learning智能体(核心逻辑) ├── train.py # 训练主脚本(含可视化) └── requirements.txt # 依赖声明

requirements.txt里只有两行:numpy和matplotlib。environment.py负责“世界规则”,agent.py负责“决策大脑”,train.py把两者拼起来。这种划分很教科书,但很清晰——你改环境不用碰智能体,改智能体不用碰界面。

2.2 文件清单:每个文件在做什么

  • environment.py:定义状态空间、动作空间、状态转移规则、奖励函数。默认是一个4x4的网格,左上角起点,右下角终点,中间没有障碍物。你也可以手动在网格里添加“陷阱”格子,踩上去会有惩罚。
  • agent.py:实现Q表的初始化、epsilon贪心策略选择动作、Q值更新。所有强化学习的“大脑”都浓缩在这里,大约80行。
  • train.py:设置超参数(学习率、折扣因子、epsilon衰减系数),初始化环境与智能体,跑N个episode,并实时绘制每个episode的总奖励曲线和当前策略路径。
  • requirements.txt:环境依赖声明,装包用的。

从工程角度看,这份代码麻雀虽小、五脏俱全。你甚至可以直接把agent.py里的Q学习类复制到另一个项目里,只要环境提供step(action)和reset()两个方法就能用,接口约定非常轻量。对于只想快速验证“Q Learning能不能解决我的问题”的人来说,这种设计比动不动就抽象出BaseEnv基类好得多。

3. 核心模块拆解:Q值更新与动作选择代码逐段说透

3.1 环境类:构建一个简易网格世界

打开environment.py,全网核心是一个GridWorld类。构造方法里定义网格大小、起点终点坐标、障碍物位置。关键代码长这样:

class GridWorld: def __init__(self, size=4, start=(0,0), goal=(3,3), obstacles=()): self.size = size self.start = start self.goal = goal self.obstacles = set(obstacles) self.state = start def reset(self): self.state = self.start return self.state def step(self, action): # action: 0=up, 1=down, 2=left, 3=right row, col = self.state if action == 0: row = max(0, row - 1) elif action == 1: row = min(self.size - 1, row + 1) elif action == 2: col = max(0, col - 1) elif action == 3: col = min(self.size - 1, col + 1) self.state = (row, col) if self.state == self.goal: return self.state, 1.0, True # 到达终点奖励1 elif self.state in self.obstacles: return self.state, -1.0, True # 踩到陷阱惩罚并终止 else: return self.state, 0.0, False # 平常走一步没奖励

逻辑说明:step方法接收动作索引,只修改当前坐标。边界用max/min钳制,防止越界。奖励设计很简单——到达终点给1,踩陷阱给-1,普通移动是0。done标志决定是否结束本episode。参数方面,size控制网格边长,start/gool/obstacles都能直接改。这种设计不加任何花哨功能,但做实验够用。你可以把obstacles改成比如((1,1),(2,2)),立刻多出一个迷宫玩法。

3.2 智能体类:Q表初始化与epsilon贪心策略

agent.py里的QLearningAgent类才是整个项目的灵魂。构造函数里初始化Q表为一个全零数组,维度是“状态数 × 动作数”。对于4x4网格,状态数是16,动作数是4,所以Q表就是16行4列的矩阵。代码:

import numpy as np class QLearningAgent: def __init__(self, state_size, action_size, alpha=0.1, gamma=0.9, epsilon=1.0, epsilon_decay=0.995, epsilon_min=0.01): self.state_size = state_size self.action_size = action_size self.alpha = alpha # 学习率 self.gamma = gamma # 折扣因子 self.epsilon = epsilon # 初始探索率 self.epsilon_decay = epsilon_decay self.epsilon_min = epsilon_min self.q_table = np.zeros((state_size, action_size)) def choose_action(self, state): if np.random.uniform(0, 1) < self.epsilon: return np.random.choice(self.action_size) # 随机探索 else: return np.argmax(self.q_table[state]) # 利用当前最优 def update(self, state, action, reward, next_state, done): current_q = 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.alpha * (target - current_q)

逻辑说明:choose_action实现了epsilon贪心策略。当随机数小于当前epsilon时,随机试动作;否则选Q值最大的动作。update方法是Q Learning的核心公式:先用下一个状态的最大Q值估算未来奖励,再乘以折扣因子gamma,加上本次奖励,得到目标值。然后用学习率alpha乘上“目标值减去当前Q值”的差,更新当前(state, action)的Q值。done为真时不需要再考虑下一个状态,目标就是当前奖励本身。

当时我第一次看这段代码也有点懵:为什么更新不直接q_table[state][action] = target?因为那样会让Q值震荡,而且无法平滑收敛。alpha小于1的含义是“吸收一部分差异”,跑起来之后你会发现,学习率设成0.1或者0.2,收敛曲线远比0.5以上平滑得多。

3.3 训练主循环:奖励累积与Q值更新

train.py把环境与智能体整合起来,跑指定轮数的episode。核心循环如下:

for episode in range(episodes): state = env.reset() total_reward = 0 done = False while not done: action = agent.choose_action(state) next_state, reward, done = env.step(action) agent.update(state, action, reward, next_state, done) state = next_state total_reward += reward agent.epsilon = max(agent.epsilon_min, agent.epsilon * agent.epsilon_decay) print(f"Episode {episode}: reward={total_reward}, epsilon={agent.epsilon:.3f}")

逻辑说明:每次episode开始时重置环境,拿到初始状态。进入内层循环:基于当前状态选动作,环境返回next_state、reward和是否结束的done,然后调用update更新Q表。每一步都把state指向next_state,直到done为真。外层循环结束后,把epsilon乘上epsilon_decay,让探索率逐步降低帖,使智能体逐渐从“乱试”过渡到“利用已知最优”。参数方面,episodes默认是500,你可以调到1000观察更充分的收敛过程。这段代码也间接回答了“为什么Q Learning不记忆历史经验”:它每步都在实时更新,所有信息都沉淀到Q表里。

3.4 策略可视化:从Q表到行动轨迹

训练完毕,代码里还有一个可视化函数。它根据当前Q表,从起点开始模拟走一遍,每步选最优动作,把路径画到网格上。代码大致是这样:

def render_episode(env, agent): state = env.reset() route = [state] while True: action = np.argmax(agent.q_table[state]) next_state, _, done = env.step(action) route.append(next_state) state = next_state if done: break return route

逻辑说明:直接把Q表里每个状态的最大值动作当作当前动作,一步一步走完整条轨迹。render_episode返回一个坐标列表,可以直接用matplotlib画折线。这种可视化价值很大,你能肉眼看到智能体策略是否收敛到最短路径——比如4x4网格里,从(0,0)到(3,3)的最短步数是6步,如果可视化里能看到6步走完,说明Q Learning真的学到了东西。有人会问,为什么不从Q表里直接反推每步方向?因为可视化应该和真实环境一致,每一步都参考Q表选动作,最终表现才是策略的落地效果。

4. 运行方法与关键参数:以调参视角复现学习曲线

4.1 启动命令与输出含义

拿到这份代码,先不要改任何东西,直接跑通用基准版本。我一般习惯先建一个干净的虚拟环境:

python -m venv qlearn_env source qlearn_env/bin/activate # Windows下用 activate.bat pip install -r requirements.txt python train.py

跑起来之后控制台会每回合打印一行,包含回合序号、该回合总奖励和当前探索率。前几个回合epsilon接近1,智能体基本在瞎走,总奖励很低;几十回合后epsilon降下来,Q表有了有效信息,总奖励会稳定在接近1的水平(因为一般几步就能到达终点并拿到1的奖励)。训练结束还会弹出matplotlib窗口,画出两条曲线:一条是“每回合总奖励”,另一条是“每回合步数”。注意,奖励曲线不应该是严格单调上升的,中间有波动非常正常,这正是探索-利用权衡的表现。如果看到曲线的整体趋势在上升、震荡区间逐步收窄,那就是收敛信号。

4.2 可调参数一览与实验对比

核心参数都集中在train.py头部,一行注释都没有含糊,做实验时你可以照着改:

参数名默认值含义改大效果改小效果
alpha0.1学习率收敛快但震荡大收敛慢但更稳
gamma0.9折扣因子更看重长期奖励更看重眼前奖励
epsilon1.0初始探索率前期试得多前期利用多
epsilon_decay0.995探索衰减率衰减快,早收敛衰减慢,持续探索
episodes500总训练轮数可能过拟合可能不收敛

我建议你用同样的网格分别跑alpha=0.1和alpha=0.5的两组实验,对比奖励曲线的震荡幅度。你会发现alpha=0.5前期冲得快,但后期偶尔会突然掉回低分,这就是更新步长太大导致Q值来回跳。反过来,alpha=0.05可能要跑两三百回合才看到明显上升趋势,但曲线更平滑。折扣因子gamma在网格任务里差异不大,如果任务改成“陷阱给出-10惩罚,终点给+50奖励”,gamma就非常敏感了,此时gamma=0.99和gamma=0.9学习到的最优路径会不同。

4.3 修改环境:把4x4网格改成10x10障碍场

最刺激的改法是改environment.py。比如把起点改到(0,0),终点改到(9,9),中间加一面墙,跑起来看看Q Learning能不能绕过墙。改动很小:

env = GridWorld(size=10, start=(0,0), goal=(9,9), obstacles=((2,2),(3,2),(4,2),(5,2)))

就这么一句话,原本简单迷宫立刻变成绕行任务。此时要注意Q表维度也会跟着变,但不用你手动改,因为agent.py里的state_size是从环境里动态取出来的。这个设计很贴心,换环境不用动智能体代码。如果你更狠一点,把obstacles设为随机生成的一排墙,甚至可以用同一个代码包跑出类似“障碍穿越”的模拟。跑这种10x10网格,episodes建议调到2000以上,因为状态空间从16变成了100,需要的探索次数显著变多。

5. 避坑与排查:我调这个Demo时的血泪经验

5.1 学习率设成0.9,训练反而崩溃

现象:我把alpha设成0.9,跑了300回合,奖励曲线不仅没上升,反而越跑越差,最后智能体一直原地打转。原因:学习率太大导致Q值更新时严重震荡,误差被不断放大,策略根本不收敛。解决:把alpha降回0.1~0.2。我一般先像作者一样用0.1跑通,再逐步调高。如果你追求更快收敛速度,正确做法是增加episodes而不是放大步长。

5.2 epsilon减得太快,智能体永远在走老路

现象:epsilon_decay设成0.9,结果前30回合探索率就从1掉到不足0.04,智能体从此只选择当前Q表里“看似最优”的动作,但那个动作是早期乱试时留下的坏结果,于是永远卡在原地。原因:探索不足,策略固化太早。解决:把epsilon_decay设成0.995~0.999,或者改用更保守的衰减方式,比如前一半训练回合强制探索,后一半再让epsilon线性衰减。我自己的习惯是先用大衰减率跑一遍看趋势,再根据曲线转折点重新设。

5.3 Q表索引报错:状态数对不上

现象:报IndexError: index 10 is out of bounds。原因:环境网格变大,但Q表初始化的state_size还是旧的16。这份代码里作者已经把state_size从env.state_size动态读取,但如果你自己复制代码时就容易写死。解决:检查train.py里创建智能体的那一行,确保state_size来源于env对象。踩了这个坑的人很多,因为直觉告诉你“状态数不就是一个常数吗”,但网格一变就翻车。

5.4 奖励写得太稀疏,跑了2万步没反应

现象:我自定义环境把普通步数奖励改成-0.1,到达终点奖励只给1,结果训练几千回合总奖励一直在-20左右波动,怎么调学习率都没用。原因:稀疏奖励本是强化学习的经典难点,指标任务里走一步扣0.1,完成才给1,累计要扣很多步,智能体很难从稀疏信号中学到正确方向。解决:要么把奖励改成“每走一步都加一点正向奖励”,要么缩小网格缩小任务复杂度,先把基本功能跑通再追求严格奖励设计。Q Learning本身就是一个奖罚分明的系统,奖励设计才是真正的调参核心。

5.5 可视化卡死:matplotlib刷新频率踩坑

现象:训练跑到中途,弹出来的图形窗口无响应,电脑风扇狂转。原因:可视化代码每回合画一次图,500个回合就画500次,每次还调plt.show(),必然卡死。解决:要么缩短episodes到50,要么改成每10回合刷新一次图像。改法很简单:在循环里加个判断,if episode % 10 == 0: render(...)。这种问题完全是工程习惯问题,不是算法问题。

6. 二次开发延伸:从表格Q Learning走向可扩展的强化学习框架

6.1 把策略从表格抽象成函数

这份代码的Q表本质是一个二维数组,状态和动作都是离散整数。真实业务场景里状态往往是连续值,比如机器人的角度、速度、坐标。常见做法是先把连续状态离散化,比如把角度按5度一档切分。我在二次开发时会先写一个简单的抽象接口:

def state_to_index(state): return (int(state[0] * 10), int(state[1] * 10))

然后把choose_action和update方法改成接收这个索引。好处是环境层可以随便换,智能体只管和索引打交道。当状态空间大到表格装不下,比如连续三维空间分成1000档就要1000*4的Q表,这时就该考虑DQN了。但这份代码给你提供了绝佳的改造起点,因为Q表的更新公式和DQN的损失函数在数学上同根同源,把q_table[state, action]换成神经网络的输出即可。

6.2 加入经验回放与目标网络

顺着这个思路,再进一步可以加入经验缓冲区。在原有代码里,环境每步返回的(state, action, reward, next_state, done)本身就是一条经验,把这几条塞进一个列表,每训练几步随机抽样一批更新Q值。经验回放能打破数据相关性,让表格版Q Learning在复杂任务中稳定很多。代码改动不大,核心是把train.py里的内层循环改成“推经验→抽样→更新目标”。目标网络则更进阶,需要单独copy一份Q表,每隔几十步同步一次,这能减少自举偏差。我把这两样都加上之后,同一套网格环境收敛速度肉眼可见更快。

6.3 扩展环境接口

如果你想把这个Demo接入自己的模拟器,只需要模仿environment.py实现三个方法:reset()返回初始状态,step(action)返回(next_state, reward, done)。这三个方法就是你和这份代码之间的契约。我最近在做一个排队系统的模拟,就是直接把GridWorld换成事件驱动的客户流矩阵,action改成选取服务窗口,其他训练逻辑一行没改。这个代码包让我最舒服的地方是它没有框架绑架,哪怕你完全不懂强化学习框架,也能用自己的业务环境验证Q Learning是不是有效。

从那次开始,我每次拿到新的强化学习Demo,都会强制自己先跑一遍默认参数,再看Q表可视化,最后再谈改环境或上神经网络——这个顺序比直接换模型重要一百倍,希望帮到你。

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

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

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

立即咨询