简介:一份基于MATLAB实现的粒子群优化算法源代码包,聚焦PSO改进K-means聚类与BP神经网络训练,适合需要完成聚类分析、神经网络调优或算法对比研究的本科生、研究生及工程师。针对K-means对初始质心敏感、BP网络易陷入局部极小的问题,资源给出了完整可运行的优化方案。压缩包共6个文件,全部为.m脚本,整体仅6KB,包含粒子更新、左右交叉算子、改进K-means主程序及PSO主循环等模块,代码紧凑,便于逐段阅读与二次开发。目前已有755人学习下载,验证了其实用热度。通过该资源,读者不仅能观察粒子群搜索最优质心、优化网络权重的实现逻辑,还能自由调整种群规模、惯性权重、迭代次数等参数,直观分析不同设置对聚类轮廓系数和网络误差的影响,为进一步改进算法或撰写实验章节提供了可复现的实验模板。
1. 为什么把 PSO 塞进 K-means 和 BP 之前,先得看清两个局部最优困局
PSO 优化 K-means、PSO 优化 BP 神经网络,是 MATLAB 课设和算法对比实验里出现频率很高的组合。K-means 的质心初始化直接决定聚类质量,BP 网络的反向传播又极度依赖权重初值,两个算法都会在局部最优里打转。粒子群优化(PSO)不依赖梯度,用一群粒子在解空间里协作搜索,正好用来给 K-means 找初始质心、给 BP 找初始权重。这篇笔记会给出两条可复现的 MATLAB 实现路径、参数选择和常见翻车点,适合正在做聚类、回归预测或课程设计的读者直接抄作业。读完之后你能回答三个问题:粒子怎么编码,适应度怎么定,跑挂了看哪里。
2. 用 PSO 替 K-means 找初始质心:粒子编码与 MATLAB 实现
2.1 K-means 的初始质心为何值得再搜一遍
K-means 本质上是一种坐标下降算法,每次迭代交替做两件事:把样本分给最近的质心,再把质心挪到当前簇的平均位置。这个流程保证 SSE 单调不增,但收敛路径完全由初始质心位置决定。初始质心如果扎堆落在数据密集区的一侧,另一侧的点就会被硬生生划进较远的簇,最终 SSE 降不下去,聚类结果怎么看怎么别扭。
常见的补救方案是“随机重启”——用kmeans的Replicates参数多跑几次,取最小 SSE。这本质上是碰运气,靠增加实验次数来覆盖糟糕的初始位置。数据维数低、簇结构清晰的时候效果尚可,但一旦数据维度超过二十,随机重启的命中率就明显下降,等于是把命运交给随机种子。
PSO 优化 K-means 的思路不是替代交替迭代,而是把“找初始质心”当成一个连续优化问题。具体做法是把 K 个质心拼成一个长为 K×d 的粒子向量,粒子群里每个个体代表一组候选质心。适应度函数取样本到最近质心距离的平方和,也就是 SSE。粒子群在这个连续空间里搜索若干轮后,把最优粒子还原成 K 个质心,交给 K-means 继续迭代。
这个方案在中小规模数据集上很实用,几百到几千样本、几十维以内的数据,PSO 阶段跑几十轮就能找到明显优于随机重启的质心初始位置。数据量过万之后,每次适应度计算都要做一遍完整距离矩阵,成本会明显上升,所以我会在后面的参数章节专门说采样加速的做法。
2.2 粒子编码与 SSE 适应度函数
编码方式只有一个原则:在代码里固定展开和还原的顺序。我习惯按行优先展开,即把质心矩阵按行拉直成向量。还原时执行reshape(particle, K, d),得到的就是 K×d 的质心矩阵。这个顺序从初始化到适应度计算必须保持一致,否则相当于粒子在乱飞。
适应度函数单独写成一个文件,方便复用。下面这个函数接收样本矩阵X、粒子向量particle和簇数K,返回 SSE:
function cost = sseCost(X, particle, K) d = size(X, 2); centers = reshape(particle, K, d); distMat = pdist2(X, centers); % 每行一个样本到 K 个质心的距离 [minDist, ~] = min(distMat, [], 2); % 取最近质心的距离 cost = sum(minDist.^2); % SSE,注意距离要平方 endpdist2输出是一个 n×K 矩阵,第 i 行第 k 列表示第 i 个样本到第 k 个质心的欧氏距离。min(..., [], 2)沿第二维取最小值,得到每个样本离最近质心的距离。最后平方求和,这就是 K-means 目标函数的原始形式。
这里有个容易踩的小坑:有同学会把适应度换成轮廓系数,因为觉得 SSE 只考虑紧密度、不考虑分离度。但轮廓系数是越大越好,直接塞进 PSO 会让搜索方向反掉。要么取负数,要么老老实实用 SSE。我一般只在最终对比评估时才看轮廓系数,PSO 搜索阶段统一用 SSE,简单且不会出方向性错误。
2.3 PSO 主循环:完整代码与边界处理
下面是一段可以直接跑的 PSO 主循环,函数用arguments块声明默认参数。这个语法要求 MATLAB R2019b 及以上,版本旧的话把函数头改成普通传参形式就行。
function [bestCenters, bestCost, history] = psoKmeans(X, K, opts) arguments X double K {mustBeInteger, mustBePositive} opts.swarmSize = 30 opts.maxIter = 100 opts.wStart = 0.9 opts.wEnd = 0.4 opts.c1 = 1.5 opts.c2 = 1.5 end [n, d] = size(X); minV = min(X, [], 1); maxV = max(X, [], 1); dim = K * d; % 将每个变量的上下界映射到粒子对应维度 lowerBound = minV(rem(0:dim-1, d) + 1); upperBound = maxV(rem(0:dim-1, d) + 1); % 初始化粒子位置和速度 swarm = rand(opts.swarmSize, dim) .* (upperBound - lowerBound) + lowerBound; vel = randn(opts.swarmSize, dim) * 0.1; pbestPos = swarm; pbestCost = inf(opts.swarmSize, 1); for i = 1:opts.swarmSize pbestCost(i) = sseCost(X, swarm(i,:), K); end [gbestCost, gbestIdx] = min(pbestCost); gbestPos = pbestPos(gbestIdx, :); history = zeros(opts.maxIter, 1); for t = 1:opts.maxIter w = opts.wStart - (opts.wStart - opts.wEnd) * t / opts.maxIter; for i = 1:opts.swarmSize r1 = rand(1, dim); r2 = rand(1, dim); vel(i,:) = w * vel(i,:) ... + opts.c1 * r1 .* (pbestPos(i,:) - swarm(i,:)) ... + opts.c2 * r2 .* (gbestPos - swarm(i,:)); % 速度上限限制在边界范围的 20%,防止粒子飞过头 velMax = 0.2 * (upperBound - lowerBound); vel(i,:) = min(max(vel(i,:), -velMax), velMax); swarm(i,:) = swarm(i,:) + vel(i,:); % 越界维度拉回边界 overLow = swarm(i,:) < lowerBound; overHigh = swarm(i,:) > upperBound; swarm(i, overLow) = lowerBound(overLow); swarm(i, overHigh) = upperBound(overHigh); cost = sseCost(X, swarm(i,:), K); if cost < pbestCost(i) pbestCost(i) = cost; pbestPos(i,:) = swarm(i,:); end if cost < gbestCost gbestCost = cost; gbestPos = swarm(i,:); end end history(t) = gbestCost; end bestCenters = reshape(gbestPos, K, d); bestCost = gbestCost; end几个参数这么设是有原因的。粒子数 30 到 40 对于 K*d 在 6 到 240 维的搜索空间来说足够覆盖,超过 80 个粒子在中小数据集上只是把计算时间拉长,收敛精度的提升很有限。惯性权重从 0.9 线性衰减到 0.4,这是一个广泛使用的配置,前期保持全局搜索能力,后期缩小步长做局部磨精细。学习因子c1和c2都取 1.5,个体经验和群体经验权重相等。
边界处理采用拉回策略,越界的维度直接赋到边界值。另一种做法是让粒子反弹、速度取反,效果差不多,但拉回边界更符合直觉——质心不可能跑到数据范围外面去。速度上限我加了 0.2 倍边界范围,不加的话,粒子前期容易飞出搜索空间,拉回边界后又继续超高速飞行,浪费大量迭代。
history数组记录了每一轮的全局最优 SSE,这个在后面验证收敛时会用到。
2.4 与内置 kmeans 函数衔接
PSO 搜索结束后,最优粒子已经是一组不错的质心位置。这时候有两种选择:直接用这组质心作为聚类结果,或者把它作为初始值再跑一轮内置kmeans。后者更常见,因为就算初始质心质量很高,K-means 继续迭代也不会更差,反而能把质心微调到精确位置。
opts = struct('swarmSize', 40, 'maxIter', 120); [initCenters, ~] = psoKmeans(X, 4, opts); % 用 PSO 结果作为初始质心 [clusterIdx, sumD] = kmeans(X, 4, 'Start', initCenters, 'MaxIter', 500); disp(['PSO+Kmeans SSE = ', num2str(sumD)]);kmeans的Start参数接受 K×d 的质心矩阵,维度刚好和bestCenters一致,直接传进去即可。MaxIter我给到 500,虽然初始质心好时实际迭代次数往往不到几十轮,但放宽上限可以避免个别数据集收敛慢时提前截断。
需要注意一个致命的细节:传给Start的矩阵里不能有 NaN。kmeans检测到 NaN 不会报错,而是默默改用随机初始化。也就是说你的 PSO 结果根本没被用到,SSE 还看起来正常,这个坑非常隐蔽。
3. PSO 优化 BP 神经网络:权重预训练加两级微调流程
3.1 两阶段训练:PSO 预搜索 + BP 微调
BP 神经网络的训练本质是梯度下降。误差曲面在权值空间中异常崎岖,局部极小点多、平坦区也不少。随机初始化的权重一旦落在某个局部盆地里,梯度下降就只能一路滑到盆底,几乎没有机会跳出来。常用的多随机种子策略是跑 N 次完整训练选最优,但计算成本直接乘以 N,训练时间在大数据集上非常难看。
PSO 优化 BP 的常见做法是两阶段训练:第一阶段用 PSO 在权值空间做有限轮次的全局搜索,评估粒子的目标函数是训练误差;第二阶段把 PSO 搜出的最优粒子还原成网络权重,再交给 BP 自身的反向传播做精细微调。
这和“用 PSO 完全替代 BP训练”是两回事。完全替代意味着每一轮都要计算所有粒子的前向传播和误差,计算量通常是单次 BP 迭代的粒子数倍,而且 PSO 在光滑局部区域的精细调参能力确实不如梯度下降。两阶段的组合拳才是性价比最高的方案:PSO 负责找到误差曲面上的一个优质山谷入口,梯度下降负责在山谷里快速下滑到谷底。
3.2 粒子维度展开与适应度函数(带验证集混合)
粒子维度等于网络所有权值和偏置的总个数。以一个三层网络为例:输入层 4 个节点、隐藏层 8 个节点、输出层 2 个节点,那么 W1 是 8×4、b1 是 8×1、W2 是 2×8、b2 是 2×1,展开后粒子维度就是 32+8+16+2=58。这里的关键还是展开顺序要固定,W1、b1、W2、b2 按顺序拼成一个向量,还原时也按同样的偏移量切回去。
适应度函数我这里做了一个关键改进:不直接用训练集 MSE。只用训练集误差做适应度,PSO 会专门挑那些在训练集上表现好的粒子,代价是泛化能力差的权重也混杂其中。操作上我把训练集和验证集的 MSE 按 0.7 和 0.3 的权重混合,作为粒子适应度。这样在 PSO 阶段就在兼顾泛化,后续 BP 微调不容易翻车。
function fitness = bpFitness(paramVec, Xtr, Ttr, Xva, Tva, dimsStruct) w1Len = dimsStruct.hiddenSize * dimsStruct.inputSize; b1Len = dimsStruct.hiddenSize; offset = w1Len + b1Len; w2Len = dimsStruct.outputSize * dimsStruct.hiddenSize; w1 = reshape(paramVec(1:w1Len), dimsStruct.hiddenSize, dimsStruct.inputSize); b1 = paramVec(w1Len+1 : offset); w2 = reshape(paramVec(offset+1 : offset+w2Len), ... dimsStruct.outputSize, dimsStruct.hiddenSize); b2 = paramVec(offset+w2Len+1 : end); % 训练集前向传播 h1 = max(0, Xtr * w1' + b1'); % ReLU 激活 ytr = h1 * w2' + b2'; mseTr = mean(sum((ytr - Ttr).^2, 2)); % 验证集前向传播 h2 = max(0, Xva * w1' + b1'); yva = h2 * w2' + b2'; mseVa = mean(sum((yva - Tva).^2, 2)); fitness = 0.7 * mseTr + 0.3 * mseVa; end隐藏层激活函数在 PSO 阶段用 ReLU,方便计算而且不容易饱和。如果你的数据集很小,换成 tanh 在 BP 微调阶段可能更平滑,但 PSO 搜索阶段对激活函数不敏感,差异不大。
3.3 完整训练流程代码与参数说明
下面是把 PSO 预训练和 BP 微调串起来的完整流程。数据按 70/30 划分训练集和验证集,验证集在 PSO 阶段用于适应度混合,在 BP 阶段用于早停判断。
rng(2024); idx = randperm(size(X, 1)); splitIdx = ceil(0.7 * size(X, 1)); Xtr = X(idx(1:splitIdx), :); Ttr = T(idx(1:splitIdx), :); Xva = X(idx(splitIdx+1:end), :); Tva = T(idx(splitIdx+1:end), :); dimsStruct.inputSize = size(Xtr, 2); dimsStruct.hiddenSize = 8; dimsStruct.outputSize = size(Ttr, 2); dim = dimsStruct.inputSize * dimsStruct.hiddenSize + dimsStruct.hiddenSize ... + dimsStruct.hiddenSize * dimsStruct.outputSize + dimsStruct.outputSize; % PSO 参数 swarmSize = 40; maxIter = 80; lower = -2 * ones(1, dim); upper = 2 * ones(1, dim); swarm = lower + rand(swarmSize, dim) .* (upper - lower); vel = randn(swarmSize, dim) * 0.05; pbestPos = swarm; pbestCost = inf(swarmSize, 1); for i = 1:swarmSize pbestCost(i) = bpFitness(swarm(i,:), Xtr, Ttr, Xva, Tva, dimsStruct); end [gbestCost, bestIdx] = min(pbestCost); gbestPos = pbestPos(bestIdx, :); for t = 1:maxIter w = 0.9 - t * (0.4 / maxIter); for i = 1:swarmSize r1 = rand(1, dim); r2 = rand(1, dim); vel(i,:) = w * vel(i,:) ... + 1.5 * r1 .* (pbestPos(i,:) - swarm(i,:)) ... + 1.5 * r2 .* (gbestPos - swarm(i,:)); % 速度限制为边界范围的 5%,BP 权重场景下不宜飞太大 velMax = 0.05 * (upper - lower); vel(i,:) = min(max(vel(i,:), -velMax), velMax); swarm(i,:) = swarm(i,:) + vel(i,:); swarm(i,:) = min(max(swarm(i,:), lower), upper); cost = bpFitness(swarm(i,:), Xtr, Ttr, Xva, Tva, dimsStruct); if cost < pbestCost(i) pbestCost(i) = cost; pbestPos(i,:) = swarm(i,:); end if cost < gbestCost gbestCost = cost; gbestPos = swarm(i,:); end end end搜索结束后还原参数并组装 MATLAB 神经网络对象,用train做微调:
% 还原参数 w1Len = dimsStruct.hiddenSize * dimsStruct.inputSize; b1Len = dimsStruct.hiddenSize; offset = w1Len + b1Len; w2Len = dimsStruct.outputSize * dimsStruct.hiddenSize; w1 = reshape(gbestPos(1:w1Len), dimsStruct.hiddenSize, dimsStruct.inputSize); b1 = gbestPos(w1Len+1 : offset); w2 = reshape(gbestPos(offset+1 : offset+w2Len), ... dimsStruct.outputSize, dimsStruct.hiddenSize); b2 = gbestPos(offset+w2Len+1 : end); % 组装网络,先 configure 再覆盖权重 net = feedforwardnet(dimsStruct.hiddenSize); net.layers{1}.transferFcn = 'poslin'; net.trainFcn = 'trainlm'; net = configure(net, Xtr, Ttr); net.IW{1} = w1; net.LW{2,1} = w2; net.b{1} = b1; net.b{2} = b2; net.trainParam.epochs = 300; net.trainParam.showWindow = false; net = train(net, Xtr, Ttr); pred = net(Xva.'); mseVaFinal = mean(sum((pred.' - Tva).^2, 2));这里有个顺序问题值得强调:必须先configure再覆盖权重。configure会初始化网络结构、维度匹配和激活函数缓存,如果你先覆盖权重再configure,权重会被configure的内部初始化覆盖掉。很多人在这一步踩坑,PSO 搜了半天结果网络用的还是随机权重,测试集误差难看也不知道怪谁。
trainlm是 Levenberg-Marquardt 算法,适合中小规模数据集,收敛快且对初始权重质量的要求比traingd低。如果用traingd,初始权重质量对结果影响更大,那 PSO 预训练的价值就更明显。
4. 调参与数据集适配:粒子数、收敛判据与对比基线
4.1 PSO 核心参数表与调整方向
下面这张表是我在 K-means 和 BP 两个场景里常用的参数默认值和调整方向。直接抄这个配置,大部分情况能跑出正常结果。
| 参数 | K-means 场景常见值 | BP 场景常见值 | 调整方向 |
|---|---|---|---|
| swarmSize | 30 ~ 40 | 40 ~ 60 | 粒子维度高时加大,但超过 80 收益变小 |
| maxIter | 80 ~ 120 | 60 ~ 100 | 数据复杂时可加到 200,观察 history 曲线判断 |
| wStart | 0.9 | 0.9 | 搜索范围大时可到 0.95,避免大于 1 |
| wEnd | 0.4 | 0.4 | 后期需要精细搜索时可降到 0.3 |
| c1 / c2 | 1.5 / 1.5 | 1.5 / 1.5 | 需要更倾向全局协作时 c2 调到 1.8 |
| 速度上限 | 0.2 倍边界 | 0.05 倍边界 | BP 权值空间密度大,速度上限太高容易震荡 |
BP 场景的速度上限我取的是边界范围的 5%,比 K-means 场景小很多。原因是权值空间的合法区域本身有限,权重初始值超过 ±2 后,ReLU 网络很容易饱和,适应度函数变成平台,粒子在平台上推来推去毫无意义。把速度上限压住,粒子反而能在有效区域里多做几次精细搜索。
4.2 数据规模与迭代轮次的取舍
PSO 优化 K-means 的计算瓶颈在适应度评估。每次评估要算一遍完整的 n×K 距离矩阵,PSO 总调用次数是swarmSize * maxIter。比如 30 个粒子跑 100 轮就是 3000 次pdist2。样本量五千以内这个开销可以接受,超过一万后就明显拖时间。
常见的加速做法是采样评估:每次迭代从数据里随机抽 500 到 1000 个样本计算 SSE,代替全量数据。本质是蒙特卡洛近似,聚类结果不会差太多,因为 PSO 搜索阶段不需要精确到小数点后几位,只要方向对就行。
% 在 psoKmeans 主循环里替换 sseCost 调用 sampleIdx = randsample(n, min(n, 800)); cost = sseCost(X(sampleIdx, :), swarm(i,:), K);采样数取 800 左右,样本量不足 800 时自然退化回全量计算。这个技巧把万级样本的 PSO 阶段耗时压到和几千样本差不多,代价是最后几轮收敛曲线的毛刺多一些,不影响选最优粒子。等到最终用kmeans微调时再用全量数据,质心位置会重新校准精确。
BP 场景的加速则简单一些:如果样本量很大,适应度函数里也用训练集的子集做前向传播。但要注意验证集混合部分得和训练集使用同一批样本的索引,免得验证集分布在粒子之间飘来飘去,造成适应度不可比。
4.3 对比基线怎么设:值不值得用 PSO 的判断标准
动手写 PSO 之前,先把基线测出来。基线很简单:直接调用内置kmeans,Replicates设成 10 或者 20,取多次运行的最小 SSE。这一步花几分钟,但能让你后续的所有对比都有参照物。
rng(42); baseSSE = inf; for r = 1:10 [~, sse] = kmeans(X, 4, 'Replicates', 10, 'MaxIter', 500); if sse < baseSSE baseSSE = sse; end end判断 PSO 值不值得用,主要看它是否明显低于基线。如果随机重启已经能拿到很低的 SSE,说明数据本身的簇结构很清晰,PSO 的搜索优势发挥不出来,这时候不如直接用kmeans,省掉额外编写和维护 PSO 的成本。如果基线 SSE 在多次运行之间波动很大,说明 K-means 对初始化敏感,PSO 优化的收益就会非常明显。
BP 那边也是一样:先多随机种子训练几次记录验证集 MSE 的均值和方差,作为基线。PSO 优化后如果验证集 MSE 的均值比基线低,而且多次独立运行的方差更小,才能说明优化有效。我习惯固定随机种子做五次独立实验,记录均值和标准差,表格列出来,课程设计报告和论文实验部分都能直接引用。
5. 常见问题避坑:PSO 联合 K-means 和 BP 的五条血泪记录
5.1 现象:PSO 之后 SSE 没有优于随机重启
状态是代码跑通了,收敛曲线也正常下降,但最后 SSE 和kmeans(X, K, 'Replicates', 10)差不多,甚至更差。这会让第一次做对比实验的人很懵。
原因大概率是数据集本身簇结构很清楚,随机重启碰几次就能找到接近全局最优的质心组合。另一种可能是 PSO 粒子数和迭代次数太小,搜索不充分,等于没优化。
解决方法是先跑基线再决定要不要上 PSO。如果基线 SSE 在十次随机重启中反复出现同一个最小值,说明数据集不需要 PSO 介入。如果非要体现优化效果,可以把swarmSize提到 60、maxIter加到 150,同时检查惯性权重是否降得太快,导致后期粒子没有足够扰动去探索新的区域。
5.2 现象:PSO 优化的 BP 在训练集上表现极好,验证集一塌糊涂
训练集 MSE 降到很低,验证集 MSE 却比随机种子的基线还高。很多人会怀疑是网络结构问题,实际上问题出在适应度函数上。
原因是 PSO 阶段只用训练集 MSE 作为适应度,粒子自然会往“过度拟合训练集”的方向飞。BP 微调阶段又延续了这个趋势,最终模型在验证集上泛化很差。
解决方法是把验证集 MSE 按 0.3 权重混入适应度函数,就是上文bpFitness的做法。另外在 BP 微调阶段开早停,net.trainParam.max_fail设置成 10 或者 20,训练过程中验证集误差连续上升若干轮就终止训练。
5.3 现象:粒子维度大时收敛极慢,后期卡在一个值附近出不来
网络规模稍大,比如隐藏层 20 个节点、输入层十来个特征,粒子维度就上百了。PSO 在 100 维以上的搜索空间里容易早熟,粒子群迅速聚拢到某个局部区域,多样性消失,收敛曲线变成一条水平线。
原因是标准 PSO 的速度更新公式缺少随机扰动机制,粒子一旦收敛到某个邻域就丧失了跳出能力。高维空间里这种现象比低维严重得多。
解决方法是两个方向。一是加变异算子,每轮以 0.05 到 0.1 的概率随机挑一个粒子,把它的部分维度重新初始化到搜索空间,这个技巧在下一章给出代码。二是降维,对 BP 的输入先做特征选择或 PCA,减少输入维度也就直接降低了粒子维度。隐藏层节点数不宜盲目加大,粒子维度随之膨胀的代价会抵消掉网络表达能力的提升。
5.4 现象:两次运行的聚类标签对不上,结果没法复用
同一份数据,同样的 PSO + K-means 流程,第一次跑出来标签为 1 的簇,第二次跑出来可能变成标签为 2,聚类结果的散点图看着一样,但标签编号完全对不上。做交叉验证或者多次重复实验记录结果时,这个问题很要命。
原因是 K-means 的本质决定了簇标签本身没有语义顺序,质心位置不同,分配标签时就给编号。PSO 阶段粒子里的质心顺序也是随机初始化的,粒子之间互相交换质心顺序但适应度不变,所以最后输出的质心矩阵没有稳定的行顺序。
解决方法是输出质心后按坐标排序,比如按质心第一个维度升序排列,再按排序顺序重新分配标签。这样至少保证多次运行之间标签有一致性。MATLAB 里可以用sortrows(centers)排完序再传给后续评估函数。
5.5 现象:kmeans 的 Start 传了质心却提示维度错误
kmeans报错说 Start 矩阵必须是 K×d,实际检查你的bestCenters确实是 K×d,但就是报错。这种玄学问题通常不是维度本身的问题,而是数据类型。
原因是psoKmeans计算过程中某些操作产生了单精度数组,或者质心矩阵里包含 NaN 或 Inf。kmeans对 Start 矩阵要求是 double 类型,且不能含非有限值。如果数据里有 NaN,吹进 PSO 后适应度也会变成 NaN,最后选出的 gbest 就是一堆 NaN。
解决方法是调用前强制转换并检查有限性。在传给kmeans之前加一行initCenters = double(initCenters);,同时检查all(isfinite(initCenters(:)))。如果返回 false,说明数据里有缺失值,需要先做数据清洗,别让 NaN 一路传到底。
6. 更进一步:给 PSO 加变异算子并用收敛曲线验证结果
标准 PSO 最怕早熟收敛,粒子群抱团后搜索能力急剧退化。给 PSO 加入变异算子是最简单的改善手段,实现成本非常低,效果却立竿见影。在psoKmeans的每次迭代末尾加一段代码:以 0.08 的概率随机抽取一个粒子,把它 20% 的维度重新随机初始化。
% 变异算子代码,加在 psoKmeans 主循环粒子更新之后 if rand < 0.08 mutateIdx = randi(opts.swarmSize); mutateDims = rand(1, dim) < 0.2; swarm(mutateIdx, mutateDims) = lowerBound(mutateDims) ... + rand(1, sum(mutateDims)) .* (upperBound(mutateDims) - lowerBound(mutateDims)); % 变异后重新计算该粒子的个体最优 newCost = sseCost(X, swarm(mutateIdx,:), K); if newCost < pbestCost(mutateIdx) pbestCost(mutateIdx) = newCost; pbestPos(mutateIdx,:) = swarm(mutateIdx,:); end end变异概率 0.08 和变异维度比例 0.2 是经验值。概率太高会退化成随机搜索,粒子群永远聚不拢;概率太低又起不到打破同质化的作用。变异之后必须重新计算该粒子的个体最优,否则下一轮速度更新会拿着过期的 pbest 位置去引导粒子,反而扰乱搜索方向。
每次 PSO 运行结束后,把history数组画出来:
figure; plot(history, 'LineWidth', 1.5); xlabel('迭代轮次'); ylabel('全局最优 SSE'); title('PSO 收敛曲线'); grid on;如果曲线前 10 到 20 轮快速下降,后面近乎水平,说明 PSO 正常收敛。如果整条曲线在后面几十轮仍然缓慢下降,说明粒子数偏少或者惯性权重降得过快,可以适当增加maxIter。如果曲线一开始就平走或者毛刺特别多,先检查适应度函数有没有问题,再查数据里有没有 NaN。收敛曲线是 PSO 过程的体检报告,比任何调试 log 都直观。
我现在跑这类实验的固定流程很固定:K-means 那边先测基线,PSO 搜完质心再交给kmeans微调,最后用轮廓系数和 SSE 双指标汇报;BP 那边固定随机种子做五次独立实验,记录验证集 MSE 均值和标准差,再对比训练曲线。整套流程跑顺之后,换数据集换 K 值都只是改参数的事。PSO 优化 K-means 和 BP 这个方向,适合作为你入门元启发式算法和神经网络调参交叉领域的第一个完整实验,代码量不大,但原理和踩坑点覆盖得很全。希望帮到你。
本文还有配套的精品资源,点击获取