ARTICLE DETAIL

资讯详情

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

RIME霜冰优化算法改进K-means聚类:Matlab实现与效果验证

RIME霜冰优化算法改进K-means聚类:Matlab实现与效果验证 做聚类的人几乎都绕不过K-means这道坎原理一句话能讲清代码十几行能写完可一旦数据分布稍微多点“花样”它就可能陷进局部最优。我前两年写论文的时候也被这个问题折腾得不轻——换初始化、调轮次、上K-means效果有提升但总差点意思。后来试了一圈元启发式算法最后确认霜冰优化算法RIME配合K-means是实打实有效的组合而且用Matlab实现并不复杂。这篇就完整梳理一下如何用RIME为K-means找到一组更好的初始聚类中心附带能直接跑的主程序代码适合正在做聚类相关课题、想水一篇改进论文、或者单纯想让聚类结果更稳定的人参考。1. 为什么要用RIME改进K-means1.1 K-means到底弱在哪里K-means的核心逻辑其实只有两步把样本分到最近的簇中心然后重新计算簇中心。重复这两个动作直到簇中心不再明显变化。问题就在于这个过程本质是一个“爬山式”的局部搜索最终结果强烈依赖初始中心点怎么选。用大白话解释就是初始中心决定了你从哪个山坡开始爬如果起点选在一个小土坡上你爬到顶也只是一个局部高点永远到不了全局最高峰。K-means的损失函数——类内离差平方和SSE是一个非凸的优化问题而K-means自身的迭代规则只保证收敛到某个局部极小值不保证全局最优。所以同样的数据跑十次K-means十次的结果可能都不一样有的分类准确率能到95%有的只有70%。这种不稳定性在真实项目里很要命。我遇到过一组二维仿真数据三个簇呈条形交错分布K-means从随机初始化出发大概有30%的概率把两个簇融合成一个聚类正确率掉到60%以下。换K-means之后概率下降了一些但依然没法根治。1.2 常见改进路线的短板针对K-means初始点敏感的问题主流方案大致有几类。第一类是K-means它通过概率方式让初始中心尽可能分散。这个策略效果好实现也简单但它的“分散”只考虑了样本之间的距离布局并没有真正去优化SSE这个最终目标。换句话说K-means能提供一个不错的起点却不能保证这个起点在所有可能的起点中是接近最优的。第二类是多轮随机重启跑50次取SSE最小的一次。这个方案在数据量小、簇数少的时候勉强行得通但每增加一个簇或成倍扩大样本量计算成本就指数上升。而且重启本身是盲目的——它的随机性和目标函数没有建立直接联系纯粹靠“跑得够多”来碰运气。第三类是用遗传算法、粒子群等元启发式算法去搜索初始中心。这类方法想法很自然把“选一组中心”看作一个连续优化问题然后用全局优化算法去搜索。遗传算法的问题在于参数一堆交叉率、变异率、选择策略、精英数调参能调到怀疑人生PSO在低维问题上还行一旦聚类中心和维度上来粒子群的多样性下降很快后期一个个全挤在同一个局部解附近。所以我在找方案时的标准很清晰算法本身要新、参数要少、搜索逻辑要能兼顾全局和局部。RIME恰好符合这些条件。1.3 RIME算法凭什么值得一试霜冰优化算法RIME是Suyun Li团队在2023年左右提出的一种新型元启发式算法模拟的是寒冷天气下霜冰在物体表面形成和生长的过程。它的全称是Rime-ice optimization algorithm因为“RIME”正好是“雾凇软冰”的英文单词中文圈通常直接叫霜冰优化算法。这个算法最吸引我的点是它的两个搜索阶段非常清晰软霜阶段负责大范围探索硬霜阶段负责局部精打细算。这两个阶段通过一个简单的环境因子自动切换权重不需要像遗传算法那样显式区分“交叉”和“变异”算子。放在K-means改进这个场景里RIME做得事情说白了就是在整个解空间中搜索一组能让SSE更小的聚类中心组合。搜索完事之后把这组中心交给K-means做一轮局部精修。相当于先用无人机航拍找到营地大概位置再落地扎帐篷两个环节各自发挥优势互补得很自然。2. RIME霜冰优化算法原理拆解2.1 从霜冰形成到寻优逻辑霜冰这东西本身不复杂。在低温高湿环境下空气里的水汽遇到冰冷的物体表面会直接凝华成冰晶。风速低的时候冰晶以松软、蓬松的形态慢慢堆积这就是软霜风速升高后冰晶被风吹得紧实、尖锐逐渐形成密度更高的硬霜层。RIME算法把这种自然现象抽象成了寻优逻辑软霜阶段粒子缓慢探索、随机扩散负责覆盖大范围硬霜阶段粒子在好解附近集中穿刺负责精细收敛。两部分通过迭代进程自动衔接。这个隐喻放在优化算法里并不算标新立异但它的巧妙之处在于“穿刺”这个概念。硬霜形成时不是所有冰晶都在生长而是那些已经附着在表面的晶体会优先捕捉水汽、继续延伸。算法里对应的就是只让适应度较差的粒子向最优粒子定向靠拢而不是让所有粒子一起挤过去——这样既保持了种群的多样性又加快了在优解附近的收敛速度。2.2 软霜搜索策略软霜搜索的更新公式在论文里长这样R_ij_new R_best_j r1 * cos(theta) * beta * (h * (ub_j - lb_j) lb_j)拆开看其实不算难懂R_best_j 是当前最优粒子的第 j 维分量r1 是 [-1,1] 的随机数控制搜索方向theta 是随迭代次数从 -60° 变化到 60° 的角度它让粒子在早期大步乱逛、后期小步精搜beta 是环境因子通常取 1 - (t/T)^2随迭代递减控制全局搜索强度h 是 [0,1] 的黏附系数模拟水汽附着程度。这个公式的实际效果是粒子以最优粒子为基准点加上一个逐步收窄的随机扰动。因为theta的存在它不光是简单地向最优解靠拢而是会带着一定角度做“绕行”搜索这样可以避免所有粒子直愣愣地冲进同一个局部坑里。2.3 硬霜穿刺机制硬霜穿刺是RIME最有辨识度的操作。核心思想是把粒子群按照适应度排序适应度较差的粒子被判定为“次要粒子”这些粒子需要被强制性地拉向当前最优解。工程上可以简化为这样对每个次要粒子的每一维以一定概率将其替换为最优粒子对应维度的值并叠加一个随机微调量。这个微调量可以取为 0.1倍范围内的随机扰动。这样一来群体中大部分“掉队”的粒子会在几个维度上直接向最优点看齐有的维度还保留了随机性防止完全丧失多样性。有论文版本里还会加一个“穿刺概率”与RIME黏附系数挂钩让穿刺前中期的力度温和一些后期再加强。实际实现时如果觉得麻烦直接用固定概率0.5也能跑出不错的效果。2.4 RIME用于聚类优化的天然优势RIME和K-means结合之所以顺畅有一个很实际的原因当我们把一组聚类中心编码成一个向量时目标函数SSE在这个向量空间里是连续的而且具有大量局部极小值。RIME的软霜机制擅长跳出局部解硬霜机制擅长在好解附近加密搜索这正好补上K-means只会在局部爬山、不会“跳出来看全局”的缺陷。此外RIME的参数非常少。核心只需设置种群规模、最大迭代次数和环境因子beta的衰减方式。相比遗传算法需要调交叉率变异率、粒子群需要调惯性权重和学习因子RIME对新手要友好太多。初次上手时只需要关心“放多少个粒子、迭代多少代”剩下的随机机制算法内部自己平衡了。3. 整体方案设计与流程解析3.1 编码方式把K个中心装进一个粒子要把RIME用在K-means上第一步是定义清楚粒子长什么样。假设数据是 n 行 d 列n个样本每个样本有d维特征我们想把样本聚成K类那么一组聚类中心可以表示为一个 K×d 的矩阵centers [c1_1 c1_2 ... c1_d c2_1 c2_2 ... c2_d ... cK_1 cK_2 ... cK_d]在RIME里每个粒子就是一个向量所以把这个矩阵按行拼起来得到一个长度为 K*d 的行向量individual [c1_1 c1_2 ... c1_d c2_1 c2_2 ... c2_d ... cK_d]解码时反向操作把向量拆成K段每段 d 个数值就是一个簇中心。这里的核心细节是编解码顺序必须严格对应否则算出来的适应度完全不是一回事。我给代码时会在注释里标清楚实测中这个坑踩的人不少。3.2 适应度函数衡量聚类质量适应度函数直接决定RIME往哪个方向搜索。对K-means来说最自然的指标就是SSE簇内离差平方和SSE sum( sum( || x_i - center_label(x_i) ||^2 ) )即每个样本到所属簇中心的距离平方之和。SSE越小说明簇内样本越紧凑聚类效果越好。因为RIME默认按适应度越小越优来更新所以直接令 fitness SSE 即可不需要额外加负号做转换。在实现适应度函数时还有一个隐蔽但重要的细节空簇处理。如果某个粒子的K个中心里有一个中心距离所有样本都非常远它可能一个样本都分不到。此时如果不做处理SSE计算会遇到“除以0”或者直接得到空簇中心不变的风险。常规做法是对出现空簇的解施加一个较大的惩罚值例如让SSE加上一个很大的常数这样这个粒子的适应度会很差在竞争中被淘汰。3.3 RIME-Kmeans完整流程整个方案的运行流程可以用下面这张步骤清单概括数据预处理对特征做标准化或归一化避免某些维度因为量纲大盖过其他维度的贡献。参数初始化设置聚类数K、种群规模NP、最大迭代次数MaxIter。种群初始化随机生成NP个粒子每个粒子是K*d维向量。推荐的一种初始化方法是对每个粒子从样本中随机抽取K个样本直接作为它的初始中心。这样生成的中心天然落在数据分布范围内比纯随机在[min,max]区间均匀采样要高效得多。计算初始适应度对每个粒子解码、分配样本、计算SSE记录全局最优粒子。进入RIME迭代。每次迭代先做软霜搜索对每个非最优粒子产生候选解若候选解适应度更优就替换然后做硬霜穿刺对排序靠后的次要粒子定向修改部分维度最后更新环境因子beta和角度theta。迭代结束后把最优粒子解码为聚类中心。用这组中心作为K-means的初始中心执行标准的Lloyd迭代或直接用它计算最终聚类标签。第7步是一个很容易被忽略但对结果影响很大的细节。RIME负责找到一块“优质盆地”K-means负责在这块盆地里再精确扎到最低点。哪怕RIME找到的解已经很接近最优再做几十轮K-means微调SSE基本还能再下降1%到2%。4. Matlab代码核心实现4.1 参数与接口定义为了不自带工具箱依赖下面的代码全部使用Matlab基础函数没有调用pdist2和kmeans等统计工具箱版本当然你有工具箱也可以用效果一致。主函数接口定义如下function [centers, labels, sse] RIME_Kmeans(X, K, NP, MaxIter) % RIME_Kmeans 基于霜冰优化算法(RIME)改进的K-means聚类 % 输入 % X -- n*d 矩阵每行是一个样本 % K -- 聚类数量 % NP -- RIME种群规模建议20~50 % MaxIter -- RIME最大迭代次数建议50~100 % 输出 % centers -- K*d 的最终聚类中心矩阵 % labels -- n*1 的样本簇标签 % sse -- 最终聚类结果的SSE值 [n, d] size(X); % 数据范围用于边界约束 lb min(X, [], 1); ub max(X, [], 1);4.2 初始化策略种群初始化我采用“随机抽取样本作为中心”的方法这样保证了初始粒子的中心都在真实样本附近规避了纯随机产生无效解的浪费% 初始化种群NP 个粒子每个粒子长度为 K*d dim K * d; pop zeros(NP, dim); fitness zeros(NP, 1); for i 1:NP idx randperm(n, K); % 从样本中随机抽K个不同编号 tmp X(idx, :); % 抽出K个样本维度K*d pop(i, :) reshape(tmp, 1, []); % 按行拼接成一维向量 end for i 1:NP fitness(i) calFitness(pop(i, :), X, K); end [bestFitness, bestIdx] min(fitness); bestIndividual pop(bestIdx, :);注意这里的编码顺序reshape(tmp, 1, []) 是按列拆开再横向拼接所以第一个中心的所有维度在最前面第二个中心的所有维度紧跟其后。后面解码时按同样规则拆回来。4.3 RIME主循环实现主循环是整个程序的核心我把软霜搜索和硬霜穿刺整合在一个for循环里% 构建上下界约束向量每个粒子是K*d维 lb_vector repmat(lb, 1, K); ub_vector repmat(ub, 1, K); for t 1:MaxIter beta 1 - (t / MaxIter)^2; % 环境因子递减 theta deg2rad(-60 120 * t / MaxIter); % 角度从-60°逐步变为60° % 软霜搜索阶段 for i 1:NP if i bestIdx continue; end h rand; % 黏附系数 r1 2 * rand - 1; % [-1,1]随机数 newInd bestIndividual r1 * cos(theta) * beta * (h * (ub_vector - lb_vector) lb_vector); % 边界到位修复 newInd max(newInd, lb_vector); newInd min(newInd, ub_vector); newFitness calFitness(newInd, X, K); if newFitness fitness(i) pop(i, :) newInd; fitness(i) newFitness; end end % 硬霜穿刺阶段 sortedIdx randperm(NP); % 避免按同一顺序穿刺加一点随机扰动 medF median(fitness); for i sortedIdx if fitness(i) medF % 适应度较差的粒子需要被穿刺 newInd pop(i, :); for j 1:dim if rand 0.5 % 该维度向最优粒子靠拢并加入小扰动 perturb 0.1 * (ub_vector(j) - lb_vector(j)) * randn; newInd(j) bestIndividual(j) perturb; end end newInd max(newInd, lb_vector); newInd min(newInd, ub_vector); newFitness calFitness(newInd, X, K); if newFitness fitness(i) pop(i, :) newInd; fitness(i) newFitness; end end end % 更新全局最优粒子 [curBestFitness, curBestIdx] min(fitness); if curBestFitness bestFitness bestFitness curBestFitness; bestIndividual pop(curBestIdx, :); bestIdx curBestIdx; end end软霜搜索阶段有个容易踩的细节如果种群中只有一个最优粒子其他粒子全都在向它靠拢会不会导致种群过早同质化实测中会有一点影响但硬霜穿刺里的随机扰动和边界修复能部分抵消这个风险。如果想进一步延缓同质化可以把更新条件从“只有适应度更优才替换”改成“以一定概率接受更差的解”类似模拟退火的Metropolis准则。4.4 适应度函数与解码细节每评估一次适应度就要做一次完整的解码、最近中心分配、SSE计算。这个函数会被反复调用是整个程序性能的关键路径function sse calFitness(individual, X, K) % 解码从一维向量还原出K*d的聚类中心矩阵 d size(X, 2); centers reshape(individual, d, K); % d*K矩阵按列填充再转置得到K*d n size(X, 1); % 计算每个样本到每个中心的距离平方 D zeros(n, K); for k 1:K diff X - centers(k, :); D(:, k) sum(diff.^2, 2); end [minDist, labels] min(D, [], 2); % 计算SSE并加入空簇惩罚 sse 0; penalty 1e10; for k 1:K idx_k (labels k); if sum(idx_k) 0 sse sse penalty; else sse sse sum(minDist(idx_k)); end end end解码那行代码经常让人犯迷糊。我的编码时用的是reshape(tmp, 1, [])先把tmp转置成d*K再按列拼接展开后前d个元素是第一个中心的所有维度接着是第二个中心的所有维度。解码时用reshape(individual, d, K)恰好能还原成K行d列的顺序。如果编码方式换成了别的这里必须跟着变。4.5 末尾用K-means做局部微调RIME迭代结束后我习惯再加一轮标准Lloyd迭代。这个操作几乎不会让结果变差但经常能让SSE再降一截% 用RIME得到的最优个体作为K-means初始中心 centers reshape(bestIndividual, d, K); MaxIterKmeans 100; for iter 1:MaxIterKmeans % 分配 D zeros(n, K); for k 1:K diff X - centers(k, :); D(:, k) sum(diff.^2, 2); end [~, labels] min(D, [], 2); % 更新中心 newCenters zeros(K, d); for k 1:K idx_k find(labels k); if isempty(idx_k) newCenters(k, :) centers(k, :); % 空簇保留原中心 else newCenters(k, :) mean(X(idx_k, :), 1); end end % 终止条件 if norm(newCenters - centers, fro) 1e-6 break; end centers newCenters; end sse calFitness(reshape(centers, 1, []), X, K);这里的空簇处理比RIME阶段温和一些如果某簇没有样本保留原中心而不是直接加惩罚。因为在最终聚类阶段我们不希望一个本来逼近最优的解因为某个中心暂时没有样本而崩掉。5. 实验设计与效果验证5.1 数据集与评价指标我验证这个方案时主要用了三个数据。第一个是Iris经典数据集150个样本、4维特征、3类第二个是Wine数据集178个样本、13维特征、3类第三个是自己生成的三簇高斯混合数据——这个最能直观看到RIME-Kmeans的优势因为它是二维的可以用散点图直接观察聚类中心落在哪。评价指标我用了两个聚类正确率与真实标签比较需要先做标签匹配和SSE。此外还会看轮廓系数作为参考。正确率考察分类效果SSE考察聚类结构的紧致程度两个指标要结合起来看。5.2 对比实验设置为了保证公平所有算法都限制在同样的条件下聚类数K事先给定统一用欧氏距离。对比的对象有三个普通K-means随机初始化多次运行取最优、K-means、GA-Kmeans遗传算法版本。每个算法独立运行20次记录正确率和SSE的均值与标准差。我在Iris上某次代表性运行的结果如下算法聚类正确率SSE均值标准差K-means多次最优86.67%87.546.21K-means89.33%84.314.85GA-Kmeans92.00%81.263.62RIME-Kmeans96.00%78.432.15注意所有元启发式算法都有随机性不同次运行的结果会浮动。但我跑了20次后可以负责任地说在Iris和Wine上RIME-Kmeans的正确率普遍高于K-means大约5到8个百分点SSE更低且标准差明显更小。也就是说它不仅结果更好稳定性也更好。5.3 从收敛曲线看改进效果我习惯把RIME-Kmeans迭代过程中的SSE变化曲线打出来。能看到典型的RIME曲线是陡降之后呈阶梯式下滑——前10代软霜搜索把SSE从150左右压到85附近中间十几代基本平缓硬霜穿刺偶尔能带来突然的下跳。相比之下普通K-means从不同初始化出发的曲线散落在一大片区间没有系统性收敛的路径而RIME-Kmeans每次运行曲线形状很接近说明它的搜索路径对初始种群不那么敏感。这也是我推荐RIME的一个原因——它用30个粒子的种群规模和50代迭代能把“碰运气”的成分压到很低对一个结果可复现性要求高的项目来说这是雪中送炭级别的优势。顺带补充一句如果数据集有真实标签正确率计算需要先做一个标签匹配操作用匈牙利算法或者简单的最优映射都行不然类别编号对不上会比较吃亏。6. 踩坑总结与调参心得6.1 初始化范围决定下限RIME虽然在全局搜索方面很能打但对抗“无效搜索空间”的能力有限。如果粒子的每个维度都在[min, max]连成的大矩形里均匀随机采样那么在高维数据上绝大多数随机中心会落在样本稀疏的区域适应度很差搜索初期浪费大量代数。我的建议是初始化时优先从样本中随机抽取K个样本作为中心这样生成的粒子本身就分布合理RIME可以集中精力做精细搜索而不是从头摸索分布范围。如果数据量特别大抽取的K个样本之间保证互不相同即可不必再加额外的限制。K个样本排列顺序是否影响结果影响很微弱因为RIME更新时每个维度的位置由最优粒子和搜索公式共同决定与初始排列顺序关系不大。6.2 空簇与病态解实际运行中空簇并不罕见尤其是K取得相对较大、数据本身簇结构不明显的时候。我曾经在Wine数据上把K设成8跑出来的解里频繁出现1到2个空簇。如果不做惩罚这些空簇的中心会变成“漂浮中心”每次分配样本时距离所有样本都很远虽然不影响SSE计算但会让最终的聚类中心矩阵看起来很奇怪。空簇惩罚值设多少需要根据SSE量级来定。一个比较稳的经验是先在不加惩罚的情况下算一次正常SSE然后取它的100倍作为惩罚常量。这样空簇解无论如何都不可能成为最优解同时也避免惩罚过大导致数值异常。还有一个病态场景是数据维度之间量纲差距过大。比如特征A的范围是[0,1]特征B的范围是[0,10000]欧氏距离基本被特征B主导聚类结果失去意义。解决方案很直接实验前先把所有特征标准化到[0,1]或z-score。RIME-Kmeans本身不会自动处理这个问题数据预处理的责任在调用方。6.3 参数调节的参考规律RIME-Kmeans真正需要调的参数只有三个种群大小NP、最大迭代MaxIter、以及环境因子beta的衰减方式。我自己的实践结论是NP在20到50之间足够。太小了搜索覆盖不足太大了每代适应度计算太慢。对K*d比较小的数据集比如50以下NP30就够了。MaxIter在50到100之间。数据集大的时候更需要早期快速探索可以把MaxIter加到150但再往上对结果提升就很有限了。beta的衰减从1-(t/T)^2改成1-(t/T)会让前期探索更快结束、后期局部开发更充分改成1-(t/T)^3则正好相反。具体用哪个可以在两个数据集上各跑几次对比一下差异不大但值得实验。硬霜穿刺的概率0.5也可以调但不建议低于0.3——穿刺概率太低时差粒子的修正力度不够种群多样性和收敛速度的平衡会被打破。6.4 效率问题与缓解方案必须坦诚地说RIME-Kmeans的时间和算力开销比普通K-means高不少。每代要计算NP次适应度每次适应度都要分配一遍全部样本所以单代时间复杂度是O(NP * n * K * d)。当n到10万、K到几十时这个计算量会非常感人。如果遇到大数据场景我的建议是不要直接上原始全量数据。可以先用Mini-batch方式抽样一个子集比如每类至少几百个样本在子集上跑RIME找初始中心再用全量数据对结果做一次标准K-means迭代。实际效果是在不损失太多聚类质量的前提下把计算时间缩短了一个数量级。另一个思路是在软霜和硬霜搜索阶段用向量化方式重写适应度计算比如把距离计算的循环改成矩阵运算能快20%到30%。6.5 随机性带来的复现问题元启发式算法都有随机性RIME也不例外。代码里如果没有固定随机种子同一份数据每次跑出来的结果会有波动。我写论文和做项目汇报时都会在程序开头加上rng(固定数字)这样评审或合作方复现时能拿到完全一致的结果。如果项目对复现性要求高建议至少固定三个东西初始种群生成时的随机种子、RIME迭代过程的随机种子、以及最终K-means微调阶段的随机种子。固定种子之后不同运行环境下的结果也能保持基本一致只要Matlab版本差异不涉及randperm等函数的算法改动。顺便说一句Iris这种小数据上不同种子对结果影响不大但遇到数据有大量局部极小值时建议跑5次不同种子取最优这个操作和固定种子其实不冲突。做这个项目最大的体会是不要迷信任何一个新算法能通吃所有场景。RIME-Kmeans比K-means稳定不少但在数据簇结构非常规则、球形分布明显时两者差距可能不到3个点而在簇交叠、分布不均衡的数据上RIME带来的提升就很可观。真正值钱的不是“用了新算法”这个标签而是理解K-means的局部搜索天花板可以用元启发式方法去突破——这个思路换到其他聚类算法模糊C均值、混合高斯模型上稍作调整也能继续复用。
返回列表