ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

倒立摆LQR控制实战:Python三行代码实现最优控制

2026/9/29 18:25:18 拓冰建站 浏览量
倒立摆LQR控制实战:Python三行代码实现最优控制 做控制的同学估计都有过这种经历模型推了一黑板仿真跑了一下午最后发现PID参数还是靠一点点试出来的。增益拧小了系统慢悠悠半天回不到原点拧大了干脆振得比蹦迪还快全程都是玄学。直到我换成LQRLinear Quadratic Regulator线性二次型调节器倒立摆这件事才算真正从“玄学”变成了“数学”——把性能要求写进代价函数让算法自动求出最优反馈增益。而且用Python实现核心代码真的就3行配合Jupyter Notebook调试和可视化整个流程非常顺。这篇文章就把我实际跑通的一整套倒立摆LQR控制方案分享出来从状态空间建模、3行核心代码到仿真动画全部给出可以直接复制的代码。适合三类人看正在做课程设计或毕业设计的学生想从PID转向现代控制理论的工程师以及想快速验证一个控制算法效果的Python玩家。你不需要懂很深的现代控制理论线性代数和一点Python基础就能跟着跑完跑通之后你会对状态空间、反馈控制、最优控制这些东西建立起非常直观的体感。1. 项目概述与整体设计思路1.1 倒立摆控制到底难在哪倒立摆是控制领域最经典的实验对象之一也是无数控制理论课程的“标配演示装置”。它本质上是一个竖直向上的不稳定平衡系统摆杆在重力作用下时刻想往下倒控制器的作用就是在它还没倒下去之前持续施加修正力把它“扶”在竖直状态。为什么大家都爱拿它练手因为它的物理结构足够简单而控制难度却一点也不低——系统本身是不稳定的即使摆杆完全静止在竖直位置任何微小的扰动都会让系统指数级地偏离平衡点而且这个偏离过程在物理上几乎不给你反应时间。我打个比方你就懂了。倒立摆就像你把手掌摊开在上面立一根扫帚。你能稳住它的时间长短取决于你眼睛观察偏差、大脑计算修正、手部施加动作这一整条链路的反应速度。如果反应慢了半拍扫帚立马倒地。控制器做的事和你的大脑一模一样测量当前状态角度、位置、速度计算应该施加多大的力然后立刻执行。LQR做的事情就是把这整条“感知-决策-执行”链路变成一个精确的数学模型每一步都算得明明白白而不是靠手感。1.2 为什么选LQR而不是PID很多人在倒立摆上的第一反应是PID。PID不是不能用而是调起来太痛苦。倒立摆有4个核心状态小车位置、摆杆角度、小车速度、摆杆角速度。单回路PID往往只能控制其中一个状态通常是角度另外3个状态基本是“放任自流”。就算你做个串级PID把角度环和位置环串起来也要在两个回路之间反复调配参数工程量大不说系统稳定性还很难保证。我在做这个项目之前试过串级PID调试过程极其磨人经常是角度环稳了位置环又飘了位置环拉回来了角度又开始抖。LQR的思路完全不同。它不是对每个状态单独调增益而是把所有状态一次性纳入一个代价函数通过求解代数黎卡提方程一次性得到一组最优反馈增益。换句话说控制器会同时“照顾”到小车位置、摆杆角度以及它们的变化率不存在“按下葫芦浮起瓢”的问题。从方法论上看PID是“单点控制”LQR是“全局最优”这就是我选择LQR的根本原因。当然LQR也有它的适用范围——它要求系统是线性的或者至少在平衡点附近可以做线性化处理。倒立摆刚好满足这个条件所以用LQR是教科书级别的合适。1.3 技术路线Python与Jupyter Notebook的搭配我用的技术路线很清晰一共四步建立倒立摆的线性化状态空间模型得到A矩阵和B矩阵。给定权重矩阵Q和R调用LQR求解器得到最优反馈增益K。将K代回系统构造闭环系统在Jupyter Notebook里跑仿真。用Matplotlib把状态曲线和倒立摆动画画出来直观验证控制效果。整套流程的运行环境是Jupyter Notebook。为什么选它因为控制算法的调试天然就是“边算边看”的过程而Notebook的交互式特性恰好能承接这种节奏。你可以把一整套流程拆成若干个单元格先运行建模部分看A、B矩阵是否合理再运行LQR求解看反馈增益的量级最后再跑完整仿真。每步的中间结果都实时可见改一个参数马上能重新跑完整个流程这种体验是写一个.py脚本然后进终端运行完全没法比的。再加上Jupyter Notebook对Matplotlib的渲染非常友好画图、动画都是一行代码的事做控制仿真实验首选它没有悬念。2. 环境准备与系统建模2.1 Python环境版本选择和依赖安装先说环境。我推荐用Anaconda或Miniconda来管理Python环境建一个独立的虚拟环境避免和别的项目互相污染。依赖一共就几个numpy、scipy、matplotlib、control、notebook。安装命令如下conda create -n lqr_demo python3.11 -y conda activate lqr_demo pip install control numpy scipy matplotlib notebook这里要特别强调一下Python版本我实测下来3.10和3.11最稳。3.12刚出来的那阵子有些科学计算轮子还没编译好装control库时容易遇到奇怪的二进制兼容性问题尤其是在Windows上。如果你已经装了3.12还报错果断降级到3.11这是性价比最高的解决方案。另外Scipy和Numpy一般会随着control库自动安装但如果之前用conda装过旧版本可能会产生依赖冲突所以建一个干净的新环境是更省心的选择。2.2 Jupyter Notebook的启动、内核与常用技巧装完依赖后在终端激活环境再启动Notebookjupyter notebook默认会在浏览器打开当前工作目录。如果没自动打开终端里会显示一个带token的localhost链接复制到浏览器即可。这里有一个小技巧你在哪个目录下执行这行命令Notebook的工作目录就是哪个目录。所以我习惯专门建一个work目录把Notebook和数据都放里面保证每次打开的基线一致。再分享一个我常用的Notebook技巧Kernel菜单下的“Restart Run All”。调参时你可能改了好几个单元格一个一个重新运行很容易漏这个功能可以一键重跑所有单元格从头到尾保证计算顺序正确。还有一个坑如果你装了多个Python环境Notebook可能默认使用系统Python而不是你conda环境里的Python导致import control失败。解决办法是在conda环境里注册内核在终端执行python -m ipykernel install --user --name lqr_demo这样Notebook的内核列表里就会出现lqr_demo这个选项选它就不会用错Python版本了。2.3 倒立摆数学模型与状态空间搭建接下来是核心的准备工作倒立摆建模。我用的模型是“小车-摆杆”结构就是一根摆杆装在可以水平移动的小车上。设小车质量为M摆杆质量为m摆杆质心到转轴距离为l重力加速度为g。状态向量取x [p, θ, p_dot, θ_dot]其中p是小车位移θ是摆杆相对竖直方向的偏角p_dot和θ_dot分别对应对这两个量的导数。通过拉格朗日方程推导并做小角近似sinθ约等于θcosθ约等于1线性化后的状态空间方程系数矩阵为import numpy as np M, m, l, g 0.5, 0.2, 0.3, 9.8 A np.array([ [0, 0, 1, 0], [0, 0, 0, 1], [0, -m * g / M, 0, 0], [0, (M m) * g / (M * l), 0, 0] ]) B np.array([ [0], [0], [1 / M], [-1 / (M * l)] ])这个A矩阵的右上角的2x2块说明位置和速度之间的关系位置p的导数是速度p_dot角度θ的导数是角速度θ_dot。左下角那两项是重力产生的加速度项θ对小车加速度的影响是-mg/M对小车的角度加速度的影响是(Mm)g/(Ml)这两项正是导致系统不稳定的根源。我建议刚接触的同学把A、B矩阵打印出来看一遍数值大小和正负都能帮助你建立对系统的直觉。3. 3行核心代码LQR求解与控制律实现3.1 3行代码逐行拆解模型就位后好戏开场。下面就是标题里说的3行核心代码import control as ct K, S, E ct.lqr(A, B, Q, R) # 第1行求解最优反馈增益 u -K x # 第2行根据当前状态计算控制量 x_new x (A x B * u) * dt # 第3行把控制量代入模型更新状态这3行其实不是一个完整可运行的程序而是整套算法的三个关键环节解算、控制律、状态更新。第1行是LQR的“灵魂”它接收状态矩阵A、输入矩阵B、权重矩阵Q和R输出反馈增益矩阵K。返回值S是代数黎卡提方程的解矩阵PE是闭环系统的极点。第2行是控制律的数学表达——控制输入等于反馈增益和当前状态的线性组合负号表示负反馈也就是“状态偏了就反向施加修正力”。第3行是最简单的欧拉积分用当前状态和计算出的控制量去推算下一个时刻的状态。就这么简单一个倒立摆的线性反馈控制器就落地了。3.2 LQR原理代价函数与黎卡提方程LQR之所以叫“线性二次型调节器”名字里其实藏着全部秘密“线性”对应线性系统模型“二次型”对应它要优化的代价函数是一个二次型积分。这个代价函数长这样J ∫₀^∞ (x^T Q x u^T R u) dt翻译成人话就是整个运行过程中状态偏差的代价加上控制能量的代价总和要最小。Q矩阵决定你有多看重状态收敛速度R矩阵决定你多心疼控制能量。LQR要做的就是找到最优控制律u -Kx让这个积分最小。从数学上看K的求解要解一个名叫代数黎卡提方程ARE的方程A^T P P A - P B R^{-1} B^T P Q 0这个方程在一般情况下没有解析解都是数值迭代计算出来的。好在Python生态里已经有很成熟的实现。除了control库的ct.lqr还可以用scipy.linalg.solve_continuous_are不过写起来稍微麻烦一点from scipy.linalg import solve_continuous_are P solve_continuous_are(A, B, Q, R) K np.linalg.inv(R) B.T P两种方式算出来的K完全一样。我个人习惯直接用ct.lqr因为它的返回值里带了闭环极点E方便直接看系统稳不稳定。3.3 权重矩阵Q和R怎么调才不玄学调参是LQR里最需要经验的地方但绝不是玄学有一套非常直观的逻辑。我的起点一般是Q np.diag([10.0, 100.0, 1.0, 1.0]) # 状态权重 R np.array([[1.0]]) # 控制权重这里的物理含义很明确Q的第二个元素是100对应摆杆角度θ权重最大意思是摆杆角度偏差的“罚款”最重控制器会优先把角度拉回来。第一个元素10对应小车位置p要求它不要跑太远。第三、四元素对应速度和角速度权重设为1是希望运动过程不要太剧烈不要出现过大的冲击。R1意味着对控制力不做太苛刻的限制给控制器足够的“力气”去干活。调参的原则可以总结成一张表现象调整方式摆杆回正太慢增大Q中θ对应的权重小车左右冲得太远增大Q中p对应的权重控制力输出太大、动作太猛增大R控制量太小、系统响应无力减小R状态在平衡点附近振荡增大Q中速度/角速度对应的权重这个“增谁减谁”的逻辑非常直观比PID那个“增P减D”的调试口诀好记多了。记住一个原则Q和R的相对比值决定了控制器的“性格”。Q相对R越大控制器越激进响应越快但动作越猛Q相对R越小控制器越保守动作越温和但收敛越慢。你完全可以通过调整这个比值来控制系统的表现。4. 仿真与可视化实战4.1 初始偏差下的闭环响应控制器算出来了接下来就是见证效果的时候。我把完整仿真代码贴出来直接在Jupyter Notebook里跑即可import numpy as np import control as ct import matplotlib.pyplot as plt M, m, l, g 0.5, 0.2, 0.3, 9.8 A np.array([ [0, 0, 1, 0], [0, 0, 0, 1], [0, -m * g / M, 0, 0], [0, (M m) * g / (M * l), 0, 0] ]) B np.array([[0], [0], [1 / M], [-1 / (M * l)]]) Q np.diag([10.0, 100.0, 1.0, 1.0]) R np.array([[1.0]]) K, S, E ct.lqr(A, B, Q, R) A_cl A - B K sys_cl ct.ss(A_cl, B, np.eye(4), np.zeros((4, 1))) t np.linspace(0, 5, 500) x0 [0.0, 0.2, 0.0, 0.0] _, x ct.initial_response(sys_cl, t, x0) plt.figure(figsize(10, 4)) plt.plot(t, x[1, :], labeltheta (rad), linewidth2) plt.plot(t, x[0, :], labelp (m), linewidth2) plt.axhline(0, colorgray, linestyle--, linewidth1) plt.xlabel(Time (s)) plt.ylabel(State) plt.legend() plt.grid(True, alpha0.3) plt.show()这里的关键是把原来的开环系统A变成闭环系统A_cl A - B K然后用ctrl.initial_response直接求解闭环系统对初始状态的响应。初始摆角我设成了0.2弧度大约是11.5度这是一个很有代表性的初始偏差——既不是小到看不出效果也不是大到超出线性化近似范围。跑完你会看到摆角从0.2弧度开始在大约2秒内被平滑地拉回零附近小车位置也会先偏移一点再回到原点整个过程几乎没有超调这比PID的典型响应要干净得多。4.2 加入扰动与噪声后的鲁棒性验证实际系统不可能这么干净所以我还会加两个压力测试一个是控制输入端加一个短暂脉冲模拟外面有人敲了一下摆杆另一个是在状态测量上加高斯噪声模拟传感器不完美。脉冲扰动可以这样加from scipy.integrate import solve_ivp def plant_with_pulse(t, x): u -K x if 1.0 t 1.05: u u 5.0 # 外部冲击持续0.05秒 return A x B * u sol solve_ivp(plant_with_pulse, [0, 5], x0, t_evalt, methodRK45)这里的5.0相当于给小车一个持续0.05秒、大小为5N的额外推力。跑完这个仿真重点观察摆角在两个周期内能不能回到零点附近能回来就说明控制器有不错的鲁棒性。我在实测中LQR对这种短时脉冲的抵抗能力非常出色摆杆会迅速偏离一个不大的角度然后很快恢复这比单纯加阻尼的PID要果断很多。如果你想模拟传感器噪声直接在状态反馈里加随机数就可以比如u -K (x noise)噪声标准差设成0.01左右看控制量是否还能稳住系统。4.3 用动画让倒立摆活起来最后做个动画这是整个项目里最直观、最出效果的部分。用matplotlib.animation的FuncAnimation把倒立摆画成一根杆子加一个方块小车from matplotlib.animation import FuncAnimation fig, ax plt.subplots(figsize(8, 4)) ax.set_xlim(-2, 2) ax.set_ylim(-0.5, 1.5) ax.set_aspect(equal) ax.grid(True, alpha0.3) car_body, ax.plot([], [], s, markersize18, color#2c7fb8) rod_line, ax.plot([], [], -, color#d95f0e, linewidth3) def init(): car_body.set_data([], []) rod_line.set_data([], []) return car_body, rod_line def update(frame): p x[0, frame] theta x[1, frame] car_body.set_data([p], [0]) x_tip p l * np.sin(theta) y_tip l * np.cos(theta) rod_line.set_data([p, x_tip], [0, y_tip]) return car_body, rod_line ani FuncAnimation(fig, update, frameslen(t), init_funcinit, interval20, blitTrue) plt.show()这段代码的思路很直接每一帧根据当前状态x计算出小车位置和摆杆末端坐标然后更新图形对象。动画一出来你会非常直观地看到摆杆从初始角度被慢慢拉回竖直的过程整个控制器的效果一目了然。建议把interval参数设成20毫秒也就是每秒50帧这个帧率最接近物理过程的真实节奏。如果在Notebook里动画不显示检查一下是否用的是%matplotlib inline换成%matplotlib notebook一般就能解决。5. 常见问题与排查技巧实录5.1 Python环境与依赖的坑我在Windows和Linux都搭过这套环境最常见的坑有三个。第一个是装了control后import一直报错提示缺少scipy或numpy。理论上scipy和numpy会在装control时被自动装上但如果之前用conda装过旧版本可能产生依赖冲突。解决方式是单独建虚拟环境后一次性pip install别混着装。第二个是Windows下运行Python命令时提示“python was not found; run without arguments to install from the Microsoft Store”。这是手动装Python后没把环境变量加入PATH的典型症状。要么重装Python时勾选Add Python to PATH要么手动在系统环境变量里把Python.exe的安装目录加进去这个坑很多人都会踩。第三个是Python 3.12以上版本部分依赖没有预编译包容易在安装阶段报错。我的建议很直接别跟版本死磕降级到3.11或3.10十分钟之内解决问题。5.2 Jupyter Notebook执行异常排查Notebook打不开或者单元格执行没反应是使用频率极高的问题。我遇到过几次排查思路如下。先在终端里运行jupyter --version如果提示命令不存在说明conda环境没激活或者pip安装路径不在PATH里。如果notebook能打开但执行单元格一直转圈没输出多半是内核挂了解决办法是Kernel - Restart清空已有状态重新跑。还有一次是内核的Python路径指向了系统Python而不是conda环境我通过注册内核解决python -m ipykernel install --user --name lqr_demo如果遇到报错“ImportError: DLL load failed while importing rpds”这个我实测下来基本是rpds-py这个包和当前Python版本不兼容执行pip install --upgrade rpds-py就能修复。这一类问题大多都和内核或依赖版本有关排查顺序建议是先看内核对不对再看依赖全不全最后看版本兼容性。5.3 LQR仿真发散与失稳排查如果你仿真时发现状态量一路狂飙到1e10这种离谱数字先别急着怪算法按下面这个顺序查第一A矩阵和B矩阵的维度对不对。用A.shape和B.shape输出看一眼A应该是4x4B应该是4x1。维度错了后面一切白搭。第二Q矩阵是否半正定、R是否正定。Q如果给了负值或者零值太多求解黎卡提方程就会出现问题。最简单的保底做法是用np.diag生成对角阵对角线元素都为正值。第三控制增益太大导致数值震荡。欧拉积分在步长较大时本身就不稳定dt取0.01秒通常没问题但如果你把dt拉到0.05以上高频模态就可能发散。可以把dt调小或者换成scipy.integrate.solve_ivp这类自适应步长求解器这是最稳妥的做法。第四闭环系统A_cl A - B K的极点是否都在左半平面。打印一下ct.poles(sys_cl)看实部只要所有极点实部都小于0系统就是渐近稳定的。如果发现有正实部极点说明K根本就没算对回头检查Q和R的正定性和模型矩阵的正确性。最后再分享一点个人经验。我在做这个项目之前对“现代控制理论更高级”这种说法一直将信将疑因为课堂上讲的状态空间和黎卡提方程总觉得飘在纸上。但真正用Python跑完这套流程后我的体会是LQR的核心思想其实不复杂复杂的是把物理问题翻译成数学问题的过程。倒立摆模型虽然简单但建模型、调权重、跑仿真、看动画这一整套闭环能让你比刷十遍教材都更深地理解状态空间和最优控制。这套代码的扩展性也很强改成二阶倒立摆只需要扩展状态维度和A、B矩阵改成旋转倒立摆就换一组运动学方程甚至拿去做四轴飞行器或自动驾驶的纵向控制LQR的设计思路都是一样的。建议你跑通之后试着改改物理参数比如把摆杆加长到0.5米再重新调一次Q和R你会对“模型变化如何影响最优增益”建立起非常具体的感知。