做数据处理的人,几乎都绕不开聚类问题。如果你正在用Matlab实现FCM聚类,而且想知道归一化这步到底该怎么做、它对之后的分类结果影响有多大,这篇文章应该能帮你把整条链路彻底走通。我会从FCM的底层逻辑讲起,再把归一化方法逐个过一遍,然后给出可以直接在Matlab里跑通的代码,最后聊聊调参、判读结果和踩坑经验。这里不会像教科书那样只堆公式,我会尽量用实际数据的效果和代码注释说清楚每个选择背后的原因。适合的人群包括:正在做课程设计或论文实验的学生、需要做客户分群或特征归类的工程师、以及想在图像分割里尝试模糊划分并且对Matlab工具链比较熟悉的开发者。
1. FCM聚类原理:模糊隶属度是如何让一个样本属于多个类别的
1.1 现实场景:边界真的清晰吗
K-means聚类是很多人最初接触的算法:它对每个样本做硬分类,也就是样本要么属于A类,要么属于B类,不存在中间状态。但在很多实际问题里,这种“非黑即白”的判断并不合理。
举个例子:你在给App用户做分群。一个用户每天既刷短视频又用搜索引擎查资料,还偶尔看看游戏直播。如果只用K-means硬性把他归到“视频用户”或“搜索用户”其中一类,这个用户的很多行为信息会被直接丢掉。而FCM聚类恰恰是为这种问题设计的:它允许一个样本同时以不同比例归属多个类别,比如“视频用户0.5、搜索用户0.3、游戏用户0.2”,这个比例就是隶属度。
另一个更直观的场景是图像分割。在医学影像中,目标区域和背景之间的灰度灰度往往存在过渡带,很难用一个硬阈值把像素完全切开。FCM的模糊属性天然适合处理这种边界问题。你可以把每个像素分配到多个区域的强度理解成一个“软标签”,后续做再处理时保留更多信息。
1.2 目标函数与两条核心迭代公式
FCM聚类的数学核心是一个带约束的优化问题。假设样本集是矩阵 X,维度为 n×d,其中 n 是样本数,d 是特征数;想要划分为 c 个簇,需要求解一个 n×c 的隶属度矩阵 U 和 c×d 的聚类中心矩阵 V。
目标函数如下:
J_m = Σ_{i=1}^{c} Σ_{k=1}^{n} u_{ik}^m · ||x_k - v_i||^2
其中 u 表示隶属度,m 是模糊指数,||x_k - v_i|| 表示样本到中心的欧氏距离。
约束条件是每个样本在所有簇上的隶属度之和为1:
Σ_{i=1}^{c} u_{ik} = 1
求解思路是交替迭代更新 V 和 U,直到目标函数不再明显下降。聚类中心更新公式:
v_i = (Σ_{k=1}^{n} u_{ik}^m · x_k) / (Σ_{k=1}^{n} u_{ik}^m)
这个公式可以理解为:所有样本对某个簇做加权平均,权重就是隶属度的 m 次方。样本对某簇越“忠诚”,它对簇中心位置的贡献越大。
隶属度更新公式看起来更复杂,但逻辑其实非常直观:
u_{ik} = 1 / Σ_{j=1}^{c} ( ||x_k - v_i|| / ||x_k - v_j|| )^{2/(m-1)}
每个样本对某个簇的新隶属度,取决于它到当前簇的距离相对所有簇距离的比例。离某个簇越近,它对这个簇的隶属度就越高;离得远了,隶属度自然降下来。
1.3 m 参数:控制模糊程度的旋钮
模糊指数 m 是 FCM 里最核心的参数。m 越小,结果越接近硬聚类;m 很大时,每个样本对每个簇的隶属度都会趋向均匀化,所有样本看起来都差不多,聚类就失去了意义。
当 m 趋近于 1 时,FCM 的行为会退化成 K-means,这个性质可以帮助你判断自己是不是把参数调得太“硬”了。常用取值为 1.5 到 2.5,Matlab 自带工具箱的默认值就是 2。这个范围能保证模糊划分有意义,同时又不会让聚类中心过度偏移。后面第 4 部分我会再展开讲如何对这种参数做实际检验。
2. 为什么归一化会影响成败:三种常用方法对比与实测影响
2.1 量纲效应:距离计算中的“以大欺小”
FCM 的目标函数依赖欧氏距离,所以特征量纲是否一致直接决定聚类结果的质量。拿一个非常简单的二维数据举例:特征一是年龄,范围 20 到 60;特征二是年收入,范围 5万 到 50万。如果不做归一化,收入特征的数值远大于年龄特征,计算欧氏距离时年龄几乎起不到作用。而聚类中心主要被收入拉开,两个距离相近但收入相同、年龄差异大的样本可能会被误判到同一类。
这不是 FCM 单独存在的问题,只要是基于欧氏距离的聚类算法(K-means、层次聚类等)都会遇到。但 FCM 因为有隶属度公式,距离误导的影响会被进一步放大:一个样本对两个簇的隶属度比例完全被错误尺度主导,后续分类结果自然偏离真实结构。
2.2 min-max、z-score 与 MAD 归一化
在 Matlab 生态里,归一化方法常见的就三种:min-max 归一化、z-score 标准化和 MAD 归一化。它们解决的问题相同,但适用场景差别挺大。
| 方法 | 公式 | 受异常值影响 | 适用场景 |
|---|---|---|---|
| min-max | (x - min) / (max - min) | 高 | 数据边界已知、分布均匀 |
| z-score | (x - mean) / std | 中等 | 数据近似正态分布、常规首选 |
| MAD 归一化 | (x - median) / mad | 低 | 存在离群点、需要稳健尺度的数据 |
先看 min-max。它把所有特征都压缩到 0 到 1 区间,简单直观。但它的致命弱点是最大值和最小值对异常值非常敏感。如果收入数据里混入一个异常值 500万,正常的收入区间会被压缩得很小,原本属于不同收入层次的人群在归一化后会挤在一起,分类效果大幅度下降。
再看 z-score。它把特征均值变为 0,标准差变为 1,是目前实践里最常用的归一化方式。只要数据不是特别偏态,z-score 一般不会出大问题。Matlab 里一行代码就能直接调用:
Xn = zscore(X);它适合大多数 FCM 场景,尤其当特征维度较多、分布比较接近正态时。
最后是 MAD 归一化。MAD 的全称是 Median Absolute Deviation,中位数绝对偏差。它的计算分为两步:先求每个特征的中位数,再求所有样本与中位数的绝对偏差的中位数。公式为:
mad = median(|x - median(x)|)
归一化后的值就是:
x_new = (x - median(x)) / mad
我自己在传感器数据的聚类里非常喜欢用 MAD 归一化。传感器读数常常会有突发尖峰,如果使用 min-max,尖峰会被当作最大值污染整个尺度;而 z-score 受到尖峰的影响也不小,因为均值本身就不是稳健统计量。MAD 用中位数替代均值,用绝对偏差中位数替代标准差,对离群点的抵抗能力强得多。在 Matlab 中可以直接写:
medianVal = median(X, 1); madVal = mad(X, 1); Xn = (X - medianVal) ./ madVal;注意 mad 函数第二个参数取 1 表示计算绝对中位差,取值范围是原数据尺度,不转换为σ估计量。如果你希望与标准差保持近似可比,可以再乘以 1.4826,但这并不影响聚类的相对结果。
2.3 一组直观实测对比
我之前用一组自己构造的二维数据做过对比。X轴是均匀分布的特征,范围0到100;Y轴的特征范围是0到1。真实结构是两个簇,但它们的区别主要在Y方向上,X方向只是噪声。直接用原始数据跑 FCM,因为X的尺度远大于Y,聚类出来的两个中心几乎只在X方向拉开,Y方向完全被忽略,划分结果几乎等于随机猜。
做 z-score 归一化后重跑一遍,聚类中心能够同时在X和Y方向反映数据结构,两类样本被正确分开。这个例子很直观地说明:归一化不是一个“可选的预处理步骤”,而是让基于距离的聚类算法真正反映数据结构的前提。
如果你用的是鸢尾花数据,也同样能看出差异。原始数据四个特征的量纲都在厘米级别,差异不算大,但随着特征增多,某些特征之间尺度不平衡会越来越明显。我做过的实验里,z-score 归一化后聚类准确率至少比不归一化提高几个百分点。这个提升不见得很快,但对于聚类结果发布或后续建模,稳定性会好很多。
3. Matlab 代码实现:从自带 fcm 函数到手写迭代的完整跑通方案
3.1 首选自带 fcm 函数
如果你的 Matlab 安装了 Fuzzy Logic Toolbox,那就直接用自带的 fcm 函数,简单可靠。先准备数据,这里用经典的 Fisher Iris 数据集:
load fisheriris.mat; X = meas; % 150x4,四个特征 trueLabel = species; % 真实类别,仅用于验证 Xn = zscore(X); % z-score 归一化 c = 3; % 簇数量,数据集本身是三种类 options = [2 100 1e-5 1]; % [模糊指数m 最大迭代次数 最小改进量 是否显示] [center, U, obj] = fcm(Xn, c, options); % 取最大隶属度作为硬分类标签 [~, label] = max(U, [], 2);需要注意:label 是簇编号,顺序可能和真实标签不对应。算准确率前先做标签映射。比如:
% 简单映射:统计每种真实标签在簇中的分布情况 confusionmat(double(grp2idx(trueLabel)), label)通过混淆矩阵自己观察一下哪个簇对应哪个真实类别,再重映射 label。如果样本标签本身是字符型,用 grp2idx 先转成数值。
这里还要特别注意 options 的含义。第二个参数是最大迭代次数,通常 100 足够;第三个参数是相邻两次迭代目标函数的最小改进量 1e-5;第四个参数控制是否在命令行显示运行信息。如果你不想看到刷屏就改成 0。
3.2 没有工具箱时手写 FCM
有人会问,没有 Fuzzy Logic Toolbox 怎么办?FCM 算法本身并不复杂,完全可以用 Matlab 原生语法实现一个教学版本。下面这个函数保存成 myfcm.m 就能用:
function [center, U] = myfcm(X, c, m, maxIter, tol) [n, d] = size(X); rng(42); % 固定随机种子,便于复现 U = rand(n, c); U = U ./ sum(U, 2); % 满足隶属度和为1的约束 for iter = 1:maxIter % 1. 用当前隶属度计算聚类中心 Um = U .^ m; center = (Um' * X) ./ sum(Um, 1)'; % 2. 计算所有样本到所有中心的平方欧氏距离 dist = zeros(n, c); for i = 1:c diff = X - center(i, :); dist(:, i) = sum(diff .* diff, 2); end dist = max(dist, eps); % 防止除零 % 3. 更新隶属度矩阵 invDist = dist .^ (-1/(m-1)); UNew = invDist ./ sum(invDist, 2); % 4. 判断收敛 if max(abs(UNew(:) - U(:))) < tol U = UNew; break; end U = UNew; end end调用方法:
Xn = zscore(X); [center, U] = myfcm(Xn, 3, 2, 100, 1e-5); [~, label] = max(U, [], 2);这段代码把迭代过程拆成了四步:更新中心、计算距离、更新隶属度、检查收敛。每一步空间复杂度都不大,适合教学和理解原理。生产环境建议还是用自带 fcm,因为工具箱版本考虑了各种数值稳定性和效率问题。
3.3 可视化与目标函数监控
聚类跑完,必须看看结果长什么样,不能只给一个准确率。最简单的可视化是画二维投影散点图:
figure; gscatter(Xn(:,1), Xn(:,2), label); hold on; plot(center(:,1), center(:,2), 'kx', 'MarkerSize', 12, 'LineWidth', 2); legend off; title('FCM聚类结果,取最大隶属度的硬划分');这里用前两个维度投影,只是因为方便观察,并不代表真实聚类效果只依赖这两个维度。如果数据本身是高维,建议先用 PCA 降到二维再看,避免误判聚类效果。
fcm 函数还会输出 obj,也就是每次迭代的目标函数值。可以直接画曲线:
figure; plot(obj, 'o-'); xlabel('迭代次数'); ylabel('目标函数值'); title('FCM收敛曲线');正常情况下目标函数曲线会迅速下降并趋于平稳。如果你看到曲线波动甚至上升,多半是初始化矩阵或者 m 值设置不合理,需要重新检查。
3.4 用真实数据验证一遍
我用 Fisher Iris 跑过一次完整流程。z-score 归一化后,取 c=3,m=2,目标函数迭代到 40 次左右就稳定了。用最大隶属度做硬分类,经过标签映射后准确率约 89%。这个结果不是最高的,但已经能明显看出 FCM 在模糊边界处的优势。
如果你也在做类似验证,建议不要只追求准确率,还要观察 U 矩阵的形态。比如样本点如果同时以 0.5、0.3、0.2 的比例归属于三个类别,说明它处于类别交界处,是 FCM 抓到的最有价值的信息。
4. 模糊指数m、初始中心与簇数选择:用轮廓系数降低调参焦虑
4.1 m 值怎么定
m 是整个 FCM 里最容易被乱调的参数。Matlab 默认值是 2,很多人就一直用 2,这没有问题。但在自己的数据上,最好做一个扫描实验:从 1.3 到 2.5,按 0.2 步长跑一遍,比较目标函数和分类稳定程度。
思路很简单:
mList = 1.3:0.2:2.5; for mi = 1:length(mList) m = mList(mi); o = zeros(1,5); for r = 1:5 [~, ~, objTmp] = fcm(Xn, c, [m 100 1e-5 0]); o(r) = min(objTmp); end meanObj(mi) = mean(o); end这里重复跑 5 次取平均值,是为了消除随机初始化带来的波动。选用 m 值的原则是:目标函数值不要太离谱,同时隶属度分布不要过于极端或过于均匀。
m 过小时,隶属度分布接近 0/1,FCM 就失去了模糊划分的意义;m 过大时,比如超过 3,所有样本的隶属度都会非常接近 1/c,簇结构会被磨平。所以对大多数标准化后的数据,m=2 都是快速省心的选择。
4.2 簇数怎么选
FCM 本身不会告诉你数据分几类最好。需要配合内部评估指标。最常用的就是轮廓系数,Matlab 里可以直接计算:
kList = 2:6; avgSil = zeros(size(kList)); for ki = 1:length(kList) k = kList(ki); [~, U] = fcm(Xn, k, [2 100 1e-5 0]); [~, label] = max(U, [], 2); avgSil(ki) = mean(silhouette(Xn, label)); end轮廓系数范围在 -1 到 1 之间,越接近 1 表示类内紧凑、类间分离。一般选平均轮廓系数最大的 k。如果几个 k 差别不大,优先选更少的簇,因为簇数越多越容易过拟合噪声。
FCM 领域还有两个专用指标:划分系数 PC 和划分熵 PE。PC 是隶属度平方和的平均值,PE 是隶属度对数值的负平均。PC 越大越好,PE 越小越好。计算代码不复杂:
PC = sum(U(:).^2) / size(U,1); PE = -sum(U(:) .* log(U(:) + eps)) / size(U,1);这两个指标不需要真实标签,只依赖隶属度矩阵,适合在无监督场景下辅助选簇数。
4.3 初始化敏感性与多次运行策略
fcm 的初始化是随机生成隶属度矩阵,这会导致每次运行结果略有不同,而且可能收敛到局部极小值。应对办法很简单:固定随机种子,或者跑多次取最优。
固定种子用 rng:
rng(2024); [center, U] = fcm(Xn, c, options);这样结果可复现,对论文实验特别重要。如果不固定种子,最好循环跑 30 次,保存每次的目标函数最小值,最终选目标函数最小的那次结果:
bestObj = Inf; for r = 1:30 [centerTmp, UTmp, objTmp] = fcm(Xn, c, options); if min(objTmp) < bestObj bestObj = min(objTmp); center = centerTmp; U = UTmp; end end这个“多起点策略”在实践中的效果很明显,尤其当数据存在多个接近的局部最小值时,它能带来更稳定的分类结果。缺点只是多花几秒钟时间,对绝大多数数据都值得。
5. FCM 的边界与扩展:高维数据、图像分割与深度学习特征结合的进阶玩法
5.1 高维数据:先降维还是先聚类
特征维度很高时,欧氏距离的区分度会下降,FCM 也不例外。常见的做法是先用 PCA 降维,再跑 FCM。但要注意,PCA 前对数据归一化也很重要,否则 PCA 找的主成分会被量纲大的特征主导。在 Matlab 中习惯这样操作:
Xn = zscore(X); [coeff, Xpca, ~, ~, explained] = pca(Xn); % 选择累计解释方差达到85%以上的主成分数量 idx = find(cumsum(explained) >= 85, 1); Xred = Xpca(:, 1:idx);这样得到的 Xred 维度更低,FCM 的迭代速度更快,结果也往往更稳定。把 PCA 和 FCM 结合,在研究高维基因表达或图像特征时非常好用。
5.2 用 FCM 做图像分割:把像素看成样本
Matlab 图像处理里做 FCM 分割,是把每个像素当做一个样本。灰度图像的话,特征只有一维,也就是像素灰度值;彩色图像的话,每个像素有三个通道,构成三维特征。示例如下:
I = imread('cells.png'); Igr = rgb2gray(I); X = double(Igr(:)); % 所有像素作为样本 Xn = X / 255; % 简单 min-max 归一化 % 如果图像太大,可以随机采样一部分像素跑 fcm,再回贴标签 k = 3; [center, U] = fcm(Xn, k, [2 50 1e-5 0]); [~, label] = max(U, [], 2); segmented = reshape(label, size(Igr)); imshow(label2rgb(segmented));图像分割中 FCM 的优势在于组织区域的过渡带。组织边界常常是渐变而不是突变,硬聚类的硬边界会产生很多颗粒状噪声;FCM 保留软隶属度,后续合并或修整时会平滑很多。不过需要注意:大图的像素数量可能达到上百万,直接 fcm 计算会很慢。可以先对灰度直方图做加权 FCM,也就是把每个灰度级的像素数量作为权重,而不是把所有像素逐一参与计算,这样速度会快很多。
5.3 与深度学习特征结合的混合方案
现在很多流程会用预训练网络提取图像特征,再用 FCM 做无监督分群。理论上讲,深度学习负责把原始像素转成更抽象的表征,FCM 负责在表征空间做软划分,两者互补性很好。在 Matlab 里可以用 pretrained network 提取激活层特征,再 z-score 之后丢给 fcm。因为网络特征维度通常很高,强烈建议先 PCA 降维到几十维,这既能提高速度,也能增强聚类稳定性。
需要注意一个误区:深度特征本身已经被网络规范化,但不代表可以跳过数据预处理。不同层输出特征的数值范围差异很大,如果不做归一化,FCM 又会被某些数值大的特征带偏。所以无论数据来自哪里,归一化这一步都不能省。
另外提醒一点:Deep Learning Toolbox 和 Fuzzy Logic Toolbox 是两个不同工具箱,如果你的项目里两者都要用,先确认自己的许可证包含这些模块,否则代码会报 undefined function。这是我实际遇到过的问题,提前检查比事后排查省事得多。
如果你正在做一个探索性研究,可以试试把 FCM 的隶属度矩阵输出当作概率特征,输入到后续的分类器里。很多数据竞赛中,模糊划分得到的软标签比硬标签信息量更大,往往能在下游模型里多挤出几个点的效果。这也是 FCM 除了直接分群之外最实用的一种用法。