从仿真到真机:用Python实现小车倒立摆的神经网络控制器

发布时间:2026/10/31 6:03:50
从仿真到真机:用Python实现小车倒立摆的神经网络控制器 简介这份Python源码演示了基于神经网络的小车倒立摆平衡控制面向自动控制、机器人及机器学习方向的初学者与开发者帮助理解不稳定系统的实时反馈控制策略与智能控制方法。压缩包内共1个py文件大小仅2KB虽文件精炼却覆盖了倒立摆系统模型、神经网络控制器结构、训练流程与仿真主循环等核心部分可在Pygame或Gym等模拟环境中运行实时观察平衡控制效果。资源已有553人次学习浏览代码中明确呈现了网络层数与节点设置、优化器选用、损失函数计算及超参数配置等关键细节方便在此基础上继续调参与改进。通过该脚本能够清晰看到状态输入到控制信号输出的完整映射过程并理解如何用仿真数据训练网络从而沿用至真实倒立摆平台的部署尝试。总体而言这是一份融合控制理论、Python编程与神经网络的精简可运行实例适合希望快速上手智能控制仿真的读者。1. 小车倒立摆为什么值得用神经网络控制从PID的手忙脚乱说起小车倒立摆hwv1这类常见实验平台是自动控制里最经典的“不稳定系统”教材却也是神经网络控制最容易出成果的试验田。用传统PID去稳定一根立在运动小车上的摆杆调试四个增益参数就能让人怀疑人生角度环快了位置发散位置环稳了摆杆又抖起来。而神经网络控制器只吃四个状态量——位置、速度、摆角、角速度——就能直接输出推力训练得当甚至可以比手调PID更鲁棒。这篇文章要解决的问题很具体用Python从零搭一个能训练、能验证、能往真机迁移的小车倒立摆神经网络控制器。适合刚接触智能控制的工程师、做自动化毕设的学生以及想验证神经网络到底能不能脱离黑匣子身份扛起真实控制任务的开发者。2. 先搭仿真环境用Python写一个能反复翻车的小车倒立摆2.1 状态空间与动力学模型写代码前先写方程倒立摆的动力学模型本质是一个二阶非线性系统。小车状态用四个量描述位移x、速度x_dot、摆角theta规定垂直向上为0逆时针为正、角速度theta_dot。控制输入是作用在小车上的水平推力F摆杆通过旋转关节连接在小车上。动力学方程忽略摩擦和阻尼的简化模型为(M m)·x_ddot m·l·theta_ddot·cos(theta) - m·l·theta_dot²·sin(theta) Fm·l·x_ddot·cos(theta) (I m·l²)·theta_ddot - m·g·l·sin(theta) 0其中 M 是小车质量m 是摆杆质量l 是摆杆半长质心到转轴距离I 是摆杆转动惯量。这里最容易埋坑的是 l 的取值——用全长还是半长算出来的角加速度会差一倍。我习惯把所有物理参数集中放在一个字典里方便后面对照真机硬件做修正。# cartpole_params.py import numpy as np PARAMS { mass_cart: 0.5, # 小车质量 kg mass_pole: 0.2, # 摆杆质量 kg pole_length: 0.6, # 摆杆全长 m注意动力学方程中 l 取半长 gravity: 9.81, # 重力加速度 m/s^2 inertia: 0.006, # 摆杆转动惯量 kg*m^2 force_mag: 12.0, # 最大推力幅值 N dt: 0.02, # 控制周期 s对应 50Hz 控制频率 }逻辑说明把物理参数集中在字典里是为了后半段把神经网络从仿真往真机搬的时候只改参数、不动网络结构。pole_length存全长但在动力学计算里除以2得到半长这样注释里写清楚避免三个月后自己回来看代码时骂娘。控制周期 0.02 秒意味着推理频率 50Hz这个频率也和后面训练数据采集、模型部署保持对齐。2.2 最小可跑的仿真循环四阶龙格库塔推进动力学方程是连续时间系统而控制是离散的。仿真推进我不用欧拉积分——倒立摆对积分精度比较敏感欧拉法在50Hz控制频率下会让训练出来的网络在仿真里“看起来能立住”一细看角度曲线全是锯齿换到真机上更是不敢想。# env.py import numpy as np from cartpole_params import PARAMS def dynamics(state, force, p): 返回状态导数 dx/dt [x_dot, x_ddot, theta_dot, theta_ddot] x, xd, theta, thetad state M, m p[mass_cart], p[mass_pole] l p[pole_length] / 2.0 # 半长 g, I p[gravity], p[inertia] sinT, cosT np.sin(theta), np.cos(theta) # 解 2x2 线性方程组得到 x_ddot 和 theta_ddot A np.array([[M m, m * l * cosT], [m * l * cosT, I m * l**2]]) b np.array([force - m * l * thetad**2 * sinT, m * g * l * sinT]) ddx, ddtheta np.linalg.solve(A, b) return np.array([xd, ddx, thetad, ddtheta]) def step(state, force, pPARAMS): 用 RK4 推进一个控制周期返回新状态 dt p[dt] k1 dynamics(state, force, p) k2 dynamics(state 0.5 * dt * k1, force, p) k3 dynamics(state 0.5 * dt * k2, force, p) k4 dynamics(state dt * k3, force, p) return state (dt / 6.0) * (k1 2 * k2 2 * k3 k4)逻辑说明dynamics里用np.linalg.solve解2x2线性方程组比手动推导出 x_ddot 和 theta_ddot 的闭式表达式更不容易出错而且以后想加摩擦力、加杆的质量分布时只改矩阵里的系数就行。RK4 每步调用四次dynamics在纯Python里跑50Hz仿真完全够用不需要上C扩展或JIT免得把环境搞复杂。2.3 为什么要自建环境而不是直接用现成库市面上有 OpenAI Gym 的 CartPole 环境很多新手上来就gym.make(CartPole-v1)。但我建议至少把自建环境先跑通原因有三个。第一Gym 的 CartPole 默认是离散动作空间左或右而真实小车倒立摆的推力是连续的。用离散动作训练出来的网络策略搬到连续推力的电机驱动上控制品质会明显下降。第二自建环境里你能精确控制每个物理参数——摩擦系数、采样延迟、传感器噪声都可以随时加进去后面对照真机做“仿真到实物迁移”时才能定位模型误差出在哪一环。第三亲手写一遍动力学方程后面调网络时心里有底输出层该不该用 tanh、奖励函数里角度权重怎么定其实都跟方程结构直接相关。如果后面想做深度强化学习自建环境同样可以封装成 Gym 接口在step外面包一层reset、observation_space、action_spacePPO 或 SAC 的训练库直接就能吃不需要换代码。3. 神经网络控制器前馈网络如何学会稳定小车3.1 输入输出与网络结构状态特征决定控制上限神经网络控制器的任务映射是四个状态量 → 一个推力。输入是[x, x_dot, theta, theta_dot]输出是F范围在[-force_mag, force_mag]之间。第一个关键决定是激活函数的选择。我的经验是隐藏层用 ReLU中间一到两层、每层64到128个神经元就够。倒立摆状态维度低、非线性程度有限不需要更深更宽的网络——加大网络容量只会让训练变慢并不会提升控制性能。输出层一定要用 tanh把网络输出限制在[-1, 1]再乘上force_mag得到实际推力。这里有个细节如果输出层用线性激活训练初期梯度爆炸的概率会大增推力动不动就冲到几百牛仿真直接崩掉。不用 LSTM 或 CNN 的原因也顺带说清楚倒立摆状态是马尔可夫的——当前状态加当前控制量就能推出下一时刻状态不存在长时序依赖LSTM 在这里只会增加训练难度和数据需求CNN 更不用说那是为图像输入准备的。前馈神经网络BP网络是这类控制任务最对口的工具反向传播一遍走完结构简单问题好排查。3.2 训练范式监督学习还是强化学习这里需要做一个选型决策。做倒立摆神经网络控制主流路线分三类。第一类是行为克隆监督学习先用传统控制器LQR 或 PD采集一批“状态-动作”数据然后训练神经网络去拟合这个策略。优点是非常稳训练快用标准反向传播流程就能搞定缺点是性能上限不会超过专家策略学不到比专家更聪明的控制方式。第二类是强化学习PPO / SAC / DQN网络自己跟环境交互通过奖励信号学策略。优点是不需要设计专家控制器理论上限更高还能学到一些反直觉的、非线性的控制策略缺点是训练时间长、奖励函数设计像玄学第一次跑大概率遇到“奖励一直不上涨”的翻车现场。第三类是两者结合先用行为克隆做预训练再用强化学习做微调。对 hwv1 这类真实硬件我建议先走“行为克隆跑通全流程”再上强化学习提升性能。理由很现实行为克隆的训练曲线是收敛的、可预期的适合建立信心强化学习第一次跑你很难判断是算法写错了、超参数不合适还是环境有问题。3.3 生成训练数据PD控制器采集与数据清洗行为克隆的第一步是采集专家数据。常见做法是用 PD 控制器先稳定倒立摆同时记录状态和对应控制力。PD 增益要调得足够硬保证控制器能把摆角快速压到接近零否则采进来的废数据会让网络学到一个“无力的策略”。# collect_data.py import numpy as np from env import step from cartpole_params import PARAMS def pd_controller(state, kp_theta50.0, kd_theta8.0, kp_x2.0, kd_x1.0): PD控制器同时稳定摆角和小车位置返回推力 x, xd, theta, thetad state # 角度环为主位置环为辅 force -kp_theta * theta - kd_theta * thetad - kp_x * x - kd_x * xd return np.clip(force, -PARAMS[force_mag], PARAMS[force_mag]) def collect_trajectories(episodes500, steps_per_episode200): 采集专家轨迹返回状态数组和力数组 states, forces [], [] for _ in range(episodes): # 初始摆角给一个小扰动模拟真实系统中的偏差 state np.array([0.0, 0.0, np.random.uniform(-0.1, 0.1), 0.0]) for _ in range(steps_per_episode): force pd_controller(state) states.append(state.copy()) forces.append(force) state step(state, force, PARAMS) return np.array(states), np.array(forces)逻辑说明PD控制器四个增益分成两组——kp_theta和kd_theta管摆角kp_x和kd_x管位置。初始摆角只给 ±0.1 弧度的小扰动因为PD控制器在较大摆角比如超过30度时本来就拉不回来那些数据采进来就是纯噪声。采集完成后有一个重要的数据清洗动作遍历轨迹把“摆角绝对值持续增大”的片段丢掉——说明PD控制器已经失效这段轨迹后面全是无效样本网络学了只会学歪。4. 训练流程与超参数让反向传播真正把杆子立住4.1 损失函数与标签设计不是所有误差都值得学行为克隆的损失函数看起来就是 MSEloss mean((pred - label)^2)。但有个细节会让新手踩坑标签是PD控制器输出的力如果数据里混入大量摆角接近零、力也接近零的样本网络会被“平均值”带偏学成一个偏向输出小力的保守策略。真正需要大推力去纠正偏差时网络反应迟钝杆子就会慢慢倒下去。我一般会在采样时做困难样本加权把摆角较大比如超过0.05弧度的状态复制两到三份直接改变数据分布让网络把“大偏差对应大推力”这个映射学得更扎实。这属于典型的“数据分布决定性能上限”问题——模型结构只是下限。另一个关键点是输出的分布。PD控制器输出的力在 ±12N 之间分布很不均匀大部分时间在 ±2N 附近。如果直接拟合原始力值网络学到的是“大多数时候输出小力”。缓解办法是对力标签做缩放除以force_mag让标签分布在[-1, 1]和 tanh 输出的范围对齐训练收敛会更快更稳。4.2 训练循环PyTorch最小实现# train.py import torch import torch.nn as nn from torch.utils.data import TensorDataset, DataLoader class PoleNet(nn.Module): 前馈神经网络控制器4维状态输入1维推力输出 def __init__(self, input_dim4, hidden_dim128): super().__init__() self.net nn.Sequential( nn.Linear(input_dim, hidden_dim), nn.ReLU(), nn.Linear(hidden_dim, hidden_dim), nn.ReLU(), nn.Linear(hidden_dim, 1), nn.Tanh() # 输出归一化到 [-1, 1]推理时乘 force_mag ) def forward(self, x): return self.net(x) def normalize_data(states, forces): 对状态做标准化对力做缩放返回统计量供推理时使用 state_mean states.mean(axis0) state_std states.std(axis0) 1e-8 states_n (states - state_mean) / state_std forces_n forces / PARAMS[force_mag] return states_n, forces_n, state_mean, state_std states_n, forces_n, state_mean, state_std normalize_data(states, forces) model PoleNet() optimizer torch.optim.Adam(model.parameters(), lr1e-3) dataset TensorDataset(torch.tensor(states_n, dtypetorch.float32), torch.tensor(forces_n, dtypetorch.float32)) loader DataLoader(dataset, batch_size256, shuffleTrue) for epoch in range(200): epoch_loss 0.0 for s, f in loader: pred model(s).squeeze(-1) loss nn.functional.mse_loss(pred, f) optimizer.zero_grad() loss.backward() optimizer.step() epoch_loss loss.item() if epoch % 20 0: print(fepoch {epoch}, loss {epoch_loss / len(loader):.4f})逻辑说明网络输出接 Tanh 把力限制在 ±1推理时乘 12N 就是物理推力。这个设计让训练初期不会出现“推力瞬间爆表”的梯度爆炸问题这是 BP 网络训练里常见的翻车点。学习率 1e-3 是 Adam 在低维回归问题上的稳定起手值不要一上来就用 1e-2——loss 经常会先降到很低然后突然跳回高位看起来像训练过程出了 bug其实只是步子迈太大。batch_size 选 256 因为数据量本身不大500条轨迹 × 200步 10万条样本大 batch 能减小梯度随机波动。4.3 训练监控loss曲线下降不代表控制成功行为克隆里有个很常见的假象训练 loss 降到 1e-4看起来完美收敛把模型接到仿真环境里一跑杆子一秒就倒。原因是 loss 衡量的是“预测力与标签力的误差”而控制任务关心的是“这个力作用在物理系统上能不能稳住”。这两个指标在数据分布健康的时候正相关数据分布稍偏比如大部分样本都是小摆角小推力就会脱钩。我每训练 50 个 epoch 就把当前模型接到仿真环境里做一次闭环验证记录 200 步内的最大摆角。具体做法把网络输出乘上force_mag作为推力输入step()推进每步把网络输出的力记录下来。如果最大摆角超过 0.2 弧度说明当前模型策略太软要么是数据不够、要么是困难样本太少。这个闭环验证指标比 loss 曲线靠谱得多。一个容易忽略的细节模型推理频率必须和仿真步进频率一致都是 50Hz。训练时如果每步推理用时超过 2ms后面上真机时控制回路带宽就不够了——这个问题到第5章避坑部分还会单独讲。5. 神经网络倒立摆控制最常翻车的5个坑现象、原因与解决5.1 摆杆持续震荡幅度越来越大停不下来现象仿真里摆杆在 ±0.2 弧度之间来回摆动不倒下但也立不直震荡频率接近控制周期。原因采集数据用的 PD 控制器增益调得太软控制力不足以抑制角速度网络继承了这套“无力策略”。解决把采集用的 PD 增益调硬。起手参数是kp_theta50, kd_theta8先确认专家控制器本身能快速把摆角压到 0.01 弧度以内再开始采集数据。如果 PD 都稳不住采集多少数据都是废的。5.2 训练收敛但仿真里连一秒都撑不住现象loss 降到 0.002 以下看起来很成功把模型放进step循环杆子在 0.5 秒内倒下。原因这是行为克隆最容易踩的坑——推理输入没做和数据采集时相同的标准化。训练代码里对状态做了(s - mean) / std推理时忘了这一步模型输入分布和训练时完全对不上。解决把state_mean、state_std和模型一起保存推理代码里先标准化再入网络。我习惯把标准化逻辑写进模型封装类里外部调用只管传原始状态。5.3 输出推力高频抖动小车在轨道上来回疯狂变向现象摆角控制住了但推力波形是高频锯齿状像白噪声小车在轨道上来回窜。原因状态归一化没做到位。x的数值范围可能是[-3, 3]而theta只有[-0.3, 0.3]网络为了压低大数值维度的误差梯度更新把权重推向一个对位置超敏感的工作点导致输出抖动。解决对所有输入维度做统一的标准化把位置和速度也缩放到和角度同一个量级。这一点在 4.2 的normalize_data里已经做了但在手工构造推理输入时很容易漏掉。5.4 换一台电脑或者换一段随机种子效果完全不一样现象本地训练的网络搬到另一台机器上摆角控制明显变差甚至直接倒下。原因模型的state_mean和state_std没随模型一起迁移。这个坑和 5.2 本质相同但更隐蔽——你重跑了采集数据的随机种子数据分布变了标准化统计量也变了但模型权重还是旧的。解决固定随机种子采集数据np.random.seed(42)并把标准化统计量序列化到模型文件里让网络自带归一化不依赖外部调用。5.5 仿真稳稳当当上真机两秒就倒现象仿真里随便拉初始角度都能立住真机上电之后小车冲向轨道尽头完全没有控制迹象。原因仿真环境里没建模摩擦力、电机响应延迟、传感器噪声真机的状态采样频率往往低于仿真设定——仿真里按 100Hz 采数据训练的模型换到真机只能跑到 50Hz控制回路带宽直接缺了一半。解决真机先用传统 PD 做闭环基准如果 PD 都在真机上稳不住神经网络更稳不住。先用 PD 调通真机再切到网络输出切的时候可以做一个“力度渐变”的过渡force alpha * net_force (1-alpha) * pd_forcealpha 从 0 慢慢调到 1。这五条是从 hwv1 这类常见小车倒立摆上验证过的共性现象。遇到问题不要先怀疑算法按“数据 → 归一化 → 频率 → 物理参数”的顺序排查90% 的问题出在这四个环节里。6. 从训练到部署把神经网络模型接到真小车上的验证闭环行为克隆模型训练完成后真正的考验在部署环节。在 hwv1 这类硬件上单片机或树莓派推理电机驱动加底层电流环我通常分三步走完闭环。第一步是模型导出与推理验证。PyTorch 模型转成 TorchScript 或 ONNX在一个独立的 Python 进程里做 200 次随机状态的推理记录单次推理耗时和输出范围。如果平均推理耗时超过 2ms就要考虑裁剪网络宽度比如把 128 降到 64如果输出饱和在 ±12N 的比例超过 30%说明输入分布和训练时偏差较大先检查归一化统计量是否一致。导出时把state_mean、state_std、force_mag三个数值写进模型文件的元数据里部署代码直接读取不给手写传参留出错空间。第二步是闭环测试矩阵。我不建议直接全状态上手而是按等级递进第一级固定摆角 ±0.05 弧度目标只求不倒第二级摆角 ±0.15 弧度考察鲁棒性第三级加外部扰动推一下摆杆观察恢复到直立的时间。每个等级跑 20 次记录最大摆角、恢复时间、小车越界次数。这个矩阵能帮你快速定位问题出在“控制器策略不行”还是“执行机构响应慢”。第三步是安全措施。所有真机实验先跑在限位轨道内小车两端留足物理限位缓冲力幅值上限设为训练时的 70%——真实电机和仿真理想推力的差距往往就是这一下打折扣换回来的。第一次切网络控制时旁边一定放一个紧急停车按钮代码里也写好保险逻辑if abs(theta) 0.6: force 0先让系统安全停下来再分析原因。这是血泪经验原来总觉得没必要直到亲眼看着小车冲向轨道端头。神经网络倒立摆控制这个方向值得投入的点在于它是验证“数据 → 策略 → 物理系统”闭环成本最低的项目。哪怕最后发现行为克隆的性能打不过调好的 LQR这一趟走下来对状态反馈、时序对齐、仿真到实物的理解远比多调几个 PID 参数值钱。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询