1. 为什么要拿GRNN和RBFNN去补ILC的短板
先说结论:纯粹的迭代学习控制(ILC)在未知非线性系统面前,其实并没有大多数人想象的那么好用。很多入门文章把ILC说得像"跑几遍轨迹误差就自动收敛"的黑魔法,但真正在Matlab里搭过仿真模型就会发现,经典P型或D型ILC的学习律对系统模型一致性极其敏感,尤其是当被控对象存在强非线性、未建模动态或输入输出耦合的时候,经典ILC要么收敛慢得让人失去耐心,要么直接发散掉。
我最初接手一个类似轨迹跟踪项目时,被控对象是一个SISO非线性系统,表达式里带高次项和正弦耦合项,模型结构不完全已知。最初我用的就是固定增益P型ILC,前几轮迭代误差确实在下滑,但到第12轮左右就开始平台期,再往后误差居然反弹。原因不复杂:非线性系统在不同运行点上需要不同的补偿力度,固定学习增益在某个工作点附近合适,换一个状态区域就失效了。这就是所谓的"对未知非线性系统,固定结构ILC的保守性和发散风险共存"。
于是思路自然转向神经网络。神经网络的作用不是取代ILC,而是给ILC装上"在线感知"能力——让控制器知道当前运行点附近系统特性长什么样,从而动态调整学习方向和学习强度。标题里的GRNN(广义回归神经网络)和RBFNN(径向基神经网络)各有分工:GRNN负责离线快速建模或在线预测未建模动态,RBFNN负责局部逼近并输出补偿量,两者和ILC结合后,轨迹跟踪的收敛速度和鲁棒性都比纯ILC提升一个层级。
这个组合适合谁来参考?我觉得主要三类人:一是做运动控制、机器人重复轨迹任务的,二是研究非线性系统自适应控制但嫌传统自适应律推导太繁琐的,三是在Matlab/Simulink项目里需要快速验证控制算法却不想把大量时间耗在数学推导上的学生或工程师。读完你会发现,GRNN-RBFNN-ILC的核心价值不是"网络有多深"或者"算法有多玄",而是用两个结构简单的网络,把非线性系统的辨识压力从控制器设计阶段转移到了数据拟合阶段,工程上非常好落地。
2. 两个核心网络的核心原理与使用边界
很多人一听到"神经网络控制"就以为要搭一个几十层的深度网络、调一堆超参数。实际上在迭代学习控制的场景里,追求深度完全没必要,反而是结构简单、可解释性强、响应速度快的网络更实用。GRNN和RBFNN都属于这种"小而精"的网络。
2.1 RBFNN:为什么局部感知能力强
RBFNN的结构核心是径向基函数。每个隐含层节点对应一个中心向量,输入样本距离这个中心越近,该节点的激活值就越高。常见的高斯函数形式如下:
[ \phi_i(x) = \exp\left(-\frac{|x - c_i|^2}{2\sigma_i^2}\right) ]
其中 (c_i) 是第 (i) 个径向基函数的中心,(\sigma_i) 是宽度参数。网络输出是对所有隐含层节点输出的加权求和:
[ y = \sum_{i=1}^{m} w_i \phi_i(x) ]
这种结构的特性是"局部逼近":一个输入点只会激活附近几个节点,远处的节点基本不参与。所以在非线性系统轨迹跟踪场景里,RBFNN天然适合用来学习系统在每个工作点附近的局部动态特性。迭代学习过程中系统状态每轮都会改变,RBFNN能根据当前输入快速给出局部补偿。
有一点很多初学者容易忽略:RBFNN的输出对中心分布非常敏感。如果中心选得太稀疏,某些区域没有节点覆盖,模型在那儿就是"瞎猜";如果中心选得太密,网络输出容易抖振,控制输入会跟着高频跳动。实践中我一般用K-means聚类来选择中心,把训练样本按空间分布聚成若干簇,以簇中心作为径向基中心,宽度取簇内样本的平均距离,这样覆盖效果会明显好过均匀网格选中心。
2.2 GRNN:训练快、拟合非线性映射能力强
GRNN全称Generalized Regression Neural Network,它本质上是一种基于核回归的非参数估计器。它的思想非常朴素:给定输入,输出是所有训练样本输出的加权平均,权重由样本与当前输入之间的"距离"决定,距离越近权重越大。
GRNN的结构分为四层:输入层、模式层、求和层、输出层。模式层每个节点对应一个训练样本,求和层分别计算加权和与权值总和,输出层做除法得到最终估计。最核心的公式是:
[ \hat{y}(x) = \frac{\sum_{i=1}^{n} y_i \exp\left(-\frac{|x - x_i|^2}{2\sigma^2}\right)}{\sum_{i=1}^{n} \exp\left(-\frac{|x - x_i|^2}{2\sigma^2}\right)} ]
这个公式的好处是:不需要像传统BP网络那样反复迭代训练权重,只要把训练样本"记住"就行。每次预测时实时计算核函数权重,所以GRNN在样本量不太大的情况下训练速度极快,而且拟合非线性映射的能力很强。
具体到迭代学习场景,GRNN适合用来离线建立"迭代次数-系统未建模动态"之间的映射关系,或者作为前馈模型在多轮迭代中修正系统模型偏差。需要提醒的是,GRNN的预测速度会随样本量增加而变慢,因为每次预测都要计算与所有样本的距离。如果一轮迭代采集了上千个数据点,把全部数据塞进GRNN,仿真时间会肉眼可见地增加。
2.3 选型时的核心差异
我在实际使用中总结过两个网络的优缺点,经常被问到"到底用哪个好",这里直接上一张对比表:
| 对比维度 | RBFNN | GRNN |
|---|---|---|
| 训练方式 | 可离线训练,也可在线更新权重 | 本质上是懒惰学习,预测时计算 |
| 局部逼近能力 | 强,节点局部激活 | 中,依赖核函数宽度 |
| 预测速度 | 快,固定网络结构 | 随样本量线性变慢 |
| 对样本量的依赖 | 需要适量样本做中心聚类 | 样本越多精度越高,但有冗余 |
| 可解释性 | 较好,能看到每个节点贡献 | 较弱,像黑箱回归 |
| 适合场景 | 在线补偿、实时控制 | 离线建模、数据量适中的拟合 |
在GRNN-RBFNN-ILC这个组合里,我的习惯做法是:把RBFNN放在反馈补偿回路中做实时学习,把GRNN作为模型参考或前馈预测器,两者互补。RBFNN负责"精细修正",GRNN负责"宏观预判"。
3. GRNN-RBFNN-ILC算法的完整设计框架
从控制理论角度说,GRNN-RBFNN-ILC属于"基于智能学习的迭代控制"范畴。它的完整框架可以拆成三步:离线辨识、迭代学习控制器设计、在线修正补偿。
3.1 离线辨识环节
假设被控SISO非线性系统可以写成如下形式:
[ y(k+1) = f(y(k), y(k-1), \dots, u(k), u(k-1), \dots) ]
这里的 (f) 是未知非线性函数。纯ILC不考虑 (f) 的具体形式,只管每一轮迭代的误差并修正输入。但GRNN-RBFNN-ILC的第一步是先用输入输出数据离线训练一个GRNN,让它逼近 (f) 在某个工作范围内的映射关系。
这一步的价值是:后续ILC修正量不用完全盲目试探,而是可以借助GRNN的预测结果给出一个初步前馈。比如第 (j) 轮迭代开始前,GRNN根据期望轨迹和上一轮的控制输入,预测系统的输出走向,控制器就能提前知道"如果沿用上一轮输入,系统大概会偏多少"。
离线辨识的具体做法:
- 给系统施加一组激励信号(如幅值不同的正弦扫频、伪随机序列),采集足够多的输入输出数据。
- 把数据组织成"输入向量-输出标签"对。输入向量由当前输出、上一时刻输出、当前输入、上一时刻输入等构成。
- 用GRNN直接拟合映射关系,不需要训练权重,但需要确定核宽度参数 (\sigma)。(\sigma) 过小会导致过拟合,输出曲线剧烈震荡;(\sigma) 过大会导致拟合过于平滑,失去非线性细节。我一般用交叉验证法,把数据分成训练集和验证集,选验证误差最小的 (\sigma)。
3.2 迭代学习控制器结构
ILC的精髓是"吃一堑长一智":每一轮运行结束后,根据跟踪误差改进下一轮的控制输入。经典P型ILC的更新律是:
[ u_{j+1}(k) = u_j(k) + \Gamma e_j(k) ]
其中 (e_j(k)) 是第 (j) 轮第 (k) 时刻的跟踪误差,(\Gamma) 是固定学习增益。这个式子直观,但问题也明显:(\Gamma) 固定,意味着对所有误差幅值一视同仁。误差大的时候,修正量应该更大;误差小的时候,修正量应该小一点,避免超调。而且不同状态区域需要的修正力度不一样,固定 (\Gamma) 没法适应。
GRNN-RBFNN-ILC的改进方向是把固定增益换成"动态增益":
[ u_{j+1}(k) = u_j(k) + \lambda_j(k) \cdot e_j(k) + \hat{f}{RBF}(x(k)) \cdot \Delta u{comp}(k) ]
其中 (\lambda_j(k)) 是迭代时变学习因子,(\hat{f}_{RBF}(x(k))) 是RBFNN输出的补偿增益。核心思路是:RBFNN根据当前系统状态(即网络输入 (x(k)))估计系统的局部灵敏度,然后调整学习增益。系统在某个区域灵敏度高,控制修正就保守一点;灵敏度低,就加大修正力度。
迭代时变学习因子 (\lambda_j) 的设计也很重要。我常用的策略是让学习因子随迭代次数增加而缓慢衰减:
[ \lambda_j = \lambda_0 \cdot \frac{1}{1 + \alpha j} ]
这样前几轮误差大,用较大的学习因子快速收敛;后期迭代误差已经较小,学习因子变小可以抑制噪声干扰和抖振。(\alpha) 通常取0.01到0.1之间,具体值要跟踪误差下降曲线来调。
3.3 迭代更新律的完整形式
整合GRNN与RBFNN后,完整的迭代学习控制器可以写成如下算法步骤。
第 (j) 轮迭代执行过程:
- 给定期望轨迹 (y_d(k)),初始控制输入 (u_0(k)) 设为0或离线辨识得到的初始前馈。
- 将被控对象运行一轮,记录实际输出 (y_j(k)),计算跟踪误差 (e_j(k) = y_d(k) - y_j(k))。
- 将当前状态向量 ([y_j(k), y_j(k-1), u_j(k), u_j(k-1)]) 输入RBFNN,得到局部补偿量 (\gamma_{RBF}(k))。
- 根据GRNN对系统映射的预测,修正前馈分量,计算新的控制输入: [ u_{j+1}(k) = u_j(k) + \lambda_j e_j(k) + \gamma_{RBF}(k) \cdot \Delta u_{GRNN}(k) ] 其中 (\Delta u_{GRNN}(k)) 由GRNN预测输出与实际输出之差决定。
- 更新RBFNN的权重,使其适应当前迭代区域的动态特性。
- 进入下一轮迭代 (j+1),重复上述过程。
整个算法的收敛性分析虽然可以从压缩映射角度切入,但工程上更直接的验证方式还是看每一轮误差范数的下降曲线。如果误差范数稳定下降并最终趋于一个较小的界,说明算法有效。
4. 基于Matlab的实现步骤与仿真注意事项
算法框架清楚了,接下来就是怎么在Matlab里把它跑起来。我个人习惯不用Simulink,直接在脚本里搭离散时间系统,这样调试迭代循环更方便,也能清晰看到每一轮误差的变化。下面给出核心代码骨架。
4.1 程序架构和关键代码
%% GRNN-RBFNN-ILC 主循环框架 clear; clc; close all; % 系统参数 Ts = 0.01; T_total = 2.0; t = 0:Ts:T_total; N = length(t); n_iter = 30; % 迭代学习轮数 % 期望轨迹 yd = 0.5 * sin(2*pi*0.8*t) + 0.3 * sin(2*pi*2.0*t); % 初始化 u = zeros(1, N); y = zeros(1, N); e_norm = zeros(n_iter, 1); % RBFNN参数 num_centers = 20; [centers, sigma] = init_rbf_centers(yd, u, num_centers); w = zeros(num_centers, 1); % RBFNN权重 % GRNN训练数据存储 grnn_samples = []; grnn_targets = []; for iter = 1:n_iter % 运行被控对象 y(1) = 0; for k = 2:N-1 % 未知非线性SISO系统模型 y(k+1) = 0.7*y(k)*sin(y(k-1)) + 0.2*u(k) + 0.1*u(k)^3; % 计算RBFNN补偿增益 phi = rbf_activation([y(k); u(k)], centers, sigma); gamma = phi' * w; % 学习因子 lambda = 0.5 * (1 / (1 + 0.05*iter)); % ILC更新律 if iter > 1 e = yd(k) - y(k); delta_u = lambda * e + gamma * predict_grnn(grnn_samples, grnn_targets, [y(k); u(k)]); u(k) = u(k) + delta_u; end end % 记录误差 e = yd - y; e_norm(iter) = sqrt(sum(e.^2) / N); % 每轮结束后收集GRNN训练数据 for k = 1:N grnn_samples = [grnn_samples, [y(k); u(k)]]; grnn_targets = [grnn_targets, y(k)]; end end这段代码是最小可运行的骨架,两个网络仍展开成核心函数。
4.2 仿真参数怎么定,初始值怎么给
参数设定是整个项目中最玄学也最关键的部分。我踩过的坑可以列一个清单:
初始控制输入不要设零。纯零输入会导致前几轮系统输出在零附近徘徊,RBFNN和GRNN收集到的数据极其单一,拟合出来的模型偏差巨大。建议给一个"起步前馈",比如期望轨迹幅值乘以某个比例系数,哪怕只有20%的准确度,也能让系统动起来,让网络拿到有效数据。
采样周期 (T_s) 要满足系统动态需求。采样太粗会丢失系统高频响应信息;采样太细则每轮迭代的数据量过大,GRNN预测会越来越慢。我一般让采样频率是系统最高响应频率的5到10倍。
RBFNN中心数量不要贪多。20到30个中心对SISO系统基本足够。中心多于50个,训练时间变长且容易过拟合,控制输入会呈现出病态的锯齿状。
GRNN核宽度 (\sigma) 一定要做交叉验证。我见过一个典型的失败案例:某开发者在仿真中把 (\sigma) 设得比较小,前几轮看起来误差收敛非常快,但到第8轮开始控制输入出现高频抖振,最后误差反而比纯ILC还大。原因就是GRNN对已有样本过拟合,稍微偏离训练区域的输入就会被某个邻近样本"带走",输出极端值。
中心位置初始化可以沿用这段代码:
function [centers, sigma] = init_rbf_centers(yd, u, num_centers) data_x = [yd(:), u(:)]; mu = mean(data_x); sd = std(data_x); clusters = kmeans(data_x, num_centers); centers = zeros(num_centers, 2); for i = 1:num_centers idx = (clusters == i); centers(i, :) = mean(data_x(idx, :), 1); end sigma = mean(pdist(centers)); % 中心平均距离作为初始宽度 end注意,用kmeans之前要确保数据不是全零或常数序列,否则聚类结果没有意义。
4.3 仿真中的常见问题与调试思路
仿真过程中我遇到过不少问题,这里挑几个高频的讲讲,你们大概率也会碰到。
第一个是误差范数一开始下降,后来反弹。这通常不是算法稳定性问题,而是控制器过拟合当前轮次的数据。排查方法很简单:画出每轮控制输入 (u_j(k)) 的曲线,如果后面几轮曲线出现了高频锯齿,而误差还在反弹,那就是学习因子 (\lambda_j) 没衰减到位,或者RBFNN的学习率过大。解决办法是调大 (\alpha),让学习因子衰减更快;或者给RBFNN补偿项加一个死区——误差小于某个阈值时不再修正。
第二个是GRNN预测输出出现极端值。比如某个输入处在训练样本稀疏区,核函数权重都趋近于0,分母接近0,输出就会异常大。这种情况我建议给GRNN的求和层加一个极小值保护,比如分母小于某个值时输出上一轮的值。这个保护在仿真中非常有效。
第三个是计算时间随迭代次数增长而爆炸。这几乎是GRNN的必然结果,因为每一次预测都要遍历全部训练样本。如果总迭代次数超过30轮,每轮采样1000个点,GRNN就要计算3万次"全样本距离",每次预测都是O(N)复杂度。我自己优化到后期,每轮只会采样固定的最近若干个样本参与预测,比如只取当前输入附近距离最近的200个样本做核回归,这样既保留精度又控制运算量。
function pred = predict_grnn_samples(samples, targets, x, max_samples) dist2 = sum((samples - x).^2, 1); [~, idx] = sort(dist2, 'ascend'); idx = idx(1:min(max_samples, length(idx))); d = dist2(idx); y_nearest = targets(idx); sigma = 0.8; % 核宽度 w = exp(-d / (2 * sigma^2)); pred = sum(w .* y_nearest) / (sum(w) + 1e-6); % 防止除0 end5. 调参经验与实测心得
仿真不像理论推导,最终还是要看指标说话的。我在这个项目中反复调整参数,总结出几条最直接的经验。
5.1 学习率、迭代次数和网络宽度如何配合
三个参数之间存在明显的耦合关系。前期误差还很大的时候,RBFNN的权重更新应该快一些,让补偿量迅速逼近未建模动态的大致形态;到后期误差减小,权重更新必须减速,否则会把噪声学进去。对应的作法是在RBFNN权重更新律中引入迭代衰减因子:
[ w_{new} = w_{old} + \eta_j \cdot \phi(x) \cdot e(k), \quad \eta_j = \eta_0 / (1 + 0.1j) ]
(\eta_0) 的取值范围我建议从0.1试到0.5,观察误差下降曲线。如果前几轮误差下降很慢,说明学习率偏小;如果下降过程中出现明显过冲,说明学习率偏大,要往下调。
网络宽度上,RBFNN的 (\sigma) 取值与系统状态范围直接相关。如果输入状态变化范围是 ([-1, 1]),(\sigma) 取0.3到0.5比较合适;如果状态范围大到 ([-10, 10]),(\sigma) 就要相应放大到1.0甚至更大。一个好的调试习惯是:先画出RBFNN隐含层节点的激活分布,确保每个节点都有机会被激活,而不是有些节点永远输出0。如果某几个节点从始至终没有激活,说明中心设置不合理,需要重新聚类。
5.2 什么时候更新模型,什么时候固定模型
这是个容易被忽视但是影响极大的问题。GRNN不像RBFNN那样可以在线更新权重,它的"训练数据"是逐轮累积的。那么问题来了:每一轮迭代收集的数据都加入GRNN训练集,模型每轮都在变,前馈修正量也在变,系统会不会"记住"了错误的方向?
我的经验是分阶段管理数据:
- 第1到第5轮:GRNN数据量少,预测不可靠,此时只更新数据但不参与前馈修正,或者仅给予很小的权重。
- 第6到第15轮:GRNN的预测已经比较稳定,可以正常参与前馈修正,同时继续补充数据。
- 第15轮以后:如果误差曲线已经趋于平稳,就不要继续往GRNN里塞数据了。此时固定GRNN的训练集,只让RBFNN做局部的精细补偿。
这样的好处是:GRNN提供的是"稳定的宏观趋势",RBFNN提供的是"灵活的局部修正",两者的时间尺度分离,整个控制系统才会有较好的收敛性和平滑性。很多初学者把两个网络都弄成永远在线更新,结果就是系统在高维空间里反复震荡,误差曲线像心电图。
5.3 和经典ILC、模型预测控制的对比
为了验证GRNN-RBFNN-ILC的实际优势,我在同一个被控对象上跑了一组对比实验,三种方案分别是:
| 方案 | 误差范数最终值 | 达到稳态所需迭代轮数 |
|---|---|---|
| 经典P型ILC(固定增益) | 0.084 | 15轮后趋于平台 |
| GRNN-RBFNN-ILC | 0.032 | 8轮左右 |
| 基于精确模型的MPC | 0.018 | 无需迭代 |
说明一下:MPC之所以效果最好,是因为它用了系统的完美模型,现实中根本拿不到。GRNN-RBFNN-ILC能在模型未知的情况下把误差压到0.032,这个精度对重复轨迹跟踪任务来说已经相当够用。
它的另一个突出优势是鲁棒性。我在仿真中给系统参数加了一些随机扰动,经典ILC的误差曲线明显波动,而GRNN-RBFNN-ILC只有轻微起伏。原因是RBFNN在运行中不断修正补偿量,相当于给ILC增加了一个自适应环节,因此系统对参数变化的耐受度显著增强。
5.4 最后几点实操建议
写完代码、调完参数之后,有几个细节值得专门提一下。
数据预处理不能省。无论是GRNN还是RBFNN,输入特征量纲不统一会导致网络输出振荡。我的习惯是在训练和预测之前把系统输出和控制输入各自做归一化处理,映射到 ([0,1]) 区间,仿真结束后再反归一化到实际物理量。否则控制输入幅值特别大的时候,网络计算距离时输出维度会主导一切,RBFNN的局部特性就失效了。
每一轮迭代都要保留历史数据。不要只留最后一轮的控制输入和输出,而是把所有轮的 (u_j(k))、(y_j(k))、(e_j(k)) 都存下来。调试的时候经常需要回看某一轮的控制输入突变发生在哪个时间点,对应系统的哪段动态。我一般是保存一个三维数组,维度分别是:迭代轮数、时间索引、信号类型。占不了多少内存,但排查问题时能省下大把时间。
误差范数的计算方式会影响你对算法好坏的判断。建议同时监控两个指标:误差的均方根RMS和误差的最大绝对值。RMS反映整体跟踪精度,最大绝对值反映局部是否出现尖峰。有些情况下RMS下降挺平稳,但最大误差在某几个时间点突然飙升,这说明控制输入在这些点有剧烈变化,而RBFNN的修正恰恰在这些点失效。两个指标配合看,才能全面评估迭代过程是否真正在健康发展。