ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

神经网络控制倒立摆:建模、训练与实机部署全解析

神经网络控制倒立摆:建模、训练与实机部署全解析 简介基于Python与神经网络的小车倒立摆控制项目适合自动化、机器人方向的学生以及AI控制入门者。压缩包内共1个文件py格式大小约2KB核心代码集成了Python环境下的倒立摆仿真与神经网络控制器麻雀虽小但结构完整覆盖从模型到训练的主线。项目以小车倒立摆为研究对象围绕经典不稳定系统的平衡控制进行设计与实现示例从系统建模、神经网络结构定义、训练数据采集到主循环控制逐步展开利用传感器反馈的角度、速度等状态量输出控制信号最终使倒立摆保持稳定同时探讨了传统PID控制与神经网络控制思路的差异并介绍借助Pygame、Gym等库搭建仿真环境以及学习率、优化器、损失函数等关键训练细节。已有553人学习浏览这份轻量级代码无多余依赖便于在课程实验或毕业设计中快速复现和二次开发有助于深入理解神经网络在动态系统平衡控制中的实际作用是理论联系实际的控制工程入门案例。1. 倒立摆系统为什么值得用神经网络做控制从hwv1说起小车倒立摆是控制领域最容易让人着迷又翻车的实验台一个电机驱动的小车车顶立着一根摆杆目标只有一个——让摆杆一直竖着不倒。传统控制课里LQR和PID是标准答案建模准确时效果不错。但真实台架的风阻、摩擦死区、编码器噪声、电机响应延迟会让理想模型变得不理想。hwv1_python倒立摆这个项目选了一条不太一样的路用神经网络直接学习“当前状态 - 控制量”的映射替代或辅助传统控制器。对Python工程师来说这个方向能跑通的最小闭环是仿真建模、数据生成、Pytorch网络训练、部署到小车电机控制主循环。适合手里有倒立摆硬件、同时想验证神经网络实时控制效果的开发者也适合还没买硬件、先用仿真把算法打通的人。本文按我实际做过的方案讲从建模到部署到踩坑尽量让你照着能做出来。2. 先把倒立摆的数学模型立住状态方程、可控性与仿真基础2.1 摆杆怎么会倒动力学方程里的四个状态要做好神经网络控制绕不开先写数学模型。我们说的“状态”一共四个小车位置 x、小车速度 v、摆杆角度 θ、摆杆角速度 ω。其中 θ0 是摆杆竖直朝上角度正负按逆时针约定。忽略电机内部动力学时hwv1这类台架可以用经典的倒立摆动力学方程近似描述(M m) * x m * L * θ * cosθ - m * L * (θ)^2 * sinθ Fm * L * x * cosθ (I m * L²) * θ - m * g * L * sinθ 0其中 M 是小车质量m 是摆杆质量L 是摆杆质心到转轴的距离I 是摆杆绕质心的转动惯量F 是电机施加在小车上的力。这个方程是非线性的Si函数和 cosθ 让模型没法直接拿来做线性控制设计所以传统做法是在 θ≈0 附近做线性化得到状态空间方程x_dot A * x B * u我一般用 Python 搭仿真环境时会直接写完整的非线性方程不做线性化因为神经网络本来就能学非线性映射。但确认系统可控性时拿线性化模型算一下可控性矩阵心里更有底。对于典型参数满秩是没问题的这意味着四个状态都能被输入 u 影响系统在理论上能稳定。2.2 用 Python 搭一个可复现的仿真环境从一个积分循环开始仿真环境的核心是数值积分。我用最基础的欧拉积分先跑通再用 scipy 的 solve_ivp 做高精度验证两种都用过之后我一般仿真里直接写 RK4步长定在 0.001 秒足够接近真实控制周期。import numpy as np def pendulum_dynamics(state, u, params): # state [x, v, theta, omega] # u 电机输出力单位N x, v, theta, omega state M, m, L, I, g, b params # 摆杆角度做余弦/正弦计算注意theta0是竖直向上 s np.sin(theta) c np.cos(theta) # 从动力学方程推导出的加速度表达式 denom (M m) * (I m * L**2) - (m * L * c)**2 x_acc (u m * L * omega**2 * s - b * v - m * L * (m * g * s * c) / (I m * L**2)) x_acc / denom / (I m * L**2) theta_acc (m * L * s * (g x_acc * c)) / (I m * L**2) return np.array([v, x_acc, omega, theta_acc]) def rk4_step(state, u, params, dt): k1 pendulum_dynamics(state, u, params) k2 pendulum_dynamics(state 0.5 * dt * k1, u, params) k3 pendulum_dynamics(state 0.5 * dt * k2, u, params) k4 pendulum_dynamics(state dt * k3, u, params) return state (dt / 6.0) * (k1 2 * k2 2 * k3 k4)这个仿真环境有三个关键点第一状态向量的顺序要固定后面训练神经网络和部署到实物时输入顺序必须和这里完全一致第二参数 b 是摩擦系数这个值很关键如果设成 0仿真里系统会过于理想训练出来的控制器拿到实物上大概率不能用第三dt 不一定要和实物控制周期一致但仿真里用 0.001 秒能更真实地反映高频动态。2.3 仿真和实物的差距哪些参数会影响神经网络的泛化能力仿真模型写得越细神经网络拿到实物上可用性越高。最容易影响泛化能力的参数有三个电机响应延迟、控制周期抖动、摩擦力模型。电机延迟可以在仿真里加一个一阶惯性环节模拟电机从输入电压到产生推力的滞后这比单纯用力作为输入更接近真实。控制周期抖动则是模拟真实系统里定时器不准的问题。我常用系数 0.5 和 0.2 分别给延迟和摩擦扰动让控制器见过足够多“不完美”的情况。# 仿真主循环中加入延迟与噪声的模拟 delayed_u 0.0 for step in range(max_steps): noisy_state state np.random.normal(0, 1e-4, 4) # u 先经过电机一阶惯性模拟响应延迟 delayed_u delayed_u 0.25 * (u - delayed_u) state rk4_step(noisy_state, delayed_u, params, dt)很多初学者做仿真控制环境过于干净网络学到的控制策略在实物上一碰就倒。原因很简单真实系统里编码器读数有量化误差电机有死区数据里没有这些特征的控制器没法泛化。在仿真里提前加噪声等于让神经网络见过更多“考试题”。3. 用前馈神经网络替代PID网络结构、数据生成与训练细节3.1 为什么前馈神经网络做倒立摆控制是够用的倒立摆的状态只有四个控制量只有一个本质上是一个低维的回归问题。前馈神经网络也就是BP神经网络在这个场景下足够表达非线性映射。不少人对神经网络控制的第一反应是强化学习但强化学习在实物上探索成本太高一杆子下去就可能把台架撞坏。我常用的做法是用传统控制器在仿真里产生高质量样本再用前馈神经网络离线模仿这种思路被称为行为克隆实现简单、训练快、效果在倒立摆上出乎意料地好。网络不必深三层足够。输入维度 4隐藏层 64 和 32输出 1。激活函数隐藏层用 ReLU输出层用线性——因为控制量是力存在负值如果用 Sigmoid 就限制了输出范围会导致小车推不动。这里有新手容易踩的坑输出激活函数选错训练 LOSS 降不下去。3.2 用 LQR 或 PID 先生成训练数据系统的第一步要先有数据。我的做法是先在仿真环境里用 LQR 控制器把摆杆稳定住记录下来每一步的状态和控制量作为训练样本。LQR 的增益矩阵可以直接用 Python 的 control 库算出来也可以用状态空间手动解 Riccati 方程。生成数据时要注意的是采样范围不能只记录稳定状态附近的数据要多加点扰动让网络见过“偏得比较远”的时刻。# 用LQR生成训练数据 import control as ct from scipy.linalg import solve_continuous_are A np.array([[0, 1, 0, 0], [0, -b/M, -m*g/M, 0], [0, 0, 0, 1], [0, 0, g/L, 0]]) # 注意这是简化模型的线性化示例 B np.array([[0], [1/M], [0], [1/(M*L)]]) Q np.diag([10, 1, 100, 5]) R np.array([[0.1]]) S solve_continuous_are(A, B, Q, R) K np.linalg.inv(R).dot(B.T.dot(S)) def lqr_controller(state): theta state[2] if abs(theta) 0.5: # 超出线性区先用力把角度拉回来 return -np.sign(theta) * 5.0 return -K.dot(state)这里有几个参数值得说明Q 矩阵里的位置加权 10、角度加权 100代表控制器更在意把摆杆立住而不是精确维持小车位置这符合倒立摆的主目标。R 矩阵为 0.1表示控制力可以有较大幅度。超出线性区时的开关策略很关键它防止 LQR 在小角度假设失效时把系统推得更乱生成的数据质量也因此更高。收集到的数据要保存成统一的格式。我一般存成 Numpy 数组每行四个状态 一个控制量后续训练时直接载入。数据量建议在一万到十万条之间太少网络欠拟合太多会重复采样边际收益递减。3.3 训练过程的关键参数归一化、学习率与随机采样训练神经网络控制器的过程就是普通的监督学习但有几个细节值得注意。第一输入输出都要归一化。状态里角度大概是 ±0.5 弧度速度可能到 ±2 弧度每秒位置和力的量纲也不同不归一化会让反向传播时梯度偏向某些维度的特征网络训练慢且更容易陷进局部最优。第二学习率建议从 1e-3 开始如果 LOSS 震荡就降到 1e-4这条经验可以描述为学习率是倒立摆网络训练里最常调的超参比换网络结构更值得先尝试。第三训练时每个 epoch 做随机打乱避免连续时间步的数据相关性影响梯度。import torch import torch.nn as nn class PendulumNet(nn.Module): def __init__(self): super().__init__() self.fc1 nn.Linear(4, 64) self.fc2 nn.Linear(64, 32) self.out nn.Linear(32, 1) def forward(self, x): x torch.relu(self.fc1(x)) x torch.relu(self.fc2(x)) return self.out(x) model PendulumNet() optimizer torch.optim.Adam(model.parameters(), lr1e-3) loss_fn nn.MSELoss() for epoch in range(200): perm torch.randperm(states.shape[0]) batch_states states[perm][:256] batch_actions actions[perm][:256] pred model(batch_states) loss loss_fn(pred, batch_actions) optimizer.zero_grad() loss.backward() optimizer.step() if epoch % 20 0: print(fepoch {epoch}, loss {loss.item():.6f})训练完后不要直接拿到实物上先在仿真环境里用这个网络替代 LQR 做闭环测试。如果仿真里能稳定超过 30 秒再考虑上实物。这里有个我一直强调的步骤数据划分时留出 10% 作为验证集LOSS 在验证集上的表现才是泛化能力的参考训练集 LOSS 降到 1e-5 而验证集停在 1e-3说明过拟合了——实机调试时这个问题会让摆杆快速倒下。4. Python控制主循环落地从仿真模型到hwv1实物的部署步骤4.1 控制频率和编码器读数先在仿真里模拟实物周期部署到实物前先回答一个问题控制频率是多少hwv1 这类台架常见的做法是 100Hz 到 500Hz。100Hz 意味着每个控制周期 10 毫秒神经网络推理一次的时间必须小于这个周期。PyTorch 的模型推理在普通 PC 上约 1-2 毫秒树莓派上可能要 3-5 毫秒如果控制频率太高会不够用。我的建议是先按 200Hz 设计总目标用仿真模拟这个周期下加噪声的表现。周期延长是神经网络控制最常见的翻车原因仿真里用 1ms 步长实物上用 5ms 步长动态特性已经变了。# 仿真里模拟固定控制周期 dt_control 0.005 # 5ms控制周期 dt_sim 0.001 # 1ms物理仿真步长 control_timer 0.0 for sim_step in range(int(10.0 / dt_sim)): control_timer dt_sim if control_timer dt_control - 1e-9: action net_control(state) control_timer 0.0 state rk4_step(state, action, params, dt_sim)这里体现的思路是仿真和控制分开跑物理仿真步长小而控制周期大模拟了真实系统的离散采样特征。如果控制周期从 5ms 改成 20ms需要重新训练网络而不是期望网络自动适应。4.2 实物控制主循环的代码骨架从脉冲计数到控制量输出摆到实物上任务从纯算法变成了工程问题。编码器读数、电机驱动、定时器、异常保护都要写好。控制主循环的骨架一般是import time import numpy as np from encoder_reader import read_encoders # 读取光电/磁编码器 from motor_driver import set_motor_pwm # 设置电机PWM占空比 control_period 0.005 previous_time time.time() # 状态滤波变量 alpha 0.5 filtered_theta_dot 0.0 while True: target_time previous_time control_period now time.time() if now target_time: time.sleep(target_time - now) # 1. 读取编码器数据计算角度与角速度 raw_pos, raw_angle read_encoders() theta ((raw_angle - angle_offset) / encoder_resolution) * 2 * np.pi theta wrap_to_pi(theta) # 角度归一化到[-pi, pi] theta_dot (theta - theta_prev) / control_period # 2. 速率为一阶低通滤波去掉电机换向带来的毛刺 filtered_theta_dot alpha * theta_dot (1 - alpha) * filtered_theta_dot # 3. 组装状态向量输入神经网络 state np.array([x, v, theta, filtered_theta_dot], dtypenp.float32) action model(torch.tensor(state).unsqueeze(0)).item() # 4. 把控制力映射到PWM占空比注意方向 pwm max(-100, min(100, action * PWM_SCALE)) set_motor_pwm(pwm) # 5. 异常保护 if abs(theta) 0.6: set_motor_pwm(0) break previous_time target_time theta_prev theta这个主循环有四处容易出错编码器零位漂移会让角度标定不准装台架时先通过手动推到竖直位置读取一次角度值作为 offset角速度用差分算出后噪声很大低通滤波的 alpha 参数要根据实际编码器分辨率调整分辨率低时 alpha 要更小PWM 与控制力的映射不是线性的接近零占空比时电机存在死区需要在主循环里做死区补偿异常保护角度阈值要留足裕量超过 0.6 弧度说明网络输出已经失控此时继续输出控制量只会让小车高速撞向限位器。4.3 推理性能优化ONNX转换是实机部署更推荐的路径PyTorch 直接推理在小车上没问题的但控制频率做不到很高时我会把模型转成 ONNX再用 onnxruntime 推理速度能快 30% 以上。转换代码很固定import torch import onnxruntime as ort dummy_input torch.randn(1, 4) torch.onnx.export(model, dummy_input, pendulum_net.onnx, input_names[state], output_names[action], dynamic_axes{state: {0: batch_size}}) sess ort.InferenceSession(pendulum_net.onnx) def net_control_ort(state): input_feed {state: state.reshape(1, -1).astype(np.float32)} return sess.run(None, input_feed)[0][0][0]ONNX 转换后的模型不依赖 PyTorch 运行时可以跑到更多环境里。但需要注意 opset 版本和算子兼容性ReLU 和全连接层都是基础算子基本不会出问题如果你用了 BatchNorm推理时要求模型处于 eval 模式再导出否则参数没有冻结。5. 神经网络控制倒立摆的5个踩坑记录发散、过拟合、时延与整定5.1 训练LOSS降得很好仿真一跑就倒过拟合到训练分布的陷阱现象训练 LOSS 到了 1e-5验证集也还过得去但放进仿真环境里摆杆几秒就倒下。原因生成的训练数据如果只有小角度范围内的状态网络没学过大幅偏转的应对策略一旦摆杆偏离训练分布的覆盖范围输出就是“编的”。这是行为克隆方法的通病——分布偏移问题控制器输出让系统进入新的状态分布网络在那里没有知识。解决训练数据里特意加入扰动后的状态除了 LQR 稳定轨迹还要记录“把摆杆从不同角度放开再拉回来”的片段。另一种做法是训练中做数据增强在每个状态上加少量噪声重新计算控制量。常用做法是数据里至少有 20% 来自偏角大于 0.3 弧度的时刻。5.2 实机上控制器输出一直在抖电机滋滋响编码器噪声放大了现象摆杆能勉强不倒但小车来回窜动电机发出高频振动声持续几十秒后过流保护。原因神经网络的输入是编码器差分角速度分辨率低时量化误差严重差分后噪声被放大模型输出的控制量跟着高频震荡。本质上控制器在响应不存在的高频状态。解决角速度必须滤波常见做法是一阶低通或滑动平均。另外可以把训练数据里的角速度也加入噪声让网络学会忽略输入抖动。整体上这是仿真到实物最典型的一个 gap 来源值得调试时排在最前面。5.3 换了一台hwv1台架就控制不住参数迁移失败现象同一份代码跑到另一台同型号台架上稳定时间从 30 秒降到 5 秒甚至起摆就倒。原因每台台架的摩擦系数、编码器安装角度、电机响应都存在个体差异。神经网络把前一台的参数特征学进去了这些特征和新台架不匹配。解决上实机前做一次简单台架辨识测量摆杆质量分布或电机死区再更新到仿真参数里重新生成数据。这就是迁移训练的通行思路——仿真模型校准后再训练比直接拿通用模型硬跑有效得多。如果两台台架差异实在太大最稳妥的方式是每台台架用同一套代码重新生成数据、重新训练一次。5.4 控制周期一改变就翻车训练时没模拟实际控制频率现象仿真里跑得好好的上调控制频率反而性能下降下调则摆杆缓慢摇摆最后倒下。原因模型是在固定控制周期下训练的周期变了实际系统的离散动态变了神经网络学到的映射不再匹配。解决训练数据生成时直接使用目标控制周期采样不要用连续仿真步长代替。如果实机最终只能跑到 10ms 周期仿真数据就必须按 10ms 步长生成。网络可以适应一定范围的周期变化但超过 1.5 倍就不行了。5.5 网络输出饱和推不动车激活函数和输出量纲不匹配现象训练 LOSS 正常但实物上摆杆快倒时小车反应迟钝感觉电机“推不动”。原因网络训练数据里的控制量范围比较大而电机 PWM 映射到实际推力有限。比如数据里 LQR 输出 ±20N但电机 max 只能给 ±10N饱和了。解决生成训练数据时把控制量的上下界裁剪到实物能承受的范围再作为标签同时调整 LQR 的 R 矩阵让控制器行为温和一些。必要时在实机代码里对网络输出做限幅这是最基础的防线必须要有。6. 让神经控制器更抗造台架参数辨识与频率优化的进阶技巧神经网络在倒立摆上跑通只是起点真正要让它稳定工作还要做两件事参数辨识与采样频率优化。参数辨识的目标是让仿真环境和实物尽可能一致。常见做法是用一次阶跃响应实验给电机一个固定 PWM记录小车加速度从响应曲线的稳态速度计算出有效摩擦系数把摆杆自由摆动时的衰减曲线拟合出阻尼系数。测出来的参数回填到仿真里重新生成训练数据通常让网络在实物上的稳定时间大幅提升。采样频率优化的目标是找到能稳定控制的最低控制周期。周期越低系统对控制器指令的响应越接近连续时间但 CPU 和电机驱动的负担也越大。我通常做法是从 2ms 开始每次翻倍观察仿真里稳定时间和控制量峰值的变化。如果从 2ms 到 8ms 性能没有明显下降那就选 8ms留出 CPU 余量给滤波、保护和监控程序。这个思路的核心在于控制频率不是越高越好够用就行尽量省出算力给更重要的保护逻辑。最后说一个我的习惯在所有代码里第一行注释写清楚模型是在哪个控制周期、哪套参数下训练的。这个习惯帮我避免过太多次“这版模型怎么控制不住”的查错——通常答案是参数不匹配。每次切换台架或调整控制频率后我都会重新生成数据并训练而不是沿用旧模型。神经网络倒立摆控制的成功不是靠网络结构而是靠数据分布、控制周期和台架参数三者的匹配希望帮到你。本文还有配套的精品资源点击获取
返回列表