资讯动态

基于BP神经网络与S函数的PID参数在线自整定Simulink实现

发布时间:2026/10/16 6:53:42 来源:尧图企业网站定制
做过程控制仿真的朋友应该都有过这种体验对象只要沾上一点纯滞后、一点饱和非线性固定参数的PID就变得特别难伺候。Kp调上去响应快了超调却压不住Kd增强一档噪声又跟着放大。我在做一个带滞后环节的温控方案对比时也被这个经典问题卡了两三天最后决定不折腾手调参数了直接把BP神经网络和PID算法在Simulink里用S函数做成一个自适应控制器让网络在线调整Kp、Ki、Kd。这个方案的思路并不复杂PID还是那个PID只是它的三个参数不再固定而是由一个三层的BP神经网络根据当前系统状态实时输出。相比直接用神经网络去替代控制器这种“神经网络在线整定PID参数”保留了PID的工程语义超调、稳态误差、调节时间这些指标依然能用PID语言去分析网络本身只负责“参数该怎么变”。经过一段时间的学习系统在非线性、大滞后对象上的表现确实比固定参数PID好不少。下面我把整个项目从原理推导、S函数代码、Simulink模型搭建到调参经验完整写一遍。这篇文章适合正在做智能控制方向课题的同学也适合想把手上的固定PID控制器升级成自适应控制器的工程师。代码可以直接复制到MATLAB里跑通然后你再根据自己被控对象去改参数范围和采样周期。1. 传统PID调不动的那类对象正好是BP-PID的用武之地1.1 固定参数PID的软肋线性假设与参数折中标准PID控制器的输出由三项线性组合构成比例项看当前误差积分项累积历史误差微分项预测误差变化趋势。这三个系数一旦定死整条控制链路实际上假设了被控对象在工作点附近可以用线性模型近似而且这个近似关系在整个运行过程中保持不变。但实际对象往往不配合这个假设。纯滞后会推迟控制器对误差的反应饱和环节限制了执行机构输出死区让小误差区间里控制量“白输出”再加上负载变化带来的参数漂移固定PID本质上是在几个性能指标之间做折中你想要快速性超调就冒出来你想压超调调节时间就拉长。对一个线性定常对象折中一次就够了对非线性、时变对象你前一天调好的参数第二天工况一变可能就不成立了。这时候最直观的念头是让参数跟着工况走。关键是怎么走、往哪走、走多少传统的增益调度需要你事先知道扰动模式而BP神经网络的意义在于它可以通过在线学习自己找出“什么工况对应什么参数”。1.2 神经网络在方案中的角色是“在线整定器”不是黑箱控制器很多初学者一听BP神经网络控制第一反应是用网络直接拟合被控对象的逆模型替代PID。这个路子能做但工程风险大逆模型不准确时控制器输出没有兜底结构系统容易失稳。BP-PID方案的设计思路要克制得多。神经网络输出层只有三个节点分别映射到Kp、Ki、KdPID算法本身照常运行。也就是说网络在每一个采样周期根据系统的状态特征输出一组合适的PID参数控制器结构、可解释性、稳定性分析框架都保留了。这种设计还有一个好处调试起来梯度更清楚。响应超调了你看是Kp太大还是Kd不足稳态误差消不掉你看Ki有没有被网络压到下限。你不会面对一个完全无法解释的黑箱网络输出的三个数本身就是PID参数你可以把它接进示波器观察变化轨迹。1.3 为什么控制器模块要用S函数落地而不是MATLAB Function用Simulink做这种控制算法可以用MATLAB Function模块也可以用S函数。两者都能完成数值计算但S函数更像是“一个真正的控制器模块”它的初始化、输出、离散状态更新被拆成了标准回调后续想换成C MEX S函数往嵌入式平台移植代码骨架不用重新设计。另外S函数可以接收向量输入、输出多个信号方便同时输出控制量和Kp、Ki、Kd三个参数的实时值这对观察网络学习过程非常重要。用普通MATLAB Function也能写但一旦网络规模变大、状态变量变多S函数的分段结构会让代码可维护性强很多。2. S函数骨架、端口设计与仿真采样设置2.1 flag分发结构本质就是一组回调事件S函数在Simulink里是个特殊的函数文件Simulink引擎会在不同阶段调用它并通过flag告诉它“现在该干什么”flag0是系统初始化flag3是计算输出flag2是更新离散状态其余flag按默认处理。控制器实现主要用到flag0和flag3。主函数的标准骨架如下function [sys,x0,str,ts] BPPIDController(t,x,u,flag) switch flag case 0 [sys,x0,str,ts] mdlInitializeSizes(); resetNetwork(); case 3 sys mdlOutputs(t,x,u); case {1,2,4,9} sys []; otherwise error([Unhandled flag , num2str(flag)]); end这里我把神经网络权重和历史的误差、控制量都放在persistent变量里因此在初始化分支里调用了resetNetwork()保证每次点击运行仿真时网络权重都被重新初始化。后面的代码会具体展开。2.2 控制器模块的输入输出端口两个输入比三个输入更省心S函数的输入输出个数在mdlInitializeSizes里声明。这个控制器的端口设计为输入1期望值r输入2反馈值y输出1控制量u输出2Kp实时值输出3Ki实时值输出4Kd实时值。误差e在S函数内部计算不用在模型外面额外做一次减法这样Simulink模型里的连线更干净也不容易搞混信号顺序。把Kp、Ki、Kd作为额外输出是为了方便把参数接到Scope里实时观察这是调网络参数时非常关键的一路信号。由于输出直接依赖于当前输入注意初始化中DirFeedthrough必须设成1。如果漏掉这一点仿真时模块读不到当前采样时刻的r和y输出会莫名其妙地滞后一拍控制性能完全乱套。2.3 采样周期与被控对象模型的匹配控制器是离散算法S函数里通过ts[0.01 0]声明采样周期0.01秒。这个0.01不是随便拍的它要和被控对象的动态特性匹配。我下面实验对象用的是一个带饱和和纯滞后的一阶惯性系统时间常数大约2秒纯滞后0.5秒采样周期取0.01秒已经足够密。仿真求解器建议设置成固定步长步长0.01秒和S函数的采样周期一致。用变步长虽然也能跑但离散模块和Transport Delay在变步长下的行为会多一层不确定性固定步长更容易复现结果。被控对象的传递函数模型我设计为G(s) 1 / (10s 1)前面串联一个上下限为±5的饱和模块后面串联0.5秒的Transport Delay。这个组合让对象同时具备饱和非线性和纯滞后是固定PID最头疼的场景之一。3. 参数在线整定的数学推导与几个容易搞错的符号3.1 网络拓扑与输入特征选择BP网络结构是三层输入层3个节点分别为r、y、e隐含层6个节点激活函数用tanh输出层3个节点分别映射Kp、Ki、Kd。输入特征选r、y、e而不是只喂误差是因为后台系统在不同期望值下需要的PID参数不一样同一个误差在目标值1和目标值5时的含义完全不同把r和y一起喂进去网络才能学会“工作点不同参数也不同”。输出层如果直接用线性输出网络训练过程中可能把Kp、Ki、Kd更新成负值导致控制器方向错误。我这里把输出层三个节点经过sigmoid映射到有界区间例如Kp范围[0, 3]Ki范围[0, 0.5]Kd范围[0, 0.5]。这样既保证了PID参数始终为正又限制了它们不会跑到荒唐的数量级对在线学习的稳定性非常关键。3.2 代价函数到输出层梯度链式规则功不可没BP-PID在线整定的核心依然是最小化误差平方E(k) 1/2 * e(k)^2控制器输出采用增量式PIDu(k) u(k-1) Kp * [e(k)-e(k-1)] Ki * e(k) Kd * [e(k)-2e(k-1)e(k-2)]对网络输出层的某个节点我们想要求出它对应的权重梯度需要把E对网络净输入的导数展开。以Kp对应的输出节点为例dE/dnet_o1 (dE/du) * (du/dKp) * (dKp/dnet_o1)其中du/dKp e(k) - e(k-1)du/dKi e(k)du/dKd e(k) - 2e(k-1) e(k-2)dKp/dnet_o1 是sigmoid映射函数的导数也就是(Kp_max - Kp_min) * sigmoid(net_o1) * (1 - sigmoid(net_o1))。对Ki、Kd对应的输出节点做同样的链式展开。有了dE/dnet_o输出层权重更新就是标准的梯度下降加动量项。3.3 dy/du的近似处理符号函数为什么能用链式求导里面还有一个关键量dE/du -e(k) * dy/du。想一想dy/du是被控对象在当前时刻的增益而我们设计仿真时可能想逃避这个信息因为被控对象有非线性、有滞后解析表达式并不容易得到精确的dy/du。实际工程和论文里常用的一种近似是把dy/du替换成符号信息dy/du ≈ sign[(y(k)-y(k-1)) / (u(k-1)-u(k-2)eps)]这相当于只告诉网络“控制量的增减方向会导致输出同向还是反向变化”而不关心变化幅度的具体大小。幅度的不确定性交给学习率去控制——这就是此类算法学习率普遍要设置得比较小的原因。这种做法的前提是被控对象的增益方向不频繁突变。对大多数工业对象来说这个前提是成立的。如果对象本身存在增益符号反转符号近似会在反转区间给出错误梯度此时只能靠控制量限幅和动量项来缓冲错误更新。仿真阶段要特别注意这个限制。3.4 权重更新与动量项输出层权重和隐含层权重的更新形式delta_wo_new -learn_rate_out * delta_o * H momentum * delta_wo_old wo_new wo_old delta_wo_newdelta_wi_new -learn_rate_hid * delta_h * X momentum * delta_wi_old wi_new wi_old delta_wi_new输出层和隐含层用不同的学习率是因为BP反向传播中梯度从输出层传到隐含层会经历一个衰减过程隐含层的学习率可以稍微给大一点让两层权重的实际更新时间尺度接近。我常用的配比是输出层0.05隐含层0.02先跑一遍看控制量是否抖动再微调。4. 可运行的S函数代码前向计算、反传更新与调试输出4.1 persistent变量里存了什么这个S函数的网络权重和各类历史值全部用persistent变量保存。persistent变量在MATLAB函数中不会随调用结束而清空适合保存跨采样周期需要保留的网络数据。控制器需要保存的内容包括输入层到隐含层权重矩阵wi维度6×3隐含层到输出层权重矩阵wo维度3×7多出的一列是偏置上一步误差e_prev、上上步误差e_prev2上一步控制量u_prev、前两步控制量u_prev2上一步反馈y_prev用于计算dy/du的符号近似上一轮的权重增量delta_wi、delta_wo用于动量项。在初始化分支里调用resetNetwork()把这些变量统一重置可以避免上一次仿真遗留的权值影响下一次启动。4.2 初始化与主函数代码function [sys,x0,str,ts] BPPIDController(t,x,u,flag) switch flag case 0 [sys,x0,str,ts] mdlInitializeSizes(); resetNetwork(); case 3 sys mdlOutputs(t,x,u); case {1,2,4,9} sys []; otherwise error([Unhandled flag , num2str(flag)]); endfunction [sys,x0,str,ts] mdlInitializeSizes() sizes simsizes; sizes.NumContStates 0; sizes.NumDiscStates 0; sizes.NumOutputs 4; sizes.NumInputs 2; sizes.DirFeedthrough 1; sizes.NumSampleTimes 1; sys simsizes(sizes); x0 []; str []; ts [0.01 0];注意NumOutputs是4因为需要同时输出控制量和Kp、Ki、Kd三个参数。NumDiscStates设成0所以我其实把神经网络权重全部放在persistent变量里而不是放入Simulink的离散状态向量。这种写法简单直观适合仿真验证。4.3 resetNetwork每次都从同一状态出发function resetNetwork() persistent wi wo e_prev e_prev2 u_prev u_prev2 y_prev delta_wi delta_wo rng(42); wi 0.5 * (rand(6,3) - 0.5); wo 0.5 * (rand(3,7) - 0.5); e_prev 0; e_prev2 0; u_prev 0; u_prev2 0; y_prev 0; delta_wi zeros(6,3); delta_wo zeros(3,7);这里用固定随机种子保证每次仿真初始权重一致方便复现对比实验。如果你想看随机初值的影响可以把rng(42)改成rng(shuffle)。4.4 mdlOutputs先算控制量再反传更新权重mdlOutputs承担了神经网络的前向计算、增量PID计算和反向传播更新。这样在一个采样周期内先用当前权值计算控制量输出然后再用当前误差更新权值下一拍就能用新权值。时序是合理的。function sys mdlOutputs(t,x,u) persistent wi wo e_prev e_prev2 u_prev u_prev2 y_prev delta_wi delta_wo % 输入 r u(1); y u(2); e r - y; % 网络输出参数范围 Kp_min 0; Kp_max 3; Ki_min 0; Ki_max 0.5; Kd_min 0; Kd_max 0.5; learn_rate_out 0.05; learn_rate_hid 0.02; momentum 0.05; % 前向计算输入层 - 隐含层 X [r; y; e]; net_h wi * X; h tanh(net_h); H [h; 1]; % 前向计算隐含层 - 输出层 net_o wo * H; % 输出层 sigmoid 映射到 PID 参数区间 sig 1 ./ (1 exp(-net_o)); Kp Kp_min (Kp_max - Kp_min) * sig(1); Ki Ki_min (Ki_max - Ki_min) * sig(2); Kd Kd_min (Kd_max - Kd_min) * sig(3); % 增量式PID delta_e1 e - e_prev; delta_e2 e - 2*e_prev e_prev2; du Kp * delta_e1 Ki * e Kd * delta_e2; u_out u_prev du; % 控制量限幅 u_lim 5; if u_out u_lim u_out u_lim; elseif u_out -u_lim u_out -u_lim; end sys [u_out; Kp; Ki; Kd]; % -------------------- 反向传播更新权重 -------------------- % dy/du 的符号近似 du_real u_out - u_prev; dy_real y - y_prev; if abs(du_real) 1e-6 sign_dyda sign(dy_real / (du_real 1e-6)); else sign_dyda 0; end % dE/du -e * sign(dy/du) dedu -e * sign_dyda; % 输出层各节点对应的 du/dK du_dK1 delta_e1; du_dK2 e; du_dK3 delta_e2; % 输出层 sigmoid 导数 d_sig sig .* (1 - sig); % 输出层误差项 delta_o (3x1) delta_o zeros(3,1); delta_o(1) dedu * du_dK1 * (Kp_max - Kp_min) * d_sig(1); delta_o(2) dedu * du_dK2 * (Ki_max - Ki_min) * d_sig(2); delta_o(3) dedu * du_dK3 * (Kd_max - Kd_min) * d_sig(3); % 输出层权重更新 delta_wo_new -learn_rate_out * delta_o * H momentum * delta_wo; wo wo delta_wo_new; delta_wo delta_wo_new; % 隐含层误差项 delta_h (wo(:,1:6) * delta_o) .* (1 - h.^2); % 隐含层权重更新 delta_wi_new -learn_rate_hid * delta_h * X momentum * delta_wi; wi wi delta_wi_new; delta_wi delta_wi_new; % 保存历史量 e_prev2 e_prev; e_prev e; u_prev2 u_prev; u_prev u_out; y_prev y;这段代码有几个细节值得专门说明首先是du_real用的是u_out - u_prev不是u_prev - u_prev2因为u_out是更新后的控制量更准确反映本次控制作用对对象的影响。其次是dy_real当控制量变化太小时符号近似会不稳定此时把sign_dyda置0相当于冻结梯度控制量不动的时候不让网络瞎更新。输出层梯度里的dedu乘了负号正好和误差项负梯度方向对上。如果你自己推导时发现权重更新方向总是反的多半就是这里少了或者多了一个负号这是BP-PID实现里最常见的错误来源。4.5 限幅必须做两层网络输出层的sigmoid映射已经限定了Kp、Ki、Kd的范围这是一层限制。但控制量u_out也要单独限幅。原因是随机初始权重下前几步的PID参数可能并不合理增量式PID算出来的u很容易冲到很大如果不限幅大控制量会让被控对象输出剧烈变化进一步导致误差剧烈变化梯度一下崩掉整个网络就发散回不来了。我的习惯是控制器内部做一次限幅Simulink模型里被控对象前面再放一个Saturation模块做第二层限幅。两层限幅的参数取同一数值保证边界一致不会造成内部和外部限幅打架。4.6 一个容易翻车的坑persistent变量没有在重跑时清空persistent变量一旦被创建就会留在函数的工作空间中直到你调用clear或者MATLAB会话结束。如果resetNetwork不是放在flag0分支里而是只放在mdlOutputs的isempty判断里第二次点击运行仿真时权值还会保留上一次仿真结束时的状态导致两次结果对不上。我见过有人把isempty(wi)当初始化条件第一次仿真没问题第二次结果就飘了。稳妥的做法是像上面那样在flag0分支里无条件调用resetNetwork。另外如果有多个S函数模块实例使用同一个m文件它们会共享同一组persistent变量互相干扰。一个模型里同时放两个BPPIDController实例仿真结果一定奇怪。解决这种问题需要升级到Level-2 S-Function用DWork vector保存每份实例独立的数据这个在后面的章节提一下。5. Simulink模型搭建、结果对比与调试经验5.1 模型搭建的具体步骤在Simulink里新建一个模型步骤如下拖入Step模块阶跃值设为1拖入Sum模块符号配置为“-”用于计算误差r-y拖入S-Function模块在参数设置里填写BPPIDControllerS-Function模块的输入一接Step输出输入二接反馈信号输出端接Saturation模块Saturation上下限设±5饱和模块后接Transfer Fcn分子[1]分母[10 1]传递函数后接Transport Delay延迟时间0.5秒Transport Delay输出反馈到Sum的减号端同时接一根线到Scope。为了观察控制量和PID参数还可以给S函数的输出接一个Demux把u、Kp、Ki、Kd四路信号分出来分别接Scope。这一步强烈建议做因为只看输出响应曲线很难判断网络是在“学习”还是在“振荡”把Kp曲线拉出来看就一目了然。仿真求解器设置成固定步长0.01秒求解器选择ode4仿真时长20秒。固定步长对离散S函数和Transport Delay都更友好。5.2 固定PID与BP-PID的阶跃响应对比我用一组经验整定的固定PID参数做对照Kp1.8、Ki0.15、Kd0.4在这个带饱和和0.5秒滞后的对象上阶跃响应的超调量大约28%调节时间大约9秒初期有明显的往返振荡。BP-PID控制器刚启动的前0.5秒因为网络权重是随机的Kp、Ki、Kd会从初始区间内往外探索响应曲线会有一段小的抖振。但随着误差持续反传网络很快把Ki调上来消除稳态误差把Kp和Kd压到合理组合大约在2秒后进入稳定状态最终超调量压到了5%以内调节时间缩短到4秒左右。下表是我在类似参数下得到的一组典型对比不同被控对象数值会有差异但趋势是稳定的。指标固定PIDBP-PID超调量28%5%调节时间9秒4秒稳态误差00控制量峰值接近限幅明显更低固定PID在纯滞后对象上的问题在于误差出现初期Kp已经加到很大等延迟的输出反馈回来时超调已经无法挽回。BP-PID则会在前几次振荡中迅速降低Kp、加大Kd相当于用几拍时间把参数整定工作自动完成了一遍。5.3 学习率、隐含层节点数、初始权重的调试经验学习率是BP-PID里最容易引发“灾难”的参数。学习率给的太大第一个阶跃响应还没出来权重就更新到了奇怪的位置控制量开始高频乱摆误差越来越大进入发散螺旋学习率给的太小网络参数几乎不动和固定PID没什么区别失去了自整定优势。我的起始配置是输出层0.05、隐含层0.02。如果控制量在响应过程中出现高频抖动先把输出层学习率降到0.02看看抖动是否收敛如果网络学习速度明显太慢比如稳态误差很长时间降不下来可以试着把输出层学习率提到0.1但一定要全程盯着控制量曲线发现锯齿波立刻停掉仿真。隐含层节点数方面6个节点对这个三输入三输出的映射已经足够。节点数从6增加到10收敛速度不会明显变快反而容易出现参数振荡。BP网络理论上可以逼近任意连续函数但那是理想情况在线学习场景下节点越多待优化参数越多随机初值带来的不确定性也越大。权重初始范围取[-0.5, 0.5]比较稳。有一点必须注意不要全部初始化为0。全零初始化和对称网络会让各节点在反向传播中得到完全相同的梯度所有隐含层节点退化成一个节点网络表达能力立刻被消灭。如果你发现Kp、Ki、Kd三条参数曲线长得一模一样先检查是不是初始化出了问题。5.4 启动瞬态“前期震荡”不是bug而是BP-PID的正常阶段随机权重意味着网络刚开始给出的PID参数完全不靠谱前几百毫秒的系统响应会出现一个明显的“探索期”幅度可能比固定PID还要难看。如果被控对象对早期振荡很敏感可以给网络加一个“预热期”前N步冻结权重用一个预设PID参数运行等系统基本稳定后再打开学习。我实际更喜欢的做法是把网络输出层的sigmoid映射中心对准经验参数。具体来说如果经验整定给出的Kp大约是1.8那就把Kp_min和Kp_max设成[0.5, 3]让初始sigmoid输出落在1.75附近而不是2.5。这样网络初始参数本来就接近可用值探索期很短响应曲线从一开始就不会离谱。这个方法比冻结权重更好因为网络从第一拍就在参与控制只是起点更聪明。实现起来也不复杂改一下各参数的上下限区间就行。6. 从仿真走向实物之前还差这几步6.1 persistent变量该换到DWork向量了仿真阶段用persistent变量非常方便但它有两个问题多个S函数实例共享同一份数据以及无法在代码生成阶段被可靠映射到实时内存。如果打算把控制器从Simulink仿真搬到实物平台建议用Level-2 MATLAB S-Function把网络权重、历史误差、控制量历史全部放进DWork向量每个实例一份独立数据。Level-2 S-Function的结构区别在于用mdlInitializeSizes注册DWork用mdlStart初始化用mdlOutputs执行前向计算和反向更新。网络算法本身不用大改主要是把persistent变量换成工作向量的索引访问。这个改造值得提前做尤其是你已经确定控制器要参与硬件在环测试。6.2 采样周期和计算量先算一笔账三层BP网络加6个隐含层节点一次前向计算加上一次反向传播纯计算量大概是几百次浮点运算。在工控机或现代嵌入式处理器上都是小意思。但要注意两点一是整型嵌入式平台上tanh函数并不是一条指令就能完成如果采样周期很紧考虑用查表或多项式近似替代tanh二是反向传播中的矩阵运算如果自己手写循环别用嵌套三层for循环做矩阵乘先用中间变量降低计算复杂度实测下来这点优化能减少一半以上的执行时间。6.3 安全网控制量限幅和增益方向突变到了实物阶段我对学习类控制器的态度永远是可以让它聪明但不能让它胡来。控制量限幅是底线PID参数的区间映射也算一条防线还应该加一个“误差超限冻结”逻辑当误差绝对值超过某个阈值时暂停权重更新先用当前参数稳住系统等误差回到安全范围再恢复学习。这个逻辑在仿真里可有可无在实物上却可能是防止发散损坏设备的关键。符号近似dy/du在对象增益方向突变时会给出错误的更新方向。工业中增益反转并不罕见比如某些阀门的流量特性在一定开度附近会反转。对这种对象符号法BP-PID并不合适换用带自适应律的模型参考控制或者对被控对象做分段线性化处理都比强行使用符号法更稳妥。我自己做完这个仿真之后最大的体会是神经网络在这里并没有“魔法”它只是把“根据误差、期望和实际输出去决定PID参数”这件事用梯度下降的方式在线地做了出来。它真正的价值不是万能而是在常规PID整定手段失效的那类非线性、时变对象上给工程师提供了一个自动寻找参数组合的路径。最后分享一个小技巧每次调完一组学习率把Scope里Kp、Ki、Kd的波形截图存下来和响应曲线放在一起看。如果误差还在下降而Kp/Ki/Kd三条线已经陷入高频振荡那就是学习率偏高如果误差长时间压不下去而三条线几乎不动那就是学习率偏低。靠着这条经验我后来调这类控制器的时候基本不需要反复试几十组参数两三轮就能收敛到合理区间。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑