MATLAB强化学习实战:Q-learning平衡小车Cart-Pole代码解析
2026/9/13 9:17:07 网站建设 项目流程

简介:面向初次接触强化学习或希望掌握智能控制实战方法的工程师与高校学生,此压缩包提供一个基于MATLAB的平衡小车倒立摆完整解决方案,涵盖环境建模、智能体训练、奖励设计和结果可视化等核心环节,兼顾理论演示与工程实现,适合控制工程、机器人及相关方向的入门与进阶实践。包内共7个MATLAB脚本,压缩后约4KB,包含主控制程序、多环境模拟模块、状态提取函数、动态绘图工具及随机初始状态生成器,代码模块化程度高,便于快速运行和二次开发。该包已有771人学习使用。通过阅读和调试脚本,读者可理解强化学习在连续控制问题中的状态/动作空间划分、奖励函数设计思路,学会调整学习率、折扣因子等关键参数,并掌握策略迭代过程;可视化脚本还能直观显示摆杆角度与小车位移动态变化,便于验证控制效果。在此基础上,可进一步扩展至DQN、DDPG等深度强化学习方法,为课程设计、毕业设计或竞赛项目提供扎实起点。

1. 平衡小车不是玩具:MATLAB 强化学习为什么拿 Cart-Pole 开刀

很多人第一次在 MATLAB 里跑强化学习,都从平衡小车倒立摆开始。表面上这只是一个小车加一根杆,但它同时包含连续状态、离散动作和稀疏奖励,正好用来检验查表型强化学习算法。pole.zip 这套代码走的是经典 Q-learning 路线:用 Cart_Pole_Boxes 把连续状态切成离散 Boxes,用 get_box 查箱号,再用 prob_push_right 输出向右推的概率,让随机策略逐步收敛成稳定控制策略。对想把手上的 MATLAB 强化学习控制课程设计做成可演示原型的人来说,这套代码是一个很合适的起点。它不像深度强化学习那样吃显存,也不要求装额外工具箱,把算法、环境和可视化拆成独立 m 文件,每一段都能单独调试。适合已经会 MATLAB 基础语法、但还没真正把强化学习跑起来的人。

2. 拆开 pole.zip:Cart_Pole.m 到 prob_push_right.m 的职责与数据流

2.1 文件不是乱堆的:一个强化学习循环对应一个文件

解开 pole.zip 后一共 8 个 m 文件,第一眼像散装代码,实际是典型的 MATLAB 强化学习控制项目结构:环境模型、学习循环、可视化三块分开。现代深度强化学习通常用类把环境包装起来,但老派做法更简单,一个函数只干一件事。Cart_Pole.m是主程序,负责环境推进、Q 表更新和回合控制;Cart_Pole_Boxes.m负责建立分箱定义并初始化 Q 表;get_box.m把连续状态映射成离散箱号;Random_Pole_Cart.m生成随机起始状态;prob_push_right.m根据 Q 表和探索率决定向右推的概率;plot_Cart_Pole.mplotcircle.m负责把小车、摆杆和参考圆画出来。

文件在强化学习循环中的角色
Cart_Pole.m主程序:物理模型、Q 表更新、训练回合控制
Cart_Pole_Boxes.m分箱边界定义、Q 表初始化,或维护多环境状态副本
get_box.m把连续状态量化成离散 Box 编号,返回 Q 表行索引
Random_Pole_Cart.m生成随机初始状态,避免策略过早陷入局部解
prob_push_right.m根据 Q 表和探索率计算向右推的动作概率
plot_Cart_Pole.m绘制小车、摆杆和力箭头,实时显示控制效果
plotcircle.m绘制摆杆末端参考圆,快速判断摆动幅度是否正常

主循环的逻辑大概率长这样,代码里把环境步进函数精简成一行,实际物理公式在主程序内部展开:

% 训练主循环:Cart-Pole 的 Q-learning 骨架 for episode = 1:max_episodes s = Random_Pole_Cart(); % 随机起始状态 s_idx = get_box(s.x, s.x_dot, s.theta, s.theta_dot, edges, n_bins); done = false; while ~done p = prob_push_right(Q, s_idx, epsilon); % 策略输出右推概率 a = (rand < p) + 1; % 动作 1=左推,2=右推 [s_next, r, done] = cart_pole_step(s, a); n_idx = get_box(s_next.x, s_next.x_dot, s_next.theta, ... s_next.theta_dot, edges, n_bins); Q(s_idx, a) = Q(s_idx, a) + alpha * ... (r + gamma * max(Q(n_idx, :)) - Q(s_idx, a)); s = s_next; s_idx = n_idx; end end

prob_push_right读的是当前 Q 表,输出一个 0 到 1 之间的概率;主循环拿到概率后转成离散动作。done标志由失效条件驱动,通常是摆杆角度超出阈值或者小车越界。注意这里cart_pole_step只是占位函数,真实项目中它应该在Cart_Pole.m内部实现四阶龙格库塔或欧拉积分,否则训练过程的响应会偏软。

2.2 状态、动作与奖励:控制问题的数学骨架

平衡小车倒立摆的状态空间是四个连续量:小车位置x、小车速度x_dot、摆杆角度theta、摆杆角速度theta_dot。动作空间只有两个:向左推和向右推。强化学习控制在这里的目标是学一个从状态到动作的映射,使得摆杆角度始终接近零。这个任务在物理上不稳定,但正是这种“一放手就倒”的特性,让算法必须学会在错误发生前提前修正。

状态量物理含义典型初始范围失效判定
x小车相对轨道中心位移-0.05 ~ 0.05 mabs(x) > 2.4 m
x_dot小车水平速度-0.05 ~ 0.05 m/s
theta摆杆相对竖直方向夹角-0.05 ~ 0.05 radabs(theta) > 0.21 rad
theta_dot摆杆角速度-0.05 ~ 0.05 rad/s

奖励函数在这套代码里通常写成每存活一步给 +1,触发失效条件则回合结束并给负奖励。注意 Matlab 移植版有的用abs(theta) > 12 * pi / 180,也就是 0.2094 rad,和表格里的 0.21 一致。这里的关键点不是奖励数值本身,而是时效性:奖励必须在摆杆倒下瞬间给出,不能等到动画播完才更新,否则 Q 表会把“临死前那一步”也记成高价值状态。

2.3 从状态到决策:prob_push_right.m 的概率策略

prob_push_right.m是这套代码里策略的出口。常见做法是 epsilon-greedy:以概率epsilon随机探索,否则选 Q 表中当前状态价值最高的动作。如果最高动作是向右推,概率为 1,否则为 0;如果两个动作价值相等,就返回 0.5,让主循环随机选一个方向。

function p = prob_push_right(Q, state_idx, epsilon) % 返回向右推的概率 p if rand() < epsilon p = 0.5; % 探索:左右等概率 else q_left = Q(state_idx, 1); q_right = Q(state_idx, 2); if q_right > q_left p = 1; elseif q_right < q_left p = 0; else p = 0.5; end end end

这个函数的设计让主循环不需要知道策略是怎么选的,拿回概率后直接rand < p就能生成动作。epsilon的传入位置很重要:如果把它写死在函数内部,训练后期就没法做退火处理,策略会一直保持随机,导致 Q 表很难收敛。所以多数项目会把epsilon作为参数从主循环传入,方便按 episode 衰减。

3. 用 Q-learning 把摆杆立起来:Boxes 分箱与 get_box.m 的实现

3.1 为什么分箱:连续状态装不进 Q 表

强化学习控制的第一步是把连续状态空间离散化,否则 Q 表根本建不起来。四维连续状态意味着无限多个状态组合,直接查表不可能。Boxes 分箱的思路是把每一维切成若干段,让状态空间变成一个有限的网格。这个网格的每个格子对应一个 Q 表行,训练时把真实状态映射到最近的格子,更新对应行的动作价值。和 DQN 相比,这种离散化方法没有神经网络,也不需要 GPU,在 MATLAB 里跑几百回合只需要几十秒,非常适合先验证算法逻辑。

从工程角度看,分箱粒度直接决定控制精度。格子太少,控制器只在箱内做粗粒度判断,摆杆会在中间区域来回震荡;格子太多,Q 表规模变大,训练回合数不够时很多格子从未被访问,策略又变空洞。所以选择分箱边界前,要先看物理量的有效范围,再决定每个维度切几段。

3.2 get_box.m 的边界查找与索引换算

get_box是 Cart-Pole 项目里被调用最频繁的函数。输入四个连续状态量和分箱边界,输出一个正整数索引。最常见实现是逐维比较,再通过sub2ind合成一维下标:

function idx = get_box(x, xd, th, thd, edges, n_bins) % 把连续状态映射到离散箱编号 b = zeros(1, 4); b(1) = sum(x > edges{1}) + 1; % 小车位置落在第几个箱 b(2) = sum(xd > edges{2}) + 1; % 小车速度所在箱 b(3) = sum(th > edges{3}) + 1; % 摆杆角度所在箱 b(4) = sum(thd > edges{4}) + 1; % 摆杆角速度所在箱 idx = sub2ind(n_bins, b(1), b(2), b(3), b(4)); end

edges{i}是第 i 维的内部边界向量。比如[-0.2 -0.1 0 0.1 0.2]表示把该维度切成 6 段。sum(x > edges)返回比当前值小的右边界个数,范围是 0 到边界数,加 1 后正好落在有效箱号内。sub2ind把四维下标折成一维索引,Q 表查询就变成Q(idx, a)。这里容易踩的坑是边界外的状态:如果小车越界到 3 m,sum(x > edges{1})会超出箱数范围,导致sub2ind计算出错误索引。所以在调用get_box之前,最好先对状态做裁剪,把越界值 clamp 到边界附近。

一个合理分箱配置可以直接作为Cart_Pole_Boxes.m的默认参数:

状态量分箱边界箱数
x-1.6 -0.8 0 0.8 1.66
x_dot-0.8 -0.4 0 0.4 0.86
theta-0.2 -0.1 0 0.1 0.26
theta_dot-0.5 -0.25 0 0.25 0.56

这个配置下 Q 表规模是 6 的 4 次方再乘 2,也就是 2592 个参数,训练压力很小。如果发现控制精度不够,优先把 theta 维度的边界加密到 12 段,而不是所有维度一起加密,这样既能提高角度分辨率,又不会让 Q 表膨胀太快。

3.3 Cart_Pole_Boxes.m 与 Q 表初始化

Cart_Pole_Boxes.m这个文件名带复数,常见作用是集中定义分箱边界、箱数和 Q 表初值。它不一定模拟多个环境,更常见的做法是返回一个结构体,让主循环和get_box共用同一组参数。初始化代码如下:

function [edges, n_bins, Q] = init_cart_pole_boxes() % 定义四个状态维度的分箱边界 edges{1} = [-1.6 -0.8 0 0.8 1.6]; edges{2} = [-0.8 -0.4 0 0.4 0.8]; edges{3} = [-0.2 -0.1 0 0.1 0.2]; edges{4} = [-0.5 -0.25 0 0.25 0.5]; n_bins = [6 6 6 6]; Q = zeros(prod(n_bins), 2); % 每行两个动作价值 end

prod(n_bins)算出总箱数,第二维固定为 2,对应向左推和向右推两个动作。Q 表初值全部给零,意味着算法在前期对所有状态一视同仁。有人喜欢把初值调成一个较大的正数,鼓励智能体尽早尝试不同动作,但在 Cart-Pole 这种奖励密集的离散任务中,零初始化加上 epsilon 探索已经足够。Cart_Pole_Boxes.m里的初始化函数最好单独抽出来,不要和主循环混在一起,否则后面想改分箱参数时,得在训练代码里到处翻。

4. 参数整定与踩坑:学习率、折扣因子和随机起始如何决定训练成败

4.1 Q-learning 更新公式和 MATLAB 写法

Q-learning 的核心更新公式是Q(s,a) = Q(s,a) + alpha * (r + gamma * max(Q(s',a')) - Q(s,a))。其中alpha是学习率,gamma是折扣因子,r + gamma * max(...)是 TD 目标。每次环境推进后,算法用当前奖励和下一状态的最大 Q 值修正当前动作的估计值。实际操作中参数取值直接影响收敛稳定性:

参数典型取值影响
alpha0.1 ~ 0.3太大导致 Q 值震荡,太小收敛慢
gamma0.9 ~ 0.995越大越看重长期收益,前几回合方差大
epsilon0.05 ~ 0.2探索率,训练后期要衰减到 0.01 以下
max_steps200 ~ 500每回合截断长度,影响累计奖励上界

训练循环里可以顺手加入 epsilon 退火,避免策略一直探索。常见做法是在每个 episode 开头重算 epsilon:

% 每回合前更新探索率,前期多探索,后期多利用 epsilon = max(0.01, 0.2 * (1 - episode / max_episodes));

episode从 1 到max_episodes增长,epsilon从 0.2 线性降到 0.01。把衰减放在主程序而不是prob_push_right里,是为了方便在不同阶段手动干预。如果发现训练后期 Q 表还在剧烈变化,就检查alpha是否过大,或者epsilon是否一直停留在 0.2 没有降下来。这类问题在 MATLAB 强化学习控制里很常见,因为脚本式代码很难让人一眼看出当前 Q 表处于什么阶段。

4.2 Random_Pole_Cart.m:随机初始状态是稳定策略的前提

如果每个回合都从同一个状态开始,Q 表只会沿着一条固定轨迹更新。摆杆倒下后,智能体对附近状态的认知是空的,下个回合又会走同样的错路。Random_Pole_Cart.m的价值就在于让初始状态覆盖更大范围,使策略在多个位置和速度组合上都被修正。常见实现是均匀采样一个小范围内的状态:

function s = Random_Pole_Cart() % 返回随机起始状态,单位采用国际单位制 s.x = (rand - 0.5) * 0.3; % 小车位置 ±0.15 m s.x_dot = (rand - 0.5) * 0.2; % 小车速度 ±0.1 m/s s.theta = (rand - 0.5) * 0.2; % 摆杆角度 ±0.1 rad s.theta_dot = (rand - 0.5) * 0.4; % 摆杆角速度 ±0.2 rad/s end

rand是均匀分布,采样值不会出现极端离群点,适合训练初期。如果改用randn做高斯采样,偶尔会产生很大的角度偏差,早期回合几乎全部失败,学习曲线会变得很难读。我一般会保持均匀采样,再把角度范围压到 ±0.1 rad 以内,让智能体从“能稳住但还是会晃”的状态开始学,而不是从“一上来就倒”的状态开始。另外注意Random_Pole_Cart返回的是一个结构体,和get_box的参数展开方式要匹配。如果get_box需要四个独立参数,主循环里就要写成get_box(s.x, s.x_dot, s.theta, s.theta_dot, ...),不要直接传结构体。

4.3 常见失败现象与排查方向

训练平衡小车时最容易遇到三类问题,现象不同,排查路径也不一样。第一种是摆杆立刻倒下,动画里小车几乎不做修正,多半是动作方向和角度正负号定义反了。Cart_Pole.m里的物理方程中,正力应该让小车向某个方向加速,而theta的正方向也要保持一致,否则 Q-learning 学到的是“错误方向的价值”,越来越差。

第二种是小车左右大幅震荡,摆杆勉强不倒但动作很频繁。这通常说明alpha偏大,或者 theta 分箱太粗,Q 表无法区分靠近竖直和略微偏离的状态。把 theta 分箱从 6 段加到 12 段,同时把alpha从 0.3 降到 0.1,震荡幅度会明显下降。

第三种是累计奖励一直上不去,训练很久还是在中低水平徘徊。这时候先看epsilon是否已经衰减到位。如果训练 800 回合后探索率还维持在 0.2,策略做过多的随机动作,控制效果自然差。把epsilon下限改成 0.01,并延长训练回合到 1500 以上,通常能解决。每次改动参数后,不要只看最终动画,要同时打印最后 50 回合的平均步数,步数逐步上升才是正常的收敛 signal。

5. 验证技巧:把策略水印留下来,用 plot_Cart_Pole.m 判断策略是否真的熟

5.1 记录轨迹而不是只看动画

肉眼判断动画只能得到“好像稳住了”的模糊结论,而且摆动幅度很难量化。更可靠的做法是写一个评估函数,用训练好的 Q 表跑若干回合,记录每回合的累计奖励和最大角度偏差。把结果保存下来,就能对比不同超参数下的控制质量。

function [cum_r, theta_max] = eval_policy(Q, edges, n_bins, max_steps) s = Random_Pole_Cart(); cum_r = 0; theta_max = 0; for t = 1:max_steps s_idx = get_box(s.x, s.x_dot, s.theta, s.theta_dot, edges, n_bins); p = prob_push_right(Q, s_idx, 0.01); % 几乎纯贪心 a = (rand < p) + 1; s = cart_pole_step(s, a); cum_r = cum_r + 1; theta_max = max(theta_max, abs(s.theta)); if abs(s.theta) > 0.21 || abs(s.x) > 2.4 break; end end end

cum_r是存活步数,theta_max是回合内最大绝对角度。训练收敛后跑 100 回合,看中位数比看单次动画可靠得多。如果theta_max中位数低于 0.05 rad,说明摆杆基本贴在竖直方向附近,策略已经稳定。plotcircle.m在这里的用途是叠加一个参考圆,用来视觉对比摆杆末端的运动轨迹。角度为零时,摆杆末端轨迹会落在这个圆附近;如果轨迹频繁冲出圆外,说明控制律还存在静差,需要细化 theta 分箱。

5.2 把 Q 表固化成控制器

验证完成后,可以把 Q 表导出成确定性策略函数,供 Simulink 或外部控制程序调用。prob_push_rightepsilon设为 0 时不再做随机探索,输出完全由 Q 表决定:

function u = q_policy(Q, x, xd, th, thd, edges, n_bins) idx = get_box(x, xd, th, thd, edges, n_bins); [~, u] = max(Q(idx, :)); % u=1 向左推,u=2 向右推 end

这个函数可以直接嵌入 MATLAB Function 模块,输入是四个状态量,输出是动作编号。注意实际部署前必须对角度做 wrap,把 theta 限制在 -pi 到 pi 之间,否则角度一旦超过边界,get_box会把它映射到错误的箱子。如果以后想把该项目扩展成平衡循迹小车,可以把目标位置从 0 改成轨迹参考点,或者在奖励函数里增加横向偏差项;要接入 IQL 这类离线强化学习,只需要把 Q 表替换成价值网络,但get_box的状态离散化思路仍然保留。最近大家讨论 Codex 能不能像执行 Python 一样操作 MATLAB 任务,实测下来让它写get_box这种纯函数问题不大,但物理响应和绘图回调还是得自己连调。先用固定步长 0.02 仿真 100 回合,把theta_max中位数打出来,再去看动画画面不迟。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询