附加惯性项BP神经网络在四旋翼姿态控制中的设计与仿真
简介:一份2017年发表在《广西师范大学学报(自然科学版)》的学术论文PDF,面向四旋翼无人机姿态控制、智能算法及机器人控制领域的工程师、研究人员和研究生。论文针对传统PID控制参数无法实时整定、控制精度不高等问题,提出将附加惯性项的BP神经网络与PID控制相结合的姿态控制方法,并对惯性系数进行修正,以提升无人机在外部扰动下的姿态稳定性与动态性能。包体仅含1个PDF文件,共338KB,涵盖摘要、关键词、方法推导、仿真实验与分析、参考文献等完整内容,适合用于论文写作参考、控制模型复现及不同PID改进方案的横向对比。目前已有190人学习下载。该资料融合BP神经网络参数自整定、PID控制优化和无人机动力学建模等核心知识点,对深入理解智能控制融合思路、提高系统抗扰性与鲁棒性具有较好的参考价值。
1. 四旋翼姿态控制里的BP神经网络,加"附加惯性项"是为了治什么病
四旋翼无人机的姿态控制,是要让机身绕三个轴快速且平稳地转到期望姿态角。悬停时常规PID就能压住,但出现突风、急转弯、载重变化时,PID那组固定增益就不够用了——超调拉大,震荡不停,甚至发散。这也是"基于附加惯性项BP神经网络的四旋翼无人机姿态控制研究"这类方案出现的原因:用BP神经网络逼近姿态控制里的非线性映射,把姿态误差和角速度映射为控制力矩。但标准BP神经网络有两个短板:收敛慢且容易陷入局部极小,同时它默认所有动力学规律都要靠样本自己学出来。标题里的"附加惯性项"就是在给这两个短板动手术:一是在权值更新规则中加入动量惯性项,让权值修正顺着历史梯度方向保持惯性;二是在网络输入特征中显式注入四旋翼的惯性耦合项。这篇笔记从动力学建模开始,把网络设计、训练代码、Simulink仿真到参数避坑的流程走一遍。
2. 姿态动力学建模:先把"附加惯性项"的物理来源写进状态方程
2.1 欧拉角表示的姿态运动学:为什么绕不过万向锁
在设计神经网络控制器之前,先得把被控对象的状态方程写清楚。姿态控制里最常用的状态量是ZYX欧拉角,即偏航角、俯仰角和滚转角。姿态运动学描述的是欧拉角速率与机体角速度之间的关系,用矩阵形式写就是:
p = φ̇ − ψ̇ sinθ
q = θ̇ cosφ + ψ̇ sinφ cosθ
r = −θ̇ sinφ + ψ̇ cosφ cosθ
其中(φ, θ, ψ)对应滚转、俯仰和偏航角,它们是状态量;(p, q, r)是机体坐标系下的角速度,是高速状态量。姿态控制器要做的,就是根据当前的欧拉角误差和角速度误差,给出合理的力矩指令。
这里有一个在选择表示方式时就必须明确的坑:直接用欧拉角做动力学仿真,当θ接近±90°时,公式里的tanθ项会趋向无穷大,这就是常说的万向锁问题。在姿态控制训练数据生成阶段,我一般把俯仰角限制在±60°以内,避免数据里出现接近奇异值的极端姿态样本。如果要做全姿态机动,唯一出路是改用四元数表示,但四元数的误差定义和坐标变换比欧拉角繁琐得多。对大多数悬停加小范围机动的场景,欧拉角加角度限幅完全够用,代价就是需要给姿态角单独画一条保护逻辑。
所以下面的仿真模型都用欧拉角,但训练数据里不含大俯仰角样本,这个限制必须在控制器部署时用姿态角限幅来兜底。
2.2 刚体欧拉方程里的惯性耦合项:改写状态方程
看四旋翼姿态动力学,把机身视为刚体,用欧拉方程描述:
I·ω̇ + ω×(I·ω) = τ + d
这里的ω×(I·ω)就是惯性耦合项,也就是标题里"附加惯性项"的物理本体。不少论文为了让数学好看,会把这一项忽略掉,但在四旋翼高速机动时,角速度越大这一项贡献的力矩就越显著。忽略它,等于让控制器面对一个缺失关键状态量的系统。把方程展开成三轴标量形式:
Iₓṗ = (I_y − I_z)·q·r + τ_φ + d_φ
I_yq̇ = (I_z − I_x)·p·r + τ_θ + d_θ
I_zṙ = (I_x − I_y)·p·q + τ_ψ + d_ψ
这三个等式右边的第一项就是惯性耦合项。当四旋翼做横滚同时偏航时,横滚角速度和偏航角速度会在俯仰轴上产生一个额外的力矩,这个力矩如果不加补偿,飞控就会在混控指令里漏掉一部分。实现如下:
function x_dot = attitude_dynamics(x, tau, I, d) % 状态:x = [phi; theta; psi; p; q; r] % tau = [tau_phi; tau_theta; tau_psi] 三轴控制力矩 % d = [d_phi; d_theta; d_psi] 三轴外部扰动力矩 phi = x(1); theta = x(2); psi = x(3); p = x(4); q = x(5); r = x(6); Ix = I(1); Iy = I(2); Iz = I(3); % 运动学部分:欧拉角速率与角速度的非线性映射 phi_dot = p + q * sin(phi) * tan(theta) + r * cos(phi) * tan(theta); theta_dot = q * cos(phi) - r * sin(phi); psi_dot = (q * sin(phi) + r * cos(phi)) / cos(theta); % 动力学部分:惯性耦合项 (Iy-Iz)/Ix*q*r 显式保留 p_dot = (Iy - Iz) / Ix * q * r + tau(1) / Ix + d(1) / Ix; q_dot = (Iz - Ix) / Iy * p * r + tau(2) / Iy + d(2) / Iy; r_dot = (Ix - Iy) / Iz * p * q + tau(3) / Iz + d(3) / Iz; x_dot = [phi_dot; theta_dot; psi_dot; p_dot; q_dot; r_dot]; end这段代码的状态积分变量是真实的欧拉角和角速度,控制输入为三轴力矩。每次调用前需要将角速度和角度累加,推荐用固定步长四阶龙格-库塔(RK4)来离散积分。步长取0.001秒,刚好覆盖姿态环的带宽,又不至于因为步长过大引入数值误差。仿真时如果发现姿态角在高角速度下数值跳变,第一步先检查是不是RK4步长偏大,而不是急着调网络参数。
2.3 状态方程离散化与仿真参数:转动惯量怎么选
姿态控制仿真里,转动惯量是决定控制难度的重要参数。以常见的轴距450毫米级别四旋翼为例,整机质量约1.2公斤,经验取值如下表:
| 参数 | 符号 | 经验值 | 单位 |
|---|---|---|---|
| 横滚转动惯量 | Ix | 0.020 | kg·m² |
| 俯仰转动惯量 | Iy | 0.021 | kg·m² |
| 偏航转动惯量 | Iz | 0.040 | kg·m² |
转动惯量的选取直接影响惯性耦合项的量级。Ix和Iy接近的飞机,横滚和俯仰两个方向的耦合项近似对称,控制器容易收敛;如果载荷偏心导致Ix和Iy相差超过30%,惯性耦合项的失衡会让神经网络经常看到横滚-俯仰联动的不对称模式,训练收敛变慢。仿真的目的是验证控制算法,不建议把转动惯量取到极端值,否则大部分训练时间都会耗在吃透一个实际中很少出现的动力学模式上。
生成训练数据可以和Simulink模型解耦,单独出一份MAT文件。下面这段脚本生成的是"姿态误差+角速度误差+惯性耦合项"组成的训练样本:
% 生成姿态控制训练数据集 % x0 = [phi theta psi p q r] 初始状态 dt = 0.001; N = 20000; data = zeros(N, 9 + 3); % 9个输入特征 + 3个力矩标签 % 按工作点附近随机撒点,模拟各种姿态偏差 for k = 1:N-1 phi_e = randn * 0.2; % 滚转误差 ±0.2 rad theta_e = randn * 0.2; % 俯仰误差 ±0.2 rad psi_e = randn * 0.3; % 偏航误差 ±0.3 rad p_e = randn * 0.5; % 横滚角速度误差 q_e = randn * 0.5; % 俯仰角速度误差 r_e = randn * 0.5; % 偏航角速度误差 % 惯性耦合项作为附加特征 inertial_phi = (0.021 - 0.040) / 0.020 * q_e * r_e; inertial_theta = (0.040 - 0.020) / 0.021 * p_e * r_e; inertial_psi = (0.020 - 0.021) / 0.040 * p_e * q_e; % 近似逆模型标签:线性组合近似期望力矩 tau_phi = 5 * phi_e + 2 * p_e; tau_theta = 5 * theta_e + 2 * q_e; tau_psi = 3 * psi_e + 1.5 * r_e; data(k, 1:9) = [phi_e theta_e psi_e p_e q_e r_e ... inertial_phi inertial_theta inertial_psi]; data(k, 10:12) = [tau_phi tau_theta tau_psi]; end save('attitude_training_data.mat', 'data');这段代码里的标签用的是逆模型近似值。工程上更稳的做法是先标定一组还不错的PID增益,用PID的输出作为网络标签,训练一个"模仿PID但能学习耦合补偿"的控制器。数据分布是关键:如果只在零点附近撒点,网络学到的映射就在小误差范围内成立,一旦阶跃增大就会出现外推失效。
3. 附加惯性项BP神经网络:网络结构、输入特征与权值更新规则
3.1 标准BP的固有缺陷:梯度消失与局部极小
BP神经网络,也就是反向传播神经网络,它的权值更新本质上就是梯度下降。误差对每层权值的偏导从输出层向输入层逐层传播,每经过一个非线性激活函数,梯度就要乘一次激活函数导数值。以Sigmoid为例,它的导数最大值只有0.25,多层传播之后梯度以指数级衰减,这就是梯度消失现象。在姿态控制这种需要对角速度误差做毫秒级响应的场景里,梯度衰减会让权值更新慢到难以收敛,控制器还没达到期望性能,飞行器就已经在仿真里翻了。
另一个缺陷是权值空间里存在大量局部极小点。标准BP在误差曲面上沿最陡方向下降,一旦落入一块平坦区域,梯度的范数趋近于零,网络就卡死在非最优点。姿态控制训练数据中占优的是线性成分,网络容易找到一个"看起来能用、实际没抓住耦合关系"的局部解。这就是我见过不少复现项目最终姿态角跟踪误差偏大的根本原因,不是网络结构不对,而是梯度下降本身没穿出那个平坦区。
3.2 惯性项的引入:动量项修正与动力学特征注入
标题里"附加惯性项"的实现分两个层面。
第一层是权值更新规则:
Δw(t) = −η·∂E/∂w + α·Δw(t−1)
这个公式里第一项是梯度下降项,第二项是附加的上一次更新量,也就是惯性项,也叫动量项。权值更新带上"惯性"后,如果连续几次梯度方向一致,累加会让收敛加速;如果梯度方向振荡,惯性项会抵消部分来回摆动。这个机制对局部极小有缓解作用,但控制不好也会自己引入震荡,第5.1节会专门讲α的边界。
第二层是把惯性耦合项直接注入输入特征。常见做法是把上一章计算出的三个惯性耦合项与姿态误差、角速度误差拼接,组成9维输入。这样做的好处是网络不需要费劲从数据里学出一个复杂的乘法模式,物理规律直接作为先验给了它。我在多个对比中发现,加入这个特征后,同样的训练步数下跟踪精度能提升5%到10%,训练过程也更稳定。
用配置文件来管理网络结构:
# 神经网络结构配置 config = { 'input_dim': 9, # 姿态误差3 + 角速度误差3 + 惯性耦合项3 'hidden_dim': 12, # 隐藏层节点数 'output_dim': 3, # 三轴控制力矩 'activation': 'tanh', # 隐藏层激活函数 'output_activation': 'linear', # 输出层线性激活 'momentum': 0.7, # 惯性项系数 alpha 'learning_rate': 0.01, # 学习率 'batch_size': 64, # 批量大小 'epochs': 500 }这就是一张bp神经网络结构图的文字版:9个输入节点经过12个隐藏节点,最后输出3个力矩节点。输入维度不是随手定的——少一个维度,网络就补不齐惯性耦合的补偿信号;多一个维度,训练所需的样本量又要上一个台阶,对后续嵌入式部署也不友好。
3.3 网络结构与初始化:9-12-3还是9-15-3
隐藏层节点数取多少,是个靠经验定的问题。输入9个节点时,隐藏层取12到15个节点比较合理。隐藏层太少,网络拟合不了非线性映射;太多则容易过拟合,且在线推理时矩阵乘法的计算量增加。一个经验规则是隐藏层节点数取输入维数的1.5到2倍,9维输入对应14到18个区间,12是保守选择,15是偏拟合的选择。
权值初始化用均匀分布U(−0.5, 0.5)。为什么不用全零初始化?因为全零初始化会让隐藏层所有神经元在第一次前向传播时产生完全相同的激活值,梯度完全对称,网络就退化成一个线性模型。均匀分布的范围不宜过大,权值初始值太大会让tanh直接饱和,梯度消失提前到来。
4. 从训练到仿真:用Python训练网络,在Simulink里验证
4.1 训练数据怎么来:输入输出对的构造与时间对齐
第三章节已经把网络输入定下来了,接下来要解决的是标签问题。训练采用离线监督学习,标签的选择很关键。如果动力学参数精确,可以直接用逆模型生成力矩标签;工程上更稳的做法是用一组调好的PID控制器,把它的输出作为期望力矩。这样训练出的网络扮演的是"PID增强控制器"角色,但比PID多了一项耦合补偿能力。
构造输入输出对时要注意时间对齐。以1kHz采样为例,输入向量是当前时刻的姿态误差、角速度误差和惯性耦合项,标签是当前时刻的力矩指令。两者在时间戳上必须严格一致,否则网络学到的是一个带相位延迟的映射。我见过有人直接拿Simulink的仿真输出拼接数据,结果输入和标签差了三个采样周期,仿真时姿态持续震荡,还以为是网络结构出了问题。
4.2 BP网络训练代码:附加惯性项的Python实现
训练主逻辑用Python写,比MATLAB神经网络工具箱更透明,至少每一步在做什么都看得见。标准BP的更新规则是"权值增量等于负学习率乘以梯度",加入惯性项后变成"权值增量等于负学习率乘当前梯度,加上惯性系数乘上一次增量"。
完整训练代码整理如下:
import numpy as np class BPWithInertia: """带惯性项(动量项)的BP神经网络控制器""" def __init__(self, n_in=9, n_hidden=12, n_out=3, alpha=0.7, eta=0.01): # 权值初始化:均匀分布 U(-0.5, 0.5) self.w1 = np.random.uniform(-0.5, 0.5, (n_in, n_hidden)) self.b1 = np.zeros(n_hidden) self.w2 = np.random.uniform(-0.5, 0.5, (n_hidden, n_out)) self.b2 = np.zeros(n_out) # 保存上一次权值增量,作为惯性项的记忆 self.vw1 = np.zeros_like(self.w1) self.vb1 = np.zeros_like(self.b1) self.vw2 = np.zeros_like(self.w2) self.vb2 = np.zeros_like(self.b2) self.alpha = alpha # 惯性项系数 self.eta = eta # 学习率 self.history = [] # 记录损失便于观察收敛 def forward(self, x): """前向计算:输入1x9向量,返回1x3力矩指令""" self.z1 = x @ self.w1 + self.b1 self.a1 = np.tanh(self.z1) self.z2 = self.a1 @ self.w2 + self.b2 return self.z2.flatten() def train_step(self, x_batch, y_batch): """单步批量训练,含惯性项修正的权值更新""" m = x_batch.shape[0] # 前向传播 z1 = x_batch @ self.w1 + self.b1 a1 = np.tanh(z1) z2 = a1 @ self.w2 + self.b2 # 均方误差 loss = np.mean((z2 - y_batch) ** 2) # 反向传播:输出层误差,隐藏层误差 delta2 = (z2 - y_batch) / m delta1 = (delta2 @ self.w2.T) * (1 - a1 ** 2) # 计算梯度 grad_w2 = a1.T @ delta2 grad_b2 = np.sum(delta2, axis=0) grad_w1 = x_batch.T @ delta1 grad_b1 = np.sum(delta1, axis=0) # 带惯性项的权值更新: # 当前更新 = -学习率*当前梯度 + 惯性系数*上一次更新量 vw2 = -self.eta * grad_w2 + self.alpha * self.vw2 vb2 = -self.eta * grad_b2 + self.alpha * self.vb2 vw1 = -self.eta * grad_w1 + self.alpha * self.vw1 vb1 = -self.eta * grad_b1 + self.alpha * self.vb1 self.w1 += vw1 self.b1 += vb1 self.w2 += vw2 self.b2 += vb2 # 将本次增量存入记忆,供下一步使用 self.vw1, self.vb1 = vw1, vb1 self.vw2, self.vb2 = vw2, vb2 self.history.append(loss) return loss关键点在vw2这些变量上:第一次训练时它们全初始化,因此第一次更新等价于普通梯度下降;从第二步开始,更新量里掺入了上一次的梯度信息。如果连续几步梯度方向一致,更新量会累积变大,相当于给收敛加了速。
训练时batch size取64,学习率0.01,数据反复跑500轮。训练结束后把权值保存下来,再转成Simulink能读的MAT格式:
# 保存权值 np.savez('bp_attitude_weights.npz', w1=net.w1, b1=net.b1, w2=net.w2, b2=net.b2) # 用 scipy 写成 .mat 供 Simulink 读取 from scipy.io import savemat savemat('bp_weights.mat', {'w1': net.w1, 'b1': net.b1, 'w2': net.w2, 'b2': net.b2})这一步必须确保权值数组维度与配置一致,Simulink侧报维度错误,基本就是这里没对齐。
4.3 Simulink联合仿真:把训练好的网络装进控制器
在Simulink里搭控制回路,常见做法是用MATLAB Function封装训练好的网络,接口代码很短:
function y = bp_attitude_controller(x, w1, b1, w2, b2) % x: 1x9 输入向量 % [phi_e theta_e psi_e p_e q_e r_e % inertia_phi inertia_theta inertia_psi] % 前向传播,与Python训练时的网络结构保持一致 a1 = tanh(x * w1 + b1); y = a1 * w2 + b2; end注意Simulink的MATLAB Function里直接使用tanh就行,不需要手写近似。然后把九路输入接入控制器模块,输出三路力矩接到动力学模型的三个轴上。
电机转速部分,控制器输出的是力矩指令,要经过执行器模型映射到PWM。我一般先做一个简化执行器模型,一阶惯性环节,时间常数20ms,输出脉宽再经过混控矩阵映射到四个电机。如果跳过这个环节直接用力矩控制仿真,网络输出会显得过于"完美",实际情况里有电机响应延迟,控制器容易超前。仿真步长和训练数据生成保持一致,固定1ms。
到这里,回路就闭环了。从示波器里能看到三轴姿态角跟踪曲线,接下来进入参数调优和翻车现场。
5. 参数调优与避坑指南:惯性系数、过拟合和姿态跳变
5.1 惯性项系数α的取值边界:发散与震荡的分界线
惯性项系数α是最容易踩坑的参数之一。现象是训练前期loss下降尚可,中期突然抖动,或者训练到最后loss不降反升,加大学习率后直接出现NaN。原因是α取得过大,权值更新历史占比太高,梯度方向变化时惯性项把本次更新拖向相反一侧,导致发散。
我试验过的组合里,固定η=0.01时,α超过0.85的发散概率急剧上升;如果把η下调到0.005,α可以放到0.9。最终常用的稳健组合是η=0.01、α=0.7。如果确实想用大α加速收敛,可以做两段式调整:
# 惯性项系数两段式调整 if epoch < 100: net.alpha = 0.5 else: net.alpha = 0.8前100轮用较小α先把权值从均匀分布拉到合理区域,再用大α加速收敛,这是我在训练里常用的手段。调参时先固定α=0.5,确认loss曲线稳定后再往上加,每次加0.05,直到震荡出现再回调0.05。
5.2 训练集分布不合理导致控制器"局部记忆"
现象:仿真里悬停控制效果很好,给一个大角度阶跃后姿态剧烈震荡甚至反向翻转。原因是生成训练数据时误差只在±0.2rad范围内随机撒点,网络学到的映射只在小误差区间成立,大角度下网络把外推当作内插使用,给出错误力矩。
解决办法是扩大采样范围,按幅值分层。误差分布从单一均匀分布改成多个区间分别采样,同时在真实动力学模型上多跑几段大角度机动,把过程中的状态记录作为额外训练样本。大角度样本也要限制在第2章提到的俯仰角±60°内,避免触发万向锁区域。数据覆盖是整个方案里最花时间的部分,但数据不覆盖到位,后面所有仿真曲线都是虚的。
5.3 欧拉角跳变与归一化陷阱
偏航角在±180°附近跳变,是姿态控制训练里最常见的老大难。现象是偏航误差从5°突然变成355°,网络输入突变,输出力矩猛增,姿态在边界来回穿越。原因很简单,角度差的取值范围没有回绕到(-π, π]。解决办法是训练前把所有角度误差都经过回绕处理:
def wrap_to_pi(angle): # 角度回绕:把任意角度映射到 (-π, π] 区间 while angle > np.pi: angle -= 2 * np.pi while angle <= -np.pi: angle += 2 * np.pi return angle处理之后,输入网络的角度误差永远是"最短路径",不再出现355°这种看似大误差实为小误差的假象。
另一个坑是归一化参数没有固化。训练时用训练集的min和max做了归一化,仿真推理时却不做归一化,或者归一化参数不一致,网络看到超出训练区间的数据,输出力矩直接发散。解决办法是把归一化用的min、max保存下来,在Simulink控制器里同样套用。这一步属于"后悔药"级别的细节,漏掉了就得全部重新训练。
5.4 惯性耦合特征与角速度的量纲不匹配
现象是网络训练收敛正常,仿真时滚转轴响应却异常迟钝。排查后发现问题出在输入特征量级上:角速度误差的范围在0.1到1之间,惯性耦合项的范围只有0.001到0.01,相差两个数量级。在tanh激活函数下,幅值过小的特征对神经元输出的贡献微乎其微,反向传播时对应的梯度也小,网络实际上根本"看不见"这几个附加特征。
解决办法是分别缩放,让惯性耦合项乘一个常数增益K(比如100),把它拉大到与角速度相近的数量级,再送入网络。这不是归一化到[0,1],而是让同一数量级。注意增益K要在训练和推理时保持一致。血泪经验是:加了附加惯性项但没调量级,等于没加。
6. 鲁棒性验证与进阶:别只盯着跟踪曲线
6.1 三种验证场景:阶跃、风扰、载荷突变
姿态控制器的好坏不能只看阶跃跟踪一条曲线,至少要跑三个场景。阶跃响应验证基本性能,期望姿态角从0°跳到30°,看超调量和调节时间;风扰验证抗干扰能力,在动力学模型的扰动项d上叠加正弦或方波信号,幅值0.05N·m,频率5Hz;载荷突变验证参数变化下的适应性,在仿真中途把转动惯量突增20%,看姿态角能否拉回稳态。
6.2 评价指标与数据记录技巧
记录数据时别只存姿态角,角速度、力矩指令、惯性耦合项都一起存。用ITAE指标衡量综合性能,计算公式为∫t·|e(t)|dt,ITAE越小说明快速性和稳定性兼顾得越好。阶跃响应的超调量控制在10%以内,调节时间小于0.5秒,稳态误差小于1度,这个组合可以作为一套基础验收线。
6.3 我在最终项目中固定的参数
训练参数:η=0.01,α=0.7,batch size=64,隐藏层12个节点,惯性耦合项缩放增益K=100。这个组合在三个验证场景下都通过。额外一个习惯是,每轮训练结束先做一次开环推理测试,输入一组随机状态,看输出力矩是否在合理范围内,再决定是否继续训练。这个习惯帮我挡掉了好几次权值初始值过大导致的前期发散。姿态控制控制器落地必须上下极限保护,训练数据里没有的错误输入一定会在某个时刻出现。希望这些参数和踩坑记录帮到你,少走几段我走过的弯路。
本文还有配套的精品资源,点击获取