蜻蜓算法优化K-Means聚类:原理、Matlab实现与参数调优

发布时间:2026/9/28 13:25:34
蜻蜓算法优化K-Means聚类:原理、Matlab实现与参数调优
做聚类分析的朋友应该都遇到过这类问题K-Means的聚类结果对初始中心点选择极其敏感随便换一个随机种子跑出来的轮廓系数可能差一大截。我在一个人群画像项目里同样的数据、同样的K值因为初始中心选得不好分类结果直接把两个核心用户群体揉在了一起业务方当场表示没法用。后来我开始把启发式算法和K-Means结合起来试来试去效果最稳的一个组合就是蜻蜓算法优化K-Means聚类分析也就是常说的DA-KMeans。这篇文章就围绕这个方法把原理、Matlab实现、参数调优和踩坑记录都过一遍。适合正在做聚类研究的学生、想改进模型效果的算法工程师以及所有觉得K-Means不够稳的人。1. 为什么用蜻蜓算法优化K-Means问题与思路1.1 K-Means聚类的痛点在哪里先说说K-Means本身。它的目标很简单把数据分成K簇让每个样本到所属簇中心的距离平方和尽量小。算法流程也通俗——随机选K个初始中心然后反复“分配样本→更新中心”直到中心不再明显变化。这个流程跑起来很快但有个绕不开的毛病结果严重依赖初始中心。我拿同一份数据做过测试连续跑20次K-Means每次随机初始化最后得到的类内距离平方和能差出10%到15%。原因是K-Means本质是一个贪心迭代过程如果初始中心恰好落在几个稠密区域的“中间地带”算法很容易收敛到局部最优把本来应该分开的簇硬生生粘在一起。另一个痛点是K值一旦取大一点比如K6或K8搜索空间变大之后随机初始化更不可控。数据维度高的时候样本之间距离趋同初始中心选不好迭代过程甚至会来回震荡怎么都收敛不到一个稳定解。这也是为什么很多聚类实战项目里大家宁可多跑几次随机初始化选一个最好结果也不敢直接用单次K-Means输出。1.2 蜻蜓算法凭什么能帮忙蜻蜓算法是Mirjalili在2016年前后提出的一种群智能优化算法核心灵感来自蜻蜓的两种群体行为觅食和迁徙。觅食时蜻蜓会形成小的族群局部移动很精细适合局部开发迁徙时大量蜻蜓组成大群体朝着一个方向快速移动探索范围很大适合全局搜索。这个特点刚好能补K-Means的短板。K-Means缺的不是局部收敛能力而是全局搜索初始中心的能力。蜻蜓算法在搜索初期会大规模探索把解空间各种位置都扫一遍到了迭代后期又把注意力集中在有希望的区域精细搜索。这种“先广后精”的策略比单纯随机初始化靠谱得多。而且DA对优化目标没有太苛刻的要求不需要算梯度只要你能写出“一个解有多好”的评估函数就能套上去。聚类中心选择问题正好满足这个条件把一组中心作为蜻蜓的位置用类内距离平方和作为适应度越小越好。DA负责在全局范围里找出一个高质量初始中心再交给K-Means去精修。1.3 优化思路全局搜索选初始中心我采用的具体思路是这样先用蜻蜓算法搜索出一组聚类中心这组中心不是最终结果而是作为K-Means的优质起点。算法运行中每次评估一个蜻蜓个体时并不是只看它当前中心直接算距离而是让它先跑几轮K-Means迭代把中心“微调”一下再计算适应度。这样做的意义在于蜻蜓算法和K-Means是真正嵌套在一起的而不是简单拼凑。DA负责跳出局部最优K-Means负责在局部范围内做梯度式的方向修正。两者配合既避免了DA在庞大的连续空间中漫无目的地乱飞又让K-Means不再被糟糕的起点拖累。从实现角度看就是把聚类中心矩阵展开成一个一维向量长度是K乘以数据维度。这个向量就是蜻蜓算法中的“位置”。每次位置更新后重新reshape成K个中心执行一轮距离分配和中心更新计算适应度。后续所有主循环、边界处理、收敛判断都围绕这个位置向量展开。2. 蜻蜓算法原理与参数设定2.1 五种行为因子与位置更新公式蜻蜓算法的核心是每只蜻蜓的位置会受五个因素影响分离、对齐、聚集、食物吸引、天敌驱散。分离就是不要和邻居挨得太近惩罚两个个体位置过于接近计算公式是当前个体与每个邻居位置差的反向累加。对齐让个体的运动方向和邻居的平均运动方向保持一致可以理解为速度趋于一致。聚集个体有向邻居质心靠拢的趋势防止整个群体散得太开。食物吸引个体向当前搜索到的最优位置移动这是算法的“拉动力”。天敌驱散个体要远离当前搜索到的最差位置避免被带进坏区域。把五个因素按权重加起来再加上上一代的运动惯性就得到步长向量更新公式。位置更新则是当前坐标加上这个步长向量。这里要注意步长向量这个“速度”本身是有记忆的惯性权重w决定了上次步长对本次影响多少。w较大时探索性强w较小时开发性强。2.2 权重与参数怎么取五个行为因子分别有各自权重我一般按迭代次数做自适应调整迭代早期分离权重大、聚集权重小让蜻蜓扩散开充分探索迭代后期聚集和食物吸引权重变大让蜻蜓收拢到高质量解附近。惯性权重从0.9左右线性降到0.2左右这是很多群智能算法的常见加热方式。这里给出我常用的参数范围基于我在自己数据上的多次调试可以作为初始参考。参数含义参考范围N种群规模20~40MaxIt最大迭代次数50~150w惯性权重0.9降至0.2s分离权重1.0降至0.5左右需按问题缩放a对齐权重0.1升到0.5c聚集权重0.1升到0.5f食物吸引力2降至0e天敌驱散权重0升到0.5特别提醒分离项是所有邻居位置差的累加实际计算出来的数值可能非常大。如果不做缩放步长很容易爆掉导致位置直接飞出边界。我习惯把分离项先除以邻居数量再做线性缩放相当于取平均差值。这样权重的数值含义更明确也更容易移植到不同数据集上。2.3 编码方式与适应度函数设计确定优化目标之后编码方式就顺理成章。假设数据是d维聚成K类那一个蜻蜓个体的位置就是长度为K*d的一维向量前d个分量表示第一个中心接下来d个分量表示第二个中心以此类推。适应度函数我建议不要直接用当前中心算距离平方和而是先让中心做几轮K-Means局部迭代。原因是我们可以用这个局部修正能力把每个个体快速带到它周围最优点上再用这个局部最优值来评估个体好坏。这样的适应度函数能更准确地反映“这组初始中心的潜力”而不是只看初始位置那一瞬间的质量。具体计算流程是把一维向量reshape成K×d矩阵作为中心计算所有样本到中心的欧氏距离按最小距离分配样本到K簇然后计算每一簇的样本均值更新中心。这个过程重复5次左右最后统计所有样本到所属中心距离平方和作为适应度值。需要遵循最小化原则适应度越小代表聚类结果越紧凑。3. Matlab代码实现与拆解3.1 程序框架与文件规划我用Matlab做了一段完整的DA-KMeans核心实现。代码逻辑上分成几个文件会好维护如果你的项目希望单文件搞定也可以把这些函数直接复制到同一个脚本末尾Matlab允许函数写在脚本尾部。主程序脚本main_da_kmeans.m负责加载数据、归一化、设置参数、调用DA并输出结果。核心函数DA_Kmeans.m负责蜻蜓种群初始化、评估、位置更新和迭代收敛。适应度函数calcFitness.m负责把蜻蜓位置向量还原成聚类中心执行K-Means局部优化返回类内距离平方和。这种拆法让调试成本低不少。我最开始把所有逻辑堆在一个脚本里调参时经常改错变量名拆开后思路清晰很多。3.2 适应度函数把K-Means融进评估适应度函数是整段代码的关键也是容易写错的地方。我贴出核心逻辑。function fit calcFitness(data, K, pos) % 将蜻蜓位置向量还原为聚类中心矩阵每行一个中心 centers reshape(pos, K, size(data, 2)); n size(data, 1); labels zeros(n, 1); % 局部K-Means精炼轮数不需要太多5轮足够 refineIter 5; for t 1:refineIter % 计算每个样本到每个中心的距离平方 dist zeros(n, K); for k 1:K diff data - centers(k, :); dist(:, k) sum(diff .^ 2, 2); end % 分配样本到最近中心 [minDist, labels] min(dist, [], 2); % 按簇均值更新中心 for k 1:K idx (labels k); if any(idx) centers(k, :) mean(data(idx, :), 1); end end end % 最终计算类内距离平方和 dist zeros(n, K); for k 1:K diff data - centers(k, :); dist(:, k) sum(diff .^ 2, 2); end fit sum(min(dist, [], 2)); end这段代码里有两个细节很容易踩坑。一个是空簇问题当某个中心附近没有样本时直接求均值会得到NaN所以更新中心前要判断簇是否非空。另一个是精炼轮数不要设太大5轮已经能把中心拉到局部极值附近再设大只是纯浪费算力。3.3 DA主循环邻居计算与位置更新原版蜻蜓算法有邻居半径和莱维飞行完整实现代码较长。我这里给出一种工程简化版把所有蜻蜓都视为彼此邻居直接计算五种行为因子。这种简化在中等规模种群下表现稳定适合作为学习框架。function [bestFitness, bestPos, conv] DA_Kmeans(data, K, lb, ub, dim, N, MaxIt) % 初始化位置和步长 X lb rand(N, dim) .* (ub - lb); V zeros(N, dim); fit zeros(N, 1); conv zeros(MaxIt, 1); for iter 1:MaxIt % 评估当前群体每个个体 for i 1:N fit(i) calcFitness(data, K, X(i, :)); end % 当前食物源最优个体位置当前天敌最差个体位置 [bestFitness, bestIdx] min(fit); bestPos X(bestIdx, :); [~, worstIdx] max(fit); enemyPos X(worstIdx, :); % 自适应权重随迭代推进逐步调整 w 0.9 - 0.7 * (iter / MaxIt); s 1.0 - 0.5 * (iter / MaxIt); a 0.1 0.4 * (iter / MaxIt); c 0.1 0.4 * (iter / MaxIt); f 2.0 * (1 - iter / MaxIt); e 0.1 0.4 * (iter / MaxIt); for i 1:N S zeros(1, dim); A zeros(1, dim); C zeros(1, dim); % 简化邻居所有个体互为邻居 for j 1:N if i ~ j S S - (X(i, :) - X(j, :)); A A V(j, :); C C X(j, :); end end S S / (N - 1); A A / (N - 1); C C / (N - 1) - X(i, :); F bestPos - X(i, :); E X(i, :) - enemyPos; % 工程版指向远离最差解的方向 % 步长向量更新再更新位置 V(i, :) s * S a * A c * C f * F e * E w * V(i, :); X(i, :) X(i, :) V(i, :); % 边界吸收防止位置跑到数据范围外 X(i, :) min(max(X(i, :), lb), ub); end conv(iter) bestFitness; end end写这段代码的时候我特意把分离项除以(N-1)因为不除的话种群一大S的值会线性增大步长很容易爆炸。这个细节早期版本吃过亏位置一更新就冲到边界聚类结果一片混乱。另外天敌项我采用的是当前解指向最差解反向的向量这样语义更直观个体要远离最差位置而不是朝它飞。3.4 主程序调用与结果输出主程序负责读数据、归一化、设置上下界和参数最后把DA结果作为K-Means初始中心再做一次精调。clc; clear; close all; rng(2025); % 以鸢尾花数据为例最后一列为标签列聚类时可以先不看 data load(iris.txt); X data(:, 1:end-1); trueLabel data(:, end); % 归一化非常重要不同量纲会把距离计算带偏 X zscore(X); K 3; N 25; MaxIt 80; dim K * size(X, 2); lb repmat(min(X), 1, K); ub repmat(max(X), 1, K); % 运行DA-KMeans [bestFitness, bestPos, conv] DA_Kmeans(X, K, lb, ub, dim, N, MaxIt); % 使用DA找到的中心作为K-Means初始中心进行最终精调 centers reshape(bestPos, K, size(X, 2)); opts statset(MaxIter, 500); [finalLabel, finalCenter] kmeans(X, K, Start, centers, Options, opts); % 计算轮廓系数和类内距离和简单输出 sil silhouette(X, finalLabel); sse sum(sum((X - finalCenter(finalLabel, :)) .^ 2, 2)); fprintf(DA-KMeans SSE: %.4f\n, sse); fprintf(DA-KMeans 平均轮廓系数: %.4f\n, mean(sil)); % 绘制收敛曲线观察DA每代的适应度变化 figure; plot(conv, LineWidth, 2); xlabel(迭代次数); ylabel(类内距离平方和); title(DA-KMeans 收敛曲线); grid on;如果机器上没有统计工具箱kmeans函数用不了可以自己再写一个距离分配和均值更新的小函数但大多数高校版本都自带工具箱。有一点要注意主程序里用了rng(2025)固定随机种子目的是让实验可复现。实际应用时去掉它即可或者多跑几种随机种子看稳定性。4. 实验对比与参数调优4.1 在公开数据上做对比实验为了验证这个组合真的比普通K-Means强我在鸢尾花数据上做了对比实验。鸢尾花是经典三维聚类数据量纲一致内部结构清晰适合做算法验证。我也在葡萄酒数据上测过一版效果规律类似这里以鸢尾花为例讲。对普通K-Means我执行了50次随机初始化每次用不同的随机种子记录类内距离平方和SSE和轮廓系数。对DA-KMeans我让DA种群25个、迭代80次也重复运行50次排除单次随机运气。结果趋势符合预期普通K-Means的SSE均值明显偏高而且每次运行之间波动很大DA-KMeans的SSE均值更低波动很小几乎每次都能收敛到接近最优的同一个谷底。轮廓系数方面DA-KMeans的平均值也稳定高于普通K-Means的最好结果说明聚类结构更清晰。下表是我在自己电脑上得到的一组典型数据具体数值会随版本和随机种子变化看相对规律即可。方法50次平均SSE标准差平均轮廓系数普通K-Means140.25.80.70DA-KMeans128.60.40.744.2 三个关键参数怎么调参数调节经验很重要直接抄默认参数可能不够。我建议重点关注三个种群规模、最大迭代次数、适应度函数里的精炼轮数。种群规模N设20到40之间比较合理。太小了比如10个DA全局搜索覆盖面不够容易和普通K-Means一样陷入局部最优太大了比如80个每代都要跑80次K-Means局部优化耗时直线上升而收益边际递减。我做实验时常用的组合是N25MaxIt80在中小规模数据集上大概十几秒能跑完。最大迭代次数MaxIt不是越大越好因为适应度曲线通常在迭代后期已经平坦。我发现很多时候60次就稳定了设到100以上只是浪费算力。但如果你用的数据特别复杂中心和样本距离关系非线性增加迭代次数仍然有帮助可以用收敛曲线作为判断依据。精炼轮数refineIter是个隐藏参数我设为5。太小比如1轮局部优化不充分DA评估的每个个体质量不够准太大比如20轮会急剧增加计算量。实际测试中5轮和10轮结果基本一致所以没必要贪多。4.3 收敛曲线与结果可视化我强烈建议每次实验都把收敛曲线画出来。DA-KMeans的收敛曲线能直观反映两种阶段前期快速下降说明DA正在全局搜索中找到更优的中心组合后期平缓说明已经进入局部精修阶段。同时可以加上聚类散点图把两个维度的真分类、普通K-Means分类、DA-KMeans分类放一起对比。这类图做完之后业务方和评审老师一眼就能看出差异比单纯贴一个SSE数字有说服力得多。画图时注意用zscore归一化后的数据否则不同维度尺度过大图上某些维度会被拉平看不出簇结构。归一化和不归一化对聚类结果影响很大。之前有个项目我把销量和用户年龄放在一起聚类销量数值是年龄的一百多倍距离计算几乎完全被销量主导聚类结果完全失真后来强制归一化才恢复正常。5. 常见问题与排查技巧实录5.1 聚类结果不稳定怎么办如果每次运行DA-KMeans结果都不一样先检查随机种子。算法本身包含随机初始化所以结果有波动很正常但波动过大就不正常。我遇到过的最常见原因是数据没有归一化。尤其是混合量纲数据距离计算被某一个维度主导很多中心虽然看着不同实际都在同一个方向上移动导致算法对初始位置极其敏感。解决方法很简单在预处理阶段加一行zscore(data)。另一个原因就是种群太小。N10的时候DA的多样性不足几次运行很可能都落在同一个差的区域。把N至少调到20稳定性会明显提升。最后再看边界处理如果位置更新后直接超出上下界部分中心会被压到边界上聚类中心挤成一堆也会造成结果不稳定。5.2 收敛太慢怎么优化DA-KMeans比普通K-Means慢是正常的因为每代都要评估整个种群每个个体都包含几轮K-Means局部迭代。如果慢到完全不可接受优先级是这样先降低精炼轮数比如从5降到3再减少种群规模最后考虑降维。还可以考虑用距离矩阵缓存单独计算每个蜻蜓个体前先固定数据矩阵减少内存访问开销。但Matlab里这点优化收益有限。我的经验是中小数据集上几十秒是可接受范围如果数据有几十万条DA方案就不是首选了可以先抽样聚一次得到中心后再映射回全量数据。5.3 类别数K怎么选DA优化K-Means只能解决初始中心问题K值还得自己定。我一般结合手肘法和轮廓系数先设几个候选K比如2到8分别跑DA-KMeans记录SSE和平均轮廓系数。SSE随K增加会一直下降但下降幅度会变缓出现一个“拐点”这个拐点就是比较合理的K。轮廓系数则是越大越好通常会在某个K处达到峰值。两个指标结合判断不要只看一个。K值定好后最好在不同K下都运行几次DA确认轮廓系数曲线不是偶然抖动出来的。有些同学会直接用普通K-Means的结果来定K但那个结果本身受初始中心影响所以定的K也不一定可靠。5.4 代码运行过程中几个容易踩的坑我在这套代码上踩过不少低级坑列几个最常见的。reshape位置向量时行列顺序容易搞反。位置向量是按聚类中心逐行展开的所以reshape成K×d矩阵。要是写成d×K后续中心切片全乱。边界上下界要和位置向量维度一致。lb repmat(min(data), 1, K)这个写法让每个维度分别有最小和最大但如果min(data)是行向量repmat之后维度就是1×Kd刚好匹配。换成“全局最小值”就是错的。使用kmeans函数时Start参数要求是一个K×d矩阵不能直接把DA输出的一维向量塞进去必须先reshape。并行计算是另一个坑。如果开启并行池循环里调用calcFitness时没有写好parfor变量传递规则很容易因为worker上没有变量而报错。最省事的做法是先不开并行跑通流程再考虑用parfor加速。空簇问题前面提过我再强调一次如果某一类的样本数恰好为0mean会得到NaN这个NaN会顺着适应度函数传染给整个种群评估导致DA完全失去方向。初始化中心时加入小扰动、更新中心前判断any(idx)是两种简单有效的处理方法。优化后的中心做最终K-Means精调时不要把DA结果直接当作最终答案。我个人的最终落地流程是先用DA搜出可靠的中心起点再丢给正规K-Means迭代到完全收敛最后拿标准聚类的输出结果去建模。这样既利用了DA的全局搜索能力也保留了K-Means久经考验的收敛精度。最后再分享一个小技巧如果你的数据不那么复杂普通K-Means多跑几十次随机初始化再选一个SSE最低的结果可能已经够用了。但数据维度高、K值大、业务上又很看重复现性的时候DA-KMeans的优势就非常明显。把这套Matlab代码当成一个模板换成自己的数据改改参数就能用。后续如果想进阶可以把DA里的邻居半径动态调整加回来或者把适应度函数换成轮廓系数做成自适应K的版本效果还能再上一个台阶。