简介这是一份面向强化学习初学者与算法实践者的Q-Learning入门资料以Matlab源代码配合详细注释的形式帮助读者理解无模型决策算法的完整实现流程。资源包内共1个doc文档大小约33KB内容涵盖可移动矩阵TT、奖赏矩阵R、Q矩阵初始化、折扣因子gamma与epsilon-greedy策略等核心代码段并附有逐行中文注释。案例以王子寻找公主的16x1网格游戏为背景演示了障碍物规避、正负奖励设置、贝尔曼方程更新Q值以及训练结束后规划最优路径的全过程训练循环每100次迭代输出一次进度。目前已有1378人学习下载适合希望从零掌握强化学习基础、需要可运行代码对照练习的读者通过这份注释完整的脚本可快速理解状态动作定义、奖励机制与策略选择之间的配合关系。1. Q-Learning 源代码及注释(matlab).doc从一份老文档里挖出能跑通的强化学习骨架很多人第一次接触强化学习不是从 PyTorch 的 DQN 教程开始而是从一份流传多年的Q-Learning 源代码及注释(matlab).doc开始的。这份文档的典型结构是一段几十行的 MATLAB 主脚本加上逐行中文注释配一个简单的格子世界Grid World环境最后画出一条从起点到终点的路径图。它解决的问题非常具体——让一个智能体在不知道环境规则的前提下通过反复试错学会走迷宫。适合谁适合刚学完马尔可夫决策过程、想亲手看到 Q 表被填满的本科生和转行工程师也适合需要给学生演示「强化学习到底在算什么」的授课者。它不解决高维状态、连续动作、深度网络这些问题但它是理解 Bellman 方程最省事的一块敲门砖。下面我按「先立住理论、再复现代码、最后讲坑」的顺序把这份文档拆成能直接抄作业的实战笔记。2. Q-Learning 的数学骨架与 MATLAB 选型理由2.1 从 Bellman 最优方程到 Q 表更新公式Q-Learning 的核心只有一行更新式但这一行背后是完整的动态规划思想。定义状态s、动作a、即时奖励r、折扣因子γ、学习率αQ 值表示「在状态 s 下执行动作 a 之后按最优策略继续走能拿到的期望累计回报」。它的更新规则是Q(s,a) ← Q(s,a) α · [ r γ · max_a Q(s,a) − Q(s,a) ]方括号里那一项叫 TD 误差Temporal Difference Error。注意max_a Q(s,a)用的是下一状态的最大 Q 值而不是实际执行动作后的 Q 值——这正是 Q-Learning 属于 off-policy 的原因它学的是最优策略哪怕当前行为策略还在随机乱走。理解这一点后面调参时才知道为什么ε可以慢慢降、而α不能乱设。在 MATLAB 里Q 表就是一个二维矩阵行是状态编号列是动作编号。格子世界有 16 个格子就开16×4的矩阵动作是上下左右四个方向。这种「状态离散、动作离散」的设定让整份代码不需要任何工具箱纯基础语法就能跑这也是它当年被大量课程选作教学案例的原因。2.2 为什么用 MATLAB 而不是 Python 复现现在主流当然是用 Python但 MATLAB 在这类教学场景里有三个现实优势。第一矩阵操作是原生语法Q(state,:)直接取一行、max(Q(next_state,:))直接取最大值不需要 import numpy对没写过代码的学生门槛极低。第二绘图是内置的plot、scatter、rectangle几行就能把格子世界和路径画出来省掉 matplotlib 的配置成本。第三脚本即文件双击就能运行不需要配虚拟环境、装依赖这对实验室机房里装不了新软件的电脑很关键。当然代价也明显没有向量化环境并行、没有 GPU、跑大状态空间会慢到怀疑人生。所以我的建议是——用 MATLAB 这份代码理解算法理解完立刻用 Python 重写一遍做扩展。下面给出可复现的 MATLAB 版本逻辑和那份文档一致但把注释补全、把随机种子固定方便你对照结果。2.3 最小可复现的 MATLAB 实现先定义环境。用一个 4×4 的格子世界起点在左上角终点在右下角中间放一堵墙。状态编号按行优先从 1 到 16。% gridworld_qlearning.m % 4x4 格子世界起点(1,1)终点(4,4)障碍在(2,2)和(3,2) clear; clc; rng(42); % 固定随机种子保证结果可复现 nRows 4; nCols 4; nStates nRows * nCols; % 16 个状态 nActions 4; % 1上 2下 3左 4右 Q zeros(nStates, nActions); % Q 表初始化全 0 alpha 0.1; % 学习率 gamma 0.95; % 折扣因子 epsilon 0.9; % 初始探索率 epsilonMin 0.01; epsilonDecay 0.995; nEpisodes 500; % 训练回合数 % 障碍与终点 obstacles [6, 10]; % 状态编号(2,2)6, (3,2)10 goalState 16; reward -ones(nStates, 1); % 每走一步 -1鼓励最短路径 reward(goalState) 10; % 到达终点给正奖励这段初始化的关键在奖励设计。每步给-1而不是0是为了让智能体有动力尽快结束回合如果每步给 0、终点给 1在折扣因子小于 1 时绕远路和走直线的累计回报差距会被压缩收敛会变慢。这是很多人照抄代码后「怎么都学不会」的第一个隐藏原因。接下来是状态转移函数。给定当前状态和动作返回下一个状态和是否撞墙。function [nextState, hitWall] step(state, action, nRows, nCols, obstacles) hitWall false; r ceil(state / nCols); % 当前行 c mod(state - 1, nCols) 1;% 当前列 switch action case 1, r r - 1; % 上 case 2, r r 1; % 下 case 3, c c - 1; % 左 case 4, c c 1; % 右 end % 越界或撞障碍都视为原地不动 if r 1 || r nRows || c 1 || c nCols nextState state; hitWall true; return; end nextState (r - 1) * nCols c; if ismember(nextState, obstacles) nextState state; hitWall true; end end这里有个容易忽略的细节撞墙时nextState返回原状态而不是报错或跳到别处。这样智能体会在墙边反复拿负奖励Q 值自然被压低最终学会避开。如果你把撞墙处理成「回合终止」智能体会把撞墙当成一种逃避负奖励的手段反而主动撞墙这是血泪经验。主训练循环如下for ep 1:nEpisodes state 1; % 每回合从起点开始 steps 0; while state ~ goalState steps 200 % ε-贪婪策略选动作 if rand epsilon action randi(nActions); else [~, action] max(Q(state, :)); end [nextState, ~] step(state, action, nRows, nCols, obstacles); % Q-Learning 更新 tdTarget reward(nextState) gamma * max(Q(nextState, :)); Q(state, action) Q(state, action) alpha * (tdTarget - Q(state, action)); state nextState; steps steps 1; end epsilon max(epsilonMin, epsilon * epsilonDecay); endsteps 200是防止智能体在早期随机策略下无限打转属于工程上的保险丝。epsilon每回合衰减一次从 0.9 降到 0.01前期多探索、后期多利用。训练完把 Q 表按最大动作走一遍就能画出路径。2.4 参数怎么设α、γ、ε 的取值边界参数常用范围设太大设太小α 学习率0.05 ~ 0.3Q 值震荡不收敛收敛极慢几百回合没动静γ 折扣因子0.9 ~ 0.99远期奖励权重过高短路径学不出来只看眼前绕路ε 初始探索率0.8 ~ 1.0前期几乎纯随机浪费回合探索不足卡在局部最优ε 衰减0.99 ~ 0.999衰减太快没探索够就贪心长期随机Q 表学不精我的习惯是 α 取 0.1、γ 取 0.95、ε 从 0.9 按 0.995 衰减500 回合内基本能稳定收敛到最短路径。如果换成更大的格子比如 10×10回合数要提到 2000 以上或者把 α 调到 0.2 加速。3. 把文档里的代码跑起来环境搭建与逐段调试3.1 文件组织与运行方式那份文档通常把所有代码塞在一个脚本里函数定义放在文件末尾。MATLAB 从 R2016b 开始支持脚本内定义局部函数但如果你用的是更老的版本必须把step单独存成step.m。我一般这样组织qlearning_demo/ ├── gridworld_qlearning.m % 主脚本 ├── step.m % 状态转移函数老版本需要 └── draw_path.m % 路径可视化运行方式就是cd到目录命令行敲gridworld_qlearning或者直接点编辑器上方的运行按钮。如果报Undefined function step八成是版本问题把函数拆出去即可。3.2 用断点验证 Q 表是否真的在更新新手最容易犯的错是「跑完了但不知道对不对」。我的做法是在主循环里加一个断点观察 Q 表的变化。具体操作在Q(state, action) ...这一行左侧点一下设断点运行后按 F10 单步看工作区里Q矩阵的值。前几个回合应该大部分是负数或零几十回合后终点附近的状态比如 11、12、15会先出现正值然后像水波一样往起点扩散。如果跑了 100 回合 Q 表还是全零检查三件事奖励是不是全设成了 0、gamma是不是设成了 0、更新式里max是不是写成了min。3.3 可视化路径与收敛曲线训练完加一段绘图把最优路径和每回合步数画出来一眼就能判断学没学会。% 按贪心策略回溯路径 path 1; state 1; while state ~ goalState length(path) 50 [~, action] max(Q(state, :)); [state, ~] step(state, action, nRows, nCols, obstacles); path(end1) state; %#okAGROW end fprintf(最优路径长度%d 步\n, length(path) - 1); % 画格子与路径 figure; hold on; axis equal; axis([0 nCols 0 nRows]); for s 1:nStates r ceil(s / nCols); c mod(s - 1, nCols) 1; if ismember(s, obstacles) fill([c-1 c c c-1], [nRows-r nRows-r nRows-r1 nRows-r1], k); elseif s goalState fill([c-1 c c c-1], [nRows-r nRows-r nRows-r1 nRows-r1], g); end end [pr, pc] ind2sub([nRows nCols], path); plot(pc - 0.5, nRows - pr 0.5, r-o, LineWidth, 2); title(Q-Learning 学到的最优路径);ind2sub把状态编号还原成行列坐标pc - 0.5是为了让点落在格子中心。如果画出来的路径穿墙说明step函数里的障碍判断写错了重点检查ismember那行。3.4 收敛曲线判断训练是否到位光看路径不够还要看每回合步数是否稳定。在循环里记录steps训练完画出来stepsHistory zeros(nEpisodes, 1); % 在 while 循环结束后加stepsHistory(ep) steps; figure; plot(stepsHistory); xlabel(回合); ylabel(步数); title(每回合步数收敛曲线); grid on;健康的曲线是前期剧烈波动几十到上百步中后期快速下降并稳定在最短路径长度附近。如果曲线一直高位震荡不下降优先怀疑epsilon衰减太慢或alpha太小如果曲线早早掉到很低但路径不是最短多半是陷入了局部最优把epsilonMin调大一点、让它保留一点探索。4. 避坑与排查Q-Learning 在 MATLAB 里最容易翻车的五个地方4.1 现象Q 表数值爆炸到 Inf 或 NaN原因通常是奖励设得过大加上gamma接近 1TD 目标被反复放大。比如终点奖励设成 1000、gamma设 0.99几十步之后gamma^n虽然衰减但中间状态的 Q 值会先冲高。解决把奖励控制在[-1, 10]这种量级或者把alpha降到 0.05 以下。另外检查reward是不是被误设成了全局变量在别处被改。4.2 现象智能体学会绕圈而不去终点这是典型的「负奖励陷阱」。如果每步奖励是 0、只有终点是正奖励且gamma偏小智能体可能发现「原地不动」的累计回报和「走远路」差不多于是选择最省事的原地打转。解决每步给一个小的负奖励-1 或 -0.1让时间本身有成本。同时确认steps上限不要设得太大否则绕圈也能撑满回合。4.3 现象每次运行结果都不一样无法复现MATLAB 默认的随机数生成器每次启动都不同。解决脚本开头加rng(42)固定种子。注意randi、rand都受这个种子控制。如果你在并行环境或parfor里跑种子管理会更复杂教学场景直接串行就好。4.4 现象状态编号和行列坐标对不上格子世界的状态编号有行优先和列优先两种约定那份文档里如果没写清楚很容易在step和绘图之间用错。行优先是state (r-1)*nCols c列优先是state (c-1)*nRows r。解决全篇统一一种并在注释里写明。我一般用行优先因为和矩阵的sub2ind默认行为一致。4.5 现象训练几百回合后 Q 表还是全零先确认更新那行真的被执行了——在循环里加disp(Q(1,:))打印第一行。如果一直是零检查tdTarget的计算里reward(nextState)是不是取错了索引应该用下一状态的奖励不是当前状态。另一个隐蔽原因是max(Q(nextState, :))在nextState是终点时返回的是终点那一行的最大值如果终点行没被更新过就是 0这本身没错但要确保终点状态至少被访问过一次。5. 从 Q 表到深度 Q 网络这份代码还能怎么往下走把这份 MATLAB 代码吃透之后最自然的进阶是把它翻译成 Python 并用神经网络替换 Q 表。但在此之前我建议先在这份代码上做两个小实验成本极低、收益很大。第一个实验是改状态表示。把 4×4 扩到 10×10观察 Q 表大小从 16×4 涨到 100×4训练回合数需要多少才能收敛。你会直观感受到「表格法随状态数线性膨胀」这件事这正是深度 Q 网络存在的理由——用函数逼近代替查表。第二个实验是把epsilon的衰减策略从乘法改成线性对比收敛曲线理解探索调度对最终策略的影响。如果要做 Python 版本映射关系是这样的Q矩阵换成nn.Linear(state_dim, action_dim)max(Q(next_state,:))换成target_net(next_state).max()rng换成torch.manual_seed。但注意Python 版必须加经验回放Replay Buffer和目标网络Target Network否则在连续状态空间下训练极不稳定。这两个机制在表格法里不需要因为表格法每次更新只影响一个格子不会像神经网络那样「改一个参数影响所有状态」。验证方法上我习惯用两个指标卡住一是训练曲线是否收敛到理论最短步数二是把epsilon设成 0 跑 100 次测试看成功率是否 100%。如果测试成功率低于训练时的表现说明过拟合了训练路径需要增大探索或增加环境随机性。最后说个我自己的教训。早年我照着这类文档改代码为了让收敛快把alpha调到 0.5、gamma调到 0.99结果 Q 值在正负之间反复横跳画出来的路径每次都不一样我以为是随机种子的问题折腾了一整天才发现是学习率过大导致 TD 误差被过度放大。后来我养成一个习惯任何 Q-Learning 实现先把alpha固定在 0.1、gamma固定在 0.95跑通最短路径之后再动参数。这个顺序能省掉大量玄学调试。希望帮到你。本文还有配套的精品资源点击获取