资讯动态

Fuzzy C-Means与K-Means对比:软聚类原理、Python实现与实战调参

发布时间:2026/10/15 17:38:21 来源:尧图企业网站定制
说实话我第一次接触Fuzzy C-MeansFCM时总觉得它只是K-Means的“模糊版”——把硬标签换成软概率换汤不换药。直到做客户分群时遇到一批“既活跃又沉默”的用户我才意识到硬划分有多别扭你强行把这种人塞进A簇或B簇后面的运营策略就会失真。而FCM给出一组连续隶属度能清楚告诉我这个人更偏向哪边、偏向到什么程度。这篇文章就按我自己的理解路径来写先讲清硬聚类和软聚类的本质差异再拆解K-Means和FCM的目标函数与迭代推导最后用纯NumPy手写两份可运行的Python代码并用一张实际生成的数据图来对比。除此之外还会聊k值、模糊指数m、初始化、数据标准化这些真正影响实战效果的细节。适合刚入门机器学习、想搞懂算法原理的读者也适合那些会用sklearn但始终把FCM当黑盒的同学。1. 先分清硬聚类和软聚类同一个聚类问题的两种解决思路1.1 一个生活案例凌晨两点的用户到底属于哪一类假设你在运营一个内容社区想按作息时间把用户分成“夜猫子”和“早起鸟”两类。凌晨三点还在发帖的用户很明确早上六点开始刷首页的用户也很明确这两类人一刀切下去干净利落。但那个每天凌晨一点睡、早上八点起床的人呢你说他算夜猫子还是早起鸟硬聚类必须给他一个答案于是系统把他扔进其中某一类哪怕他其实两边都沾。这种“非此即彼”的划分方式就是硬聚类K-Means是其中最典型的代表。软聚类就不一样。FCM会给这个用户输出一组数比如“0.6属于夜猫子0.4属于早起鸟”。两个值加起来等于1但都不是0或1表示他处在两个簇的过渡地带。这个过渡信息看起来不起眼但在真实业务里非常值钱——你可以针对“0.4~0.6”这个区间的人单独设计运营策略而不是硬性归边。理解了这层差异后面所有数学公式就都有了直觉意义软聚类不是要模糊处理而是想把数据本身带有的“中间状态”显式表达出来。1.2 硬分配矩阵与软隶属度矩阵的数学表达用数学语言来描述上面的差异其实是换了一种数据结构。K-Means内部维护一个硬分配矩阵R维度是n×kn个样本、k个簇。矩阵第i行第k列的元素r_ik∈{0,1}每行有且只有一个1表示第i个样本被唯一分配给第k个簇。其他位置全是0。FCM维护的则是一个隶属度矩阵U维度是n×cc表示簇数。元素u_ik∈[0,1]每行满足Σu_ik1。u_ik越大表示样本i越“属于”簇k。注意这里的u_ik不是概率它是一个程度值衡量的是样本与簇中心之间“有多像”。概率描述的是随机事件的不确定性而隶属度描述的是对象本身在多大程度上具有某个类别的属性。数据结构一变目标函数和迭代方式全变了。所以K-Means和FCM虽然是亲戚但血缘关系没有你想的那么近。K-Means是硬划分的贪心优化FCM是软划分下的连续优化后者的解空间更大表达力更强代价是计算量和参数复杂度也随之上升。2. K-Means原理拆解目标函数、两步迭代与收敛性2.1 目标函数为什么是误差平方和K-Means要解决的问题用一句话说给定n个样本把它们分成k个簇让每个样本到所属簇质心的欧氏距离平方和最小。写成公式是J Σ_{i1}^{n} Σ_{k1}^{K} r_ik · ||x_i - μ_k||²这里的μ_k是第k个簇的质心r_ik就是上一节说的硬分配标记。这个目标函数叫误差平方和SSE也叫簇内惯性。如果你去掉平方直接用距离绝对值的和优化起来会出现质心是“中位数”而不是“均值”的情况那就不是K-Means了。平方项让远离质心的点受到更大惩罚同时使得“均值”成为平方误差下的最优解——这正是K-Means名字里“Means”的来源。一个很反直觉的点是K-Means其实是一个优化问题但它的目标函数是非凸的。这意味着存在大量局部最小值算法只能找到其中一个不保证全局最优。这就是为什么K-Means对初始化这么敏感也解释了后面要讲到的K-Means和多次重启。2.2 分配与更新坐标下降视角下的迭代逻辑K-Means的迭代过程只有两步看起来异常简单但背后有个很漂亮的优化结构。第一步叫分配固定所有质心把每个样本分给离它最近的质心。这一步是在优化指标矩阵R公式是r_ik 1当且仅当k是距离x_i最近的质心索引。第二步叫更新固定所有样本的分配结果重新计算每个簇的质心。因为簇内均值就是平方误差下的最优质心这一步是在优化质心矩阵μ。交替执行这两步本质上是一种坐标下降法。你把所有优化变量拆成R和μ两组先固定一组优化另一组再反过来。关键是每一步都不会让目标函数J变大所以算法保证收敛。但注意它收敛到的是局部最优而不是全局最优。我在学习时最大的误区是“两步迭代的算法一定很简单不需要深究”。其实理解坐标下降视角对调试很有帮助。比如如果某次运行结果明显不合理你可以检查初始质心是否选在了同一个簇里或者是否陷入了一个差劲的局部最优。这些判断都建立在对“每一步都在降J”的理解之上。2.3 初始化与K值K-Means、肘部法则和轮廓系数随机初始化是K-Means最大的软肋。如果初始质心选得不好很容易收敛到差解。最典型的补救是K-Means第一个质心随机选之后每个新质心以“距离已选质心越远越容易被选为下一个质心”的概率被挑出来。这样初始质心在数据空间里比较分散能显著减少糟糕的局部最优。K值的选择则更偏向经验性。最常用的肘部法是画出“不同k对应的SSE曲线”找一个弯曲明显的拐点。但实际数据里拐点经常不明显这时候我会配合轮廓系数来判断某个样本的轮廓系数是(b - a)/max(a, b)a是它跟簇内其他样本的平均距离b是它到最近邻簇的平均距离。轮廓系数接近1表示划分清晰接近0表示样本在边界上接近-1表示可能分错了簇。最后还有一个容易被忽略的细节多次运行最小SSE选择。sklearn的KMeans里n_init10默认就是这么干的。你的手写实现完全可以直接调n_init参数做同样的事。3. Fuzzy C-Means原理推导隶属度、模糊因子与拉格朗日乘子法3.1 模糊目标函数与两个约束FCM的第一步是把K-Means里的硬分配标记r_ik替换成连续隶属度u_ik然后目标函数变成J_m Σ_{i1}^{n} Σ_{k1}^{c} u_ik^m · ||x_i - v_k||²这里u_ik∈[0,1]v_k是模糊质心。和K-Means相比最大的变化是u_ik的m次方。这个m叫模糊指数它控制了“软”的程度。为什么偏偏是幂次而不是直接乘以u_ik因为如果m1这个优化问题的最优解又会回到硬分配只有m1时目标函数才对中间隶属度有“容忍度”允许过渡地带存在。约束条件还是那条每个样本对所有簇的隶属度加起来等于1即对任意iΣ_{k1}^{c} u_ik 1。所以现在的问题是在每行隶属度和为1的约束下求一组质心v_k和一组隶属度u_ik让J_m最小。这个优化没有解析解但可以通过拉格朗日乘子法得到两个交替迭代的更新公式。3.2 从拉格朗日乘子法到两个更新公式这个推导不复杂但它很重要我建议你至少亲手推一遍。先把约束条件写进拉格朗日函数L J_m - Σ_{i1}^{n} λ_i · (Σ_{k1}^{c} u_ik - 1)第一步对u_ik求偏导并令其等于0得到m · u_ik^(m-1) · d_ik² λ_i其中d_ik ||x_i - v_k||。利用约束Σu_ij1化简消去λ_i可以得到隶属度更新公式u_ik 1 / Σ_{j1}^{c} (d_ik / d_ij)^(2/(m-1))这个公式看起来很复杂但你只看它的行为如果样本i离簇k的质心特别近d_ik很小那么u_ik就会接近1如果它离所有簇都很远且远近差别不大隶属度就会趋向均匀。第二步对v_k求偏导并令其等于0得到质心更新公式v_k Σ_{i1}^{n} u_ik^m · x_i / Σ_{i1}^{n} u_ik^m注意分母上的u_ik^m。它相当于给每个样本的“投票权”做了模糊加权离质心越近、隶属度越高的样本对质心位置影响越大离得很远的样本虽然也会参与计算但权重会被m次方压得很小。迭代时你先用当前质心算隶属度再用新隶属度更新质心循环往复直到隶属度矩阵的变化量小于阈值。理论上和K-Means类似每一步都在降低目标函数所以FCM保证收敛到某个局部最优。3.3 模糊指数m的语义与选择逻辑m到底取多少是整个FCM里最需要经验的地方。常见建议是m2大多数人直接从2开始试。但你要理解m变化时发生了什么才能试得有理有据。当m趋近于1时隶属度会迅速向0和1两极分化最终退化成K-Means的行为当m趋近于无穷大时所有隶属度都趋向1/c所有质心都逼近整个数据集的均值聚类完全失去区分度。所以m不是一个可以随便调大的旋钮m越大聚类结果越模糊极限情况下等于“不聚类”。实际操作中我一般会跑一组m[1.5, 2.0, 2.5, 3.0]的对比实验观察两个指标一是质心是否稳定二是每个样本最大隶属度是否过低。如果大量样本的最大隶属度都小于0.5说明m太大簇结构已经糊了。也可以用模糊划分系数F(U) Σ_i Σ_k u_ik² / n值越接近1表示聚类越锐利越接近1/c表示越模糊作为衡量m是否合适的参考。4. Python实现从零手写K-Means和FCM并可视化对比4.1 环境准备与数据生成建议直接在虚拟环境里装numpy、matplotlib、scikit-learn。scikit-learn不是必须品但make_blobs和KMeans参考实现都用得上。FCM如果要现成库可以pip install scikit-fuzzy不过我会先手写一遍因为库只能给你结果给不了理解。先用make_blobs生成一组明显重叠的二维数据这是观察软聚类和硬聚类差异的好素材。import numpy as np import matplotlib.pyplot as plt from sklearn.datasets import make_blobs from sklearn.preprocessing import StandardScaler # 生成4簇数据加大方差让簇之间存在明显重叠 X_raw, y_true make_blobs(n_samples500, centers4, cluster_std1.6, random_state42) X StandardScaler().fit_transform(X_raw)标准化这步在正式项目里几乎总要做原因我会在第5章细说。这里先做一个铺垫如果不缩放量纲不同的特征会直接主导欧氏距离。4.2 手写K-Meansnumpy广播计算距离矩阵核心是两步迭代。距离矩阵用numpy广播一次算出来不要写三层for循环。def kmeans(X, k, max_iters100, tol1e-5, seed42): rng np.random.default_rng(seed) n X.shape[0] # 随机选k个样本点作为初始质心 idx rng.choice(n, sizek, replaceFalse) centers X[idx].copy() for _ in range(max_iters): # 第一步分配。计算每个样本到每个质心的距离取最近质心 dists np.linalg.norm(X[:, None, :] - centers[None, :, :], axis2) labels np.argmin(dists, axis1) # 第二步更新。每个簇的新质心就是簇内样本的均值 new_centers centers.copy() for j in range(k): mask labels j if mask.sum() 0: new_centers[j] X[mask].mean(axis0) # 收敛判断质心最大分量变化足够小就停止 shift np.abs(new_centers - centers).max() centers new_centers if shift tol: break return centers, labels有几个细节值得说。一是空簇处理如果某次迭代某个簇没有样本我选择保持旧质心不动。更稳的做法是给空簇随机重新初始化但在多数数据集里保持旧值已经够用。二是收敛阈值tol我习惯用1e-5太大结果不稳定太小则白白增加迭代次数。三是最小化SSE的重启逻辑头脑清醒的人会写个循环跑多种初始化取最优而不是只跑一次就收工。4.3 手写FCM隶属度矩阵迭代的正确姿势FCM手写版比K-Means多了两张矩阵要维护隶属度矩阵U和距离矩阵D。核心代码其实更短。def fcm(X, c, m2, max_iters100, tol1e-5, seed42): rng np.random.default_rng(seed) n X.shape[0] # 随机初始化隶属度矩阵每行归一化使和等于1 U rng.random((n, c)) U U / U.sum(axis1, keepdimsTrue) for _ in range(max_iters): # 第一子步按当前隶属度更新质心 um U ** m centers (um.T X) / np.maximum(um.sum(axis0), 1e-12)[:, None] # 第二子步按当前质心更新隶属度 dists np.linalg.norm(X[:, None, :] - centers[None, :, :], axis2) dists np.maximum(dists, 1e-12) # 防止除零错误 weight dists ** (-2.0 / (m - 1.0)) U_new weight / weight.sum(axis1, keepdimsTrue) # 收敛判断隶属度矩阵的最大变化量 if np.max(np.abs(U_new - U)) tol: U U_new break U U_new labels np.argmax(U, axis1) return U, centers, labels这段代码的关键点有三个。第一质心更新的矩阵写法um.T X就是所有样本的模糊加权和除数是每个簇的隶属度m次幂之和对应公式v_k Σ u_ik^m x_i / Σ u_ik^m。第二隶属度更新的向量化写法weight dists^(-2/(m-1))然后每行除以行和。这正是u_ik公式里权重比d_ik^(-2/(m-1)) / Σ d_ij^(-2/(m-1))的形式比双重for循环快得多。第三dists被clip到1e-12是为了避免样本质心与质心完全重合时出现除零。如果某个点距离某个质心为0clip后它的weight巨大归一化后隶属度趋近1符合理论预期。4.4 造数据对比硬边界与模糊过渡的直观差异跑一遍两个函数然后把结果画出来。可视化不是为了好看而是为了验证你对软聚类的理解。k c 4 centers_km, labels_km kmeans(X, k, seed42) U_fcm, centers_fcm, labels_fcm fcm(X, c, m2, seed42) fig, axes plt.subplots(1, 2, figsize(12, 5)) # 左图K-Means硬聚类结果 axes[0].scatter(X[:, 0], X[:, 1], clabels_km, cmapviridis, s40) axes[0].scatter(centers_km[:, 0], centers_km[:, 1], cred, markerx, s120, labelcenters) axes[0].set_title(K-Means) axes[0].legend() # 右图FCM硬标签着色 透明度表达最大隶属度 max_u U_fcm.max(axis1) sc axes[1].scatter(X[:, 0], X[:, 1], clabels_fcm, cmapviridis, s40, alphamax_u) axes[1].scatter(centers_fcm[:, 0], centers_fcm[:, 1], cred, markerx, s120, labelcenters) axes[1].set_title(FCM, alpha max membership) axes[1].legend() plt.tight_layout() plt.show()你会在右图里看到一个很有价值的效果重叠区域的点颜色很淡因为这些点虽然被argmax分配到了某个簇但最大隶属度只有0.5左右。左图里同样的区域颜色很深看不到任何过渡。这就是我在第1章强调的硬聚类强行切了一个不存在的边界软聚类把这个边界的模糊程度直接画了出来。如果还想验证FCM的质心和K-Means的质心差异可以打印两个质心数组。因为FCM的质心是模糊加权均值对于重叠数据它会稍微偏向全局结构而不是硬边界。这个差异通常不大但在业务上“可能的边界”比“唯一的边界”更重要。4.5 不想手写sklearn和scikit-fuzzy的参考用法手写能加深理解但生产环境我还是会优先用现成实现。K-Means直接用sklearnfrom sklearn.cluster import KMeans km KMeans(n_clusters4, n_init10, random_state42).fit(X) labels_km_sk km.labels_FCM用scikit-fuzzy注意它的接口要求将数据转置成“特征×样本”格式其他地方和手写版一致import skfuzzy as fuzz cntr, U_sk, u0, d, jm, p, fpc fuzz.cluster.cmeans( X.T, c4, m2, error1e-5, maxiter100, initrandom, seed42 ) labels_fcm_sk np.argmax(U_sk, axis0)这里的U_sk是c×n的矩阵如果你要拿它和手写版的U_fcm对比先确认两者的列顺序和簇编号是否一致。我遇到过多次因为簇编号对应不上导致结论反转的情况所以比较之前最好先对齐质心。5. 实战中的参数选择与常见坑m、k、初始化怎么定5.1 一张表格看清K-Means和FCM的差异选型之前先把两种算法的硬差异摆出来下面是我整理的一张速查表对比维度K-MeansFuzzy C-Means归属方式硬分配r_ik∈{0,1}隶属度u_ik∈[0,1]目标函数ΣΣ r_ik·d_ik²ΣΣ u_ik^m·d_ik²迭代内容质心 硬标签质心 隶属度矩阵输出每个样本一个标签标签 n×c隶属度矩阵关键参数k、初始化次数c、m、初始化方式单次迭代开销距离矩阵O(n·k·d)距离隶属度归一化约O(n·c²·d)重叠簇表现强行切硬边界保留模糊过渡并量化噪声响应离群点可能扭曲质心模糊加权一定程度抑制孤立点影响K-Means的优势在于简单、快、可解释性强FCM的优势在于输出信息量大、能表达边界状态。它们不是谁替代谁的关系而是不同粒度决策下的工具。5.2 聚类数、模糊指数、初始化三件套的调法先定c或k。这一步无论用什么算法都躲不过去。我的顺序是先用肘部法看SSE曲线再用轮廓系数验证候选值最后回到业务约束。数据如果有明确的运营目标比如“分成3类方便做策略”那参数k3就直接用。不要为了追求高轮廓值而盲目加大k聚类最终要服务的还是业务动作。m值的调节我会在FCM上专门做一轮实验。选一个区间比如[1.3, 2.0, 2.5, 3.0]对每个m跑一次观察最大隶属度的分布。如果大量点在0.8以上说明m偏小结果接近K-Means如果大量点低于0.5说明簇结构模糊得没法用m需要回调。从m2出发做微调通常是最有效的路径。初始化是第三个坑。K-Means用K-Means加多次重启基本可以放心。FCM手写版我上面用的是随机归一化U这也是一种常见做法但稳定性一般。更稳的玩法是先用K-Means跑一遍把硬标签转成one-hot矩阵作为FCM的初始U再跑FCM。这样不仅能加速收敛还能明显减少随机初始化造成的差异。5.3 数据标准化与特征工程别让量纲主导距离K-Means和FCM的底层都是欧氏距离所以量纲问题会被直接放大。比如一个特征“年收入”取值6万另一个特征“年龄”取值30如果不做标准化距离几乎完全由年收入决定年龄等于摆设。这就是为什么我在第4章的数据生成里对X_raw做了StandardScaler。标准化可以选Z-score也可以选MinMaxScaler。数据分布近似高斯时Z-score更自然有界特征或需要保持稀疏结构时MinMaxScaler更方便。标准化之外还要关注特征冗余问题。高度相关的特征会放大某些方向的权重让簇形状被拉长这时考虑PCA降维或删除冗余特征聚类效果通常会明显提升。还有一个很多人会踩的坑聚类之前对全量数据做标准化可视化时却忘记要还原坐标尺度的映射关系。如果直接画原始特征的散点而质心是标准化坐标两边的图就对不上业务解释也会乱。我习惯把所有处理后的数据、质心、隶属度放在同一个标准化空间里分析和可视化最后只在汇报阶段做逆变换。5.4 从聚类输出到业务决策软信息怎么用K-Means给出一组标签后你只能回答“这个样本属于哪个簇”。FCM还允许你追问“它有多确定”。这在很多场景里比标签本身更重要。我做过一个用户运营项目目标是划分高活跃、潜在活跃、低活跃三类用户。K-Means把所有边界用户硬分到某一类后续权益发放策略就全乱了。FCM跑完之后我专门把最大隶属度在0.4~0.6之间的人拉出来这批人才是最值得运营策略重点关注的“摇摆用户”。他们不是忠诚于任何一方而是处在转化窗口期。软隶属度还可以直接作为特征输入下游模型。比如把U矩阵里的每个值拼进用户特征向量相当于给下游分类器多提供了“这个样本和每个簇的相似度”信息很多情况下比硬标签效果更好。但注意u_ik不是概率不要把“隶属度0.4”解读成“有40%概率属于这个簇”至少在汇报给业务方时要换一种严谨的说法宁可叫它“偏度”或“相似度”。最后提醒一点无论用哪个算法聚类结果一定要做稳定性验证。同一个算法同一个k换几个随机种子跑看簇中心的归属是否接近。如果换个种子结果天翻地覆说明数据本身不存在稳定簇结构这时候先别急着优化参数回去重新审视特征和数据分布才是正路。我个人的项目习惯是聚类任务永远先跑一遍K-Means拿基线结果确认数据分布大致合理后再决定要不要上FCM。如果上FCM我会把最大隶属度在0.4~0.6之间的样本单独导出来这批“模糊样本”往往比簇中心的样本更能暴露业务问题。比起争辩K-Means和FCM哪个更先进这些写在边界上的信息才是聚类算法真正值钱的地方。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑