ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

蜻蜓算法优化Kmeans初始质心的聚类方法及Matlab实现

蜻蜓算法优化Kmeans初始质心的聚类方法及Matlab实现 聚类分析里Kmeans几乎是每个接触数据挖掘的人都会先跑的算法但真正拿它在真实数据上跑过几轮的人多半都被“初始质心”坑过同样一份数据换一个随机种子就可能收敛到完全不同的簇结构有时聚类结果明显不合理算法自己却根本意识不到问题。原因在于Kmeans把质心初始化交给随机数而它的迭代过程又只能在当前归属关系下做局部调整起点选偏了基本就认命了。蜻蜓算法DA是一种群智能优化算法它把“找一组好质心”当成一个全局优化问题用模拟蜻蜓成群飞行时的分离、对齐、聚集等行为去寻找更优的起点再交给Kmeans去精修。这篇博文把这套“DA找初始质心Kmeans局部收敛”的方案讲透并给出完整Matlab代码适合做课设、写论文或者在实际聚类任务里被随机初始化折腾过的人参考。1. 方案选型为什么要用蜻蜓算法优化Kmeans1.1 Kmeans聚类分析的真正痛点Kmeans本质上是一个坐标下降式的迭代算法固定质心划分样本固定样本归属再更新质心。这个过程是非凸的目标函数类内距离平方和SSE存在大量局部极小值。你可以把它想象成一位只盯着脚下路走的登山者每一步都往最近的低处走但从不抬头看远处还有没有更矮的山谷。只要初始质心之间距离过近或者恰好落在某个局部密度聚集区里迭代就会顺着一条糟糕的路径收敛下去而且没有任何机制能把它拉回来。实际工程里的表现就是两件事第一聚类结果和随机种子绑定复现性差第二当聚类数目变大比如K8或者特征维数升高时这种随机性会被放大。很多同学在做实验时遇到“上次跑出来三个类很清楚这次跑出来四个类糊成一团”本质就是这个原因。因此想提升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列就是一组完整的初始质心。适应度函数就是SSESSE Σ Σ ||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需要设置的参数主要是蜻蜓种群数、最大迭代次数、边界策略和权重衰减范围。我按不同的数据规模给了一组参考配置数据规模种群数迭代次数说明小样本n500d1020~3050~100迭代次数是主要计算成本中等样本n≈5000d≈2030~50100~200矩阵距离一次算完可控高维特征d5040~60150~300维度增加时单个个体计算量显著增大实际跑的时候可以先从种群30、迭代100开始观察收敛曲线是否在末段趋于平缓。如果曲线还在明显下降说明迭代次数不够先加迭代次数而不是加种群数只有当收敛速度太慢、最优值一直找不到时才考虑增加种群数。4.2 对比实验随机初始化Kmeans、Kmeans与DA-Kmeans我用鸢尾花数据集做过一次对比实验标准化后K3每个方法重复20次统计SSE的均值和标准差。以我本地测试的结果为例随机初始化Kmeans的SSE均值在157.9左右标准差超过10Kmeans的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个样本、K10时每轮适应度计算都要好几秒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也很方便函数接口不用变。建议先跑通基线再把收敛曲线保存下来写论文的时候这个图比任何描述都有说服力。
返回列表