MATLAB强化学习实战:Q-learning平衡小车Cart-Pole代码解析 📅 发布时间:2026/9/13 9:17:25 👁 浏览次数: 简介面向初次接触强化学习或希望掌握智能控制实战方法的工程师与高校学生此压缩包提供一个基于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.zipCart_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.m和plotcircle.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 endprob_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 mx_dot小车水平速度-0.05 ~ 0.05 m/s无theta摆杆相对竖直方向夹角-0.05 ~ 0.05 radabs(theta) 0.21 radtheta_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)); endedges{i}是第 i 维的内部边界向量。比如[-0.2 -0.1 0 0.1 0.2]表示把该维度切成 6 段。sum(x edges)返回比当前值小的右边界个数范围是 0 到边界数加 1 后正好落在有效箱号内。sub2ind把四维下标折成一维索引Q 表查询就变成Q(idx, a)。这里容易踩的坑是边界外的状态如果小车越界到 3 msum(x edges{1})会超出箱数范围导致sub2ind计算出错误索引。所以在调用get_box之前最好先对状态做裁剪把越界值 clamp 到边界附近。一个合理分箱配置可以直接作为Cart_Pole_Boxes.m的默认参数状态量分箱边界箱数x-1.6 -0.8 0 0.8 1.66x_dot-0.8 -0.4 0 0.4 0.86theta-0.2 -0.1 0 0.1 0.26theta_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); % 每行两个动作价值 endprod(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 endrand是均匀分布采样值不会出现极端离群点适合训练初期。如果改用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 endcum_r是存活步数theta_max是回合内最大绝对角度。训练收敛后跑 100 回合看中位数比看单次动画可靠得多。如果theta_max中位数低于 0.05 rad说明摆杆基本贴在竖直方向附近策略已经稳定。plotcircle.m在这里的用途是叠加一个参考圆用来视觉对比摆杆末端的运动轨迹。角度为零时摆杆末端轨迹会落在这个圆附近如果轨迹频繁冲出圆外说明控制律还存在静差需要细化 theta 分箱。5.2 把 Q 表固化成控制器验证完成后可以把 Q 表导出成确定性策略函数供 Simulink 或外部控制程序调用。prob_push_right的epsilon设为 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, :)); % u1 向左推u2 向右推 end这个函数可以直接嵌入 MATLAB Function 模块输入是四个状态量输出是动作编号。注意实际部署前必须对角度做 wrap把 theta 限制在 -pi 到 pi 之间否则角度一旦超过边界get_box会把它映射到错误的箱子。如果以后想把该项目扩展成平衡循迹小车可以把目标位置从 0 改成轨迹参考点或者在奖励函数里增加横向偏差项要接入 IQL 这类离线强化学习只需要把 Q 表替换成价值网络但get_box的状态离散化思路仍然保留。最近大家讨论 Codex 能不能像执行 Python 一样操作 MATLAB 任务实测下来让它写get_box这种纯函数问题不大但物理响应和绘图回调还是得自己连调。先用固定步长 0.02 仿真 100 回合把theta_max中位数打出来再去看动画画面不迟。本文还有配套的精品资源点击获取