简介:这份资源面向强化学习入门者与需要动手复现经典算法的学习者,提供Q-Learning算法的Matlab完整实现与逐行中文注释,帮助读者在无模型环境下理解决策策略的学习过程。代码以16×1网格世界为实验场景,王子需避开障碍与陷阱、抵达第16格的公主,涵盖可移动矩阵TT、奖赏矩阵R、Q矩阵初始化、折扣因子gamma与epsilon-greedy策略选择,并演示贝尔曼方程更新Q值、900轮训练循环及训练后最优路径规划,注释细致便于对照理解每一步逻辑。资源包共1个doc文件,大小约33KB,内容紧凑、开箱即可在Matlab中运行调试。目前已有1380人学习下载,适合作为强化学习课程实验、算法入门练手或教学演示的参考素材,读者可据此掌握环境建模、奖励机制设计与策略迭代的完整流程。
1. Q-Learning 的“无模型”到底省掉了什么:从一张 16×16 的矩阵说起
很多人第一次看 Q-Learning 的代码,会觉得它“不像学习”——没有梯度下降,没有损失函数,甚至连一次像样的反向传播都没有,只有一行Q(state,action)=R(state,action)+gamma*max(Q(action,:))反复执行。可就是这一行,让一个在 16 格网格里乱撞的智能体,最终能稳定找到通往终点的路径。反直觉的地方在于:它压根没学过环境长什么样,却表现得像掌握了环境。这份 MATLAB 源码把这件事摊得很平——可移动矩阵T、奖赏矩阵R、Q 矩阵Q三张表,加上折扣因子gamma和探索概率epsilon,就把无模型强化学习的骨架搭完了。搞懂它为什么能 work,比会调用一个库有价值得多,因为后面所有深度强化学习的变体,几乎都是在这行更新公式上做文章。
2. 环境建模:为什么用矩阵而不是函数来描述世界
2.1 可移动矩阵 T 与状态转移的显式表达
这份代码最值得先看的是环境怎么被“写死”成矩阵。16 个位置,每个位置能往哪走,全部塞进一个 16×16 的T矩阵里,T(i,j)=1表示能从 i 走到 j。上行动作对应i-1,下行动作对应i+1,左行动作对应i-4,右行动作对应i+4——这是把一维编号按 4×4 网格展开后的偏移量。边界判断用的是ismember(i,[1,5,9,13])这类硬编码,本质是在说“第一列不能往上走”。
% 定义可移动矩阵 T T=zeros(16); for i=1:16 % 上行动作 N:不在第一列才能上移 if(not(ismember(i,[1,5,9,13]))) T(i,i-1)=1; end % 下行动作 S:不在最后一列才能下移 if(not(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这段循环的机制含义是:状态转移是确定性的,给定状态和动作,下一个状态唯一。T矩阵的行是当前状态,列是目标状态,值为 1 代表这条边存在。参数上,i-1、i+1、i-4、i+4这四个偏移量不是随便定的,它们对应 4 列网格的上下左右,一旦网格尺寸变了,这四个常数就得跟着改,这是硬编码带来的耦合。
常见的实现思路是把这个矩阵换成邻接表或转移函数next_state(s,a),省内存也省得改常数。但用矩阵的好处是查询极快,find(T(state,:)==1)一行就能拿到所有合法动作,对教学和小规模问题足够。这里的设计取舍很清楚:用空间换表达清晰,用硬编码换零抽象。
2.2 障碍物与跳跃:在转移矩阵上做“减法”和“改写”
障碍物不是单独的数据结构,而是直接在T上抹掉:T(:,[10,11,14])=0把所有指向 10、11、14 的边清零,等于这三个格子不可达。跳跃则是先清空第 13 行的所有出边T(13,:)=0,再单独开一条T(13,15)=1。这两步操作揭示了一个重要机制:环境的所有规则,最终都收敛到“哪些转移合法”这一件事上。
% 定义不可达到区域(障碍物) T(:,[10,11,14])=0; % 定义跳跃:13 号位置只能跳到 15 T(13,:)=0; T(13,15)=1;注意T(13,:)=0必须在T(13,15)=1之前执行,顺序反了跳跃边会被自己清掉,这是血泪经验级别的细节。障碍物用列清零而不是行清零,意味着“别人进不来”,但障碍物自己作为起点的出边还在——不过训练循环里用while ismember(state,[10,11,14])重新采样初始状态,规避了从障碍物出发的情况。这种“矩阵改写即规则”的做法,好处是规则集中、可审计,坏处是每加一条特殊规则就要动一次矩阵,规模一大就难维护。
2.3 奖赏矩阵 R:稀疏信号如何塑造行为
R=-1*ones(16)先把所有奖赏设成 -1,然后R(:,6)=0、R(:,7)=-5、R(:,16)=10、R(13,15)=5。这里有个容易看漏的点:奖赏是按“目标状态”给的,R(:,16)=10表示任何状态转移到 16 都得 10 分,而不是只有特定动作才给。这种按列赋值的写法,本质是把奖赏定义在转移的目标端。
-1 的全局惩罚是这份代码最关键的激励设计。它让智能体每走一步都“掉血”,从而逼出最短路径,而不是绕远路刷分。如果全设 0,智能体可能学会原地打转或走最长路径,因为到达终点前没有任何成本。R(:,7)=-5是陷阱列,R(13,15)=5是跳跃奖励,两者叠加在同一个 Q 更新公式里,靠max(Q(action,:))把未来价值往回传。奖赏稀疏且带负步数惩罚,是网格世界问题的经典配置,也是理解“为什么 Q 值会收敛到负数和正数混合”的入口。
3. Q 值更新:一行贝尔曼方程背后的收敛逻辑
3.1 更新公式拆解:R 加 gamma 乘 max 到底在算什么
核心更新只有一行:
% 贝尔曼方程的单步采样更新 Q(state,action)=R(state,action)+gamma*max(Q(action,:));拆开看:R(state,action)是这一步的即时奖赏,max(Q(action,:))是到达下一个状态后,从那里出发能拿到的最大 Q 值,gamma=0.8是折扣因子,把未来价值打折。整行的含义是“当前这一步的价值 = 即时收益 + 打折后的未来最优收益”。这不是标准的 Q-Learning 增量更新(标准形式带学习率 α 和 TD 误差),而是直接赋值式更新,等价于 α=1 的极端情况。
参数上,gamma=0.8意味着 5 步之后的奖赏只剩约 0.33 的权重,10 步之后只剩 0.1。这个值决定了智能体“看多远”:太小会短视,只盯着眼前的 -1;太大会让远处奖赏传播慢、训练轮数变多。epsilon=0.3是探索概率,30% 随机走、70% 走当前最优,这个比例在 900 轮训练里足够让所有状态-动作对被访问到。
直接赋值式更新有个隐患:它假设环境是确定性的,且每次更新都完全信任新样本。在随机环境里,这种写法会震荡,标准做法是Q(s,a) = Q(s,a) + α*(R + γ*max Q - Q(s,a)),用 α 平滑。这份代码因为环境确定、状态空间小,α=1 也能收敛,但换成随机转移就会翻车。这是理解“为什么工业级实现都带学习率”的最好反例。
3.2 epsilon-greedy:探索与利用的开关怎么拨
if rand()<epsilon % 探索:随机选一个合法动作 choose=unidrnd(length(possible_actions)); action=possible_actions(choose); else % 利用:选当前 Q 值最大的动作 [~,index]=max(possible_Q); action=possible_actions(index); endpossible_actions=find(T(state,:)==1)先拿到合法动作集合,possible_Q=Q(state,possible_actions)再取对应的 Q 值。注意max返回的是索引index,要再用possible_actions(index)映射回真实动作,这一步不能省,否则会把“第几个合法动作”当成“哪个状态”用,路径直接错乱。
epsilon-greedy 的机制价值在于:没有探索,Q 矩阵里没被访问过的状态-动作对永远是 0,智能体会卡在局部最优;没有利用,学到的 Q 值用不上,等于白学。0.3 这个固定值在 900 轮里不做衰减,意味着训练后期仍在随机乱走,这会让 Q 值收敛后仍有小幅波动。常见的改进是让 epsilon 随轮数衰减,前期多探索、后期多利用。这份代码没做衰减,是简化,也是可以深挖的改进点。
3.3 训练循环的状态流转:从随机起点到终点
for episode=1:900 state=unidrnd(16); while ismember(state,[10,11,14]) state=unidrnd(16); % 重采样直到不在障碍物上 end while state~=16 possible_actions=find(T(state,:)==1); possible_Q=Q(state,possible_actions); % ... epsilon-greedy 选动作 ... Q(state,action)=R(state,action)+gamma*max(Q(action,:)); state=action; % 状态前移 end end外层 900 次 episode,内层一直走到状态 16 为止。state=action这行是状态转移的执行,因为T里T(state,action)=1已经保证了 action 是合法下一状态,所以直接把 action 赋给 state 即可,不需要额外的转移函数。这种“动作即下一状态”的表示,是网格世界把动作空间和状态空间统一编号后的便利,换成连续控制就不成立了。
900 这个轮数不是随便定的:状态空间 16、动作最多 4 个,总共几十个状态-动作对,每对平均被访问几十次就足够让 Q 值稳定。轮数太少,远处奖赏传不回来;轮数太多,纯属浪费。训练中每 100 轮打印一次进度,是排查“到底有没有在学”的最朴素手段——如果 Q 值一直不涨,先看打印,再看更新公式里的max是不是取错了维度。
4. 路径提取与常见翻车点:Q 矩阵学完之后怎么用
4.1 贪心解码:为什么 max 的方向不能反
训练结束后,路径提取是另一段独立逻辑:
start_0=1; path=start_0; while path(end)~=16 [~,next]=max(Q(start_0,:)); % 当前状态下 Q 最大的下一状态 path=[path,next]; start_0=next; end这里max(Q(start_0,:))取的是当前状态那一行里 Q 值最大的列,列号就是下一状态。方向不能反:如果写成max(Q(:,start_0)),取的是“哪些状态转移到当前状态最划算”,那是入边不是出边,路径会完全错乱。这个坑很隐蔽,因为矩阵是对称形状,转置一下不报错,只是结果莫名其妙。
贪心解码不加 epsilon,是纯利用模式,前提是 Q 矩阵已经收敛到每个状态都有明确的唯一最优动作。如果训练不充分,某些状态的最大 Q 值并列或指向错误方向,解码可能走进环。这份代码没做环检测,while path(end)~=16在极端情况下会死循环,实际使用时值得加一个步数上限。
4.2 避坑清单:五条踩过的坑
现象一:路径在几个格子之间来回跳,永远到不了 16。原因:Q 矩阵没收敛,某些状态的最大 Q 值指向了回头路,贪心解码陷入二点环。解决:增加训练轮数,或检查gamma是否过大导致远处奖赏传播太慢;也可以在解码时记录已访问状态,发现重复就换次优动作。
现象二:训练打印显示 Q 值全是 0,一点没学。原因:R矩阵赋值顺序错了,比如先R(:,16)=10又执行了R=-1*ones(16)把它覆盖掉。解决:确认R的初始化在最前面,后续按列赋值在其后;打印R(1,16)验证是否为 10。
现象三:智能体从不走跳跃边 13→15。原因:T(13,:)=0写在了T(13,15)=1后面,跳跃边被清掉;或者R(13,15)=5被后面的赋值覆盖。解决:严格保证“先清零再设边”的顺序,训练前打印T(13,15)和R(13,15)各确认一次。
现象四:初始状态采样偶尔落在障碍物上,导致该轮直接卡死。原因:重采样循环while ismember(state,[10,11,14])的条件写漏了某个障碍编号,或障碍列表和T的列清零列表不一致。解决:把障碍编号抽成一个变量obstacles=[10,11,14],T清零和重采样都引用它,避免两处硬编码不同步。
现象五:换了网格尺寸后,上下左右动作全乱。原因:i-1、i+1、i-4、i+4这四个偏移量是按 4 列写死的,改成 5 列后i-4不再是左移。解决:把列数抽成参数cols,偏移量改成i-1、i+1、i-cols、i+cols,边界判断也改用mod运算而不是硬编码列表。
提示:这五条里前三条都跟“赋值顺序”有关,矩阵式环境建模最大的敌人不是算法,是覆盖写。训练前把
T、R的关键元素打印一遍,比事后调试省时间。
5. 从这张 16×16 矩阵看 Q-Learning 的设计取舍
这份代码最值得琢磨的,不是它实现了 Q-Learning,而是它用最少的抽象暴露了 Q-Learning 的全部假设。直接赋值式更新假设环境确定性,固定 epsilon 假设探索预算均匀,矩阵式环境假设状态离散且规模可控,贪心解码假设 Q 已收敛。每一条在网格世界里都成立,换到真实问题里几乎都要改。我一般会这么理解:这行更新公式是一个“价值传播”的迭代器,max负责把未来的好结果往回拉,gamma负责控制拉多远,R负责给每个转移打标签,三者缺一不可。真正决定它能不能 work 的,是状态-动作对是否被充分访问——这也是为什么探索策略比更新公式更值得花心思。
| 设计点 | 这份代码的选择 | 代价 | 常见替代 |
|---|---|---|---|
| 环境表示 | 16×16 矩阵硬编码 | 换尺寸要改常数 | 转移函数 / 邻接表 |
| Q 更新 | 直接赋值(α=1) | 随机环境下震荡 | 带学习率 α 的 TD 更新 |
| 探索 | 固定 epsilon=0.3 | 后期仍乱走 | epsilon 衰减 |
| 奖赏 | 全局 -1 步惩罚 | 需调 gamma 配合 | 势函数塑形 |
| 解码 | 纯贪心无环检测 | 可能死循环 | 带访问记录的贪心 |
验证这套机制是否真的被理解,有个简单办法:把gamma从 0.8 改成 0.1,重新训练,观察路径是否变短但绕路变多;再把R的全局 -1 改成 0,看路径是否变长甚至打转。两次改动分别验证折扣因子和步数惩罚的作用,比读十遍公式管用。源码和注释都在那份文档里,矩阵怎么建、循环怎么写、路径怎么提,照着跑一遍再改参数,比只看结论扎实得多。
从那以后我每次碰强化学习的代码,都强制先把环境矩阵和奖赏矩阵打印出来核对一遍赋值顺序,再谈训练——因为翻车从来不在公式,在那些被覆盖掉的一行。希望帮到你。
本文还有配套的精品资源,点击获取