简介:这份资源面向强化学习入门者与需要动手复现经典算法的学习者,提供Q-Learning算法的Matlab完整实现与逐行中文注释,帮助读者在无模型环境下理解决策策略的学习过程。代码以16×1网格世界为实验场景,王子需避开障碍物与陷阱、抵达第16格的公主,涵盖可移动矩阵TT、奖赏矩阵R、Q矩阵初始化、折扣因子gamma与epsilon-greedy策略选择,并演示贝尔曼方程更新Q值、900轮训练循环及训练后最优路径规划,注释细致便于对照理解每一步逻辑。资源包共1个doc文件,大小约33KB,内容紧凑、开箱即用。目前已有1380人学习下载,适合希望从代码层面吃透Q-Learning基本流程、并可直接运行调试的读者参考。
1. 从一份 16 格迷宫说起:为什么用 Q-Learning 交强化学习课设
很多同学做强化学习课设,第一反应是找个现成框架跑个 CartPole,结果答辩时被问“状态转移矩阵长什么样”“奖励怎么设计的”就卡住了。这份 MATLAB 版 Q-Learning 源代码走的是另一条路:不依赖任何工具箱,用 16 个状态、四个动作、一张手写的可移动矩阵,把无模型决策的完整闭环摊开给你看。它适合两类人——要交强化学习课设、毕设,需要一份能讲清原理又能跑出路径的学生;以及想快速验证奖励设计思路的从业者。整套代码不到百行,但环境建模、Q 值更新、epsilon-greedy 策略、路径回溯一个不少,属于那种“麻雀虽小、五脏俱全”的参考实现。
2. 环境建模:16 状态网格怎么变成可计算的矩阵
Q-Learning 的第一步从来不是写更新公式,而是把“世界”翻译成矩阵。这份代码最值得抄的地方,就是它用三个矩阵把迷宫讲清楚了:可移动矩阵T描述“能不能走”,奖赏矩阵R描述“走了值多少”,Q 矩阵描述“学到的经验”。三者维度一致,都是 16×16,状态编号 1 到 16 对应网格位置,动作则直接编码成“目标状态编号”,这个设计省掉了单独的动作表,是整份代码最巧妙的一处。
2.1 状态编号与动作编码的对应关系
代码把 16 个格子按行优先编号,1 到 4 是第一行,5 到 8 第二行,依此类推。四个动作 N/S/W/E 分别对应编号减 1、加 1、减 4、加 4。这种“动作即目标状态”的编码方式,让T(i,j)=1直接表示“从 i 能一步到 j”,后续选动作时find(T(state,:)==1)就能拿到所有合法后继,不需要再维护一张动作映射表。
% 定义可移动矩阵 T,T(i,j)=1 表示可从 i 一步移动到 j T = zeros(16); for i = 1:16 % 上行动作 N:不在第一行才能向上 if ~ismember(i, [1,5,9,13]) T(i, i-1) = 1; end % 下行动作 S:不在最后一行才能向下 if ~ismember(i, [4,8,12,16]) T(i, i+1) = 1; end % 左行动作 W:编号大于 4 才能向左 if i > 4 T(i, i-4) = 1; end % 右行动作 E:编号小于 13 才能向右 if i < 13 T(i, i+4) = 1; end end这段循环的含义是:先假设所有格子都能四向移动,再用边界条件把越界动作砍掉。ismember(i,[1,5,9,13])判断是否在第一行,是则不能向上;i>4判断是否不在第一行,是则能向左。参数上唯一要改的是网格尺寸,如果换成 5×5,边界判断里的行首行尾编号和i±5的步长都要同步调整,否则矩阵会错位。
2.2 障碍物、跳跃与奖赏矩阵的设计
基础移动定义完后,代码用三行覆盖式赋值把“地形”改了:T(:,[10,11,14])=0把第 10、11、14 号格子设为不可进入,相当于障碍物;T(13,:)=0清空 13 号格子的所有出边,再单独T(13,15)=1开一条跳跃通道。这种“先全连、再切断、最后补特殊边”的写法,比逐个 if 判断可读性高得多,改地图时只需要动这几行。
奖赏矩阵R的设计更值得琢磨。代码先R=-1*ones(16)把所有转移设为 -1,意思是“每走一步都有代价”,这会逼着智能体找最短路径;然后R(:,6)=0让进入 6 号格子的奖励归零,R(:,7)=-5把 7 号设为陷阱,R(:,16)=10让到达 16 号公主位置拿到最大奖励,R(13,15)=5给跳跃通道一个中等奖励。注意R(:,6)=0用的是整列赋值,表示“任何状态进入 6 都拿 0”,这和R(6,:)完全不同,前者是入边奖励,后者是出边奖励,写反了学习目标就变了。
提示:奖赏矩阵按“目标状态”索引,即
R(s,a)中 a 是动作后的状态。这是本实现的核心约定,改奖励时务必沿列方向改。
3. 训练循环:epsilon-greedy 与贝尔曼更新的落地细节
环境搭好后,学习过程其实就三件事:选动作、更新 Q、换状态。这份代码的训练循环外层跑 900 个 episode,内层用while state~=16一直走到终点,结构非常干净。真正容易翻车的是几个数值和策略细节,下面拆开讲。
3.1 随机初始化与障碍物规避
每个 episode 开始时,代码用unidrnd(16)随机选起点,再用一个while ismember(state,[10,11,14])循环把落在障碍物上的起点重抽。这个细节很多人会漏,结果智能体一开局就卡在障碍物里,内层循环永远出不来,程序直接挂死。
for episode = 1:900 % 随机化初始状态,并避开障碍物 state = unidrnd(16); while ismember(state, [10,11,14]) state = unidrnd(16); end % 未到终点则持续决策 while state ~= 16 ... end endunidrnd是 MATLAB 统计工具箱里的均匀整数采样函数,如果环境没装工具箱,可以换成randi(16),行为一致。900 这个数字不是硬性要求,网格小、奖励稀疏时 500 到 1000 都能收敛,判断标准是路径稳定不再变化,而不是死记次数。
3.2 epsilon-greedy 选择与 Q 值更新公式
内层循环先find(T(state,:)==1)拿到所有合法后继,再取对应的 Q 值possible_Q。然后按rand()<epsilon分流:小于 0.3 就随机探索,否则选 Q 最大的动作。这里epsilon=0.3意味着三成概率乱走、七成概率走当前最优,前期保证探索、后期靠贪心收敛。
possible_actions = find(T(state,:) == 1); possible_Q = Q(state, possible_actions); if rand() < epsilon % 探索:随机选一个合法动作 choose = unidrnd(length(possible_actions)); action = possible_actions(choose); else % 利用:选当前 Q 值最大的动作 [~, index] = max(possible_Q); action = possible_actions(index); end % 贝尔曼更新:即时奖励 + 折扣后的未来最大 Q Q(state, action) = R(state, action) + gamma * max(Q(action, :)); state = action;更新公式Q(s,a) = R(s,a) + gamma * max(Q(a,:))是标准的单步 Q-Learning。gamma=0.8表示未来奖励打八折,值越接近 1 越看重长远,越接近 0 越短视。注意这里用的是max(Q(action,:))而不是max(Q(state,:)),因为动作已经执行,新状态是action,取的是新状态下所有出边的最大 Q。写错成Q(state,:)是新手最常见的 bug,表现为 Q 值不收敛、路径绕圈。
3.3 训练监控与路径回溯
训练过程中每 100 次打印一次进度,属于最低成本的监控手段。真正交付时要看的是 Q 矩阵是否稳定,可以加一句norm(Q - Q_old)观察变化量。训练结束后,路径回溯从起点开始,每步取max(Q(start_0,:))对应的下一状态,直到走到 16。
start_0 = 1; % 设置起点 path = start_0; % 路径向量,初始含起点 while path(end) ~= 16 [~, next] = max(Q(start_0, :)); path = [path, next]; start_0 = next; end disp(path)这段回溯是纯贪心,没有探索,所以能直接反映学到的策略。如果path出现重复状态或死循环,说明 Q 矩阵还没收敛,或者某条边的奖励设计让智能体更愿意绕路。实际调试时我一般会先把gamma调到 0.9、epsilon调到 0.1 再跑一轮,对比路径是否一致,两次结果相同才认为策略稳定。
4. 避坑与排查:这份代码最容易翻车的五个点
代码短不代表没坑,恰恰因为短,每个细节都直接决定能不能跑通。下面五条是这类网格 Q-Learning 实现里最高频的问题,按“现象 → 原因 → 解决”整理,照着排查基本能定位。
4.1 起点落在障碍物导致死循环
现象是程序卡在训练循环里不动,CPU 占满但没有任何输出。原因是随机初始化没有排除 10、11、14 号格子,while state~=16永远不满足。解决办法就是代码里那个while ismember(state,[10,11,14])重抽循环,缺了它必挂。更稳妥的做法是把合法起点单独存成向量,用datasample或randi索引,避免反复重抽。
4.2 Q 更新用错状态维度
现象是训练几百轮后路径仍然乱跳,或者 Q 值整体不增长。原因是把max(Q(action,:))写成了max(Q(state,:)),等于用旧状态的最优值更新,贝尔曼方程的自洽性被破坏。排查方法是打印单步更新前后的 Q 值,看新状态的最大 Q 是否被正确引用。修正后 Q 值应随 episode 单调趋稳。
4.3 奖赏矩阵行列写反
现象是智能体明明到了 16 号却不拿奖励,或者疯狂往 7 号陷阱里钻。原因是R(:,16)=10被写成R(16,:)=10,前者是“进入 16 奖励 10”,后者是“从 16 出发奖励 10”,语义完全相反。检查方法是看R的非 -1 元素分布,正确情况下 10 应该在第 16 列。
4.4 epsilon 固定不衰减导致策略抖动
现象是路径每次运行都不一样,答辩演示时结果不可复现。原因是epsilon=0.3全程不变,训练后期仍有三成概率乱走,Q 值一直在小幅震荡。通常的做法是让 epsilon 随 episode 线性衰减,比如epsilon = max(0.05, 0.3 * (1 - episode/900)),前期多探索、后期多利用,路径会明显稳定。
4.5 跳跃通道破坏连通性
现象是某些起点永远到不了终点,或者路径长度异常。原因是T(13,:)=0清空了 13 号格子的所有出边,只留T(13,15)=1,如果起点在 13 附近且没有其他通路,就会困死。排查时用graph或邻接矩阵做一次连通性检查,确认从每个合法起点都能到达 16,再开始训练。
注意:改地图后务必重跑连通性检查,这是血泪经验,跳过这一步的调试时间往往比写代码还长。
5. 验收与答辩:怎么证明这套 Q-Learning 真的学到了
课设验收和工程交付不一样,老师或评审看的不是你代码多优雅,而是你能不能证明“学习确实发生了”。我的习惯是准备三样东西:一张收敛曲线、一组对比路径、一份参数敏感性表。收敛曲线用每个 episode 的步数或累计奖励画,横轴 episode、纵轴步数,正常情况应该从高位震荡逐步下降到稳定值;如果一直不降,说明奖励或更新公式有问题。对比路径则是固定起点,分别用训练前(Q 全零)和训练后的 Q 矩阵回溯,前者基本是乱走或原地打转,后者应该是干净的最短路径,这个对比最能说明问题。
参数敏感性表可以按下面这个结构准备,答辩时被问“gamma 为什么取 0.8”直接翻表:
| 参数 | 取值 | 观察到的现象 | 结论 |
|---|---|---|---|
| gamma | 0.5 | 路径偏短视,愿走陷阱抄近路 | 折扣过强,不推荐 |
| gamma | 0.8 | 路径稳定,避开 7 号陷阱 | 本场景合适 |
| gamma | 0.95 | 收敛变慢,前期绕路多 | 需更多 episode |
| epsilon | 固定 0.3 | 路径偶有抖动 | 可复现性差 |
| epsilon | 线性衰减 | 路径稳定可复现 | 推荐做法 |
答辩时还有一个高频追问:“为什么不用 SARSA?”回答抓住一点即可——Q-Learning 是 off-policy,更新用max直接逼近最优策略,SARSA 是 on-policy,更新用实际采取的下一动作,在陷阱密集的环境里 SARSA 更保守、Q-Learning 更激进。这份代码的奖励设计(陷阱 -5、终点 +10)适合 Q-Learning 的激进风格,换成 SARSA 也能跑,但路径可能更绕。被问到“900 次够不够”,不要答“够”,要答“看 Q 矩阵变化量,连续 100 轮norm小于阈值就认为收敛,900 只是本场景的经验值”。
最后说个我自己的教训:早年交这类课设,我只贴了最终路径图,结果被问“你怎么知道它不是碰巧走对的”,当场答不上来。从那以后我每次做强化学习 demo,都强制走一遍“训练前 vs 训练后”的对比回溯,再附一张步数收敛曲线,两个证据一摆,基本没人再质疑学习是否发生。希望帮到你。
本文还有配套的精品资源,点击获取