简介:面向Quanser QUBE-Servo 2硬件平台与MATLAB使用者的旋转倒立摆强化学习方案包,聚焦DDPG与SAC两类算法,解决真实实验台上的平衡控制建模、训练和部署问题,可广泛应用于控制类课程设计、研究生科研和机器人竞赛准备等场景。压缩包共16个文件,整体大小1.74MB,包含三套Simulink模型(slx),分别对应仿真、硬件在环和QLabs虚拟平台场景;还包括策略参数文件(mat)、MATLAB脚本(m)、MLX交互式配置向导,以及Python辅助脚本、结果图和授权说明等,便于按模块查阅和复用。已有32人学习。包内提供从仿真环境到硬件在环再到QLabs虚拟平台的三套完整模型,用户可按需切换;预训练策略文件可直接加载运行,省去从头训练的时间;SAC策略裁剪脚本用于精简网络层以适配嵌入式部署;参数配置脚本和评估工具则支持快速调参与效果验证。整套方案注重工程实用性,从环境搭建到策略部署均有清晰路径,适合具备一定控制或强化学习基础的高校学生、科研人员及竞赛团队快速上手。
1. 项目整体思路与方案选型
1.1 硬件平台与问题定义
Quanser QUBE-Servo 2 是很多高校控制实验室里常见的旋转倒立摆平台。相比传统的一维直线倒立摆,它的结构更紧凑,由直流电机驱动一个水平旋转臂,臂末端铰接一根摆杆,电机转动时带动摆杆在竖直平面内运动。控制目标非常直接:让摆杆从自然下垂位置被甩起来,并在竖直向上的倒立位置稳定住,同时让旋转臂保持在某个参考角度附近。
这个系统最大的特点是“看着简单,控制起来不简单”。电机轴角度、摆杆角度、对应的角速度,这四个状态变量构成了一个典型的欠驱动非线性系统——你只有电机一个输入,却要同时管住两个旋转自由度。而且系统还带有电机摩擦、摆杆阻尼、信号量化误差等一大堆“不干净”的因素。用经典控制方法做倒立摆,LQR、极点配置、PID这些方法都能跑,但都有一个共性前提:你必须先建立一个相对精确的数学模型,然后再基于模型去设计控制器。
这次实现包换了一条路,直接用强化学习来做。核心思路是:不给控制器显式的数学模型,而是把“维持平衡”这件事变成一个试错学习任务,让智能体在仿真环境中不断尝试控制策略,通过奖励信号学会“什么样的电压输出能让摆杆稳在竖直位置”。这个思路本身不新,但在MATLAB的强化学习工具箱环境下,能不能顺利跑通、能不能从仿真迁移到实物,才是真正值得写一讲的经验。
1.2 为什么选择强化学习线路
做控制的人第一个反应通常是:倒立摆这种教科书级系统,LQR就够用了,为什么还要上强化学习?
我的看法是,两者解决的不是同一个问题。LQR要求系统模型已知或者能被线性化,QUBE-Servo 2在教学场景下没问题,但如果你把摆杆换成柔性结构、给系统加上未知负载、或者让摆杆参数发生漂移,LQR的重整定成本会非常高。强化学习的思路是把控制器变成一个“策略网络”,它的输入是传感器观测,输出是控制指令,中间这一大坨非线性映射完全靠数据去逼近——不需要精确模型,只要仿真环境足够接近实物,策略就能学到。
当然,强化学习也有自己的代价:训练时间、超参数调优、奖励函数设计,每一个环节都能让人反复折腾。这个项目选择MATLAB而不是Python,主要原因有三个。第一,QUBE-Servo 2官方提供了完整的Simulink模型和QUARC实时控制接口,MATLAB里可以直接用带硬件支持的Simulink模型跑强化学习环境,不需要像Python那样自己写底层通信。第二,Reinforcement Learning Toolbox提供了现成的DDPG、TD3、SAC、PPO等算法实现,不用自己从零写Actor-Critic网络。第三,从仿真到实物部署时,MATLAB生成的代码可以直接部署到Speedgoat或兼容实时目标机,迁移链路很完整。
1.3 算法选型的对比与取舍
QUBE-Servo 2的控制动作是电机电压(或者电流指令),这是一个连续动作空间问题。可选的主流算法有DDPG、TD3、SAC、PPO。我在这套实现包里主要用TD3和SAC做了对比,简单说一下结论。
| 算法 | 动作噪声处理 | 样本效率 | 调参难度 | 实物部署友好度 |
|---|---|---|---|---|
| DDPG | 简单高斯噪声 | 中等 | 较高,容易发散 | 较好 |
| TD3 | 目标策略平滑 | 较高 | 中等 | 较好 |
| SAC | 熵正则化探索 | 高 | 中等 | 中等 |
| PPO | 重要性采样裁剪 | 低 | 较低 | 好,但样本效率差 |
最终选TD3作为默认方案,原因很实际:QUBE-Servo 2的摆杆在仿真环境里可以被精确建模,样本效率高的算法能更快收敛;而在实物迁移时,TD3只有一个确定性策略网络,部署逻辑更简单,不会像SAC那样因为随机策略在实物上产生不必要的抖动。如果你只想快速看效果,DDPG也能跑通,但收敛稳定性确实差一个档次。
2. 环境搭建与状态动作设计
2.1 MATLAB/Simulink环境配置
这个项目依赖的软件环境有三块:MATLAB本体(建议R2022b及以上,旧版本在强化学习工具箱的接口上有差异)、Simulink、Reinforcement Learning Toolbox,以及QUARC(Quanser的实时控制工具包,实物实验需要)。如果只做仿真不碰实物,QUARC可以不装,但需要用QUBE-Servo 2官方提供的Simscape模型替代。
第一件要做的事是获取QUBE-Servo 2的Simulink模型文件。Quanser官网的学习资源库里提供了“QUBE-Servo 2 Modeling and Control”系列文件,里面有完整的非线性模型和线性化模型。拿到模型后,先跑一遍开环仿真,确认摆杆从垂直下垂位置开始的响应曲线正常。这一步很多人跳过,直接进强化学习,结果训练半天发现环境里角度信号本身就有问题,白费时间。
用强化学习工具箱的rlSimulinkEnv函数把Simulink模型包装成强化学习环境时,需要明确三个接口:观测信号从哪取、动作信号从哪进、终止条件怎么判定。我习惯的做法是把观测信号通过一个Bus Selector汇总到一个向量端口,动作端口直接连到电机的电压输入。终止条件的设置比较关键,不能把“摆杆掉落”直接作为终止,否则智能体会学到“快速倾倒”这种规避行为,而是要设计一个平衡区域,出区域才算失败。
2.2 状态空间与观测变量设计
QUBE-Servo 2 有四个核心状态:电机轴角度、电机轴角速度、摆杆角度、摆杆角速度。但直接把这四个原始量喂给强化学习智能体,效果往往不太理想,原因是角度单位(弧度)和角速度数值范围差了一个数量级,网络训练时梯度容易不稳定。
我在实现包里做了一组观测变换,把观测向量设计为:
obs = [theta_motor * scale_angle, theta_pend * scale_angle, omega_motor * scale_angular_velocity, omega_pend * scale_angular_velocity, sin(theta_pend), cos(theta_pend)]加sin和cos分量是强化学习倒立摆任务里一个非常实用的技巧。摆杆角度在竖直向上附近时,角度值本身在0附近摆动,但如果摆杆翻转一圈,角度会发生跳跃,直接用原始角度会让网络很难理解“角度是循环的”这一事实。而正弦余弦永远连续,相当于给了网络一个“角度位置在哪”的稳定编码。
有一个容易踩的坑:电机轴角度是否要加入观测取决于控制目标。如果只要求摆杆倒立稳定,电机轴角度可以不进观测;但如果你希望旋转臂同时锁定在某个固定角度(比如0度),那这个状态必须进观测,否则智能体没法学到“回中”行为。这个实现包的目标是“摆动起摆+稳定平衡+电机轴回中”,所以观测里包含了电机角度。
2.3 动作空间与执行器限制
动作空间的设置直接跟物理设备绑定。QUBE-Servo 2的电机标称电压范围是±6V(峰值可能到±10V,但长时间跑建议限制在±6V以内,电机线圈会过热)。在强化学习环境里,动作量需要归一化处理——网络输出一个范围在[-1, 1]的信号,然后通过一个Saturation模块映射到实际电压。
这里有一个我做第一版时犯过的错:直接用网络输出作为电压指令,没有做平滑处理。强化学习策略网络产生的动作会有高频抖动,前期探索阶段尤其严重。这种抖动的电压信号直接作用在直流电机上,会导致电流尖峰,还可能让电机的编码器读到被噪声污染的角速度信号。后来我在动作输出路径上加了一阶低通滤波,时间常数选在0.02秒左右,抖动问题明显改善。
另外要注意,如果仿真模型里电机饱和特性没有建模,训练出来的策略很可能会学到“加大电压到极限值”的暴力策略,在实物上直接触发电流保护。所以环境模型里必须包含饱和模块,最好还加上电流限制逻辑,让强化学习从一开始就知道“动作是有代价的”。
2.4 奖励函数设计:从稀疏到密集
奖励函数是强化学习项目里最考验“手工设计能力”的地方。网上一搜“强化学习遇到错误奖励”,能找到大量讨论——奖励函数没写好,智能体训练不收敛是小事,更糟的是模型“自以为是”地学到了完成任务但行为完全错误的对策。
倒立摆任务最朴素的奖励函数是位置误差加权:
R = -(theta_pend^2 + 0.1 * omega_pend^2 + 0.05 * theta_motor^2 + 0.01 * omega_motor^2)这个设计思路直接来源于LQR的代价函数——把状态量的二次型加权和作为负奖励。这样做的好处是每个时刻都有反馈,奖励是密集的,智能体不会陷入真空探索期。但问题也随之而来:如果四个状态量的权重分配不合理,智能体会发现“牺牲摆杆角度来减小电机角度误差”很划算,学成一个偏心也不平衡的控制器。
我建议的做法是:开始训练时先把电机回中的权重拉低,让智能体优先学会“把摆杆立起来”,等摆杆稳定后再把回中权重逐步提高。这种课程式的奖励调度(curriculum reward shaping)实现起来并不复杂——在Simulink里加一个随时间变化的权重模块,或者在MATLAB脚本里分段更新奖励函数参数都可以。
3. 训练实现与核心环节
3.1 Actor-Critic网络结构与初始化
Reinforcement Learning Toolbox里创建TD3智能体时,网络结构通过createTD3Agent或rlTD3Agent指定。我最终跑通的网络结构如下:
% 观测维度:6(4个状态 + sin/cos) obsDim = 6; actDim = 1; % Actor网络:两层隐藏层,每层256个神经元 actorNetwork = [ featureInputLayer(obsDim, 'Normalization', 'none', 'Name', 'obs') fullyConnectedLayer(256, 'Name', 'fc1') reluLayer('Name', 'relu1') fullyConnectedLayer(256, 'Name', 'fc2') reluLayer('Name', 'relu2') fullyConnectedLayer(actDim, 'Name', 'action') tanhLayer('Name', 'tanh') ]; % Critic网络:输入包含观测和动作 criticNetwork = [ featureInputLayer(obsDim, 'Normalization', 'none', 'Name', 'obs') fullyConnectedLayer(256, 'Name', 'fc1') reluLayer('Name', 'relu1') fullyConnectedLayer(256, 'Name', 'fc2') reluLayer('Name', 'relu2') fullyConnectedLayer(1, 'Name', 'qvalue') ];Actorn网络最后一层接tanh,输出范围自动限制在[-1, 1],正好和前面说的归一化动作空间匹配,省去了额外裁剪。两层256个神经元的规模对于这类低维控制问题来说已经足够,再大反而容易过拟合仿真环境里的特殊噪声。
有个小细节:Critic网络的标准做法是把动作和观测拼接后一起输入网络,但TensorFlow和MATLAB的实现细节略有不同。MATLAB的rlQValueFunction支持同时接收观测和动作输入,所以Critic网络里观测和动作是分开两个输入分支的。我一开始按PyTorch的习惯把动作拼到观测里作为特征输入,结果训练速度明显变慢且不稳定——虽然理论上可行,但工具箱对分叉输入的处理做了优化,顺着官方文档来更稳。
3.2 训练超参数与收敛判断
TD3算法在MATLAB工具箱里的关键超参数如下,这是我调了很多轮之后稳定下来的配置:
| 参数 | 数值 | 说明 |
|---|---|---|
| 采样时间Ts | 0.005s | 200Hz控制频率,QUBE-Servo 2支持 |
| 折扣因子 | 0.99 | 标准设置,兼顾短期和长期奖励 |
| 经验池容量 | 200000 | 足够容纳多次完整训练回合 |
| 最小批量 | 256 | 太大训练慢,太小不稳定 |
| Actor噪声 | 0.1 | 训练时动作探索噪声标准差 |
| 目标噪声 | 0.2 | 目标策略平滑噪声 |
| 噪声裁剪范围 | [-0.5, 0.5] | 防止目标动作偏移过大 |
| 学习率 | 3e-4 | Actor/Critic统一 |
| 软更新系数τ | 0.005 | 目标网络更新平滑程度 |
训练循环我一般设2000个回合,每个回合设2000步(10秒仿真时长)。QUBE-Servo 2的实物起摆过程一般不超过3秒,所以10秒的回合长度足够包含完整的“起摆-稳定”过程。
一个值得注意的细节是,Simulink环境里的强化学习训练速度很受采样时间和仿真步长的影响。如果把仿真步长设成固定步长0.005秒,2000回合需要跑很长时间。我的做法是:在训练阶段用一个稍大的控制周期(比如0.01秒)快速验证算法逻辑,等确认策略能收敛了,再把采样时间改回0.005秒做精细训练。这样前期迭代效率高,后期精度也够。
如何判断训练真正收敛而不是假收敛?我总结了三个信号:第一,训练曲线里的平均奖励值连续100回合不再上升;第二,在固定初始状态下,策略能稳定完成“起摆+平衡”的完整流程;第三,把训练好的策略放到一个“没训练过的初始摆角”下测试,仍然能恢复平衡——这个泛化性测试比奖励曲线更可靠。
3.3 仿真验证与评估指标
训练完成后,先把Agent导出:
% 保存训练好的智能体 save('trainedAgent.mat', 'agent'); % 在仿真环境中评估 evalOpts = rlEvaluationOptions('NumEpisodes', 10, ... 'MaxSteps', 2000, ... 'UseParallel', false); results = evaluate(agent, env, evalOpts);评估时的观察重点不是奖励值本身,而是时域响应曲线。我会画三张图:电机角度随时间变化、摆杆角度随时间变化、电压输出随时间变化。如果摆杆角度能稳定在±5度以内,电压噪声不明显,说明策略质量可以接受。
不过仿真能过线,不意味着实物就没问题。QUBE-Servo 2的Simulink模型忽略了很多高频动力学特性,比如电刷换向的转矩脉动、编码器量化误差、电压源的内阻压降。仿真里的策略拿到实物上,一开始大概率会出现“小幅抖动但能立住”的现象,这只是运气好。真正的考验是模型不确定性,比如摆杆重心不完全在模型标称位置、摩擦力矩方向随机变化等。我强烈建议做一轮域随机化(domain randomization)——在仿真模型的参数里加入±10%的随机扰动,比如摆杆惯性矩、阻尼系数、电机力矩常数,让策略见到更多样的环境。这样再迁移到实物,成功率会显著提升。
4. 常见问题与排查技巧实录
4.1 训练发散:最常见的“奖励黑客”问题
我在早期版本里遇到过一次特别典型的发散问题:训练到200多回合时,奖励突然从-50跳到-3000,智能体开始“原地疯狂翻转摆杆”。排查后发现,问题出在终止条件的设置上。
我把终止条件设置成了“摆杆角度超过60度即终止”,但在摆动起摆阶段,摆杆角度在某个瞬间会超过60度,这时回合被强制终止,智能体获得的奖励不仅低,还会把这种“早死”与状态关联起来。最终学到的策略变成了:快速晃动机电臂,让摆杆获得一个很大的初速度甩过去。
解决方法是把终止条件改成“摆杆角度在平衡位置附近保持超过10秒才终止”,其余情况都不终止,只是给出负奖励。这就逼着智能体必须在完整回合内学会“起摆-稳定”的串联动作,而不是投机取巧。这个改法看似简单,但直接决定了训练能不能收敛。
4.2 奖励函数设计错误:局部最优陷阱
另一个常见的坑是奖励函数里的权重失衡。我试过一版把电机回中权重设得特别高的奖励函数,结果智能体学成了“宁可让摆杆倒下也要让电机归零”的诡异行为——它在电机回中后立刻放弃稳定摆杆,导致摆杆反复倒下又立起。根本原因是回中奖励在每一时刻都成了主导项,智能体发现先回正电机“占便宜”更大。
这个问题的排查思路是:分别单独评估奖励函数里每一项的数值量级。用MATLAB的step函数手动跑几个回合,把每一步的各个奖励分量都记录下来,算平均值,看看哪个分量占据了总奖励的90%以上。如果某个分量完全主导,说明其他目标的信号被淹没了,需要调整权重。我最后用的权重比例是摆杆角度:电机角度:角速度 = 10:1:1,这样既保障了摆杆平衡这个最核心的目标,又不会让次要目标喧宾夺主。
4.3 仿真到实物的迁移失败
这是这个项目里最让人头疼的一环。即使仿真里跑得再漂亮,实物上一旦出现以下现象,基本可以断定是迁移问题:
- 摆杆在高频抖动,但电机轴温度飙升
- 策略在起摆阶段过于激进,触发电流保护
- 摆杆稳定后持续有低频振荡,无法收敛
最有效的手段是在仿真环境里加“不确定性注入”。具体操作很简单:用rng种子在每次回合开始时随机初始化摆杆长度、惯量、电机力矩常数,让策略学到的是一个鲁棒控制策略而不是某个精确模型的“记忆”。
另一个实操技巧是,在实物部署前给动作输出加一个限制变化率模块。仿真里的电机模型允许电压瞬变,但实物的电源和电机驱动有自己的爬坡率限制,过快的电压变化会引入噪声和机械冲击。把变化率限制在每步0.5V以内,可以显著减少抖动。
4.4 MATLAB软件版本的兼容性
最后提一个容易被忽视的问题:MATLAB版本。Reinforcement Learning Toolbox在R2021a、R2022b、R2023b之间有一些接口变化,比如rlTD3Agent的参数传递方式、rlSimulinkEnv的终止条件函数格式等。如果你的Simulink模型是用旧版本建的,新版本打开后可能会出现“int等类型不匹配”的错误。
我的建ed建议是:项目开始前就确定一个MATLAB版本,整个开发过程不随意升级。如果确实需要在新版本里打开旧模型,先用upgradeadvisor检查模型兼容性。另外,QUARC工具包和MATLAB版本的对应关系非常严格,装错版本会导致Simulink实时模块无法启动,这类问题通常不是代码问题,纯粹是环境匹配问题,查兼容性矩阵比改代码更快。
5. 个人的实操体会
这个项目做了几轮迭代,我最大的感受是:强化学习控制倒立摆,难点不在算法本身,而在“环境建模的忠实度”和“奖励函数设计的意图对齐”这两件事上。算法参数调来调去,最后发现影响最大的还是Simulink模型里电机饱和模块的参数和奖励函数权重的比例。
如果你正准备在自己的设备上复现这个实现包,我的建议是:第一次训练一定要用保守的奖励函数,先跑通完整链路,再去优化稳态性能。不要一上来就追求“又快又稳又省电”,那会让训练过程充满挫败感。还有一个小技巧是,训练过程中保持Simulink的仿真加速模式,用set_param把仿真模式设为'accelerator',能让训练时间缩短到原来的三分之一左右,这在反复调参阶段能省下大量时间。
最后再分享一个细节:保存模型参数时,除了保存Agent对象,记得把训练曲线数据、超参数配置、奖励函数版本都一起存下来。训练配置分散在多个文件里,几天后你一定会忘记当时跑的版本是“加了低通滤波”还是“改了终止条件”的。这个习惯让我少走了很多弯路。
本文还有配套的精品资源,点击获取