简介面向强化学习初学者的Q-Learning算法MATLAB实现与注释文档以王子寻找公主的16格网格世界为实验环境演示无模型场景下如何通过试错逐步优化决策策略。文档为1个doc文件压缩包仅33KB覆盖可移动矩阵、奖赏矩阵、Q矩阵初始化、折扣因子与探索率设置、ε-greedy策略选择以及贝尔曼方程更新核心代码并配有逐行中文注释便于对照理论理解。训练过程设置900次迭代随机初始状态会避开障碍物每100次输出一次进度最终通过最大Q值回溯生成从起点到公主所在位置的最优路径完整展示强化学习“探索-利用”平衡和累积奖励设计思路。已有1378人学习适合机器学习入门阶段对Q-Learning原理感兴趣或需要轻量化MATLAB示例辅助实验的读者阅读后能获得可运行调试的算法骨架、参数调节经验以及从矩阵建模到策略输出的排错思路。1. 强化学习入门就从这个 16×16 网格世界开始Q-Learning 王子找公主源码拆解这份 Q-Learning 源码把强化学习最经典的算法压在了不到百行的 MATLAB 代码里场景是一个 16 格的编号网格世界王子从任意位置出发上下左右移动目标是走到 16 号格找到公主同时躲开 7 号格的陷阱还能利用 13 号格跳到 15 号格的“近道”。代码不长但环境矩阵、奖励矩阵、epsilon-greedy 策略、贝尔曼更新、路径回溯全齐了非常适合刚接触强化学习、想把 Q 表更新公式落到矩阵运算上的读者。比看公式推导更直接的是你能亲眼看到 Q 矩阵从全零变成一条可走的路径这也是我做强化学习 Demo 时最常用的一份复现底稿。2. 环境建模可移动矩阵、障碍物与跳跃规则是怎么写进代码的强化学习三要素在源码里的映射很清晰状态就是 16 个格子动作只有上、下、左、右四种奖励则全部收在奖赏矩阵 R 里。这个例子没有用复杂的仿真环境而是用两个 16×16 矩阵把整个游戏规则“编码”进代码理解清楚这一步后面训练循环读起来会顺很多。2.1 四个移动方向与 16×16 状态转移矩阵源码先用一个 16×16 的全零矩阵来存“从 i 号格子能否移动到 j 号格子”1 表示可移动0 表示不可移动。这个矩阵我一般称它为状态转移矩阵它其实就是环境的静态规则表。构建部分的代码我加了注释% 定义可移动矩阵 TT(i,j)1 表示从 i 可以移动到 j T zeros(16); for i 1:16 % 上行动作 N从 i 移动到 i-1 % 1,5,9,13 是每一列的最上方一格不能再往上 if ~ismember(i, [1,5,9,13]) T(i, i-1) 1; end % 下行动作 S从 i 移动到 i1 % 4,8,12,16 是每一列的最下方一格不能再往下 if ~ismember(i, [4,8,12,16]) T(i, i1) 1; end % 左行动作 W从 i 移动到 i-4 if i 4 T(i, i-4) 1; end % 右行动作 E从 i 移动到 i4 if i 13 T(i, i4) 1; end end这里的编号规则是按列排的1-4 是第一列5-8 是第二列9-12 是第三列13-16 是第四列。所以 i-1 和 i1 是在同一列内上下走i-4 和 i4 才是左右跨列。边界条件写得很明确最上方一格不能再上、最下方一格不能再下、第一列不能左、第四列不能右。判断ismember(i,[1,5,9,13])其实就是判断 i 是否落在每一列的顶部。我每次复现这段代码时都会顺手把 T 矩阵打印出来看一眼确认对角线方向上有没有多余的 1 或者漏掉的 0。因为T(i,i-1)这类赋值方式如果边界判断写错很容易把对角线上的非法动作放进去而这个错在训练阶段很难察觉只会表现为某些路径特别怪。2.2 障碍、陷阱与跳跃奖赏矩阵 R 的设定逻辑和三个隐藏细节环境规则定义完后源码接着做了三件事设置障碍物、设置跳跃、设置奖励。代码不长但信息量很大% 定义不可达到区域障碍物10,11,14 三列全部置 0 % 表示任何状态都不能移动到这三个格子 T(:,[10,11,14]) 0; % 定义跳跃13 号位置只能跳到 15 号位置 T(13,:) 0; T(13,15) 1; % 定义奖赏矩阵 R默认进入任意格子都是 -1 R -1 * ones(16); % 进入 6 号格奖励 0安全通道 R(:,6) 0; % 进入 7 号格惩罚 -5陷阱区域 R(:,7) -5; % 到达 16 号格奖励 10终点公主所在地 R(:,16) 10; % 从 13 跳到 15 额外奖励 5 R(13,15) 5; % 初始化 Q 矩阵为全零 Q zeros(16);这里要注意 R 的赋值方式是R(:,k)意思是“所有状态执行动作后进入 k 号格”都拿到对应奖励。也就是说奖励绑定的是目标格子不是动作本身。这是强化学习里很常见的设定——进入某个状态本身带有好坏属性。三个容易忽略的细节第一T(:,[10,11,14])0清零的是列不是行。也就是说10、11、14 这三个格子“不能作为目的地”但它们作为出发地的动作行还保留着。底层逻辑是训练起点用while循环避开了这三个格子所以 Q 更新几乎不会从它们开始启动。如果你直接把这段环境代码拿去做别的实验最好把T(10,:)、T(11,:)、T(14,:)也置零否则会出现“从障碍物出发”这种逻辑上说不通的动作。第二跳跃的设计是单向的13 号只能跳到 15 号但 15 号不能跳回 13 号。T(13,:)0先把 13 号所有出边清空再单独打开T(13,15)1。这种写法比逐个赋值干净是环境建模里值得借鉴的小技巧。第三R 矩阵默认全是 -1意味着几乎所有移动都会带来一个小的负奖励。这个设定会让 Q 矩阵大部分值是负数但这是正常的不是 bug。负奖励相当于给王子一个“时间成本”的暗示让它更倾向于走短路径而不是毫无目的地乱逛。3. 训练循环epsilon-greedy 策略、Q 值更新与两个核心参数怎么调环境矩阵定义好后真正跑强化学习的核心是训练循环。源码的循环结构很标准随机起点、循环直到终点、在每个状态选动作、执行动作后立即更新 Q 值。这一章把训练主循环逐段拆开讲再聊两个关键参数的取值逻辑。3.1 主循环拆解随机起点、可选动作和贝尔曼更新的一次往返训练部分源码我加上了行级注释% 折扣因子 gamma控制未来奖励的重要程度 gamma 0.8; % epsilon 是探索概率30% 的概率随机动作70% 的概率选当前最优 epsilon 0.3; % 训练 900 轮 for episode 1:900 % 随机初始化起点并避开障碍格 10,11,14 state unidrnd(16); while ismember(state, [10,11,14]) state unidrnd(16); end % 一直训练到到达终点 16 为止 while state ~ 16 % 找出当前状态下所有可执行的动作 possible_actions find(T(state,:) 1); possible_Q Q(state, possible_actions); % epsilon-greedy以 epsilon 概率随机探索 if rand() epsilon choose unidrnd(length(possible_actions)); action possible_actions(choose); else % 否则选择 Q 值最大的动作 [~, index] max(possible_Q); action possible_actions(index); end % 贝尔曼更新 Q(state, action) R(state, action) gamma * max(Q(action,:)); % 移动到下一个状态 state action; end % 每 100 轮输出一次进度 if mod(episode, 100) 0 fprintf(训练次数%d\n, episode); end end这段代码最值得看的是更新那一行Q(state,action) R(state,action) gamma * max(Q(action,:))。它对应的就是贝尔曼最优方程里“当前奖励 折扣后的未来最优价值”这一项。max(Q(action,:))是在计算下一状态所有可能动作的 Q 值里最大的那个代表“假设下一步也采取最优策略”的价值。需要注意的是这个更新是直接赋值不是增量更新。标准 Q-Learning 更新式其实是Q(s,a) Q(s,a) alpha * (R gamma * max(Q(s,a)) - Q(s,a))源码相当于把学习率 alpha 直接取成了 1。这样的好处是收敛快坏处是如果奖励矩阵噪声大Q 值会抖动。不过在这个 16 格小世界里状态空间小、规则确定用 alpha1 完全没有问题。unidrnd(16)是均匀随机取 1 到 16 的整数这里就是随机初始化起点。find(T(state,:)1)返回当前状态所有可移动目标格的索引。possible_Q只取出当前状态下可行动作对应的 Q 值避免在不可行动作上做 argmax。这几个小细节新手很容易抄错尤其是possible_Q的索引过滤漏掉它会让算法去更新根本不能执行的动作。3.2 gamma 与 epsilon 的选择收敛速度与探索深度的权衡源码里 gamma0.8、epsilon0.3 其实是两个非常保守但常用的默认值。我把参数的作用和调法整理成了一张表参数当前值作用调大调小gamma0.8折扣因子控制未来奖励相对当前奖励的权重更看重远期收益适合长路径规划更看重眼前收益适合短视策略epsilon0.3探索概率决定随机动作的占比探索更多能发现新路径但收敛变慢更早利用已有策略可能过早陷入局部最优gamma 在这个例子里调到 0.9 后路径会更倾向于绕开 7 号陷阱、优先走 13→15 的跳跃因为远期收益被放大了。而调到 0.5 以下时王子很容易被 6 号格的 0 奖励吸引停留在“安全但离终点远”的区域因为未来收益打个五折后终点那 10 的奖励已经不够诱人了。epsilon 的调节要结合训练轮数看。如果 900 轮训练后起点 1 到终点 16 的路径还是绕远我一般先把 epsilon 提到 0.4 增加探索而不是急着加训练轮数。反过来如果路径已经稳定但偶尔会抽到随机动作绕路说明 epsilon 偏大降到 0.2 会更快收敛。这个案例里还有一个容易被误解的点源码注释把 epsilon 叫作“策略选择概率也就是学习率”但它并不是严格意义上的学习率。真正的学习率是更新公式里控制“旧 Q 值保留多少、新信息吸收多少”的 alpha源码里没有显式定义。我在下面第四章节里会专门讲这个。4. 避坑与常见问题这个 Q-Learning 案例里最容易被误解的五个细节这一段写的是我复现这份源码时真实踩过的坑以及从结构上看起来不明显、但跑起来会让人困惑的地方。每条都按“现象 → 原因 → 解决”的线索整理新手直接照做能少走很多弯路。4.1 “TT 定义、T 使用”的变量名不一致复制代码后第一处报错现象源码开头写的是TTzeros(16)但后面所有逻辑用的都是T第一次运行会报“未定义变量 T”或者“索引超出矩阵维度”。原因原文档可能是从某个调试版本里摘出来的声明变量的名字和实际使用变量没统一。MATLAB 工作区如果之前跑过别的脚本恰好残留了同名变量这种错误还不会第一时间暴露而是带着旧数据往下跑结果更隐蔽。解决复制代码后先统一变量名。我一般直接在头部把声明改成T zeros(16)然后全选搜索一遍确保没有TT残留。如果是在已有工作区里反复调试先执行一次clear all或clear T避免旧变量串台。4.2 epsilon 被叫成“学习率”探索率不是学习率源码里没有 α现象很多读者看到注释写着“策略选择概率,也就是学习率”就把 epsilon 当成了 Q 值更新幅度调大后路径反而上下乱跳。原因epsilon 控制的是“有多大概率随机选动作”它只影响策略不直接影响 Q 值更新的步长。源码里没有学习率 alpha更新行是直接赋值Q(state,action) R gamma * max(Q(_,:))相当于 alpha1每次更新都完全覆盖旧值。解决把这个概念在脑子里拆开。如果想让 Q 值更新更平滑按标准形式写Q(s,a) Q(s,a) alpha * (R gamma * max(Q(s,:)) - Q(s,a))alpha 取 0.1 到 0.5 之间。在状态空间大的场景下alpha1 会导致 Q 值震荡但在 16 格的例子里直接用源码写法没问题。4.3 负奖励矩阵让 Q 值全为负这不代表训练失败现象900 轮训练完打印 Q 矩阵发现绝大多数格子是负值怀疑是不是奖励设计错了、Q-Learning 没收敛。原因R 默认是 -1只有进入 6 号是 0、进入 16 号是 10所以大部分状态-动作对的 Q 值算出来都是负的。反复加总负值和打折的折扣因子时这些负数相互叠加Q 矩阵整体偏负是正常现象。解决不要看 Q 值的正负看相对高低。以起点 1 为例通往 16 方向的动作 Q 值会明显高于朝反方向的动作。我一般会打印终点的邻接状态比如 15 号状态中往 16 走的 Q 值接近 10而往 14 走的 Q 值则远低于它这就是收敛的微观证据。4.4 路径回溯可能原地绕圈只看 max(Q) 不处理平局现象设置起点为 1 后回溯出来的路径出现 1→2→1→2 这类反复横跳甚至始终到不了 16。原因原回溯代码的伪逻辑是“每次取 Q 值最大的动作一直走到终点”。但如果有多个动作的 Q 值相同max返回的是第一个最大值的索引不一定是逻辑上的最优方向。当两个格子互相把对方当作“次优解”时回溯就会在两个状态间循环终点条件永远触发不了。解决给回溯加一个访问记录和步数上限。我在复现时习惯这样改visited zeros(1,16); step 0; max_step 20; while path(end) ~ 16 step max_step [~, next] max(Q(start_0,:)); % 发现访问过同一个格子说明陷入了循环 if visited(next) 1 fprintf(路径在第 %d 步进入循环建议重新训练\n, step); break; end visited(next) 1; path [path, next]; start_0 next; step step 1; end加了max_step后即使陷入平局也能快速退出不会让脚本卡死。如果路径在中途 break我多半会把 epsilon 调大或者增加 episode 数量重新训练。4.5 障碍格列清零但行没清零从障碍“出发”的动作仍然存在现象训练完手动查看 Q 矩阵时发现 10、11、14 这几个格子对应的行里还有非零值好像“障碍物也会主动行动”。原因T(:,[10,11,14])0只清零了这三列也就是禁止所有状态“移动到”它们但T(10,:)、T(11,:)、T(14,:)这几行没动从障碍格出发的动作仍是 1。训练里随机起点被 while 循环避开所以不影响学习但 Q 矩阵里如果恰好有其他路径经过它们就会产生语义矛盾。解决如果你想把障碍做彻底在环境初始化阶段把障碍状态对应的行也置零T([10,11,14],:) 0; T(:,10) 0; T(:,11) 0; T(:,14) 0;这样障碍格就变成了真正不可进也不可出的“死块”。不过要注意这样改之后原本从某些格子能移动到 10/11/14 的动作全部失效等价于改变了原有地图结构训练出的路径会和原版有差异。5. 路径回溯与结果验证把训练好的 Q 矩阵变成一条可直接巡检的路线训练只是让 Q 矩阵收敛真正检验训练效果的是把 Q 矩阵“翻译”成一条路径。这个案例的一个优点是环境足够小训练完成后可以立刻从任意起点回溯到终点整个过程肉眼可见非常适合用来验证对 Q-Learning 的理解。5.1 从任意起点回溯路径源码怎么用回溯部分的源码很短核心逻辑就是不断选择 Q 值最大的动作% 设定起点 start_0 1; % path 以起点开始 path start_0; % 反复选择 Q 值最大的动作直到到达终点 16 while path(end) ~ 16 [~, next] max(Q(start_0,:)); path [path, next]; start_0 next; end disp(path);以起点 1 为例训练正常的话这条路通常会走成 1→5→9→13→15→16也就是第一列向上走到 13 号利用跳跃到 15 号再向下移动到 16 号终点。这个路径避开了 7 号陷阱也没有绕到 10/11/14 障碍区说明奖励矩阵里的负惩罚和跳跃正值共同起了作用。我一般在跑完回溯后会顺手验证两个边界一是把起点改到 2、3 等中间位置确认任意起点都能收敛到终点二是检查路径长度如果起点 1 走出来的路径超过 8 步我会认为训练没有充分收敛。5.2 验证技巧打印 Q 矩阵并观察关键位置的数值梯度回溯路径只是“结果正确”想确认“训练过程正确”我还会打印一次 Q 矩阵的关键行。重点看 15 号状态和 13 号状态15 号往 16 方向的动作 Q 值应该最高13 号往 15 方向的动作 Q 值也应该显著高于其他方向。如果 15 号状态里往 16 的 Q 值只有 3 左右说明 gamma 设置的未来奖励权重偏低或者训练轮数不够终点的 10 分奖励还没完全传导到前面几个状态。这时候我一般会做两件事把 gamma 提到 0.9或者把 episode 从 900 加到 1500。还有一个很实用的技巧把 Q 矩阵转成热力图看。16×16 的矩阵直接看数字不够直观但画成图像后能一眼看出终点 16 列上方块颜色明显更深而 7 号陷阱附近颜色偏暗。这种视觉检查在碰到更大的状态空间时尤其好用能快速定位“哪个区域没有被训练覆盖”。从那以后我每次复现 Q-Learning 的 Demo不管状态空间多小都强制走一遍“打印 Q 矩阵关键行 带步数上限的路径回溯 热力图检查”这条流程。很多以为训练失败的情况其实只是循环或平局没处理很多以为成功的路径也可能只是因为运气好跳过了一个未覆盖状态。这套验证习惯帮我省了不少调试时间。希望帮到你。本文还有配套的精品资源点击获取