Python手搓倒立摆:神经网络控制实战与域随机化
发布时间:2026/10/1 13:11:54来源:尧图网络
简介这份资源面向控制理论、机器学习与Python编程的学习者聚焦小车倒立摆这一经典不稳定系统用神经网络作为控制器实现平衡控制。倒立摆涉及动态系统稳定性、反馈控制策略与自适应学习是理论结合实践的典型案例。压缩包内共1个文件为单个py脚本整体约2KB代码中应包含系统模型定义、神经网络结构、训练过程与主循环等关键模块便于读者直接阅读与运行。目前已有553人学习下载说明该案例在控制与机器学习入门群体中具有一定参考价值。通过研读代码读者可以理解如何用Python搭建仿真环境、设计神经网络控制器、调整网络架构与优化器超参数并思考从仿真到真实系统的部署思路适合希望打通控制理论与编程实践的学习者作为练手项目。1. 从零手搓 hwv1_python倒立摆为什么神经网络控制值得你花一个周末如果你手头正好有一辆两轮差速小车或者一块带编码器的直流电机驱动板那你大概率动过「让它自己站起来」的念头。倒立摆这个被控制界玩了几十年的经典对象本质上就是一个天然不稳定的非线性系统——你不控它它必倒你想控它传统 PID 调到你怀疑人生。而 hwv1_python倒立摆 这个方向核心思路是用 Python 搭一套仿真环境把神经网络塞进控制回路里让模型自己学出一个能稳住小车的策略。它解决的不是「能不能立起来」这种玩具问题而是「当系统参数漂移、地面摩擦变化、负载不确定时控制器还能不能扛住」的工程问题。适合谁适合已经会写 Python、装过 numpy但被 LQR 和 PID 参数整定折磨过、想看看 learning-based control 到底能不能落地的一线工程师。你不需要 GPU 集群一台普通笔记本跑仿真足够你也不需要先啃完《强化学习导论》因为这里用的是最朴素的前馈网络做监督学习不是 PPO 那套。2. 倒立摆的动力学建模与 Python 仿真环境搭建2.1 为什么先建模再谈神经网络状态方程是控制器的地基很多人一上来就import torch结果训练半天 loss 不降回头一看状态量定义错了。倒立摆的物理模型不复杂但符号约定必须统一。我一般用小车-摆杆模型小车质量 M摆杆质量 m摆杆半长 l小车位移 x摆杆角度 theta垂直向上为 0。忽略摩擦和空气阻力时动力学方程可以写成(M m) * x_ddot m * l * theta_ddot * cos(theta) - m * l * theta_dot^2 * sin(theta) F m * l * x_ddot * cos(theta) m * l^2 * theta_ddot - m * g * l * sin(theta) 0这两个方程联立后解出 x_ddot 和 theta_ddot就是仿真推进的核心。为什么强调这个因为神经网络控制器的输入是状态 [x, x_dot, theta, theta_dot]输出是力 F。如果你把 theta 的正方向搞反了网络学出来的策略就是「越倒越推」仿真里直接飞出去。常见做法是用 sympy 做符号推导或者直接手推后写成数值函数。我一般手推因为符号计算在实时循环里太慢。2.2 用 Python 写一个可复现的倒立摆仿真步进函数下面这段代码是仿真环境的最小核心不依赖 gym纯 numpy 实现。你把它存成inverted_pendulum.py就能跑。import numpy as np class InvertedPendulum: def __init__(self, M1.0, m0.1, l0.5, g9.8, dt0.02): self.M M # 小车质量 kg self.m m # 摆杆质量 kg self.l l # 摆杆半长 m self.g g # 重力加速度 self.dt dt # 仿真步长 s self.state np.array([0.0, 0.0, 0.05, 0.0]) # x, x_dot, theta, theta_dot def dynamics(self, state, F): x, x_dot, theta, theta_dot state M, m, l, g self.M, self.m, self.l, self.g cos_t np.cos(theta) sin_t np.sin(theta) # 联立求解 x_ddot 和 theta_ddot temp (F m * l * theta_dot**2 * sin_t) / (M m) theta_ddot (g * sin_t - cos_t * temp) / (l * (4/3 - m * cos_t**2 / (M m))) x_ddot temp - m * l * theta_ddot * cos_t / (M m) return np.array([x_dot, x_ddot, theta_dot, theta_ddot]) def step(self, F): # 四阶龙格库塔积分比欧拉法稳 s self.state k1 self.dynamics(s, F) k2 self.dynamics(s 0.5 * self.dt * k1, F) k3 self.dynamics(s 0.5 * self.dt * k2, F) k4 self.dynamics(s self.dt * k3, F) self.state s (self.dt / 6.0) * (k1 2*k2 2*k3 k4) return self.state逻辑说明dynamics里那个4/3来自均匀摆杆的转动惯量近似如果你用的是质点摆把4/3换成1即可。step用 RK4 而不是欧拉是因为倒立摆在不稳定平衡点附近对积分误差很敏感欧拉法在 dt0.02 时几步就发散。参数方面M 和 m 的比例影响控制难度m/M 越大越难控l 越长摆杆倒得越慢但所需推力也越大。我一般先用 M1.0, m0.1, l0.5 这组参数跑通再改到你的实际小车参数上。提示仿真步长 dt 不要大于 0.05否则 RK4 也会失真实际部署到 STM32 时控制周期通常取 5ms 到 10ms仿真里可以先用 20ms 验证算法。3. 前馈神经网络控制器的设计与训练数据生成3.1 为什么选前馈网络而不是 LSTM 或 Transformer倒立摆的状态维度只有 4控制输出只有 1 维力时序依赖并不强——当前状态几乎决定了当前该出多大力。LSTM 和 Transformer 在这里属于杀鸡用牛刀而且训练数据需求量大、推理延迟高放到 STM32 上跑不动。前馈神经网络两层隐藏层、每层 64 个神经元足够拟合这个非线性映射。我试过 3 层 128 神经元效果没提升多少但推理时间翻倍。所以选型原则是状态维度低、控制频率高、算力受限就用最浅的前馈网络。激活函数用 tanh 而不是 ReLU因为控制输出需要平滑ReLU 的零梯度区会让力输出出现死区摆杆在小角度时得不到修正。3.2 用 LQR 生成专家数据让神经网络先模仿再超越神经网络不能凭空学控制你得给它「标准答案」。最省事的办法是用 LQR 算出一批状态-力对然后让网络去拟合。LQR 的权重矩阵 Q 和 R 需要调但比 PID 好调得多。下面代码生成 5000 条训练数据。import numpy as np from inverted_pendulum import InvertedPendulum from scipy.linalg import solve_continuous_are def lqr_gain(A, B, Q, R): P solve_continuous_are(A, B, Q, R) K np.linalg.inv(R) B.T P return K # 在 theta0 附近线性化 M, m, l, g 1.0, 0.1, 0.5, 9.8 A np.array([[0, 1, 0, 0], [0, 0, -m*g/M, 0], [0, 0, 0, 1], [0, 0, (Mm)*g/(M*l), 0]]) B np.array([[0], [1/M], [0], [-1/(M*l)]]) Q np.diag([1.0, 1.0, 10.0, 1.0]) # theta 权重最大 R np.array([[0.1]]) K lqr_gain(A, B, Q, R) env InvertedPendulum() data [] for _ in range(5000): state np.array([np.random.uniform(-0.5, 0.5), np.random.uniform(-1.0, 1.0), np.random.uniform(-0.3, 0.3), np.random.uniform(-1.0, 1.0)]) F -K state F np.clip(F, -20, 20) # 限制最大推力 data.append((state, F)) np.save(train_data.npy, np.array(data, dtypeobject))逻辑说明solve_continuous_are解代数黎卡提方程得到最优增益 K。Q 矩阵里 theta 的权重给到 10是因为角度偏差比位置偏差危险得多。R 越小控制越激进但实际电机推力有限所以后面用np.clip限幅。数据覆盖范围要略大于你期望的工作区间比如 theta 取 ±0.3 rad实际控制时摆杆不会偏这么远但训练时见过大偏差网络泛化更好。参数方面Q 和 R 没有唯一最优我一般先按经验设一组跑仿真看恢复时间如果摆杆回正太慢就加大 theta 权重如果小车跑太远就加大 x 权重。3.3 训练网络时 loss 不降检查输入归一化和输出量纲数据生成好了接下来用 PyTorch 或纯 numpy 写训练循环。我倾向用 PyTorch因为自动求导省事。但有个坑状态里 x 的范围是 ±0.5theta 是 ±0.3而 x_dot 和 theta_dot 可能到 ±1.0量纲不统一。如果不做归一化网络会偏向数值大的维度。我一般对每个维度减均值除标准差输出力也除以最大推力做缩放。训练时用 MSE lossAdam 优化器学习率 1e-3batch size 64跑 200 个 epoch 基本收敛。如果 loss 卡在 0.01 不降先检查归一化再检查网络输出有没有加 tanh 导致饱和。4. 把训练好的网络部署到仿真闭环参数、延迟与稳定性排查4.1 闭环仿真代码从状态到力的完整回路训练完的网络要放回仿真里跑才能知道它到底能不能稳住。下面代码加载模型跑 10 秒闭环。import torch import numpy as np from inverted_pendulum import InvertedPendulum class Net(torch.nn.Module): def __init__(self): super().__init__() self.fc torch.nn.Sequential( torch.nn.Linear(4, 64), torch.nn.Tanh(), torch.nn.Linear(64, 64), torch.nn.Tanh(), torch.nn.Linear(64, 1) ) def forward(self, x): return self.fc(x) net Net() net.load_state_dict(torch.load(controller.pth)) net.eval() env InvertedPendulum() env.state np.array([0.0, 0.0, 0.1, 0.0]) # 初始偏 0.1 rad state_mean np.array([0.0, 0.0, 0.0, 0.0]) state_std np.array([0.3, 0.5, 0.2, 0.5]) F_max 20.0 for t in range(500): s_norm (env.state - state_mean) / state_std with torch.no_grad(): F net(torch.tensor(s_norm, dtypetorch.float32)).item() * F_max env.step(F) if t % 50 0: print(ft{t*0.02:.1f}s x{env.state[0]:.3f} theta{env.state[2]:.3f})逻辑说明state_mean和state_std必须和训练时用的一致否则输入分布偏移网络输出会乱。F_max也要和训练时 clip 的上限一致。跑的时候观察 theta 是否收敛到 0 附近如果振荡发散先降低 F_max 试试再检查网络是不是过拟合了训练数据里的小角度区域。4.2 控制周期与推理延迟仿真里 20ms实际部署要打对折仿真里 dt0.02 跑得挺好不代表实际能行。神经网络推理在 PC 上可能只要 0.1ms但放到 STM32 上即使量化成 int8一次前向传播也要 1ms 到 2ms。加上传感器采样、滤波、PWM 输出整个控制周期最好留 5ms 以上。我一般做法是仿真里先用 dt0.01 跑看网络能不能扛住更快的控制频率如果能实际部署时用 5ms 周期留足余量。如果仿真里 dt0.01 就发散说明网络对延迟敏感得在训练时加入延迟扰动或者改用更浅的网络。注意不要直接在仿真里用 dt0.001 去模拟「高精度」那只会让 RK4 算得更准但控制周期还是 20ms没有意义。控制周期和积分步长是两回事。5. 避坑与常见问题倒立摆神经网络控制翻车实录5.1 现象仿真里立得好好的一上实车就抖成筛子原因仿真没有建模传感器噪声和电机死区。编码器角度有量化误差电机在低占空比时不转这些在仿真里都是理想化的。解决在仿真里给状态加高斯噪声给力加死区非线性重新训练网络。我一般加 0.01 rad 的角度噪声和 0.5N 的力死区网络学出来的策略会鲁棒很多。5.2 现象网络输出力一直在正负最大值之间跳变原因训练数据里 LQR 的输出是平滑的但网络拟合时如果用了 ReLU 或者学习率太大输出会变成 bang-bang 控制。解决换 tanh 激活降低学习率到 1e-4增加训练数据量。另外检查归一化如果 theta 的 std 设得太小网络对小角度变化过于敏感。5.3 现象摆杆能立住但小车一直往一个方向跑原因LQR 的 Q 矩阵里 x 权重给太小专家数据本身就允许小车漂移。解决加大 Q 中 x 的权重重新生成数据训练。或者在网络输出后加一个位置环补偿但那样就不是纯神经网络控制了看你的工程取舍。5.4 现象训练 loss 降到 1e-4 以下但闭环仿真几步就飞原因过拟合。5000 条数据里大部分集中在 theta 接近 0 的区域网络在这附近拟合得很好但一旦 theta 偏到 0.2 rad 以上网络输出完全错误。解决增加大偏差区域的采样或者在 loss 里给大偏差样本加权。我一般把 theta 在 ±0.3 以外的数据量翻三倍。5.5 现象换了台电机参数变了网络直接失效原因网络学的是特定 M、m、l 下的映射参数一变动力学变了策略就不对了。解决要么重新训练要么在训练时做域随机化——每次采样时随机扰动 M、m、l 各 ±20%让网络见过参数变化。这个技巧来自域对抗神经网络的思想但实现起来很简单就是在生成数据时随机化物理参数。6. 进阶技巧用域随机化让一个网络适配多套硬件如果你不想每换一台车就重新训练域随机化是最实用的技巧。具体做法在生成训练数据时不固定 M、m、l而是每次从均匀分布里采样。比如 M 在 [0.8, 1.2] 之间m 在 [0.08, 0.12] 之间l 在 [0.4, 0.6] 之间。这样训练出来的网络对参数变化有天然的鲁棒性。我实测过用域随机化训练的网络在 M 变化 ±15% 时仍然能立住而不做随机化的网络变化 5% 就倒。代码改动很小在生成数据的循环里加几行for _ in range(5000): M np.random.uniform(0.8, 1.2) m np.random.uniform(0.08, 0.12) l np.random.uniform(0.4, 0.6) env InvertedPendulum(MM, mm, ll) # 重新计算 LQR 增益 K A np.array([[0, 1, 0, 0], [0, 0, -m*g/M, 0], [0, 0, 0, 1], [0, 0, (Mm)*g/(M*l), 0]]) B np.array([[0], [1/M], [0], [-1/(M*l)]]) K lqr_gain(A, B, Q, R) state np.array([...]) # 同上 F -K state data.append((state, F))逻辑说明每次循环重新算 K保证专家数据始终是最优的。这样网络学到的不是某一个固定系统的逆动力学而是一族系统的近似逆动力学。代价是训练数据需要更多我一般加到 20000 条。验证方法训练完后用几组没见过的 M、m、l 跑闭环看能不能立住。如果能说明泛化能力够了。最后一个习惯我每次训练完网络都会先用torch.jit.trace导出成 TorchScript再在 PC 上用 C 加载跑一遍确认推理结果和 Python 一致。这一步能提前发现算子不支持、精度损失的问题省得到 STM32 上再抓瞎。倒立摆这个方向仿真到实车的 gap 永远存在但把域随机化和延迟扰动做扎实能少走很多弯路。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网