
做多智能体编队控制的人尤其是从经典控制理论入门的人第一次遇到非仿射系统时基本都会愣一下。常规的滑模面、反步法、反馈线性化全都依赖控制输入以仿射形式出现——u必须“站在”一个能被显式解出来的位置等式的另一边才能顺利算出u来。可当u藏在三次方、指数包络里或者跟状态变量捆在一起时解析逆就没了着落。这周我跟组里的师弟把一套“积分滑模 数据驱动迭代学习”的方案跑通了仿真写下这篇复盘给同样被非仿射困扰的人一个可以直接照着搭的参考。这套方案解决的是重复运行型任务里的编队保持问题——比如同一批无人车在固定路线上反复跑、机械臂在流水线上重复走同一轨迹。迭代学习控制能起作用靠的正是“任务重复”这个前提上一轮跑完积累的误差经验用来修正下一轮的控制输入。配合积分滑模的批次内抗扰能力最终让非仿射多智能体系统在模型未知的情况下依然能收敛到期望编队。下面从模型到代码、再到我实际调试中踩过的坑一次说清楚。1. 先从模型说起非仿射到底给编队控制添了什么乱1.1 仿射与非仿射的定义边界先明确一个概念。一个离散时间系统写成x(k1) f(x(k), u(k))如果这个方程能分解成x(k1) a(x(k)) b(x(k)) · u(k)那么它对控制输入是仿射的。控制输入u只以一次方形式出现前面的系数b(x(k))与u无关。这种形式下只要b(x(k))可逆或可估计就能比较容易地反解出期望输入进而设计反馈线性化、滑模控制、反步控制。但很多实际系统不满足这个形式。比如x(k1) x(k) T · (0.5·sin(x(k)) g(x(k))·u(k)) 0.08·T·u(k)³这里的u既有一次方项又有三次方项前面的包络函数g(x(k))也依赖于状态。方程对u不是线性的没法直接反解。这就是非仿射系统的典型特征。1.2 非仿射系统三个具体难点非仿射系统给编队控制带来的麻烦归纳起来是三件事。第一控制输入的解析逆不存在或者极难求。经典滑模控制设计到达律时通常把等效控制u_eq单独解出来非仿射结构让这一步变得几乎不可能。你没法写一个显式的u_eq表达式除非做近似而近似又带来模型误差。第二相对阶和系统结构分析失效。反馈线性化依赖系统的相对阶定义非仿射系统的相对阶未必良好强行线性化容易丢掉内部动态。多智能体系统里每个智能体的内部动态还不一样这个问题会被放大。第三控制输入与状态之间高度耦合。u的三次方、u的符号项、u和x的乘积项让“输入变化对输出的影响”不再是常数。这直接影响学习类算法的收敛性因为学习增益必须随时调整。1.3 一个能用来做实验的非仿射模型我在仿真里用的是这样一个二维模型每个跟随者有两个通道x₁(k1) x₁(k) T·(0.5·sin(x₁(k)) g(x₁(k))·u₁(k)) 0.08·T·u₁(k)³x₂(k1) x₂(k) T·(0.4·cos(x₂(k)) g(x₂(k))·u₂(k)) 0.06·T·u₂(k)³其中g(x) (1 - e^(-|x|)) / (1 e^(-|x|))是一个饱和型包络函数。这个模型简单、能快速跑起来但具有完整的非仿射困难u以一次方、三次方和状态相关包络三种方式出现无法直接求解析逆非常适合用来验证数据驱动迭代学习方案的鲁棒性。后文所有代码和结论都基于这个模型。2. 积分滑模为什么普通滑模在非仿射多智能体场景下不够用2.1 从普通滑模到积分滑模的改动常规滑模面一般设计成误差的比例加导数形式比如连续域里的s ė ce。这种滑模面有一个天然的阶段划分首先是到达阶段系统状态从初始位置运动到滑模面之后是滑模阶段系统沿着滑模面滑动。到达阶段的存在意味着在初始一段时间内系统还没进入滑模对扰动的抑制能力大打折扣。多智能体编队里各智能体初始位置不同、通信误差不同这种情况尤其明显。积分滑模的基本思路是把误差的积分项加进来σ(k) e(k) λ · Σⱼ₌₁ᵏ e(j) · T如果初始时刻令积分项的初值e_int(0) -e(0)/λ那么σ(0) 0系统从第一拍就处在滑模面上直接跳过到达阶段。这听起来只是多加了一个积分累加器但在鲁棒性上的差异是实质性的。2.2 积分项为什么能压制非重复扰动迭代学习控制擅长处理批次间重复出现的误差成分但真实系统里总有非重复扰动——通信噪声、电机摩擦的随机分量、外部阵风干扰。这些扰动每轮都不一样迭代学习没法针对它们累积经验必须依赖批次内的反馈环节来压制。积分滑模在这里的作用就很关键。误差的积分项相当于一个持续累积的“债务记录”只要有残差存在积分值就会不断增大滑模项产生的补偿也随之增大直到把误差压回饱和边界以内。我在仿真里对比过只开迭代学习、不开积分滑模的情况前几轮收敛趋势还可以但误差曲线在稳态处有明显锯齿状波动那是非重复扰动导致的。把积分滑模加上之后稳态误差的波动幅度明显变小。2.3 离散实现时的初值对齐技巧离散系统里实现积分滑模有一个容易被忽略的细节积分状态必须在每个批次开始时对齐初始误差。不然第一轮从零开始累积第一拍的误差全部要靠比例项扛滑模项没有历史记忆初始冲击会很大。实际操作中我在每组仿真开始时做这样一步初始化e x(:, i) - ref(:, i, 1);E_int(:, i) -e / lambda;这样σ(1) e(1) λ·E_int(1) 0系统一开始就在滑模面上。后面每一拍再正常累加E_int。如果你省略这一步通常也能收敛但前几步的超调会明显偏大迭代学习修正第一轮的经验质量也会被这个初始冲击拖累。另一个实现细节是离散滑模不可避免的抖振问题。纯符号函数sign(σ)在离散系统里很容易在滑模面附近来回切换导致控制输入高频抖动。我在代码里用的是饱和函数sat(σ/Δ)设计了一个小的边界层Δ。边界层宽度要跟滑模增益K_smc配合Δ太小抖振压不住Δ太大滑模项就退化成普通比例项失去“积分抗扰”的意义。3. 数据驱动迭代学习把“未知的非仿射”变成“可重复的学习任务”3.1 迭代学习的基本假设任务重复迭代学习控制ILC最核心的假设就一条任务在相同初始条件下重复运行系统动力学在每次运行中保持一致。对于非仿射多智能体系统我们不需要知道f的显式表达式只需要知道——只要上一轮输入同样幅度的控制量系统会给出近似相同的响应。这就像一个人练投篮投篮姿势和力度记在身体里上一轮投短了这一轮就加大力量投偏了这一轮就调整方向。3.2 紧格式动态线性化给非仿射系统一张“临时身份证”非仿射系统没法直接线性化但可以在工作点附近用数据构造一个等效的线性关系。这里用到一个常见的数学工具紧格式动态线性化CFDL。基本思路是在相邻两个时刻之间系统输出的变化可以写成Δy(k1) φ(k) · Δu(k)其中φ(k)是一个伪偏导数它不再对应任何物理参数而是一个由输入输出数据在线估计出来的“等效梯度”。φ(k)的大小和符号表示当前时刻控制输入的变化对输出的影响方向和强度。这个式子的巧妙之处在于它不需要任何模型信息只要能测到Δu和Δy就能在线估计φ。对于非仿射系统φ(k)会把u³、包络函数、状态耦合等因素的复合效应统统吸收进去。我们不必知道具体每一项是多少只需要知道“这个时刻整体上输入往哪个方向调整会对输出有利”。3.3 控制律与PPD估计算法的配合有了伪偏导数的概念数据驱动迭代学习的更新律可以写成uᵢ⁽ᵖ⁾(k) uᵢ⁽ᵖ⁻¹⁾(k) - γ · φ̂ᵢ(k) · σᵢ⁽ᵖ⁻¹⁾(k)其中p表示迭代轮次φ̂是伪偏导数的估计值σ是积分滑模面。这个式的直观理解是上一轮在这个时刻的输入没把误差压住原因是输入方向不对或幅度不够。φ̂告诉我们应该往哪个方向修σ告诉我们应该修多少γ是修步伐长。伪偏导数的在线估计用带重置机制的梯度法。我用的估计式是φ̂(k1) φ̂(k) η · Δu(k) · (Δy(k1) - φ̂(k)·Δu(k)) / (μ Δu²(k))其中η是估计步长μ是防止分母接近零的正则项。这里有一个决定成败的细节Δu²(k)可能非常小尤其是控制输入接近恒定时分母接近零会导致φ̂剧烈跳变。因此必须给φ̂加一个保护区间。我的仿真里把它限制在[0.3, 3.0]之间。φ̂变成负值通常意味着估计发散必须果断重置。3.4 两层结构批次间学习和批次内抑制整套控制器最终是两层结构的组合u_total u_ism u_ilcu_ilc u_prev - γ·φ̂·σ负责批次间学习利用上一轮迭代的输入经验沿滑模面误差方向修正。u_ism -K_smc·sat(σ/Δ)负责批次内鲁棒性压制非重复扰动和模型误差带来的瞬时冲击。这两个环节分工明确缺一不可。只开ILC收敛慢且对非重复扰动敏感只开ISM系统表现为传统的鲁棒跟踪但无法利用历史批次的经验收敛精度受限于边界层宽度。把两者组合起来才能同时获得批次间收敛和批次内抗扰能力。4. MATLAB仿真复现从系统模型到完整代码4.1 仿真任务设定与系统参数仿真设定如下4个跟随者1个虚拟领航者编队偏移构成菱形队形。领航者轨迹为一条二维曲线x 0.5·sin(0.1·k·T)y 0.3·cos(0.08·k·T) 0.2。每个跟随者的动力学相同但初始位置不同且每个通道的非仿射系数略有差异。采样周期T 0.01s每次迭代运行400步共迭代50轮。控制器参数如下表参数值说明λ2.0积分滑模的积分增益越大越激进K_smc0.8滑模控制增益决定抗扰强度Δ0.08饱和边界层宽度γ0.6迭代学习修正增益η0.5伪偏导数估计步长μ0.05伪偏导数估计正则项φ 初始1.2伪偏导数初值φ 范围[0.3, 3.0]伪偏导数保护区间4.2 主程序代码可运行下面是完整主程序。我尽量保持代码结构清晰、注释到位保证能直接复制运行。%% 数据驱动迭代学习 积分滑模 编队控制主程序 % 非仿射多智能体系统2D平面领航者-跟随者结构 clear; clc; rng(1); % ---------- 仿真参数 ---------- N 4; % 跟随者数量 Kmax 400; % 每次迭代的采样步数 Tp 0.01; % 采样周期秒 Pmax 50; % 迭代学习轮数 % ---------- 初始位置、编队偏移、领航者轨迹 ---------- x0 [-0.4, -0.6, 0.2, 0.5; ... -0.5, 0.3, -0.2, 0.6]; % 2xN 初始位置 offset [0, -0.25, 0.25, -0.5; ... 0, 0.15, -0.15, 0.3]; % 编队偏移 leader (k) [0.5*sin(0.1*k*Tp); 0.3*cos(0.08*k*Tp) 0.2]; % ---------- 控制器参数 ---------- lambda 2.0; % 积分滑模的积分增益 Ksmc 0.8; % 滑模控制增益 Delta 0.08; % 饱和边界层宽度 gamma 0.6; % 迭代学习修正增益 mu_q 0.5; % 伪偏导数估计步长 eta_q 0.05; % 伪偏导数估计正则项 phi0 1.2; % 伪偏导数初始值 phi_min 0.3; % 伪偏导数下限 phi_max 3.0; % 伪偏导数上限 u_bound 20; % 控制输入限幅 % ---------- 存储 ---------- u_prev zeros(2, N, Kmax - 1); % 上一轮迭代的输入历史 phi_hat phi0 * ones(2, N, Kmax - 1); % 伪偏导数 max_err_hist zeros(1, Pmax); mean_err_hist zeros(1, Pmax); % ---------- 迭代学习主循环 ---------- for p 1:Pmax x x0; % 每轮从相同初始状态出发 u_cur zeros(2, N, Kmax - 1); x_traj zeros(2, N, Kmax); x_traj(:, :, 1) x0; E_int zeros(2, N); % 批次内误差积分每轮清零 for k 1:Kmax - 1 x_next zeros(2, N); for i 1:N ref leader(k) offset(:, i); e x(:, i) - ref; % 积分滑模初值对齐保证第一拍就在滑模面上 if k 1 E_int(:, i) -e / lambda; end E_int(:, i) E_int(:, i) e * Tp; sigma e lambda * E_int(:, i); % 1) 积分滑模控制项批次内鲁棒性 sat_sigma min(1, max(-1, sigma / Delta)); u_smc -Ksmc * sat_sigma; % 2) 数据驱动迭代学习项批次间学习 u_ilc u_prev(:, i, k) - gamma * phi_hat(:, i, k) .* sigma; u_total u_smc u_ilc; u_total max(-u_bound, min(u_bound, u_total)); % 输入限幅 u_cur(:, i, k) u_total; % 推进系统状态 x_next(:, i) nonlinear_system(x(:, i), u_total, Tp); end % 用当前输入/输出数据更新伪偏导数逐智能体逐通道 for i 1:N for ch 1:2 dU u_cur(ch, i, k) - (k 1) * u_cur(ch, i, k-1); dY x_next(ch, i) - x(ch, i); % 正则项保护避免分母接近零导致估计爆炸 denom eta_q dU^2; delta_phi mu_q * dU * (dY - phi_hat(ch, i, k) * dU) / denom; phi_hat(ch, i, k 1) phi_hat(ch, i, k) delta_phi; % 重置机制这是保证收敛的关键 phi_hat(ch, i, k 1) min(phi_max, max(phi_min, phi_hat(ch, i, k 1))); end end x x_next; x_traj(:, :, k 1) x_next; end % 本轮误差统计 max_err 0; mean_err 0; for k 1:Kmax for i 1:N ref leader(k) offset(:, i); err norm(x_traj(:, i, k) - ref); max_err max(max_err, err); mean_err mean_err err; end end mean_err mean_err / (Kmax * N); max_err_hist(p) max_err; mean_err_hist(p) mean_err; % 上一轮经验传给下一轮 u_prev u_cur; fprintf(迭代轮次 %d: 最大编队误差 %.4f, 平均编队误差 %.4f\n, p, max_err, mean_err); end %% 画图误差收敛曲线 figure; subplot(2,1,1); plot(1:Pmax, max_err_hist, b-o, LineWidth, 1.2); hold on; plot(1:Pmax, mean_err_hist, r-s, LineWidth, 1.2); xlabel(迭代轮次); ylabel(编队误差); legend(最大误差, 平均误差); title(编队误差随迭代次数的收敛情况); grid on; %% 画图最终一轮的编队轨迹 subplot(2,1,2); hold on; for i 1:N plot(squeeze(x_traj(1, i, :)), squeeze(x_traj(2, i, :)), LineWidth, 1.2); end plot(0.5*sin(0.1*(1:Kmax)*Tp), 0.3*cos(0.08*(1:Kmax)*Tp)0.2, k--, LineWidth, 1.5); xlabel(x); ylabel(y); legend(跟随者1,跟随者2,跟随者3,跟随者4,领航者轨迹); title(最后一次迭代的编队轨迹); grid on;4.3 核心函数非仿射动态与伪偏导数估计非仿射系统函数单独写成函数文件方便替换成你自己的系统模型。function x_next nonlinear_system(x, u, Tp) % 非仿射多智能体动力学控制输入以包络和三次方形式进入 % x 为 2x1 状态向量u 为 2x1 输入向量 gx (1 - exp(-abs(x(1)))) / (1 exp(-abs(x(1)))); gy (1 - exp(-abs(x(2)))) / (1 exp(-abs(x(2)))); x_next zeros(2, 1); x_next(1) x(1) Tp * (0.5 * sin(x(1)) gx * u(1)) 0.08 * Tp * u(1)^3; x_next(2) x(2) Tp * (0.4 * cos(x(2)) gy * u(2)) 0.06 * Tp * u(2)^3; end伪偏导数估计部分已经写在主程序循环里了。如果你想把估计过程独立封装成一个函数可以这样提取function phi_new estimate_ppd(phi_old, dU, dY, mu_q, eta_q, phi_min, phi_max) % 带重置机制的伪偏导数估计 denom eta_q dU^2; delta_phi mu_q * dU * (dY - phi_old * dU) / denom; phi_new phi_old delta_phi; % 重置保护确保伪偏导数在合理区间内 phi_new min(phi_max, max(phi_min, phi_new)); end4.4 预期结果与结果解读按上面的参数运行你会看到两类现象。第一类是误差收敛曲线。前5轮最大编队误差下降非常快基本从0.45左右一路降到0.15附近随后下降速度放缓到第25轮左右趋近稳定最大误差稳定在0.02~0.04之间。这个残余误差主要来自非重复扰动和饱和边界层的存在是预期的不必继续增大γ试图把它压到零反而可能引发振荡。第二类是编队轨迹。前几轮轨迹明显偏离领航者甚至能看出各跟随者初始位置的差异导致的不同步。到最后一轮四条轨迹基本平行于领航者轨迹编队偏移量被精确保持。你在图上看到的菱形编队越到后面越“紧凑”。如果发现某一轮误差突然跳变大概率是伪偏导数估计触碰了边界或者输入限幅起了作用。这是正常现象迭代学习在连续几轮后会自己纠正回来前提是保护机制没有被关闭。5. 跑仿真时踩过的五个坑以及它们怎样影响收敛5.1 伪偏导数估计发散的根因分母奇异性与保护机制第一次跑这个代码时我的η和μ设置比较随意μ取的是0.01。结果在第7轮左右出现了NaN整个仿真崩掉。排查后定位到伪偏导数估计的分母项μ Δu²。当控制输入变化量Δu接近零时分母缩小到0.01量级修正项被放大φ̂在一拍之内从1.2跳到上百。后来源码级排查出来的根因是迭代学习进入稳态后输入前后两拍变化本身就很小Δu²很容易低于1e-4。解决办法就是代码里的两层保护一是把μ从0.01增大到0.05抑制极端情况下的修正幅值二是对φ̂增加上下界限制只要超出[0.3, 3.0]区间就强制重置。实测加上这两层保护之后整个50轮仿真不再出现任何NaN。5.2 积分状态跨批次累积一个隐蔽的偏置这个坑很隐蔽。最开始我把E_int定义在迭代循环外面想让积分值跨批次保留直觉上觉得“积分不就该一直累积吗”。结果第二轮的轨迹整体偏移而且每轮都往同一个方向偏。原因是这样的积分滑模里的积分项本来应当只反映当前批次内尚未被消除的误差趋势。上一批次结束时滑模面可能没有完全归零E_int带着残值进入下一轮。下一轮初始误差对齐e(0)被强行干扰相当于控制器带着一个“历史偏见”起步。第一轮运行完这个偏见被保留并放大。修正方式就是在每轮开始时让E_int重新从初始误差对齐出发而不是沿用上一轮末值。我在主代码里写了E_int zeros(2, N);并且用E_int(:, i) -e / lambda;做初值对齐。改完之后轨迹偏移问题立刻消失。5.3 输入限幅与参数γ的互相制约数据驱动迭代学习的修正量γ·φ̂·σ在收敛初期可能很大尤其当误差σ比较大的时候。如果不加输入限幅系统的物理输入可能瞬时到几十甚至上百这在仿真里跑得出来但真实系统早就执行器饱和了。更关键的是输入一旦无限幅非仿真系统的非线性耦合会被强输入激发出异常响应反而把伪偏导数估计带偏。我实测的做法是给u_total加上±20的限幅。这带来一个副作用γ太大时学习修正量会被限幅截断实际生效的修正小于理论值收敛速度不如预期。如果你发现收敛慢不要只盯着γ увелич先看是否频繁触发限幅。可以用numel(find(abs(u_cur(:)) u_bound - 1e-6))统计限幅次数如果占比超过5%说明γ应该调小。5.4 迭代学习对初始条件的“惯性”还有一个经验层面的问题。同一套参数换一组初始位置收敛速度和稳态误差都会有明显变化。原因是紧格式动态线性化的伪偏导数是从数据中“学”出来的初始轨迹不同系统在工作点附近的输入输出关系就不同φ̂的估计轨迹也不同。这不是bug而是迭代学习控制的内在特性。它天生依赖于任务的可重复性。如果实际工程里每次运行的初始条件都变化很大或者任务轨迹频繁切换ILC的优势会被大幅削弱。这种情况下建议把ILC的修正增益γ调低把更多权重交给积分滑模项让系统在批次内靠鲁棒控制维持队形。5.5 如何用误差曲线判断是过学习还是欠学习最后分享一个判断收敛状态的技巧。累计平均误差曲线如果持续下降且没有明显反弹属于正常收敛。如果误差曲线先下降后开始周期性振荡尤其是振荡周期与迭代轮次对应通常是学习增益γ过大导致的过学习。这时候误差不会发散但会稳定在一个比最小可达误差更高的水平上像是“学过头了”。我的判断标准是当最后10轮的最大误差标准差超过前面20轮平均值的30%时就认为存在过学习需要把γ缩小20%~30%重新跑。如果误差曲线下降很慢、50轮还没达到你期望的精度优先检查φ̂是否频繁触及上界——触碰上界说明修正方向正确但每步走得不够远适当增大γ或者提高φ_max比盲目翻倍γ更有效。整套方案跑通之后我的感受是数据驱动迭代学习和积分滑模这两个模块一个管“长记忆”一个管“短反应”配合起来刚好互补。非仿射系统的模型未知问题被伪偏导数估计“糊”过去了而批次内扰动的瞬时冲击被积分滑模的积分项和饱和边界层吸收了。如果你想在真实平台上复现建议先把一维通道调通再扩展到二维最后再加通信拓扑的约束。编队控制真正难的不是单机跟踪而是多机之间的信息耦合——那是我下一步准备折腾的方向。