简介:基于粒子群算法优化卷积神经网络(PSO-CNN)的回归预测Matlab源码,面向需要多变量输入回归建模的科研与工程人员,也适合作为深度学习相关课程设计参照。资源将粒子群算法与一维卷积神经网络结合,自动寻优学习率、批大小、正则化系数等关键超参数,省去大量手工调参工作,并提供完整可运行实现。压缩包共5个文件、约17KB,包含4个m脚本与1个xlsx数据集,m脚本分别承担主程序、粒子群初始化、PSO迭代寻优和误差计算等功能,代码注释清晰、结构紧凑,便于替换为自己的多变量数据快速开展实验。评价指标覆盖R²、MAE、MSE、RMSE、MAPE及RPD,示例结果决定系数约0.919,误差指标良好,可直接查看模型拟合效果和误差水平,适合回归预测课题的算法对比与扩展研究。目前已有1268人学习下载,可作为学习PSO-CNN原理及Matlab实现的实用参考资料。
1. PSO-CNN回归预测是什么:多变量输入下的超参数自动寻优
做回归预测时,最烦人的不是建模那一下,而是CNN的卷积核个数、学习率、隐藏层神经元到底该设多少。手动试错一轮就是几十分钟,试到后面全靠玄学。PSO-CNN这个方案,就是拿粒子群优化算法去自动搜CNN的超参数组合,用验证集的回归误差当打分标准,让粒子群替你迭代出「当前数据上最合适的一套超参数」,最后用这套参数重建CNN做多变量输入回归预测。适合做风功率预测、负荷预测、材料性能预测这类多变量回归任务的人,也适合手里有一份Matlab数据、想把预测精度往上再推一截的从业者。下面这套流程,就是我在Matlab里从数据准备到PSO寻优再到最终评估跑通的全过程。
2. 为什么选PSO-CNN而不是纯CNN或BP:卷积在回归里的作用与PSO的优化对象
2.1 多变量回归里的卷积到底在提取什么
卷积神经网络(cnn卷积神经网络)在图像上很出名,但回归预测里它同样有用。多变量输入模型拿到的数据,一行是一个样本,一列是一个特征,比如风功率预测里的风速、温度、湿度、气压。这些特征之间往往存在局部联动关系,风速和温度相邻排列时,卷积核扫过去就是在做局部加权求和,相当于自动提取相邻变量的联合特征。纯BP网络每个输入都独立进全连接层,特征之间的位置关系被彻底打散,而CNN保留了这种局部模式。
Matlab里做这件事有个很方便的等价做法:把每个样本的特征向量排成 1×F 的「单通道图像」,用卷积核高度为1的二维卷积去扫。数学上和一维卷积完全等价,但避开了Matlab里 sequenceInputLayer 和 convolution1dLayer 对数据维度比较挑剔的问题。这样处理后,CNN需要的训练数据量也比LSTM少得多,没有长时依赖问题时完全没必要上LSTM。
2.2 PSO优化的不是CNN权重,是CNN的「形状参数」
这是刚接触PSO-CNN的人最容易绕晕的地方。CNN本身的权重是反向传播负责更新的,粒子群优化算法完全不碰权重。PSO里每个粒子是一个向量,向量每个维度对应CNN一个超参数,一个粒子就是一组超参数组合。拿回归预测来说,我常优化的参数是:
| 超参数 | 搜索范围 | 说明 |
|---|---|---|
| 初始学习率 | 1e-4 ~ 1e-2 | Adam优化器初始学习率 |
| 第一层卷积核个数 | 4 ~ 32 | 卷积层输出通道数 |
| 卷积核大小 | 3 ~ 9 | 沿特征方向的扫过宽度 |
| 全连接层神经元数 | 8 ~ 128 | 回归头之前的隐层宽度 |
| L2正则系数 | 1e-6 ~ 1e-2 | 防止过拟合 |
| MiniBatchSize | 16 ~ 64 | 每次迭代样本数 |
粒子群优化的过程是:每个粒子解码成一组超参数,调用CNN训练一次,把训练好的模型在验证集上算RMSE,这个RMSE就是粒子的适应度。粒子群根据自己历史最优位置和群体历史最优位置更新速度,一代代逼近误差最小的超参数组合。
2.3 网格搜索和贝叶斯优化为什么不如PSO顺手
网格搜索在这个场景下有个硬伤:上面6个参数,每个取5个水平,就是5的6次方一万五千多次训练,每次都完整跑CNN,基本不可行。贝叶斯优化理论上调用次数少,但对离散参数(卷积核个数)和连续参数(学习率)混合的搜索空间处理起来很麻烦,而且Matlab内置bayesopt配合trainNetwork使用时,每次拟合都要重新构建网络对象,代码绕。
PSO的优势在于实现简单、对参数类型不敏感、天然支持并行。Matlab里particleswarm函数可以直接用,但完整源码方案我一般手写粒子群主循环,因为要控制惯性权重递减、速度钳制这些细节,内置函数反而像个黑匣子。手写版本配合并行池,每个粒子的CNN训练分配到不同worker上,一轮迭代十几个粒子几分钟就能跑完。
3. 多变量输入的数据准备:归一化、划分、reshape成CNN能吃的形状
3.1 数据组织:从N×F表格到1×F×1×N的图像格式
多变量输入数据通常存在CSV或Excel里,列是特征,最后一列是要预测的y。读取后用table2array转成数值矩阵,再切成X和y。这里有个关键操作:Matlab的imageInputLayer要求输入是 H×W×C×N 四维数组,H是高度,W是宽度,C是通道数,N是样本数。我们把每个样本特征向量排成一行,通道数设为1,所以整个训练输入要reshape成 1×F×1×N,即每张「图」是1行F列。
% 读取数据,假设最后一列是目标值 data = readmatrix('multi_variable_data.csv'); X = data(:, 1:end-1); % N×F 特征矩阵 y = data(:, end); % N×1 目标值 N = size(X, 1); F = size(X, 2); % 转置后reshape成 imageInputLayer 要求的四维格式 % X'是F×N,reshape成 1×F×1×N,每个样本变成1行F列的"图像" X4D = reshape(X', [1, F, 1, N]); % 目标列向量保持N×1即可 y = y(:);这段代码里有两个参数需要按自己数据改:readmatrix的文件路径,以及X4D里F这个维度,它必须和后面网络定义的输入层尺寸完全一致。reshape的写法是固定套路,先转置再reshape是为了让第i个样本的特征填满X4D(1,:,1,i)。如果数据里包含时间戳或类别列,读进来之前先在Excel里删掉。
3.2 归一化必须在训练集上拟合,验证集和测试集只能被变换
多变量输入的各个特征量纲差异很大,温度是几十,风速是个位数,功率是几千,不归一化直接喂给CNN,梯度更新会被大数值特征主导。Matlab里常用mapminmax把数据缩放到[-1,1]。
% 先划分再归一化,顺序不能反 rng(42); idx = randperm(N); trainIdx = idx(1:round(0.7*N)); valIdx = idx(round(0.7*N)+1:round(0.85*N)); testIdx = idx(round(0.85*N)+1:end); % 只在训练集上估计min和max [XtrainNorm, ps] = mapminmax(X(trainIdx, :)', -1, 1); XtrainNorm = XtrainNorm'; % 验证集和测试集用同一组参数做变换 XvalNorm = mapminmax('apply', X(valIdx, :)', ps)'; XtestNorm = mapminmax('apply', X(testIdx, :)', ps)'; YtrainNorm = mapminmax(y(trainIdx)', -1, 1)'; YvalNorm = mapminmax('apply', y(valIdx)', ps_y)'; YtestNorm = mapminmax('apply', y(testIdx)', ps_y)';踩过的坑是有人直接在全体数据上调用一次mapminmax,再划分训练集测试集。这样测试集的min和max混进了归一化参数,属于信息泄露,最后测试集指标会虚高。ps结构体里保存的就是每行的最小值xmin和缩放因子,测试集变换时只做 apply 不做 fit。目标值y也要单独做一套归一化参数ps_y,后面反归一化预测结果时用。
3.3 交叉验证在这里不适用:PSO打分器与最终考试要分开
回归预测里有人习惯用K折交叉验证评估模型,但在PSO-CNN方案里我一般不用交叉验证。粒子群迭代二三十次,每次都要训练CNN,再乘上K折,计算量直接乘K倍,多数机器扛不住。更合理的做法是分三段:训练集、验证集、测试集。验证集专门给PSO当打分器,每个粒子的适应度都在同一个验证集上算,保证粒子之间比较公平。测试集从头到尾不参与任何PSO迭代,只在最后用最优超参数重建模型后测一次。
这个划分比例我常用70/15/15。数据量特别少时(比如几百条),验证集比例可以提到20%,但测试集至少要留10%,否则最后评估的置信度不够。对于时序数据,比如按小时排列的负荷数据,划分时不能用randperm随机打乱,必须按时间顺序前70%训练、中间15%验证、最后15%测试,不然就是穿越预测。
4. PSO-CNN在Matlab里的完整实现:从CNN建网函数到粒子群主循环
4.1 把CNN回归网络写成可参数化的buildCNN函数
手写PSO时,每个粒子都是一组超参数,意味着要反复构建结构相同但参数不同的CNN。直接在主脚本里堆网络代码会变得没法维护,正确做法是封装成一个函数,输入超参数结构体,输出训练好的网络:
function [net, info] = buildCNN(params, X4D, Y, XVal4D, YVal) % 输入层:1×F×1,features是全局变量或参数传入 layers = [ imageInputLayer([1 size(X4D, 2) 1], 'Normalization', 'none') convolution2dLayer([1 params.filterSize], params.numFilters, ... 'Padding', 'same', 'Name', 'conv1') reluLayer fullyConnectedLayer(params.hiddenUnits) fullyConnectedLayer(1) regressionLayer ]; options = trainingOptions('adam', ... 'InitialLearnRate', params.lr, ... 'L2Regularization', params.l2, ... 'MiniBatchSize', params.miniBatchSize, ... 'MaxEpochs', params.maxEpochs, ... 'ValidationData', {XVal4D, YVal}, ... 'ValidationPatience', 8, ... 'Verbose', 0); [net, info] = trainNetwork(X4D, Y, layers, options); endbuildCNN函数里值得注意的点:Normalization要设成none,因为数据在外面已经做完了归一化,网络内部再做一次白化反而干扰;Padding用same保证卷积输出尺寸不变,这样全连接层输入维度可预测;回归任务最后一层用fullyConnectedLayer(1)接regressionLayer。trainingOptions里ValidationPatience设8,意思是在验证集误差连续8轮不下降就提前结束训练,这一步能省掉后面粒子群迭代的大量时间。
4.2 手写PSO主循环:粒子解码、训练、打分、更新
粒子群主循环是整个源码的核心。每个粒子位置是一个d维向量,d等于要优化的超参数个数。粒子位置的每个维度都做了归一化映射到[0,1],解码时线性映射回真实超参数范围。这么做的原因是PSO的速度更新公式对不同量纲的参数敏感,学习率是0.001量级,卷积核个数是32量级,直接混在一起算,学习率那维基本学不动。
function [gBest, gBestScore] = psoCNN(objfun, dim, lb, ub, swarmSize, maxIter) % 初始化粒子位置和速度,位置在[0,1]区间 pos = rand(swarmSize, dim); vel = zeros(swarmSize, dim); pBest = pos; pBestScore = inf(swarmSize, 1); gBest = pos(1, :); gBestScore = inf; wStart = 0.9; wEnd = 0.4; % 惯性权重线性递减 c1 = 1.49; c2 = 1.49; % 个体学习因子和群体学习因子 for iter = 1:maxIter w = wStart - (wStart - wEnd) * iter / maxIter; % 逐个粒子解码并训练 for i = 1:swarmSize params = decodeParams(pos(i, :), lb, ub); rmse = objfun(params); % 训练CNN并返回验证集RMSE if rmse < pBestScore(i) pBestScore(i) = rmse; pBest(i, :) = pos(i, :); end if rmse < gBestScore gBestScore = rmse; gBest = pos(i, :); end end % 更新速度和位置,钳制位置到[0,1] for i = 1:swarmSize vel(i, :) = w * vel(i, :) ... + c1 * rand(1, dim) .* (pBest(i, :) - pos(i, :)) ... + c2 * rand(1, dim) .* (gBest - pos(i, :)); pos(i, :) = pos(i, :) + vel(i, :); pos(i, :) = max(0, min(1, pos(i, :))); end end endPSO更新公式三个部分的含义:w×vel是惯性项,维持粒子当前运动方向;c1×rand×(pBest - pos)是个体认知项,把粒子拉向自己历史上最优点;c2×rand×(gBest - pos)是群体认知项,把粒子拉向全局最优点。惯性权重从0.9降到0.4是标准做法,前期全局搜索、后期局部精搜。decodeParams函数把[0,1]映射到真实范围,比如学习率用 10.^(-4 + pos2),卷积核个数用 round(4 + pos28)。
这里的objfun是核心中的核心,它在内部完成超参数解码、调用buildCNN训练、在验证集上计算RMSE并返回。需要注意的是objfun每次调用都会完整跑一次trainNetwork,这一步通常是最耗时的。如果Matlab有Parallel Computing Toolbox,可以把for循环改成parfor,但要注意worker上要能访问到数据文件和函数文件。
4.3 用最优超参数重建CNN并完成最终评估
粒子群迭代结束后,gBest就是找到的最优超参数组合。很多人的做法是直接拿粒子群里最后一次训练的模型去测测试集,这其实不严谨。因为那个模型是在验证集上挑出来的,训练集和验证集的数据没有完全用足。正确做法是用gBest解码出来的超参数,在训练集加验证集的合并数据上重新训练一次,然后再评估测试集:
% 解码最优粒子 bestParams = decodeParams(gBest, lb, ub); % 合并训练集和验证集,重新训练最终模型 X4DAll = cat(4, Xtrain4D, Xval4D); YAll = [YtrainNorm; YvalNorm]; % 这里的maxEpochs可以比粒子群内部训练更大,收敛更充分 bestParams.maxEpochs = 300; [finalNet, ~] = buildCNN(bestParams, X4DAll, YAll, Xtest4D, YtestNorm); % 测试集预测并反归一化 YpredNorm = predict(finalNet, Xtest4D); Ypred = mapminmax('reverse', YpredNorm, ps_y); YtestReal = mapminmax('reverse', YtestNorm, ps_y); rmse = sqrt(mean((Ypred - YtestReal).^2));这里cat(4, ...)的作用是把训练集和验证集在样本维度拼接,因为X4D的第四维是样本数。最终模型在更多数据上训练,泛化能力通常比粒子群内部的模型更好。predict函数在R2021之后的版本直接传四维数组就行。最后用ps_y把预测值反归一化回真实量纲,再和真实值算RMSE。
5. PSO-CNN踩坑与排查:5个让结果翻车的常见问题
5.1 不固定随机种子:PSO迭代曲线像心电图
现象:粒子群迭代过程中,同一个粒子重复评估时适应度忽高忽低,导致PSO分不清是超参数变好了还是随机波动。严重时gBest在迭代后期还会被一个偶然的低RMSE样本带偏。
原因:CNN权重初始化是随机的,mini-batch采样也是随机的,只要rng种子不固定,同样的超参数每次训练结果都不一样。PSO把这种噪声当成了优化信号,收敛会非常不稳。
解决:所有随机源统一控制。脚本开头rng(42)固定全局种子,trainNetwork内部的数据打乱也基于这个种子。如果随机性还是大,把适应度改成同一组超参数训练3次的平均RMSE,虽然耗时但显著平滑。我一般在首次粗搜时用单次训练,锁定最优区域后细搜阶段改成3次平均。
5.2 每个粒子都从头训满epochs:一晚上跑不出结果
现象:PSO设了20个粒子、20次迭代,每个粒子训500轮,跑了一晚上还在第3次迭代。
原因:CNN每训练一轮要看一遍全部训练数据,500轮是很重的开销。粒子群每轮都要训练20个CNN,计算量直接是20×20=400次完整训练。
解决:训练参数上做三处收敛控制。第一,MaxEpochs先限制在150以内;第二,ValidationPatience设8~10,验证集误差不降就提前终止;第三,粒子数先从10个起步,第一次迭代先用粗网格定位最优超参数的大致区域,第二次迭代把搜索范围缩到最优值附近再细搜。另外,粒子群的目标是找超参数趋势,不是找精度极限,所以粒子群内部训练的epochs可以比最终模型小很多。
5.3 归一化混用全量数据:测试集信息提前泄露
现象:测试集RMSE低得离谱,但模型一上真实新数据就翻车,误差比训练时大一倍还多。
原因:归一化时用了全体数据的min和max,测试集的分布特征在训练阶段就以归一化参数的形式偷偷参与了建模,相当于模型提前见过测试集的边界。
解决:严格按第3章的顺序,mapminmax只在训练集上fit,验证集和测试集只apply。用ps结构体记录训练集的xmin和xmax,新数据进来时也要沿用这套参数。这个坑在数据划分之后做归一化才能根本上规避。
5.4 卷积核尺寸覆盖整个特征宽度:维度报错与信息冗余
现象:trainNetwork报错说卷积输出尺寸和全连接输入尺寸对不上,或者模型能训练但训练集误差一直下不去。
原因:filterSize设得太大,比如特征总共10个,卷积核就设了9,卷积一次就把全部特征压成一个数,局部特征提取完全失效。卷积核宽度覆盖整个特征宽度时,CNN退化成了全连接层,而且参数量暴涨。
解决:卷积核大小建议限制在特征总数的三分之一以内,我常用3、5、7三档让PSO选。Padding设same可以避免维度报错,但即便维度不报错,信息冗余问题也看不出来,要靠训练集误差反馈。
5.5 粒子早熟收敛:所有粒子挤在一起,RMSE不再下降
现象:迭代到第10代左右,所有粒子的位置几乎重叠,gBestScore连续多轮不变,适应度曲线变成一条平线。
原因:标准PSO在惯性权重固定不变时,粒子一旦都收敛到局部最优点附近,速度会衰减到接近0,缺乏跳出局部最优的机制。
解决:两个手段。一是惯性权重线性递减,让粒子群前期保持高探索性,这是解决早熟最有效的办法;二是给粒子加变异机制,每隔几轮随机挑一个粒子重新随机初始化位置,就像遗传算法里的变异操作。我实际跑下来,变异幅度控制在搜索范围的5%以内,只在前期有效,后期变异反而会破坏收敛精度。
6. 结果怎么验收:R2、RMSE、MAPE三指标与残差散点图
6.1 多指标一起看:loss曲线会骗人
粒子群迭代结束后不要急着下结论,只看训练loss曲线是完全不够的。训练loss下降只能说明模型在训练集上拟合得好,回归预测真正要关心的是测试集上的泛化表现。我每次都在测试集上同时算三个指标:RMSE看绝对误差大小,MAPE看相对误差百分比,R2看模型对真实方差的解释程度。
Ypred = mapminmax('reverse', YpredNorm, ps_y)'; Ytest = mapminmax('reverse', YtestNorm, ps_y)'; n = length(Ytest); rmse = sqrt(mean((Ypred - Ytest).^2)); mape = mean(abs((Ypred - Ytest) ./ Ytest)) * 100; SSres = sum((Ytest - Ypred).^2); SStot = sum((Ytest - mean(Ytest)).^2); r2 = 1 - SSres / SStot; fprintf('RMSE=%.4f, MAPE=%.2f%%, R2=%.4f\n', rmse, mape, r2);某次实际预测结果里RMSE看着还行,但MAPE偏高,一查是少数小数值样本的预测偏差被相对误差放大了。三个指标里任何一个异常都要排查,尤其是MAPE高但RMSE低,通常是模型对小值样本拟合差。
6.2 真实值-预测值散点图与残差系统性偏移
量化指标之外,散点图能看出指标看不出的东西。画真实值为横轴、预测值为纵轴的散点图,点越贴近y=x线说明预测越准。如果点整体平行偏离对角线,说明存在系统性偏差,通常是数据归一化或反归一化时ps_y用错;如果点在对角线两侧呈喇叭状散开,说明异方差性,小值预测稳、大值预测飘。
我最初做PSO-CNN时只看RMSE就以为模型没问题,后来画了残差图才发现大值区间预测偏低。排查结果是训练数据里大值样本占比太少,CNN对大值区间拟合不充分。解决方式是数据加权或对大值样本过采样,这一轮优化后测试集R2提升了近5个百分点。这也是我到现在都坚持每个回归模型都画残差图的原因。模型验证不是看一次指标就完事,多指标加残差检查,才能确认这个PSO-CNN模型是真的能用于生产,希望帮到你。
本文还有配套的精品资源,点击获取