简介:这份资源面向控制理论、机器学习初学者及希望将神经网络落地到实际控制问题的开发者,围绕小车倒立摆这一经典不稳定系统,提供基于Python与神经网络的平衡控制实现方案。压缩包内共1个文件,为单个py脚本,整体约2KB,属于轻量级代码示例,便于直接阅读与二次修改。脚本中应涵盖系统模型定义、神经网络结构搭建、训练流程与主循环等关键环节,读者可借此理解如何用传感器状态作为输入、以小车驱动力作为输出,训练网络学习控制策略,并结合仿真环境验证效果。目前已有553人学习下载,说明该案例在控制与机器学习交叉领域具有一定参考价值。通过研读代码,读者能掌握倒立摆动态特性建模、网络架构与超参数调整、损失函数与优化器选择等实践要点,为后续迁移到真实硬件或更复杂控制任务打下基础。
1. 倒立摆控制为什么让神经网络成了刚需
小车倒立摆是控制领域最经典的验证平台,没有之一。一根摆杆铰接在小车上,小车左右移动,摆杆要立住,同时小车不能跑出轨道边界。传统做法是LQR或者PID串级,线性化点附近效果很好,但摆杆一旦偏角超过十几度,线性模型失效,控制器直接翻车。更麻烦的是摩擦、皮带间隙、电机死区这些非线性因素,用固定参数根本兜不住。
神经网络在这里的价值不是赶时髦,而是它天然适合拟合非线性映射。用Python搭一个前馈神经网络,输入是小车的位移、速度、摆杆角度、角速度四个状态量,输出是电机的PWM或者力指令,训练数据来自传统控制器的成功轨迹或者仿真环境里的随机探索。训练完之后,网络能在更大范围内保持稳定,甚至对参数摄动有一定鲁棒性。
这套方案适合谁?有Python基础、想从仿真过渡到实物控制的学生和工程师。不需要你精通深度学习框架,但得会装numpy、会调参、能看懂状态反馈的基本逻辑。下面从环境搭建到实物部署,把这条路走通。
2. 用Python搭一个能跑的前馈神经网络控制器
2.1 为什么选前馈网络而不是LSTM或CNN
倒立摆的状态量只有四个,是典型的低维连续控制问题。前馈神经网络(FNN)足够表达从状态到控制量的非线性映射,训练快、推理延迟低,在树莓派或者STM32上都能跑。LSTM适合时序依赖强的场景,但倒立摆的马尔可夫性很好,当前状态就包含了决策所需的全部信息,加循环结构反而增加训练难度和推理延迟。CNN更不用考虑,输入不是图像,一维卷积在这里没有优势。
网络结构我一般用4-32-32-1,两个隐藏层,激活函数用tanh而不是ReLU。原因在于控制量需要正负对称输出,tanh的值域是[-1,1],天然匹配归一化后的力指令,而且tanh在零点附近光滑,梯度不会像ReLU那样在负半轴直接截断。输出层不加激活函数,直接线性输出,训练时用MSE损失。
2.2 仿真环境搭建与数据采集
没有实物之前,先在Python里把仿真跑通。用欧拉法或者四阶龙格库塔积分倒立摆动力学方程,步长1ms。下面是最小可运行的仿真代码:
import numpy as np # 物理参数 M = 0.5 # 小车质量 kg m = 0.2 # 摆杆质量 kg l = 0.3 # 摆杆半长 m g = 9.81 # 重力加速度 dt = 0.001 # 仿真步长 def dynamics(state, force): x, dx, theta, dtheta = state sin_t, cos_t = np.sin(theta), np.cos(theta) # 倒立摆非线性动力学方程 temp = (force + m * l * dtheta**2 * sin_t) / (M + m) ddtheta = (g * sin_t - cos_t * temp) / (l * (4/3 - m * cos_t**2 / (M + m))) ddx = temp - m * l * ddtheta * cos_t / (M + m) return np.array([dx, ddx, dtheta, ddtheta]) def step(state, force): # 四阶龙格库塔积分 k1 = dynamics(state, force) k2 = dynamics(state + 0.5*dt*k1, force) k3 = dynamics(state + 0.5*dt*k2, force) k4 = dynamics(state + dt*k3, force) return state + dt/6 * (k1 + 2*k2 + 2*k3 + k4)这段代码定义了倒立摆的连续动力学,dynamics返回状态导数,step做一步积分。参数M、m、l要和你的实物一致,否则训练出来的网络搬到实物上会因为模型失配而振荡。数据采集用PD控制器先跑出稳定轨迹,把(state, force)对存下来,再加一些随机扰动让状态覆盖更广。每个episode跑10秒,采集1000个episode,大概10万条样本。
2.3 网络训练与验证
训练代码用numpy手写反向传播,不依赖PyTorch,方便后续移植到嵌入式端:
import numpy as np class FNN: def __init__(self, sizes=[4, 32, 32, 1]): self.W = [np.random.randn(sizes[i+1], sizes[i]) * 0.1 for i in range(len(sizes)-1)] self.b = [np.zeros((sizes[i+1], 1)) for i in range(len(sizes)-1)] def forward(self, x): self.a = [x.reshape(-1, 1)] for i in range(len(self.W)-1): z = self.W[i] @ self.a[-1] + self.b[i] self.a.append(np.tanh(z)) z = self.W[-1] @ self.a[-1] + self.b[-1] self.a.append(z) # 输出层线性 return z.flatten() def train(self, X, Y, epochs=500, lr=0.01): for ep in range(epochs): loss = 0 for xi, yi in zip(X, Y): out = self.forward(xi) err = out - yi loss += err**2 # 反向传播 delta = err.reshape(-1, 1) for i in range(len(self.W)-1, -1, -1): grad_W = delta @ self.a[i].T grad_b = delta if i > 0: delta = (self.W[i].T @ delta) * (1 - self.a[i]**2) self.W[i] -= lr * grad_W self.b[i] -= lr * grad_b if ep % 50 == 0: print(f"epoch {ep}, loss {loss/len(X):.6f}")forward里tanh的导数用1 - a**2计算,这是tanh激活的经典性质。训练时学习率0.01,500轮足够收敛。验证方法:在仿真里从不同初始角度(±0.3rad)和初始速度出发,看网络能否在2秒内把摆杆拉回竖直。如果失败,优先检查数据分布是否覆盖了这些初始条件,而不是急着加层。
提示:训练数据里的力指令要归一化到[-1,1],推理时再乘以实际最大推力。不归一化的话,输出层梯度会爆炸。
3. 从仿真到实物:部署时最容易翻车的四个环节
3.1 状态观测的噪声与滤波
仿真里状态是干净的,实物上编码器有量化噪声,陀螺仪有零漂。直接把原始值喂给网络,输出会抖得像筛糠。常见做法是加一阶低通滤波,截止频率设在30Hz左右。代码就三行:
alpha = 0.3 # 滤波系数,越小越平滑但延迟越大 x_filt = alpha * x_raw + (1 - alpha) * x_filt_prev角度和角速度都要滤,但角速度滤波系数可以稍大,因为微分噪声更严重。注意滤波会引入相位滞后,如果摆杆振荡频率接近截止频率,系统可能失稳。我一般先用示波器看原始信号频谱,再定截止频率。
3.2 控制周期与推理延迟
神经网络推理在树莓派4上大概0.5ms,在STM32F103上跑定点化版本要3ms。控制周期必须大于推理时间加传感器读取时间。我一般把周期定在5ms,留足余量。如果周期抖动超过20%,网络输出的等效控制量会失真,表现为小车一顿一顿的。解决办法是用定时器中断触发控制循环,而不是while循环里延时。
3.3 输出限幅与积分饱和
网络输出是连续的,但电机PWM有上下限。直接截断会导致积分饱和,摆杆往一边倒的时候控制器拼命输出但被限幅,等摆杆回来时输出又来不及撤。正确做法是在训练数据里就把力指令限制在物理极限内,推理时再做一个软限幅:
def soft_limit(u, umax): return umax * np.tanh(u / umax)这样输出永远不会超过umax,而且在大误差时梯度依然存在,不会像硬截断那样梯度为零。
3.4 模型失配的在线补偿
仿真用的质量和长度是标称值,实物上摆杆质量分布不均匀,等效长度有偏差。网络在仿真里训练得再好,搬到实物上也可能因为模型失配而振荡。我一般加一个简单的自适应项:用PD控制器做底层稳定,网络输出作为前馈补偿,两者叠加。这样即使网络输出有偏差,PD也能兜住。等网络在线微调收敛后,再逐步减小PD增益。
4. 避坑与排查:倒立摆神经网络控制的五个血泪教训
现象一:仿真里稳如泰山,实物上摆杆高频抖动。原因:仿真步长1ms,实物控制周期5ms,离散化误差被网络放大了。解决:把仿真步长改成和控制周期一致,重新训练。或者用零阶保持器离散化动力学方程。
现象二:网络输出恒定值,摆杆直接倒。原因:训练数据里力指令的方差太小,网络学到了均值。解决:检查数据采集时的激励信号,确保力指令覆盖[-umax, umax]的80%以上。可以加白噪声激励。
现象三:摆杆能立住但小车一直往一个方向跑。原因:训练数据里小车位移没有回归到零,网络没学会位置控制。解决:在损失函数里加位移惩罚项,或者用LQR生成的数据做示范,LQR本身就会把小车拉回中心。
现象四:换了块电机,网络就失效了。原因:电机死区和摩擦特性变了,网络没有在线适应能力。解决:加一个在线学习率极小的微调层,只更新最后一层权重,学习率设0.0001,防止破坏已学到的特征。
现象五:训练loss降到很低但验证效果差。原因:过拟合。10万条样本对4-32-32-1的网络来说偏少。解决:加L2正则化,或者把数据增强——对状态量加高斯噪声再喂给网络,让网络学会抗噪。
5. 进阶技巧:用在线微调让网络适应不同摆杆
训练好的网络是离线产物,但实物系统会磨损、会换电池、会变温度。我习惯在部署后加一个在线微调环节,只更新输出层权重,学习率设得很小,用PD控制器的输出作为监督信号。这样网络会慢慢逼近当前系统的真实逆模型,同时PD保证稳定性。
具体做法:每控制一步,计算PD输出u_pd,用(u_pd - u_nn)的平方作为损失,只对最后一层做梯度下降。学习率0.0001,每100步更新一次。跑10分钟之后,网络输出和PD输出的差距会缩小到5%以内,这时候可以把PD增益降到原来的30%,让网络主导控制。
验证方法很简单:用手轻推摆杆,看它回正的速度和超调量。如果回正时间比纯PD短且超调小于5度,说明在线微调生效了。如果振荡加剧,说明学习率太大或者PD监督信号本身有问题。
我自己的习惯是每次换摆杆或者换场地,先跑5分钟在线微调再正式测试。这个习惯帮我省了很多次重新训练的时间。希望帮到你。
本文还有配套的精品资源,点击获取