最近在弄一个工业时序数据的分类预测项目,样本量不大,但序列之间的顺序依赖非常关键。试了一堆传统机器学习模型,精度始终卡在瓶颈上,后来把注意力机制搬过来,提升是有的,但新的麻烦也来了——Transformer的超参数多到让人头疼。编码器层数、注意力头数、嵌入维度、dropout、学习率,随便一个组合变动都能让验证集精度上下波动好几个点。手工调参太看缘分,网格搜索又扛不住训练代价,于是就把目光投向了粒子群优化算法(PSO)。这几个月断断续续在Matlab里把PSO和Transformer拼在一起,做成了一个分类预测模型,效果比手调好不少。今天把这套方案的完整思路、关键代码以及在实操中踩过的坑整理出来,给那些想在Matlab里用Transformer做分类预测、又不想被调参折磨的朋友做个参考。
1. 为什么非要用PSO去优化Transformer:分类预测场景下的两个痛点
1.1 Transformer在分类预测里的优势与参数敏感
Transformer最早是给自然语言处理设计的,但用在序列分类预测上同样成立。它的核心是自注意力机制,可以一次性建模序列中任意两个位置之间的依赖关系,这恰好补上了RNN/LSTM逐步传递信息导致的长程记忆丢失问题,也比CNN只看局部感受野要更全局。对很多分类预测任务来说,比如机械设备振动信号故障分类、电力负荷类别判断、股票涨跌方向预测,模型的输入本质上就是一段序列,Transformer能直接捕捉到关键时间点之间的相互影响,这是它优于传统模型的地方。
但Transformer有个让人头大的特性:参数极度敏感。嵌入维度取32还是64,注意力头数取4还是8,前馈网络维度是128还是256,dropout是0.1还是0.3,初始学习率是1e-3还是1e-4,这些参数互相耦合,而且每一组组合都在非线性地影响最终结果。我自己手调的时候经常遇到这样的情况:把学习率调小,验证集F1涨了1个点,于是兴冲冲去改层数,结果精度又掉回原样。这种“按下葫芦浮起瓢”的体验,经历过的人都懂。如果数据量再小一点,模型对dropout的取值就更敏感,稍微调大一点就欠拟合,调小一点就过拟合,手动搜索空间非常痛苦。
1.2 PSO适合做什么:全局搜索与超参数优化
粒子群优化算法是一种经典的群体智能搜索方法,灵感来自鸟群觅食。每个粒子代表搜索空间中的一个候选解,通常是一组超参数组合,粒子有自己的位置和速度,在每一轮迭代中,粒子会朝着两个方向飞行:一个是它自己历史找到的最优位置(pbest),另一个是整个群体历史找到的最优位置(gbest)。这个过程不需要梯度信息,也不要求目标函数有解析表达式,所以特别适合Transformer这类“参数和精度之间没有闭式函数”的黑箱优化场景。
相比网格搜索,PSO不需要枚举所有组合,计算量小得多,尤其在高维连续空间中优势明显。相比贝叶斯优化,PSO在Matlab里的实现非常简单,不带工具箱也能自己写,而且不容易因为核函数选择不当而失效。我在实际使用中感觉,PSO在搜索维度不超过10的情况下,收敛速度和稳定性都不错,而Transformer主要可调的超参数正好在这个量级。当然,PSO也有自己的毛病,比如容易早熟收敛,但这个问题可以通过调整惯性权重和粒子数来缓解。总体而言,PSO是“时间有限、想自动化调参”时的一个很划算的选择。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 模型搭建前的数据准备与评估口径:这一步决定了PSO能不能收敛
2.1 输入序列的构造方式
不管用什么网络做分类预测,送到Transformer里的输入都必须是固定形状的张量。假设原始数据有N个样本,每个样本是一条长度为L的时间序列,每个时间步有F个特征,那么输入张量尺寸一般是N×L×F,其中N是样本数,L是序列长度,F是特征维度。这里最容易翻车的是把L和F搞混。Transformer内部做的是特征维度的线性变换,注意力计算的是序列位置之间的权重,所以你的数据到底哪个维度代表时间步、哪个维度代表特征,必须从一开始就理清楚。
在Matlab里,深度学习工具箱通常接受numObservations×numTimeSteps×numFeatures的排布(在trainNetwork里也支持cell数组存放变长序列)。如果所有样本等长,直接用数值数组就可以;如果长度不齐,需要用cell数组或者对短序列做zero-padding。分类预测场景下,为了省事,我建议用固定长度的滑动窗口来截取样本。例如一段连续传感器信号,每隔step个点滑动取一个长度L的窗口,窗口对应的标签就是该窗口末端的类别。这样既增加了样本量,又保留了序列顺序信息。
matlab复制% 假设原始信号是signal,对应的类别标签序列是labels
winLen = 48;
step = 10;
numSamples = 0;
for t = 1:step:(length(signal)-winLen+1)
numSamples = numSamples + 1;
end
X = zeros(numSamples, winLen, 1);
Y = categorical(zeros(numSamples, 1));
idx = 1;
for t = 1:step:(length(signal)-winLen+1)
X(idx,:,1) = signal(t:t+winLen-1);
Y(idx) = labels(t+winLen-1);
idx = idx + 1;
end
注意,如果原始数据是多特征,那么第三维就是特征个数,窗口截取的时候要把所有特征都保留下来。
2.2 分类指标的选择与训练/验证集划分
PSO的适应度函数需要量化评估一组超参数的好坏,所以评估指标必须在搜索开始前就定下来。对于二分类,可以用准确率、F1分数、AUC等;多分类则常用总体准确率和宏平均F1。我自己的习惯是使用验证集上的宏平均F1作为适应度,因为实际拿到的分类数据往往类别不平衡,只看准确率很容易被多数类带偏。比如二分类中正类只占10%,模型全部预测负类也能拿到90%的准确率,但F1会很低,这才更真实反映模型性能。
验证集的划分方式在时序任务里是个大坑。千万不要用传统的随机打乱划分,因为相邻时间点的样本高度相关,随机划分会把“未来”信息混进训练集,造成数据泄漏。正确做法是按时间顺序切分:例如前70%的样本做训练集,后30%做验证集。PSO搜索时就在这个验证集上算F1。如果你还有独立的测试集,要等到搜索结束后再做最终评估。我之前在项目里用随机划分跑过一版,PSO搜索出的参数在随机验证集上F1高达0.98,但放到真实滚动预测场景里直接崩塌,后来排查发现是数据泄漏导致的。这一点一定要提前避坑。
2.3 数据标准化与类别权重
Transformer对输入特征的尺度比较敏感,尤其是多特征序列,不同特征的数值范围可能差几个数量级。所以建模前要对每个特征单独做标准化,最常用的是z-score,即减去均值除以标准差。注意,标准化的参数只能在训练集上计算,然后应用到验证集和测试集上,不能在划分数据集之前用全局统计量做标准化,否则同样有信息泄漏。
如果类别严重不平衡,除了用F1做指标外,还可以在分类层中设置类别权重,或者对少数类样本做重采样。但注意,过采样会改变样本的时间顺序,如果用了滑窗生成样本,简单的SMOTE可能会破坏序列的时序结构,所以实现起来要特别小心。在PSO框架下,最简单有效的做法是直接让分类层的ClassWeights向量反映类别频率的倒数,这样模型天然更关注少数类。
3. PSO-Transformer的Matlab实现:从粒子编码到迭代收敛
3.1 粒子编码:把超参数映射成粒子位置
用PSO优化超参数,第一步就是确定每个粒子位置的维度。在分类预测场景下,我选了五个最核心的超参数:嵌入维度d_model、注意力头数nhead、编码器层数numLayers、dropout比率、初始学习率。前馈网络维度可以设置为d_model的固定倍数,比如4倍,这样能少搜一个维度,加速收敛。
粒子位置是一个5维连续向量,每一维对应一个超参数的值或对数变换后的值。注意,PSO里的位置是连续浮点数,而Transformer的超参数有的是整数(头数、层数),有的有明确范围(dropout在0到1之间),所以解码时要做取整和限幅。比如嵌入维度的搜索范围设为[8,64],解码时用round取整后还要保证是偶数,因为多头注意力的head维度通常是d_model/nhead,如果不能整除会报错。学习率建议用log10编码,搜索范围设为[-5,-2],解码时用10^位置值得到实际学习率,这样粒子在-5到-2之间均匀移动时,学习率在每个数量级上的搜索概率相当,比直接搜索1e-5到1e-2的原始值要高效得多。
matlab复制lb = [8, 1, 1, 0.05, -5]; % [d_model, nhead, numLayers, dropout, log10(lr)]
ub = [64, 8, 4, 0.60, -2];
这样编码后,粒子的搜索空间是一个5维超矩形,PSO在这个连续空间里移动,每次评估时再解码成真正使用的超参数。
3.2 适应度函数:用验证集精度驱动搜索
适应度函数是整个PSO框架里最核心的部分,也是计算开销最大的地方。每评估一个粒子,都要完成一次“解码超参数→搭建Transformer→在训练集上训练→在验证集上计算F1”的完整流程。如果单次训练需要1分钟,那一个8粒子的PSO跑10代就是80次训练,80分钟就这么没了,所以这个函数一定要写得干净高效。
我封装了一个psoTransformerFitness函数,输入粒子位置和划分好的数据,输出适应度值。函数内部先解码超参数,再调用一个createTransformerNet函数构建网络,然后用trainNetwork训练,最后用classify得到验证集预测标签并计算宏平均F1。由于粒子群优化默认是求最小值,这里把F1取负号作为适应度,即适应度越小代表模型越好。
matlab复制function fitness = psoTransformerFitness(pos, XTrain, YTrain, XVal, YVal)
% 解码超参数
d_model = round(pos(1));
d_model = d_model + mod(d_model, 2); % 保证偶数
nhead = round(pos(2));
numLayers = round(pos(3));
dropout = pos(4);
lr = 10^pos(5);
% 构建网络
lgraph = createTransformerNet(d_model, nhead, numLayers, dropout, numClasses);
% 固定随机种子,保证同参多次训练结果可复现
rng(42);
options = trainingOptions('adam', ...
'InitialLearnRate', lr, ...
'MaxEpochs', 50, ...
'MiniBatchSize', 32, ...
'Shuffle', 'never', ...
'Verbose', false);
net = trainNetwork(XTrain, YTrain, lgraph, options);
% 验证集预测
YPred = classify(net, XVal);
% 计算宏平均F1
C = confusionmat(YVal, YPred);
numCls = size(C, 1);
f1Sum = 0;
for i = 1:numCls
tp = C(i,i);
fp = sum(C(:,i)) - tp;
fn = sum(C(i,:)) - tp;
precision = tp / (tp + fp + eps);
recall = tp / (tp + fn + eps);
f1Sum = f1Sum + 2*precision*recall / (precision + recall + eps);
end
fitness = -f1Sum / numCls;
end
这里有几个关键点。第一是rng(42)必须在训练前设置,而且trainingOptions里的Shuffle要设为'never',否则每次训练的数据批次顺序不同,同一组超参数也会得到不同精度,适应度函数会产生大量噪声,PSO就很难收敛。第二是confusionmat的输入类别标签必须是完整分类列表,如果验证集里恰好缺少某个类别,要提前用categories补齐。第三是因为训练过程中的随机性不可能完全消除,所以同一个粒子的适应度每次跑会有些微波动,这也是后面要设置随机种子和多次评估的原因。
3.3 主循环:PSO迭代与Transformer训练
我建议不要直接使用particleswarm内置函数,因为内置函数不好控制中间日志,也不支持在训练过程中保存断点。自己写一个标准PSO也就七八十行,可控性反而更强。
标准PSO的流程是:初始化一群粒子,随机位置和速度;对每个粒子计算适应度;更新个体最优pbest和全局最优gbest;然后按速度更新公式改变粒子的速度和位置;重复直到满足最大迭代次数或精度提升小于阈值。
速度更新公式如下:
matlab复制v = w * v + c1 * r1 .* (pbest - x) + c2 * r2 .* (gbest - x);
x = x + v;
其中w是惯性权重,c1和c2是学习因子,r1和r2是[0,1]之间的随机向量。惯性权重w在迭代过程中从0.9线性减小到0.4,前期侧重全局探索,后期侧重局部收敛;c1、c2通常取1.5左右。
matlab复制numParticles = 8;
maxIter = 10;
dim = 5;
wMax = 0.9; wMin = 0.4;
c1 = 1.5; c2 = 1.5;
positions = lb + rand(numParticles, dim) .* (ub - lb);
velocities = -0.1 * (ub - lb) + 0.2 * rand(numParticles, dim) .* (ub - lb);
pbest = positions;
pbestFitness = inf(numParticles, 1);
for it = 1:maxIter
for p = 1:numParticles
fitness = psoTransformerFitness(positions(p,:), XTrain, YTrain, XVal, YVal);
if fitness < pbestFitness(p)
pbestFitness(p) = fitness;
pbest(p,:) = positions(p,:);
end
end
[gbestFit, bestIdx] = min(pbestFitness);
gbest = pbest(bestIdx, :);
w = wMax - (wMax - wMin) * it / maxIter;
for p = 1:numParticles
r1 = rand(1, dim);
r2 = rand(1, dim);
velocities(p,:) = w * velocities(p,:) + c1 * r1 .* (pbest(p,:) - positions(p,:)) + c2 * r2 .* (gbest - positions(p,:));
positions(p,:) = positions(p,:) + velocities(p,:);
positions(p,:) = max(min(positions(p,:), ub), lb);
end
fprintf('Iter %02d, best unweighted F1: %.4f\n', it, -gbestFit);
end
实际运行中,每个粒子都要训练一次Transformer,8个粒子迭代10次就是80次训练。如果数据集不大(万级样本),单次训练在GPU上可能要20到60秒,整体下来至少一两个小时。所以最好先把粒子数和迭代次数设小,比如4个粒子迭代5跑,确认代码能跑通,再放大到8×10甚至12×15。
3.4 断点续跑与结果保存
PSO跑起来时间很长,中途一旦停电或者不小心关了Matlab,就要从头开始,那真是欲哭无泪。所以最好在每一代迭代结束后把当前粒子群状态、pbest和gbest保存到mat文件里,下次直接从文件恢复。
matlab复制save('pso_status.mat', 'positions', 'velocities', 'pbest', 'pbestFitness', 'gbest', 'it');
下次启动时用load检查是否存在该文件,如果存在就从断点继续。这里我还会把每一代的gbest位置和适应度都追加写进日志,方便后面绘制收敛曲线。
4. 关键代码段拆解:从Transformer前向传播到PSO更新
4.1 基于内置层的Transformer网络搭建
Matlab从R2021a开始提供了transformerLayer、positionEmbeddingLayer等内置层,用起来非常方便,不需要自己实现多头注意力。如果你的版本比较老,建议直接升级,因为手写自注意力层虽然也能做,但涉及到前向传播和反向传播的自定义层写法,不仅代码量大,还容易出边界bug,不划算。
网络结构大致是:序列输入层 → 位置嵌入层 → 若干Transformer编码器层 → 序列聚合层 → 全连接层 → softmax → 分类输出层。其中transformerLayer本身支持设置注意力头数、编码器层数、dropout等。位置嵌入层需要指定嵌入维度(即d_model)和最大序列长度。聚合层我用的是globalAveragePooling1dLayer,对序列所有时间步做平均池化,这样不管序列多长,最终输出都是一个固定长度的向量,再接全连接层做分类。
matlab复制function lgraph = createTransformerNet(d_model, nhead, numLayers, dropout, numClasses, maxLen)
layers = [
sequenceInputLayer(1, 'Name', 'input') % 如果是多特征,第一个参数改成特征数
positionEmbeddingLayer(d_model, maxLen, 'Name', 'posemb')
transformerLayer(d_model, nhead, ...
'NumLayers', numLayers, ...
'Dropout', dropout, ...
'Name', 'transformer')
globalAveragePooling1dLayer('Name', 'gapool')
fullyConnectedLayer(numClasses, 'Name', 'fc')
softmaxLayer('Name', 'softmax')
classificationLayer('Name', 'classout')
];
lgraph = layerGraph(layers);
end
这里有一个容易踩的坑:transformerLayer的语法在不同Matlab版本里略有差异,比如dropout参数名是'Dropout'还是'dropout',NumLayers表示的是重复的编码器层数。因此第一次写的时候一定要用doc transformerLayer查清楚当前版本支持的参数名。我身边就有朋友因为参数名大小写问题查了半个下午,最后发现是版本差异。
positionEmbeddingLayer还有一个需要注意的地方:它需要指定maxLength,这个值必须大于等于输入序列的长度,但不能太大,否则会白白增加参数量和显存占用。例如序列长度是48,设置成64就够用,不要一拍脑袋写个500。
4.2 序列聚合方式:平均池化还是取最后时间步?
Transformer的输出仍然是序列,要接分类层,必须先把它变成单个向量。常见做法有两种:一种是所有时间步取平均池化(GAP),另一种是取最后一个时间步的输出。在实验里,我发现平均池化在小样本、类别不平衡的场景下更稳。原因是最后一个时间步的输出容易受序列末端噪声影响,而平均池化把整个序列的信息都利用上了,相当于一个简单的时间维特征融合。
如果用平均池化,Matlab里可以直接用globalAveragePooling1dLayer。如果不用内置层,也可以用自定义层,但没必要。如果你希望做更复杂的聚合,比如注意力池化,那可以再接一个自定义注意力层,但这属于进阶玩法,一般分类预测项目用平均池化就足够了。
4.3 训练选项的设置细节
训练选项的设置在PSO框架里特别重要,因为你要保证同一组超参数在每次评估时的训练条件一致。我常用的trainingOptions设置如下:
matlab复制options = trainingOptions('adam', ...
'InitialLearnRate', lr, ...
'MaxEpochs', 50, ...
'MiniBatchSize', 32, ...
'GradientThreshold', 1, ...
'Shuffle', 'never', ...
'Verbose', false, ...
'Plots', 'none'); % 别开训练曲线图,否则PSO会卡死
Plots一定要设为'none',否则每训练一次就弹一个训练进度窗口,PSO跑十几次你的屏幕就堆满了,甚至会导致Matlab响应变慢。GradientThreshold设了1可以防止梯度爆炸,Transformer在小数据上训练时偶尔会有梯度暴涨的问题,这个设置能帮大忙。
另外,如果训练过程中验证集精度长期不提升,可以设置'ValidationPatience'来做早停,但这会使得每次训练的epoch数不固定,导致同一组参数的适应度不完全可比。所以我更倾向于固定MaxEpochs,不用早停,保证所有粒子都在同样的训练预算下评估。虽然这样可能会让某些参数组合欠拟合,但一致性比单次精度更重要。
4.4 PSO速度与位置更新公式的Matlab写法
前面已经给出了标准PSO的写法,这里再说几个容易被忽略的实现细节。首先是速度限幅:如果不限制速度,粒子有时候会飞得很远,位置被夹到边界上,导致速度更新失去意义。我习惯把速度最大值设为搜索范围的20%,也就是Vmax = 0.2 * (ub - lb),每次更新后都对速度做裁剪。
其次是边界处理。我的做法是直接把位置裁剪到边界内,简单粗暴,但效果不错。另一种做法是让粒子在碰到边界时反弹,但那样会增加计算量,对于超参数优化这种每个评估都很贵的场景,直接裁剪更省事。
最后是惯性权重的设置。如果一开始就用固定权重0.7,可能后期收敛慢;我这里用线性递减,从0.9到0.4,偏向于前期探索、后期精修。如果想更简单,也可以用固定w=0.7,效果差别不会太大。但注意动态调w是白送的好处,何乐而不为。
5. 实验对比与调参心得:PSO跑出来的配置确实比手工靠谱
5.1 基线对比:BP、LSTM、手工Transformer
为了验证PSO-Transformer的效果,我在一个公开的时序二分类数据集上做了对比实验。数据集是某种机械振动信号,样本量1200,序列长度48,特征数是1。手工Transformer是我之前自己试出来的一套“看着顺眼”的参数:d_model=32,nhead=4,layers=2,dropout=0.2,lr=1e-3,验证集宏平均F1约0.84。BP网络在同样的数据上F1只有0.72,LSTM是约0.80。用PSO搜索了6个粒子、8代,总共48次训练后,找到的最好参数是d_model=24,nhead=2,layers=3,dropout=0.31,lr=2.5e-4,验证集F1达到0.88。提升了4个点,这个幅度在分类任务里已经是肉眼可见的差别。
更关键的是,PSO找出来的参数组合看起来并不“直觉”。dropout=0.31比平时我习惯的0.2要高,学习率2.5e-4又明显低于常规默认值。如果靠手工调参,我大概率不会同时往这两个方向试。这说明PSO的价值不只是省时间,更能跳出人工经验形成的路径依赖,探索到一些反直觉但有效的配置。
5.2 粒子数、迭代次数与计算成本的平衡
PSO的粒子数和迭代次数是计算成本的主要来源,也是需要权衡的两个变量。粒子数太少,比如4个,种群多样性不够,很容易早熟收敛到一个局部最优;粒子数太多,比如20个,计算时间翻倍,但最终结果未必比10个粒子更好。我实测下来,对于5维搜索空间,8到12个粒子是比较合适的区间。
迭代次数方面,我是边跑边看收敛曲线。如果后面几次迭代中gbest适应度还在明显下降,说明还没收敛,可以继续加迭代;如果曲线已经平了,再跑下去也是浪费算力。一个实用技巧是:在PSO前期(比如前一半迭代)把训练epoch设小一点,比如20个epoch,用来快速过滤掉明显差的参数;等PSO跑到后期,再对候选参数用50甚至100个epoch精调。这样能节省大量时间,因为前期粒子之间差距很大,用粗略训练也足以区分优劣。
5.3 踩坑记录:数据泄漏、随机种子、显存溢出
这个项目里踩过的坑必须拿出来说一说。
第一个坑就是数据泄漏。前面提过,我最早用随机划分做训练验证集,PSO搜索出来的参数在验证集上F1高到0.98,一上真实测试就崩到0.7。原因就是时序数据相邻样本高度相关,随机划分导致训练集里混进了一大堆验证集样本的“孪生兄弟”。改成按时间顺序划分后,验证集F1回落到0.87,但测试性能稳定了,两者差距很小,这才是正常的。
第二个坑是随机种子不一致导致适应度抖动。Matlab的trainNetwork内部如果用GPU跑,每次执行结果都会有一点随机性,如果不固定rng,那么你在迭代后期同一个粒子可能算出来的F1会有1%甚至2%的波动,PSO直接没法判断谁更优。我后来在适应度函数开头强制rng(42),并把Shuffle设为'never',才让适应度曲线变得平滑。
第三个坑是显存溢出。Transformer的自注意力权重矩阵大小是序列长度的平方,如果你的序列长度很大(比如超过500),即使样本数量不多,GPU显存也可能不够。分类预测任务的序列长度一般不会太长,但如果用了很大的maxLength(比如1000),positionEmbeddingLayer会额外占一块显存。实测下来,把maxLength设置成略大于实际序列长度,能减少不少显存占用,训练速度也会快一些。
5.4 多分类场景下的扩展
上面例子是二分类,多分类同样适用,只需要把输出层的节点数改成类别数,classificationLayer会自动处理softmax交叉熵。但要注意一点:多分类时如果某些类别的样本数量特别少,宏平均F1会很低,PSO会努力去提升少数类的表现,但同时可能降低多数类的准确率。此时可以给分类层设置ClassWeights,把少数类权重调高一些,让PSO的搜索方向更符合业务需求。
另一个多分类的细节是,在适应度函数中使用confusionmat时,必须保证预测标签和真实标签都包含所有类别,否则矩阵维度对不上。我通常会在计算前用double(YVal)和double(YPred),或者用categories(YVal)显式指定所有类别,避免只有一个类别的尴尬情况。
6. 从“能用”到“好用”:PSO-Transformer的进阶扩展方向
6.1 把注意力可视化,检查模型到底学到了什么
PSO把参数搜出来只是第一步,模型能不能用、能不能解释,还需要看注意力权重。Transformer的注意力矩阵可以告诉我们模型在预测时重点关注哪些时间步,这对故障诊断或者医疗信号分类非常有价值。Matlab里想拿到真正的attention权重,比较费劲,因为transformerLayer没有直接输出权重矩阵的接口,需要自定义一个修改版的层。但如果只是验证模型是否合理,可以简化处理:对输入序列做敏感性分析,比如逐点遮蔽时间步,观察预测结果的变化,变化大的时间步就是模型关注的区域。这种方法虽然朴素,但在工程上足够实用。
6.2 引入更多元启发式算法对比:GA、GWO是否比PSO更强
如果是做研究或者写论文,通常需要对比几种优化算法的性能。在同一个适应度函数下,遗传算法(GA)需要设置交叉概率、变异概率,灰狼优化(GWO)没有速度概念,但位置更新公式不同。我在同样的计算预算下试过GA,最终结果和PSO差不多,但GA需要的参数更多,调起来更麻烦。GWO我做过简单实验,收敛速度比PSO快一些,但更容易陷入局部最优,尤其在低维度搜索空间里PSO的表现相对均衡。所以如果是工程落地,我强烈推荐PSO;如果是学术对比,建议至少跑GA、GWO和PSO,再加上贝叶斯优化,把收敛曲线画出来。
6.3 将PSO搜索出的参数作为训练起点,再做一轮精细调参
PSO返回的gbest是一个全局较优的区域,但不一定是局部最优的峰顶。拿到这组参数后,我通常会再做一步操作:以这个位置为中心,把搜索范围缩小到原来的20%,重新跑一遍PSO或者直接在这个小范围内随机采样训练。这种“先粗后细”的两阶段策略在实践上很有效,有时候能再提升0.5到1个点的F1。另外,如果最终部署时计算资源充足,可以把PSO选出的最优参数直接用100个epoch训练到底,通常比搜索时的50个epoch收敛得更充分。
6.4 集成与部署思路
如果分类任务对稳定性的要求很高,一个模型可能不够。一个思路是记录PSO迭代过程中前几名gbest,比如保留top-5的超参数组合,各自训练一个模型,在预测时做投票集成。这样虽然训练成本翻了5倍,但鲁棒性会明显提升。部署到实际系统时,可以把训练好的网络保存为MAT文件,或者用exportNetworkToONNX导出到ONNX格式,再在其他平台推理。这也是Matlab比较方便的一点,训练和部署的生态比较完整。
最后再分享一点个人体会。用PSO优化Transformer在Matlab里确实可行,但不要指望它像魔法一样自动给你一个完美模型。核心还是要把数据划分和适应度函数设计对,否则搜索出来的参数再漂亮也是空中楼阁。另外一个很实在的建议是:先跑小规模验证,用最少的粒子数和迭代次数把流程走通,再放心大胆地加到完整配置。毕竟一次训练就要几十秒,如果中途因为数据格式问题报错,返工成本是非常高的。另外,如果你在跑的过程里发现某些粒子适应度退化非常快,先别急着改代码,检查一下是不是训练时梯度爆炸或者学习率太小,很多时候问题不在优化器本身。总之,这个组合是我目前试下来“自动化调参”里最靠谱的路线,希望这篇分享能帮你少走一些弯路。
