
简介这份资源面向控制理论、机器学习与Python编程的学习者聚焦小车倒立摆这一经典不稳定系统用神经网络作为控制器实现平衡控制。倒立摆涉及动态系统稳定性、反馈控制策略与自适应学习是理论结合实践的典型工程案例。压缩包内共1个文件为单个py脚本整体约2KB代码中应涵盖库导入、系统模型定义、神经网络结构、训练过程与主循环等关键部分便于读者直接阅读与运行。目前已有553人学习下载说明该案例在控制与机器学习入门群体中具有一定参考价值。通过研读这份代码读者可以理解如何用Python搭建倒立摆仿真环境、设计神经网络控制器、调整网络架构与优化器超参数并思考从仿真到真实系统的部署思路适合作为控制工程与神经网络入门的学习素材。1. 从一份只有两个文件的倒立摆工程说起hwv1.zip解压后只有一个hwv1.py没有 requirements、没有 README、没有模型权重这种「裸奔」结构在 GitHub 和各类课程设计资源里非常常见。它要解决的是控制理论里最经典的 benchmark——小车倒立摆一根铰接在小车上的杆天然不稳定必须靠实时调节小车加速度把杆顶住。传统做法是 LQR 或 PID需要先线性化模型、手调 Q/R 权重这份代码换了个思路用神经网络直接学状态到控制量的映射省掉建模和调参的一部分工作量。适合谁看正在做控制课设、想找一个能跑通的 Python 倒立摆最小实现、或者想对比「神经网络控制 vs 传统控制」差异的人。它不依赖 Gym 的复杂环境封装仿真循环和物理积分都写在单个文件里改起来直观。但要注意单文件工程意味着所有逻辑耦合在一起读代码时得先理清主循环的调用顺序否则很容易在训练和仿真之间绕晕。2. 神经网络控制器怎么替掉 LQR状态、动作与损失函数2.1 倒立摆的状态空间与神经网络输入输出定义小车倒立摆的完整状态通常取四个量小车位置x、小车速度x_dot、杆角度theta、杆角速度theta_dot。传统控制里这四个量直接进状态反馈矩阵神经网络控制里它们就是输入层的四个节点。输出层一般是一个连续值代表施加在小车上的水平力F范围常见取[-10, 10]牛顿具体看仿真里设定的质量参数。为什么用神经网络而不是查表因为状态空间是连续的四维空间里离散化格子数量会爆炸。神经网络的好处是函数逼近——给足够多的采样状态和对应的「正确控制量」它能把映射关系拟合出来。这里的关键是「正确控制量」从哪来。常见做法有两种一是用 LQR 或 MPC 先算一批专家轨迹做监督学习二是直接定义奖励函数用策略梯度或进化策略去优化。hwv1.py这种单文件工程大概率走的是第一种因为实现简单、训练稳定不需要处理强化学习里的探索和方差问题。输入归一化是必须做的。角度theta在物理上范围很小通常 ±0.2 弧度以内而位置x可能到 ±2.4 米速度量级也不一样。如果直接喂给网络梯度会被大量级维度主导训练很难收敛。常见做法是对每个维度减均值除标准差或者手动缩放到[-1, 1]。这一步在代码里往往就几行但漏掉的话训练 loss 会卡住不动属于典型的「玄学不收敛」来源。2.2 用 Pygame 搭仿真循环物理积分与渲染分离单文件工程里仿真和渲染通常写在同一个while循环里但逻辑上要分开看。物理更新用欧拉法或 RK4 积分渲染只负责画小车和杆。下面这段是常见骨架参数按典型小车倒立摆取值import numpy as np import pygame # 物理参数 M, m 1.0, 0.1 # 小车质量、杆质量 l 0.5 # 杆半长 g 9.8 # 重力加速度 dt 0.02 # 仿真步长对应 50Hz 控制频率 def dynamics(state, force): x, x_dot, theta, theta_dot state # 标准倒立摆非线性方程此处省略中间推导 # 返回 [x_dot, x_ddot, theta_dot, theta_ddot] ... def step(state, force): # 欧拉积分简单但 dt 大时精度差 dstate dynamics(state, force) return state dt * np.array(dstate) pygame.init() screen pygame.display.set_mode((600, 400)) state np.array([0.0, 0.0, 0.05, 0.0]) # 初始给一点角度扰动 while True: for event in pygame.event.get(): if event.type pygame.QUIT: pygame.quit() sys.exit() force controller(state) # 神经网络前向推理 state step(state, force) # 渲染部分根据 state 画小车和杆 ... pygame.time.delay(int(dt * 1000))逻辑说明dynamics是倒立摆的微分方程输入当前状态和控制力输出状态导数。step做一步积分dt0.02意味着控制频率 50Hz这个值不能太大否则欧拉积分会发散杆会直接飞出去。controller(state)就是神经网络推理入口输入四维状态输出标量力。渲染和物理更新放在同一循环里但渲染不影响状态演化调试时可以先把渲染注释掉只跑物理和控制器看状态是否收敛。参数怎么改M和m影响动力学方程系数改完要重新训练网络否则控制器输出和实际系统不匹配。l是杆的半长不是全长很多开源代码在这里写错导致力矩算错。dt如果调到 0.05欧拉法误差会明显增大建议换 RK4 或者保持 0.02 以下。2.3 训练数据的来源LQR 专家轨迹还是随机采样如果hwv1.py里没有强化学习框架的痕迹那训练数据大概率来自 LQR 或 PD 控制器生成的专家轨迹。流程是先写一个传统控制器在仿真里跑若干回合记录每一步的(state, force)对然后用这些数据监督训练神经网络。这样做的好处是训练目标明确loss 用 MSE 就行收敛快。# 用 LQR 生成专家数据 from scipy.linalg import solve_continuous_are A ... # 线性化后的系统矩阵 B ... # 输入矩阵 Q np.diag([1.0, 1.0, 10.0, 10.0]) # 角度和角速度权重给大 R np.array([[0.1]]) P solve_continuous_are(A, B, Q, R) K np.linalg.inv(R) B.T P # LQR 增益 def lqr_controller(state): return float(-K state) # 采集数据 data [] state np.array([0.0, 0.0, 0.1, 0.0]) for _ in range(5000): force lqr_controller(state) data.append((state.copy(), force)) state step(state, force) if abs(state[0]) 2.4 or abs(state[2]) 0.5: state np.array([0.0, 0.0, np.random.uniform(-0.1, 0.1), 0.0])逻辑说明Q矩阵里角度和角速度的权重给到 10位置和速度给 1是因为倒立摆的首要目标是保持杆不倒位置漂移可以容忍。R越小控制越激进但力太大会导致仿真不稳定。采集数据时每回合结束要重置状态重置时给随机小角度扰动增加数据多样性。5000 步在 50Hz 下对应 100 秒仿真数据量对于四输入单输出的网络足够。注意LQR 基于线性化模型在大角度下专家数据质量会下降。如果训练出的网络在大扰动下表现差可以限制采集时的初始角度范围或者用 MPC 生成更高质量的专家数据但 MPC 实现复杂度高单文件工程里少见。3. 把 hwv1.py 跑起来环境、依赖与主循环拆解3.1 Python 环境与依赖安装的版本坑这类课程设计代码通常写于两三年前依赖版本和现在的最新版可能有冲突。常见依赖是numpy、pygame如果用了 PyTorch 或 TensorFlow 还会有深度学习框架。建议用虚拟环境隔离不要直接装在系统 Python 里。python -m venv venv # Windows venv\Scripts\activate # Linux/macOS source venv/bin/activate pip install numpy pygame # 如果代码里 import torch pip install torch --index-url https://download.pytorch.org/whl/cpu逻辑说明venv创建独立环境避免和系统里其他项目的包版本打架。pygame在 Python 3.11 以上版本安装时可能需要编译工具Windows 上如果报错可以先用pip install pygame --pre装预发布版。PyTorch 的 CPU 版本足够跑这种小网络不需要 CUDA--index-url指定官方源避免从第三方源拉到不匹配的版本。版本坑numpy2.0 之后有些旧 API 被移除比如np.float已经删了。如果hwv1.py里用了np.float会直接报AttributeError。解决办法是降到numpy2.0或者手动把代码里的np.float改成float。这是跑老代码最常见的翻车点先检查一遍 import 和类型转换。3.2 主循环拆解从状态初始化到控制量输出hwv1.py的主循环结构一般是这样初始化 pygame 和网络权重进入while循环每个 tick 里先处理退出事件然后取当前状态送进网络得到控制力再调物理更新最后渲染。读代码时按这个顺序找对应函数不要从头逐行读否则容易被 pygame 的事件处理代码带偏。# 伪代码结构对应 hwv1.py 里的主循环 model load_model() # 加载训练好的权重 state init_state() # 初始状态通常给一个小角度 running True while running: for event in pygame.event.get(): if event.type pygame.QUIT: running False # 关键三步 state_tensor normalize(state) # 归一化 force model(state_tensor) # 网络推理 state step(state, force) # 物理更新 render(screen, state) # 渲染 pygame.time.delay(20) # 控制帧率逻辑说明normalize必须和训练时用的归一化参数一致如果训练时用了均值方差推理时也要用同一组值否则输入分布偏移输出力会完全错误。model如果是 PyTorch 写的记得加torch.no_grad()否则会累积计算图导致内存涨。pygame.time.delay(20)对应 50Hz和dt0.02匹配如果 delay 和 dt 不一致仿真速度和真实时间会对不上但物理正确性不受影响只是看起来快慢不同。如果代码里没有单独的normalize函数归一化可能硬编码在训练脚本里推理时漏掉了。这时候网络输出会异常大或异常小杆会瞬间倒下。排查方法打印网络输入和输出的数值范围和训练数据对比看是否在同一量级。3.3 训练脚本与推理脚本的分离与合并单文件工程可能把训练和推理都塞在hwv1.py里用命令行参数或全局变量切换。常见做法是if __name__ __main__:下面根据sys.argv判断是train还是run。读代码时先找这个入口确定当前默认走的是哪条路径。import sys if __name__ __main__: mode sys.argv[1] if len(sys.argv) 1 else run if mode train: train_model() else: run_simulation()逻辑说明默认走run说明作者预期你已经有训练好的权重。如果直接python hwv1.py报错找不到权重文件就改成python hwv1.py train先训练。训练时间取决于网络大小和数据量四输入单输出的小网络几千条数据CPU 上几分钟就能跑完。如果训练 loss 不下降先检查数据归一化和标签范围再看学习率是不是太大。注意有些代码把权重保存成.pth或.h5但没在 README 里写清楚路径。如果报FileNotFoundError在代码里搜torch.save或model.save找到保存路径确认文件是否存在。不存在就重新训练不要从网上随便下权重结构不匹配会报 shape 错误。4. 避坑与排查倒立摆神经网络控制里最容易翻车的五件事4.1 现象仿真一开始杆就飞出去角度瞬间超过 90 度原因控制力方向反了。神经网络输出的力符号和物理方程里的符号约定不一致或者 LQR 专家数据生成时K的符号搞错。倒立摆的力方向取决于角度定义theta从竖直向上算还是从竖直向下算会导致控制力符号完全相反。解决先用一个简单的 PD 控制器测试物理仿真是否正确。PD 控制器输出force kp * theta kd * theta_dot如果杆能短暂立住再倒说明物理没问题是网络或数据的问题。然后检查训练数据里force和theta的相关性正常情况应该是正相关角度偏正力也偏正去拉回来如果负相关说明符号反了在推理时加个负号或者重新生成数据。4.2 现象训练 loss 降到很低但仿真里杆还是倒原因训练数据和仿真初始状态分布不匹配。训练时用的初始角度范围是 ±0.05 弧度仿真时初始角度给了 0.2 弧度网络没见过这么大的偏差输出力不够。这是监督学习控制里的经典问题——分布偏移。解决扩大训练数据的初始状态范围覆盖仿真时可能出现的所有状态。或者在仿真里加一个 fallback 控制器当角度超过训练范围时切回 PD 控制。更彻底的做法是用 DAgger 思路用当前网络跑仿真收集失败状态让专家标注后再训练迭代几轮。单文件工程里一般不做这么复杂手动扩大数据范围就够了。4.3 现象pygame窗口卡死或无响应原因主循环里没有处理pygame.event.get()或者pygame.time.delay时间太长导致事件队列堆积。Windows 上 pygame 窗口如果长时间不响应事件系统会标记为无响应。解决确保每个循环都调用pygame.event.get()即使不处理具体事件也要清空队列。delay不要超过 50ms50Hz 仿真用 20ms 合适。如果渲染很慢可以把渲染频率降到物理更新频率的一半比如物理每步都更新渲染每两步画一次。4.4 现象numpy报VisibleDeprecationWarning或AttributeError原因代码写于旧版本 numpy用了已废弃的 API。常见的有np.float、np.int、np.bool以及np.array的ragged序列创建方式。解决全局搜索np.float、np.int、np.bool替换成 Python 内置的float、int、bool。如果报setting an array element with a sequence检查是不是把不同长度的列表传给了np.array加dtypeobject或者统一长度。最省事的办法是pip install numpy2.0但长期看还是改代码更稳妥。4.5 现象网络输出恒定值不随状态变化原因输入归一化参数错误导致所有输入被压到接近零网络相当于只看到偏置。或者网络权重初始化后没有训练直接推理。解决打印归一化后的输入确认四个维度的值在[-1, 1]附近且有变化。如果全是零检查归一化里的除标准差步骤标准差可能算成了零。如果输入正常但输出恒定检查权重是否加载成功打印第一层权重的范数如果全是零或 NaN说明训练出了问题或者权重文件损坏。5. 从仿真到实车控制频率、状态观测与安全兜底仿真跑通之后如果想往真实小车上搬最先遇到的不是算法问题而是状态观测。仿真里theta和theta_dot直接能从状态向量里取实车上得靠编码器或 IMU 测角度再差分或滤波得角速度。差分噪声大常见做法是加低通滤波或者用卡尔曼滤波估计。控制频率方面仿真里 50Hz 够用实车上建议提到 100Hz 以上因为真实系统的延迟和扰动比仿真大控制周期太长会导致相位滞后杆还没拉回来就已经倒了。另一个关键是安全兜底。神经网络控制器在训练分布内表现好但遇到没见过的状态比如被人推了一把、地面不平可能输出离谱的力。实车上必须加一层保护角度超过阈值直接切电力超过上限直接截断。我一般会在控制输出后面串一个饱和函数和看门狗饱和函数限制最大力看门狗检测状态是否发散发散就停机。这些在仿真里看不出差别但实车上能避免烧电机。验证方法上可以先把训练好的网络在仿真里跑一批随机初始状态统计成功率和平均存活时间。成功率高不代表实车一定行但成功率低实车肯定不行。另一个技巧是对比网络输出和 LQR 输出的差异在状态空间里采样一批点看两者的力差多少。如果差异集中在某些区域说明网络在那片区域学得不好可以针对性补数据。从那以后我每次把仿真控制器往实车搬都强制先跑一遍饱和和看门狗测试确认异常状态下能安全停机再上电。希望帮到你。本文还有配套的精品资源点击获取