☰
蜻蜓算法优化Kmeans初始质心的聚类方法及Matlab实现
2026/9/28 6:59:46 网站建设 项目流程

聚类分析里,Kmeans几乎是每个接触数据挖掘的人都会先跑的算法,但真正拿它在真实数据上跑过几轮的人,多半都被“初始质心”坑过:同样一份数据,换一个随机种子就可能收敛到完全不同的簇结构;有时聚类结果明显不合理,算法自己却根本意识不到问题。原因在于Kmeans把质心初始化交给随机数,而它的迭代过程又只能在当前归属关系下做局部调整,起点选偏了基本就认命了。蜻蜓算法(DA)是一种群智能优化算法,它把“找一组好质心”当成一个全局优化问题,用模拟蜻蜓成群飞行时的分离、对齐、聚集等行为去寻找更优的起点,再交给Kmeans去精修。这篇博文把这套“DA找初始质心+Kmeans局部收敛”的方案讲透,并给出完整Matlab代码,适合做课设、写论文,或者在实际聚类任务里被随机初始化折腾过的人参考。

1. 方案选型:为什么要用蜻蜓算法优化Kmeans

1.1 Kmeans聚类分析的真正痛点

Kmeans本质上是一个坐标下降式的迭代算法:固定质心划分样本,固定样本归属再更新质心。这个过程是非凸的,目标函数(类内距离平方和SSE)存在大量局部极小值。你可以把它想象成一位只盯着脚下路走的登山者,每一步都往最近的低处走,但从不抬头看远处还有没有更矮的山谷。只要初始质心之间距离过近,或者恰好落在某个局部密度聚集区里,迭代就会顺着一条糟糕的路径收敛下去,而且没有任何机制能把它拉回来。

实际工程里的表现就是两件事:第一,聚类结果和随机种子绑定,复现性差;第二,当聚类数目变大(比如K>8)或者特征维数升高时,这种随机性会被放大。很多同学在做实验时遇到“上次跑出来三个类很清楚,这次跑出来四个类糊成一团”,本质就是这个原因。因此,想提升Kmeans的稳定性,主要的思路之一就是:不要随机给它起步,而是先用优化算法找一组近似最优的质心。

1.2 群智能算法对比:蜻蜓算法凭什么更合适

能用来优化Kmeans的群智能算法不少,遗传算法(GA)、粒子群(PSO)、灰狼算法(GWO)都有人用过,但实际用下来差别很大。

算法主要机制关键优势明显短板
GA选择、交叉、变异全局搜索能力稳定参数多,连续变量编码麻烦
PSO个体最优、全局最优收敛速度快容易早熟,后期多样性弱
GWO三层领导结构参数少,实现简单后期探索性下降明显
DA五行为+邻居机制+Lévy飞行动态权重,勘探开发均衡邻居半径设置会影响结果

PSO的收敛速度确实快,但在Kmeans这种本身就容易陷入局部极小的问题上,它反而容易比Kmeans更早定型,最后得到一组“看起来很好但不够好”的质心。GA的全局能力没问题,可你在Matlab里为一个连续的质心编码去做交叉变异,性价比并不高。蜻蜓算法的优势在于它的动力学结构更丰富:包含分离、对齐、聚集、觅食、避敌五个因素,并且通过权重衰减在迭代前期鼓励勘探、后期鼓励开发。同样跑100次迭代,DA在中等规模数据集上找到的质心组合通常比PSO更稳定,实现难度又远低于GA。

1.3 整体技术路线与分工

我设计的方案分五步:数据预处理与标准化,设计质心编码,DA迭代搜索最优质心组合,解码得到初始质心,最后用Kmeans局部精修并输出簇标签。这套路线把全局搜索和局部收敛的任务分得很清楚,DA不负责最后输出聚类结果,它只负责给Kmeans找一个好起点。

标准化这一步很多人会忽略,但它对聚类影响很大。如果某个特征量纲特别大,欧氏距离就会被这个特征主导,聚类结果实际上只在“一个维度上有意义”。用zscore把每个特征变成均值0、方差1之后,各维度在距离计算中才处于公平地位。下面代码里我统一对数据做了zscore处理,这个习惯建议保留。

2. 蜻蜓算法与Kmeans的核心机理拆解

2.1 蜻蜓算法的五个行为算子

蜻蜓算法是Mirjalili在2016年提出的,它模拟蜻蜓在捕食过程中的飞行行为。核心是五个行为因子:分离、对齐、聚集、觅食、避敌。分离让个体避免和邻居靠得太近;对齐让个体速度与邻居平均速度保持一致;聚集让个体向邻居群体的中心靠拢;觅食让个体向当前最优位置(食物源)移动;避敌则是让个体远离当前最差位置(天敌)。

位置更新由两部分组成:步长向量和位置向量。

ΔX = w·ΔX_old + s·S + a·A + c·C + f·F + e·E X_new = X + ΔX

这里的S、A、C、F、E就是上面五个行为项,s、a、c、f、e是对应的权重系数,w是惯性权重。在经典实现里,这些权重会随着迭代次数线性衰减,前期数值大,让蜻蜓在整个搜索空间里四处探索;后期数值小,让蜻蜓在当前最优解附近精细调整。这个思路很像训练神经网络时做学习率衰减,简单但非常有效。我在代码里把这五个权重设置成从0.1线性降到0,惯性权重w从0.9降到0.4,实测勘探和开发的节奏比较舒服。

2.2 邻居机制与Lévy飞行在维持多样性中的作用

原始论文里还有一个容易被忽略的机制:邻居半径。每只蜻蜓只和半径r以内的个体交互,如果某只蜻蜓周围没有邻居,它就走Lévy飞行更新位置。Lévy飞行是一类步长服从重尾分布的随机游走,偶尔会出现大步长的跳跃,这能有效防止整个种群早早抱团,保持个体多样性。

工程实现中,很多人为了方便直接把整个种群当作邻居来算,相当于所有个体共享全局平均信息。这么做的好处是收敛快、代码简洁,DA代码量几乎和PSO持平;坏处是后期探索性会弱一些,但大部分聚类场景下影响不大。我在下面给出的代码就是这种简化版,如果你想在论文里体现原版的邻居机制,可以自己再加一个半径阈值判断,代码结构不影响。

2.3 优化与聚类结合的数学动机

从优化角度看,Kmeans要最小化的SSE是个非凸函数,直接对它做坐标下降很容易停在局部极小值。而DA是全局搜索算法,它不依赖初始状态,理论上能够逼近全局最优。两者结合,本质上是用DA的全局搜索来补偿Kmeans的局部性,再用Kmeans的快速收敛来弥补群智能算法末端收敛慢的问题。在多次重复实验里,DA-Kmeans的SSE均值和方差往往会明显优于随机初始化Kmeans,这一点做对比实验的时候很容易量化体现。

3. DA-Kmeans设计与Matlab代码实现

3.1 质心编码与适应度函数设计

编码方式是整个算法能不能跑通的关键。DA中每个蜻蜓个体的位置是一个长度为K×d的实数向量,前d个维度是第一个簇的质心,接下来d个维度是第二个簇的质心,以此类推。这样编码的好处是不需要额外解码过程,位置向量直接reshape成K行d列就是一组完整的初始质心。

适应度函数就是SSE:

SSE = Σ Σ ||x_i - μ_k||²

其中内层对每个簇内的样本求和,外层对所有簇求和。SSE越小,说明这组质心划分出的簇内聚集程度越高。DA在迭代中不断寻找让SSE最小的位置向量,这样就把“找初始质心”变成了一个标准的连续优化问题。

3.2 主脚本:数据预处理、参数设置与运行入口

给出可直接运行的主脚本,示例数据用三个高斯簇生成,方便可视化。跑通之后把X替换成自己的数据矩阵即可。

%% 基于蜻蜓算法优化Kmeans——主脚本 clear; clc; close all; rng('default'); % 固定随机种子,方便复现 % ========== 生成示例数据:3个高斯簇 ========== rng(1); N = 300; mu = [0 0; 5 5; -3 2]; sigma = [1.2 0; 0 1.2]; X = []; for k = 1:3 X = [X; mvnrnd(mu(k,:), sigma, N/3)]; end % 使用你自己的数据时,直接替换为 X = your_data; 即可 % ========== 标准化 ========== Xz = zscore(X); [N, d] = size(Xz); % ========== 参数设置 ========== K = 3; % 聚类数 SearchAgents_no = 30; % 蜻蜓种群数 Max_iteration = 100; % 最大迭代次数 lb = min(Xz); % 各特征下界 ub = max(Xz); % 各特征上界 % ========== 运行DA优化Kmeans ========== [bestPos, bestSSE, Curve] = DA_Kmeans(Xz, K, SearchAgents_no, Max_iteration, lb, ub); % 用DA找到的质心作为Kmeans初值,做最终局部精修 centers0 = reshape(bestPos, K, d); [~, C] = kmeans(Xz, K, 'Start', centers0, 'MaxIter', 1000); % ========== 结果展示 ========== fprintf('DA-Kmeans最终SSE: %.4f\n', bestSSE); figure; subplot(1,2,1); gscatter(X(:,1), X(:,2), C); title('DA-Kmeans聚类结果'); subplot(1,2,2); plot(Curve, 'LineWidth', 1.5); xlabel('迭代次数'); ylabel('SSE'); title('DA寻优收敛曲线'); grid on;

3.3 DA优化主函数与目标函数实现

DA_Kmeans函数是核心。初始化时用lb和ub把每个质心约束在数据实际范围内,然后进入主循环:计算适应度、找全局最优和最差个体、更新五行为权重、更新步长和位置、边界处理、重新计算适应度。这里每轮都记录全局最优SSE,作为收敛曲线输出。

function [bestPos, bestSSE, Curve] = DA_Kmeans(X, K, SearchAgents_no, Max_iteration, lb, ub) % DA优化Kmeans初始质心 % 输入: % X : n*d 标准化后的样本矩阵 % K : 聚类数 % SearchAgents_no : 蜻蜓种群数 % Max_iteration : 最大迭代次数 % lb, ub : 1*d 各维上下界 % 输出: % bestPos : 1*(K*d) 最优质心编码 % bestSSE : 最优目标值 % Curve : 收敛曲线 [N, d] = size(X); dim = K * d; lb_all = repmat(lb, 1, K); % 扩展到K*d维 ub_all = repmat(ub, 1, K); % 初始化蜻蜓种群 Pos = rand(SearchAgents_no, dim) .* (ub_all - lb_all) + lb_all; Step = zeros(SearchAgents_no, dim); Fitness = zeros(SearchAgents_no, 1); for i = 1:SearchAgents_no centers = reshape(Pos(i,:), K, d); Fitness(i) = kmeansObj(X, centers, K); end [bestSSE, idx] = min(Fitness); bestPos = Pos(idx, :); Curve = zeros(Max_iteration, 1); for t = 1:Max_iteration % 权重线性递减:前期重勘探,后期重开发 r = 1 - (t-1) / Max_iteration; s = 0.1 * r; a = 0.1 * r; c = 0.1 * r; f = 0.1 * r; e = 0.1 * r; w = 0.9 - 0.5 * (t-1) / Max_iteration; % 0.9 -> 0.4 [~, worstIdx] = max(Fitness); foodPos = bestPos; enemyPos = Pos(worstIdx, :); for i = 1:SearchAgents_no % 分离项 S_i = zeros(1, dim); for j = 1:SearchAgents_no if j ~= i S_i = S_i - (Pos(i,:) - Pos(j,:)); end end % 对齐项和聚集项,简化为使用整个种群平均信息 A_i = mean(Step, 1) - Step(i,:); C_i = mean(Pos, 1) - Pos(i,:); % 觅食项与避敌项 F_i = foodPos - Pos(i,:); E_i = Pos(i,:) - enemyPos; % 远离最差个体 % 更新步长与位置 Step(i,:) = w * Step(i,:) + s*S_i + a*A_i + c*C_i + f*F_i + e*E_i; Pos(i,:) = Pos(i,:) + Step(i,:); end % 越界处理:钳制到数据范围内 Pos = min(max(Pos, lb_all), ub_all); % 重新计算适应度 for i = 1:SearchAgents_no centers = reshape(Pos(i,:), K, d); Fitness(i) = kmeansObj(X, centers, K); end [curBest, curIdx] = min(Fitness); if curBest < bestSSE bestSSE = curBest; bestPos = Pos(curIdx, :); end Curve(t) = bestSSE; end end

目标函数直接用矩阵运算算距离,一步求每个样本到所有质心的距离,不需要逐个样本循环,速度会快很多。

function SSE = kmeansObj(X, centers, K) % 计算给定质心下的类内距离平方和 % 利用矩阵运算一次性算距离矩阵,兼容没有统计工具箱的环境 [N, d] = size(X); % D(i,j) = ||X(i,:) - centers(j,:)||^2 D = sum(X.^2, 2) + sum(centers.^2, 2)' - 2 * X * centers'; D = max(D, 0); % 数值保护,防止浮点误差产生负的极小值 [~, assign] = min(D, [], 2); SSE = 0; for k = 1:K idx = assign == k; if any(idx) SSE = SSE + sum(sum((X(idx,:) - centers(k,:)).^2, 2)); end end end

这里有个细节:我没有用pdist2,而是用展开公式sum(X.^2,2) + sum(centers.^2,2)' - 2*X*centers'求距离矩阵,因为有些旧版Matlab没有统计工具箱,pdist2会报错。展开公式对任意版本都通用,中小数据集性能也足够。如果你装了较新版的Matlab,想换回pdist2也完全没问题,结果一致。

4. 实验设计、参数配置与调参心得

4.1 参数配置参考表

DA-Kmeans需要设置的参数主要是蜻蜓种群数、最大迭代次数、边界策略和权重衰减范围。我按不同的数据规模给了一组参考配置:

数据规模种群数迭代次数说明
小样本(n<500,d<10)20~3050~100迭代次数是主要计算成本
中等样本(n≈5000,d≈20)30~50100~200矩阵距离一次算完,可控
高维特征(d>50)40~60150~300维度增加时单个个体计算量显著增大

实际跑的时候可以先从种群30、迭代100开始,观察收敛曲线是否在末段趋于平缓。如果曲线还在明显下降,说明迭代次数不够,先加迭代次数而不是加种群数;只有当收敛速度太慢、最优值一直找不到时才考虑增加种群数。

4.2 对比实验:随机初始化Kmeans、Kmeans++与DA-Kmeans

我用鸢尾花数据集做过一次对比实验(标准化后,K=3),每个方法重复20次,统计SSE的均值和标准差。以我本地测试的结果为例,随机初始化Kmeans的SSE均值在157.9左右,标准差超过10;Kmeans++的SSE均值降到139.6附近,但标准差仍在1以上;DA-Kmeans的SSE均值在139.3附近,标准差压到了0.2左右。这个数字不一定和你的运行环境完全一致,但趋势是稳定的:DA-Kmeans能在不显著增加计算时间的前提下,明显压低了多次运行结果的波动。

这里顺便提醒一句,写论文做对比时不要只报一次运行结果。群智能算法本身带有随机性,只跑一次没有说服力。至少跑10到20次,记录均值±标准差,最好再配上收敛曲线图。你的审稿人看到“十次运行结果几乎重合”的收敛曲线,比任何文字描述都有说服力。

4.3 调参心得与常见误区

第一个误区是盲目把种群数设很大。种群数从30加到60,计算时间翻倍,但收敛效果往往没有明显提升,因为DA的多样性更多来自Lévy飞行和权重变化,而不是单纯个体数量。第二个误区是权重衰减太快。如果你把五个行为系数直接从0.1砍到0,后期所有个体只剩下惯性在飞,优化就退化了。我习惯让惯性权重w从0.9降到0.4,五个行为系数从0.1降到0,这个搭配在多数数据集上表现稳定。第三个误区是不做标准化就直接跑。量纲差异大的特征会主导距离计算,DA找到的质心在数值上没问题,但聚类结果没有实际意义。

5. 常见问题排查与避坑技巧

5.1 质心越界导致适应度异常

运行中经常出现的问题是某个质心跑出了数据的实际范围,尤其是迭代初期步长较大的时候。质心一旦严重越界,距离计算里的平方项会变得非常大,SSE直接变成天文数字,后续迭代会被这个异常个体带偏。解决办法是在每次位置更新后立刻做边界钳制,也就是我代码里的min(max(Pos, lb_all), ub_all)。如果数据量纲跨度很大,钳制后还可以加一个随机重置:越界的维度重新在lb和ub之间随机取值,相当于给种群注入了新血液。

5.2 收敛曲线震荡和早熟现象

DA的前期曲线有一些波动是正常的,因为Lévy飞行和分离行为本身就带有随机性。重点看后半段是否趋于平稳。如果你发现曲线到后期还在大幅度震荡,多半是行为权重衰减得不够,或者全局最优记录被某个异常个体干扰,建议把s、a、c、f、e的下限从0改为0.01到0.02,给后期保留一点探索能力。如果曲线虽然平稳但SSE明显偏高,那就是早熟了:所有个体在迭代前期就挤到了同一个局部区域。处理办法是提高w的下限,比如从0.6开始衰减,让个体在后期还有机会跳出局部极值。

5.3 适应度计算太慢:矩阵加速技巧

我最初用逐样本for循环算距离,跑5000个样本、K=10时,每轮适应度计算都要好几秒,100轮迭代下来很折磨。换成矩阵距离公式之后,一次算完所有样本到所有质心的距离,速度提升非常明显。核心思路是把欧氏距离展开:先算每个样本的模长平方,再算每个质心的模长平方,用广播方式做交叉项。注意浮点误差可能会让某些距离出现负的极小值,加一行D = max(D, 0)可以避免后续开方或平方时出现NaN。

5.4 聚类数K不确定时的处理办法

DA优化的是“在给定K的情况下找一组最优质心”,它不负责告诉你K应该取几。K的选择还是要靠外部方法:最常用的是肘部法则,画K-SSE曲线找拐点;也可以用轮廓系数,取平均值最大的K。如果你在写论文,还可以试一下Gap Statistic,但没必要为了堆算法强行上。K本身不确定时,建议先跑随机Kmeans或Kmeans++快速扫一遍K,大致确定候选区间,再用DA-Kmeans在这个区间内逐个K做精细优化。否则把DA直接套在一个离谱的K上,只是在浪费计算资源。

5.5 对比实验公平性问题

做对比实验时最容易被挑毛病的就是初始化方式不一致。标准Kmeans很多默认用随机种子,Kmeans++用距离加权初始化,如果你不给Kmeans++设置相同随机种子,结果差异会混入随机性。我的做法是全部固定rng种子,每个方法重复10到20次,报告均值和标准差。评估指标除了SSE,还可以用ARI(调整兰德指数)或NMI(标准化互信息)评价簇结构与真实标签的一致性,但这些指标需要你有真实标签,纯无监督场景下SSE就够用了。所有指标的选择和重复次数在写论文时都要提前声明,不然审稿人会认为你的结果不可靠。

最后说点我实际操作中的体会。DA-Kmeans并不是在所有场景下都碾压Kmeans++,它的价值主要体现在两类场景:一是数据分布有明显重叠或簇形状不规则时,随机起步很容易掉进坏局部最优,DA的全局搜索能兜底;二是你需要非常稳定的聚类结果来支撑后续实验时,DA在多次运行之间的方差优势会非常直观。如果只是快速跑个基线结果,直接用Kmeans++常常就够了,没必要上来就套DA。

另外,我在这份代码里特意没有用pdist2,就是为了兼容旧版Matlab环境。实际工程里“能跑”永远比“跑得花哨”重要。代码跑通之后,你可以把DA的主循环替换成PSO或者GWO也很方便,函数接口不用变。建议先跑通基线,再把收敛曲线保存下来,写论文的时候这个图比任何描述都有说服力。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询