news 2026/10/14 17:21:35

MATLAB版Q-Learning源码实战:从贝尔曼公式到参数调优

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
MATLAB版Q-Learning源码实战:从贝尔曼公式到参数调优

简介:文档以王子寻公主小游戏为场景,完整呈现 Q-Learning 算法的 Matlab 实现与逐行注释,适合强化学习初学者对照理解无模型决策流程。案例构建了 16 个位置的网格世界,王子可上下左右移动,设有障碍物、陷阱与终点奖励,代码逐步定义可移动矩阵与奖赏矩阵,并基于全零 Q 表、折扣因子 gamma 和探索概率 epsilon 启动训练。训练中采用 epsilon-greedy 策略选择动作,依据贝尔曼方程迭代更新 Q 值,直到形成可用的状态—动作价值表;训练结束后还演示了如何从任意起点读取 Q 矩阵规划最优路径,并包含训练进度输出与路径回溯逻辑。资源整体打包为 1 个 doc 文件,约 33KB,内容精炼、注释完整,已有 1378 人学习下载,特别适合想搞懂强化学习核心概念并迁移到导航、决策类小项目的读者。

1. 这份MATLAB版Q-Learning源码,为什么值得你花一下午读透

第一次调Q-Learning参数时,我把学习率设成0.8、折扣因子设成0.5,跑了两百回合智能体还是找不到终点,后来发现是探索率衰减设计出了问题,白白熬了几个通宵。Q-Learning是强化学习里最直观的时序差分算法,而一份带注释的MATLAB源码,恰好能把Q表更新、动作选择、奖励反馈这些抽象概念落到能跑、能调、能改的代码上。它适合两类人:一类是刚入门强化学习、想搞懂Q表到底怎么更新的初学者;另一类是要在课题或小项目里快速验证某个想法、不想引入Python重框架的工程师。看透这份代码,你就能自己搭一个网格世界实验台,改一遍参数就能观察Q表热力图的完整变化轨迹。

2. 拆开Q-Learning核心逻辑:公式和MATLAB代码怎么一一对应

2.1 为什么选MATLAB跑Q-Learning:矩阵、脚本和可视化三板斧

Q-Learning核心数据结构就是一张Q表——状态数乘动作数的矩阵。MATLAB处理矩阵几乎是零成本的,初始化一张16×4的表只需要一行,打印出来看也是一目了然。这是它比C语言更适合做算法验证的原因之一。

第二个优势是脚本化的调试体验。在MATLAB编辑器里给某一行Q更新公式设断点,运行到断点时可以直接在命令行查看Q(s,a)、reward、max(Q(s_next,:))这几个变量的中间值,逐步核对公式算得对不对。这种逐行确认的体验,比在Python里打一堆print要顺手得多。

第三个优势是可视化。imagesc(Q)画出来的热力图,可以直接观察Q值在不同训练阶段的变化趋势,很多收敛性问题一眼就能看出来。这个技巧我在第5章展开讲。

本质上,这份代码不依赖任何强化学习工具箱,纯手写,大概五十到八十行就能完整实现一个可训练的Q-Learning智能体,非常适合作为理解算法细节的底稿。

2.2 贝尔曼更新公式拆解:符号与代码变量的对照

Q-Learning最核心的动作就是那一行更新公式:

Q(s,a) ← Q(s,a) + α × [ r + γ × max(Q(s',a')) - Q(s,a) ]

这个公式里四个角色要分清:

  • Q(s,a):当前状态s下执行动作a的估计价值,是表格里某个格子的值;
  • r:执行动作a之后环境返回的即时奖励;
  • s':执行动作后转移到的下一个状态;
  • max(Q(s',a')):在下一个状态s'里,所有可能动作中最大的Q值,它代表“未来最优收益”的估计;
  • α和γ分别控制学习速率和远期收益的折扣程度。

初学者最容易看懵的,是Q(s',a')哪来的。它不是单独维护的一张表,而是对同一张Q表、在状态s'那一行里取最大值。这意味着更新过程是自举的:用旧的估计去修正新的估计,这正是时序差分学习的核心。

把这个公式翻译成MATLAB的矩阵操作,就是代码里反复出现的这一行:

Q(s, a) = Q(s, a) + alpha * (reward + gamma * max(Q(s_next, :)) - Q(s, a));

其中max(Q(s_next, :))把s_next整行传给max函数,MATLAB会沿列方向扫描,返回该行中最大的Q值,正好对应公式里的max(Q(s',a'))。整套运算都不需要手动写循环遍历动作集,这是MATLAB矩阵思维比较典型的一个例子。

2.3 网格世界建模:状态编号、动作集合与奖励函数

为了把Q表落到具体的代码上,需要一个最简单的实验环境:网格世界(Grid World)。假设是4×4的网格,共16个格子,智能体从左上角出发,目标是到达右下角。每一步可以选择四个动作:上、下、左、右。

状态的空间建模方式是关键。代码里通常用线性索引1到16表示每个格子,而不是用二维坐标(row, col)。原因在于:Q表需要一个行数等于状态数的矩阵,线性索引可以直接当行号用。如果代码里需要把状态转成坐标来算移动,只需调用ind2sub;反过来,坐标转状态号用sub2ind。

% 4x4网格世界:状态编号1~16,按列优先排列 % 状态1在左上角(第1行第1列),状态16在右下角(第4行第4列) [s_row, s_col] = ind2sub([4, 4], s); % 线性编号 -> 行列坐标 s_next = sub2ind([4, 4], s_row, s_col); % 行列坐标 -> 线性编号

这里有个容易出错的地方:MATLAB的sub2ind和ind2sub默认按列优先编号。也就是说,编号2不在(1,2)而在(2,1)位置。如果按行优先的思维去画网格图,Q表热力图会和实际布局发生错位。建议在注释里把编号和坐标的对应关系写清楚,我自己第一次画热力图时就在这里栽过跟头。

奖励函数也很简单:每走一步给-0.1的小惩罚,撞墙额外扣1分,到达终点给+10分。稀疏奖励和密集奖励的差别,我在第4章单独展开。

3. 读懂代码骨架:从参数初始化到主循环的四个模块

3.1 参数定义与Q表初始化:改哪些值前先看这里

任何Q-Learning代码的第一步都是参数初始化。常见做法是把所有可调参数集中写在文件头部,方便统一修改,不用到处翻代码。核心参数有四个:学习率alpha、折扣因子gamma、探索率epsilon、训练回合数episodes,加上环境尺寸nS和动作数nA。

%% ===== 参数定义区 ===== nS = 16; % 状态数,4x4网格 nA = 4; % 动作数:1上 2下 3左 4右 alpha = 0.1; % 学习率,越大单步更新越猛 gamma = 0.9; % 折扣因子,越接近1越看重远期收益 epsilon = 0.3; % 初始探索率 episodes = 500; % 训练回合数 Q = zeros(nS, nA); % Q表初始化,全0起步

Q表为什么初始化为全零而不是随机数?因为在确定性环境里,全零初始化意味着“所有动作价值相等”,智能体前期完全靠探索来积累信息,这让学习过程更稳定。如果环境是随机的,偶尔也会用随机小值初始化,但全零起步始终是最容易排查问题的选择。

参数区是整个代码的“后悔药”集中地:训练不收敛、震荡剧烈、跑得慢,第一件事都该回头检查这几个值,而不是去改环境逻辑。

3.2 ε-greedy动作选择:探索与利用的平衡函数

动作选择策略决定了智能体怎么在“尝试新动作”和“走已知最优”之间做权衡。最常用的方案是ε-greedy:以概率ε随机选一个动作,以概率1-ε选当前Q值最大的动作。代码如下:

function a = choose_action(Q, s, epsilon) if rand() < epsilon a = randi([1, nA]); % 探索:随机挑一个动作 else [~, a] = max(Q(s, :)); % 利用:挑Q值最大的动作 end end

这里有个细节:max(Q(s, :))返回两个输出,第一个是最大值本身,第二个是它所在的列位置,也就是动作编号。用~丢弃第一个值,直接用a接收动作编号。如果某个状态两列Q值恰好相等,max会返回列索引较小的那一个,在调试时要注意这一点。

epsilon的取值很敏感。固定为0.3意味着30%的步数在随机乱走,后期会显得很笨拙;固定为0则完全没有探索,容易锁死在次优路径上。更稳妥的做法是让epsilon随训练进程衰减,第4章展开讲。

3.3 环境交互与奖励计算:状态转移函数的边界处理

环境交互是哑巴模块,你给它当前状态和动作,它返回下一个状态和奖励。网格世界里最需要注意的就是越界处理:往左走出边界,不能索引到不存在的列,应该留在原地并给惩罚。

function [s_next, reward, done] = env_step(s, a) [r, c] = ind2sub([4, 4], s); nr = r; nc = c; switch a case 1, nr = r - 1; % 上 case 2, nr = r + 1; % 下 case 3, nc = c - 1; % 左 case 4, nc = c + 1; % 右 end if nr < 1 || nr > 4 || nc < 1 || nc > 4 s_next = s; % 撞墙,留在原地 reward = -1; else s_next = sub2ind([4, 4], nr, nc); if s_next == 16 reward = 10; % 到达终点 done = true; else reward = -0.1; % 每步小代价 done = false; end end end

边界处理这段是代码里最容易出隐蔽bug的地方。比如“下”动作在第4行执行时,nr会变成5,如果不做边界判断,sub2ind会生成一个超出16的编号,后续Q表索引直接越界报错。更隐蔽的是“左”动作在第1列执行时,nc变成0,sub2ind可能不会报错但会返回一个错误的编号,这种bug最折磨人。所以边界判断一定要先于sub2ind执行。

奖励函数也集中在这个模块。走一步-0.1、撞墙-1、终点+10的组合,目的是让智能体既不愿意原地打转,也不愿意鲁莽撞墙,同时给一个足够大的终点奖励让Q值有区分度。

3.4 Q值更新与训练主循环:一回合学习的完整链条

把前面几个模块串起来就是训练主循环。每一回合从起点开始,反复执行“选动作→走一步→拿奖励→更新Q表”直到到达终点,然后进入下一回合。

%% ===== 训练主循环 ===== for ep = 1:episodes s = 1; % 每回合重置到起点 done = false; while ~done a = choose_action(Q, s, epsilon); [s_next, reward, done] = env_step(s, a); Q(s, a) = Q(s, a) + alpha * (reward + gamma * max(Q(s_next, :)) - Q(s, a)); s = s_next; end % 探索率衰减:前期多探索,后期多利用 epsilon = max(0.05, epsilon * 0.98); end

整体结构是双层循环:外层循环控制回合数,内层循环控制单回合内每一步。注意Q更新发生在环境返回s_next之后、状态变量s被覆盖之前,这样s_next还是旧状态的下一个状态标识,时序不会错乱。

epsilon的衰减写法max(0.05, epsilon * 0.98)是很多代码里常见的模式。0.98表示每回合乘以0.98,500回合衰减到约0.3的0.98的499次方,大概0.00045,但被下限0.05托住,保证后期还有少量探索。下限值设多少需要根据环境调节:环境随机性强,下限要适当提高,否则智能体学到的是旧环境的规律,环境一变就失灵。

4. 参数怎么调:学习率、折扣因子、探索率的组合实验

4.1 三组关键参数的取法:参数表与推荐起点

参数调优是Q-Learning里最接近“玄学”的部分,但你完全可以用表格化的组合实验来找出合理的起点。下面这张表是我在类似网格世界任务里验证过的取值区间,可以当作参考。

参数含义常见区间推荐起点调大后的影响
alpha学习率0.01~0.50.1更新步子大,收敛快但容易震荡
gamma折扣因子0.8~0.990.9越接近1越重视远期收益,前期学习变慢
epsilon初始值初始探索率0.1~0.50.3越大越爱乱走,Q表前期铺得开
衰减系数每回合乘上的衰减0.95~0.990.98越大探索维持越久,训练总时长增加
奖励值终点/步进/撞墙按任务设计+10/-0.1/-1奖励跨度越大,Q值差异越明显

学习率和折扣因子是“一对儿”。alpha太大,单步更新幅度过大,Q值会在最优值附近反复震荡;gamma太小,智能体只顾眼前利益,走一步算一步,远期目标的价值体现不出来。常见做法是先固定gamma=0.9,把alpha从0.05到0.3之间按0.05步进扫一遍,看哪组收敛曲线最平滑。

扫参数时有个技巧:记录每个参数组合到达终点的总步数曲线,画在同一张图上对比。最平滑、最终步数最低的曲线所对应的参数就是适合当前环境的起点,后续再做局部微调。

4.2 探索率衰减策略:固定、线性、指数三种写法

探索率衰减策略直接决定训练前中后期智能体的行为模式。三种常见写法各有适用场景:

%% 固定探索率:简单但后期浪费 epsilon = 0.2; % 缺点:训练后期智能体已经掌握路径,仍在随机乱走,收敛不稳定 %% 线性衰减:可控但需要调范围 epsilon = max(0.05, 0.3 * (1 - ep / episodes)); %% 指数衰减:前期铺得开,后期稳得住(推荐) epsilon = max(0.05, 0.3 * 0.98^(ep-1));

我的经验是:环境实时反馈稳定、状态数少时用指数衰减最省心;环境本身随机性大时需要把下限抬高,不然epsilon衰减到0.05后,随机扰动会导致Q值轻微震荡无法消除。线性衰减的好处是衰减速度恒定,便于规划,但要同时调初始值、下限、总回合数三个变量,比较费神。

另一个容易被忽略的细节:epsilon衰减的步长单位应该是“回合”而不是“步数”。如果放在内层循环里按步数衰减,一个回合假设20步,探索率每回合实际衰减0.98的20次方约0.66,衰减速度远超预期,训练可能还没探索完就进入纯利用阶段。

4.3 奖励设计的影响:稀疏奖励为什么把Q值训练成玄学

奖励设计是最容易翻车但也最容易被忽视的环节。一种极端做法是“只有到终点才给+1,其他全是0”,这是典型的稀疏奖励任务。在这类设置下,前几十回合智能体完全得不到正向反馈,Q表全为零,所有动作等概率被选,训练基本靠运气。

如果只是Q-Learning实验,建议还是用密集奖励:每走一步给个小负值,让智能体学会“少走冤枉路”。但负值不能太大,否则智能体会为了少走步数而故意撞墙自杀——撞墙-1、走路-0.1,如果终点奖励只有+1,撞墙自杀反而是“最优策略”。这是奖励设计里一个经典的坑:惩罚和奖励的比例失衡会诱导出你完全意想不到的行为。

我一般会把奖励设成“终点正奖励的绝对值 ≥ 理论最短步数 × 单步负奖励 + 一个余量”。在4×4网格里理论上最短步数是6步(右右下下或下下右右),每步-0.1,总共-0.6,终点+10远大于这个绝对值,智能体才有动力走向终点。这个不等式可以作为奖励参数设计的通用模板,换到其他环境也适用。

5. 跑Q-Learning避坑:四个高频翻车现场与排查方法

5.1 Q值震荡不收敛,训练曲线像锯齿

现象:训练过程中的累积奖励曲线上下剧烈波动,没有平稳下降或上升的趋势;Q表数值一阵大一阵小。

原因:最常见的是学习率alpha偏大,加上epsilon衰减过快,导致后期每次随机探索都会对Q值造成大幅扰动。另一个原因是单回合更新次数太少,Q值还没来得及稳定就进入了下一回合。

解决办法:把alpha降到0.05或0.01试一次;把epsilon衰减系数从0.98改成0.99或0.995,让探索期拉长。如果曲线仍是锯齿状,检查奖励函数是否给了相互矛盾的信号——例如同时惩罚步数和惩罚撞墙,两个惩罚数值差得太大时,智能体会在两种“坏选择”之间反复横跳。

5.2 智能体学会了走固定路径,但路径明显绕路

现象:训练结束后,从Q表提取策略,智能体走的路径比最优路径长,甚至出现原地打转。

原因:探索率前期偏低,智能体还没探索到所有状态,Q表对部分状态的估计是缺失的,策略提前被锁死在一条局部最优路径上。这也被称为“过早收敛”。

解决办法:提高初始epsilon到0.4或0.5,重新训练;或者训练完成后不改算法,把epsilon固定为0.05再跑100回合做一次“纯利用验证”——如果走过的路径仍不是最优,基本可以确认是探索不足,加大探索率重训即可。

5.3 训练时间越来越长,状态一多就跑不动

现象:把网格从4×4扩大到10×10,训练时间从几十秒变成几十分钟,操作起来明显卡顿。

原因:Q表按状态数乘动作数存储,10×10网格就是100×4的表,更新本身不慢,慢的是每回合需要走的步数变多了,尤其是前期探索阶段,一个回合可能走上千步。

解决办法:先检查主循环的内层是否需要加一个最大步数限制,防止某个回合因为奖励设置问题而陷入无限循环。我的习惯是在while循环里加一个计数器,超过最大步数强制结束本回合。其次可以把网格缩小到6×6验证参数,确认没问题再放大。如果环境本身就是高维连续状态空间,Q表方案就不合适了,该考虑DQN这类函数逼近方法,而不是继续在Q-Learning上调参。

5.4 智能体行为稳定但无法到终点,Q表热力图看不懂

现象:Q表数值看起来分布合理,但提取策略后智能体不断撞墙,或者热力图上一片区域全是亮色,看不出清晰路径。

原因:如果撞墙的Q值惩罚不够低,智能体可能认为撞墙是可接受的行为。比如撞墙只给-0.1,而走到终点需要经过6步、累计-0.6,那么撞墙和正常走路惩罚相同,探索阶段随机乱撞反而容易形成误导。热力图看不懂往往是因为状态编号是按列优先排列,而网格图是按行画的,两者错位导致视觉判断失误。

解决办法:先检查奖励数值比例,把撞墙惩罚调低到-1或-2,拉开区分度。然后在画热力图时做一次转置加翻转,让热力图的坐标和网格图对得上。具体代码在我的第6章里会写到。

在查看在线代码时还有一个老问题:从.doc文档里复制MATLAB代码到.m文件,中文注释经常变成乱码。这是因为旧版.doc文件用的是GB2312编码,而新版MATLAB默认用UTF-8读取。解决方法是先用文本编辑器把代码转存为UTF-8编码,再打开.m文件;或者干脆把所有中文注释改成英文,一劳永逸。

6. 进阶玩法:把这份源码改造成自己的强化学习实验台

6.1 换一个环境:从网格世界到自定义地图

读完源码后,第一步改造是换地图。把4×4网格改成带障碍物的地图,只需修改env_step里的合法性判断。定义全局变量mapMatrix存放地图,1表示可通行,0表示障碍物,然后判断下一个状态对应位置是否为1,不是就留在原地并给惩罚。

global map map = [1 1 0 1; 1 0 0 1; 1 1 1 1; 0 1 1 1]; % 6个障碍物 if nr < 1 || nr > 4 || nc < 1 || nc > 4 || map(nr, nc) == 0 s_next = s; reward = -1; end

这样改完之后,起点和终点也可以从硬编码改成变量,地图的复杂度随意调整,代码主体完全不用动。

6.2 画一条收敛曲线和一个Q表热力图

验证这个实验台是否正常工作,不能只看肉眼观察,需要写一个评估脚本。记录每回合的总步数和累计奖励,训练结束后画两张图:一张看收敛趋势,一张看Q表分布。

steps_per_episode = zeros(1, episodes); for ep = 1:episodes s = 1; done = false; step_cnt = 0; while ~done && step_cnt < 200 a = choose_action(Q, s, max(0.05, epsilon)); [s_next, reward, done] = env_step(s, a); Q(s, a) = Q(s, a) + alpha * (reward + gamma * max(Q(s_next, :)) - Q(s, a)); s = s_next; step_cnt = step_cnt + 1; end steps_per_episode(ep) = step_cnt; end figure; plot(steps_per_episode, 'LineWidth', 1.5); xlabel('Episode'); ylabel('Steps to Goal'); title('Convergence Curve'); figure; imagesc(reshape(max(Q, [], 2), 4, 4)); % 每个状态的最大Q值 colorbar; title('Q Value Heatmap');

注意最后一行,reshape是按列填充的,如果地图是按行定义的,需要加一个转置:imagesc(reshape(max(Q, [], 2), 4, 4)')。这个细节能让热力图和地图对上号,排查问题省一大半时间。

6.3 从Q-Learning到SARSA:三行代码切换算法

把Q-Learning改成SARSA是验证理解深度和增强调试经验的经典练习。两者唯一的区别在于更新公式中的未来价值项:Q-Learning用max(Q(s_next,:)),SARSA用实际执行的那个动作的Q值Q(s_next, a_next)。改动如下:

% Q-Learning版本 Q(s, a) = Q(s, a) + alpha * (reward + gamma * max(Q(s_next, :)) - Q(s, a)); % SARSA版本 a_next = choose_action(Q, s_next, epsilon); Q(s, a) = Q(s, a) + alpha * (reward + gamma * Q(s_next, a_next) - Q(s, a));

这个改动会让两种算法在含陷阱的环境里表现出明显差异:SARSA学出的策略更保守,Q-Learning更激进。你可以把两种算法在同一个地图上各跑20次,比较平均累积奖励,这是一个非常好的算法对比实验素材,也能加深对on-policy和off-policy区别的理解。

回头来看,想要真正吃透这份源码,最有效的做法不是把每行注释抄一遍,而是先跑通、再画图、最后改一个小模块。把网格改成障碍物地图、把Q更新改成SARSA,每一步都踩一遍真实的问题,比读十遍注释都管用。希望这些从实际调试里攒下的经验,能帮你省下几个通宵,少走一些我之前走过的弯路。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/14 17:21:34

科研idea挖掘与落地实用指南:助力学术创新高效产出

刚接触一个新领域&#xff0c;最怕的就是迷失在海量的外国文献里&#xff0c;读了很多篇还是理不清脉络。我曾经也以为“研究现状”只能靠逐篇阅读、手动总结&#xff0c;直到发现了一些能生成“知识图谱”的神器。它们能让你像开了上帝视角一样&#xff0c;瞬间看清一个领域的…

作者头像 李华
网站建设 2026/10/14 17:20:55

数据包寻址链路:MAC、IP、DNS与CDN全解析

如果你正打算认真啃一遍计算机网络基础&#xff0c;我的建议是别一头扎进教科书里背七层模型&#xff0c;而是沿着一条实际问题链条走下来&#xff1a;一个数据包从网卡出发&#xff0c;如何靠 MAC 找到直连设备&#xff0c;如何靠 IP 跨网寻路&#xff0c;如何靠 DNS 把域名翻…

作者头像 李华
网站建设 2026/10/14 17:19:56

大浪淘沙见真章:湘楚有才单招,湖南单招赛道本土标杆的成长之路

一、浪潮奔涌,谁在深耕,谁在浮沉近年来,随着职业教育改革持续深化、湖南高职分类考试招生规模逐年扩大,单招这条升学通道,正被越来越多的考生家庭视为一条务实而重要的人生路径。需求升温,市场也随之热闹起来。各路机构竞相涌入,宣传话术一个比一个响亮,"保过""…

作者头像 李华
网站建设 2026/10/14 17:19:14

知识图谱问答论文整理:主流技术流派、可复现代码与数据集全解析

做知识图谱问答研究最让人崩溃的时刻&#xff0c;往往不是论文看不懂&#xff0c;而是读完之后满怀期待打开作者主页&#xff0c;发现代码链接失效、数据集要写邮件申请、依赖版本还停留在三年前的旧环境。这种经历只要碰到一两次&#xff0c;就会让人在整理论文清单时本能地加…

作者头像 李华