GMM高斯混合模型聚类原理与Matlab实现:从EM算法到BIC选择
2026/9/13 2:50:26 网站建设 项目流程

简介:这是一份GMM高斯混合聚类算法的Matlab完整实现,基于matlab2023b编写,适合计算机、电子信息工程、数学等专业学生用于课程设计、期末大作业和毕业设计,也适合需要快速搭建聚类实验的研究者。代码采用参数化编程思路,注释清晰,结构紧凑便于二次开发,聚类数目、协方差类型等关键参数集中可调,运行后可直接输出聚类结果图,以及不同协方差矩阵类型对模型性能的影响图,省去繁琐调试,一键即可复现典型实验。压缩包共2个文件,包含1个主程序m脚本和1份特征数据xls表格,整体仅17KB,轻量易用。目前已有123人学习下载。借助这套源码,读者可直观理解高斯混合模型在多簇数据上的建模过程、EM迭代与后验概率计算,并通过协方差对比把握模型复杂度与拟合效果之间的权衡;xls数据也能便捷替换为自有特征集,用于课程实验或算法对比,是一份教学与实战兼顾的参考实现。

1. GMM聚类不是“高级K-means”,而是一个概率生成模型

拿到这份Matlab源码时,我最先做的是直接运行main.m。特征数据.xls被读入后,代码自动完成标准化、模型拟合和出图,输出聚类结果图和协方差矩阵类型对模型性能的影响图。在matlab2023b环境下确实能做到一键出图,这对课程设计、期末大作业来说非常省事,但如果你只把它当成一个能输出标签的黑盒,遇到重叠簇和不均衡数据时就会无从下手。

GMM和K-means最大的区别在于:GMM为每个样本计算出属于每个簇的后验概率,而不是一个硬标签;它假设数据由K个高斯成分加权混合生成。理解了这个概率生成视角,再看后面的EM迭代、协方差矩阵类型和BIC选择,才不会觉得参数设置是玄学。

2. 高斯混合模型与EM算法:先搞清楚GMM在算什么

2.1 为什么单个高斯分布不够用

假设特征只有两维,数据自然地聚集在三个区域,每个区域的散点大致呈椭圆形。用一个二元高斯分布去拟合,得到的是一个中心点和一条协方差椭圆,显然无法描述三个聚集中心。高斯混合模型把概率密度写成K个高斯成分的加权和:

p(x) = sum_{k=1}^{K} π_k · N(x | μ_k, Σ_k)

其中π_k是第k个成分的混合权重,满足π_k≥0且Σπ_k=1。每个成分就是一个局部高斯分布,它的均值μ_k决定了簇中心,协方差矩阵Σ_k决定了该簇的形状和朝向。这种加权叠加让GMM能够拟合任意光滑的多峰分布,这也是它常被用作密度估计器的原因。

2.2 EM的E步和M步:责任度与参数更新

直接最大化对数似然没有闭合解,因为每个样本具体来自哪个成分是隐藏的。EM算法通过两步交替迭代逼近最优参数:E步固定当前参数,计算每个样本x_i由第k个成分生成的后验概率γ_{ik},也叫责任度;M步用这些责任度作为软权重,重新估计π、μ、Σ。下面的Matlab风格代码展示了核心更新逻辑:

% X : N x d 数据矩阵,K : 成分个数 % gamma: N x K 责任度矩阵 % E步:计算每个样本对每个成分的责任度 log_pdf = zeros(N, K); for k = 1:K % mvnpdf 为多元高斯密度值 log_pdf(:, k) = log(pi(k)) + log(mvnpdf(X, mu(k,:), Sigma(:,:,k))); end % 减去每行最大值,防止 exp 下溢 log_pdf = log_pdf - max(log_pdf, [], 2); gamma = exp(log_pdf) ./ sum(exp(log_pdf), 2); % M步:按责任度加权更新参数 Nk = sum(gamma, 1); % 每个成分的有效样本数 pi_new = Nk / N; % 权重更新 for k = 1:K mu_new(k,:) = sum(gamma(:,k) .* X, 1) / Nk(k); X_centered = X - mu_new(k,:); Sigma_new(:,:,k) = (X_centered' * (gamma(:,k) .* X_centered)) / Nk(k); end

这段代码不是让你替换fitgmdist,而是解释内置函数在迭代什么。E步里的数值稳定处理很重要:直接计算exp(log_pdf)在数据维度高或者密度值很小时会下溢成0,减掉每行最大值后结果不变,但计算范围安全很多。M步本质上是加权平均,责任度大的样本对μ和Σ的更新贡献更大。实际开发中我会用Matlab的fitgmdist直接拟合,自己写EM还要额外处理协方差奇异、初始化不稳定等问题,性价比太低。

2.3 协方差矩阵类型与SharedCovariance参数的四种组合

fitgmdist通过两个参数一起控制协方差结构:CovarianceType决定每个成分的协方差是否只保留对角元素,SharedCovariance决定所有成分是否共享同一个协方差矩阵。两两组合得到四种模型,对应从“最灵活”到“最精简”的复杂度谱系:

CovarianceTypeSharedCovariance含义参数数量适用场景
fullfalse每个成分独立的完整协方差矩阵最多各个簇形状、朝向差异大
fulltrue所有成分共享同一个完整协方差矩阵中等各簇大小和朝向接近
diagonalfalse每个成分独立,但特征之间不相关中等偏少特征相关性弱,要防过拟合
diagonaltrue所有成分共享一个对角协方差矩阵最少样本少或只想粗略拟合

这里要注意,Matlab中没有“tied”这个选项,共享协方差通过SharedCovariance为true实现,别和Python的sklearn混淆。对二维特征来说,full协方差能旋转椭圆方向,而diagonal只能生成与坐标轴对齐的椭圆。原始代码里那张“协方差矩阵类型对模型性能的影响图”,本质上就是在循环这四种组合后对比BIC或正确率。选择原则是:样本量足够且簇形状差异明显时用full+false;样本少或特征维度高时优先考虑diagonal+true,防止过拟合。

2.4 用BIC/AIC给模型复杂度上锁

GMM需要预先指定K,但K通常未知。BIC和AIC是常见的选择依据:BIC = -2·logL + p·log(N),AIC = -2·logL + 2·p,其中p是参数个数。BIC对参数数量的惩罚更重,适合样本量中等的情况。在Matlab中,直接调用gmm.BIC可以拿到拟合模型的BIC值。如果想比较不同K,可以这样写:

K_list = 1:6; bic_values = zeros(size(K_list)); gmm_models = cell(size(K_list)); for i = 1:length(K_list) gmm_models{i} = fitgmdist(X, K_list(i), ... 'CovarianceType', 'full', ... 'SharedCovariance', false, ... 'Options', statset('Display', 'off')); bic_values(i) = gmm_models{i}.BIC; end % 绘制 BIC 随 K 变化的曲线 figure; plot(K_list, bic_values, 'o-'); xlabel('Number of components K'); ylabel('BIC');

这段循环把不同K下的模型存起来,方便后续对比轮廓系数或直接导出标签。注意Fitting过程中如果某个K下出现协方差奇异,fitgmdist会报错,常见处理是给fitgmdist加'RegularizationValue',比如1e-6,这在后面会提到。BIC越小代表在拟合度和复杂度之间取得平衡越好,但不要只看BIC的最小值,还要看曲线下降的“肘部”:下降突然变缓的点往往比全局最小值更稳健。

3. main.m源码逐段拆解:从readmatrix到一键出图

3.1 数据读取与标准化:特征数据.xls进内存

源码包里的main.m是整个流程的入口,文件结构很清晰,主要分为四个区段。第一段是数据读取与预处理。由于数据存放在特征数据.xls中,源码使用readmatrix读取,这一步比旧版xlsread更快,且能自动识别数值列。我摘出的关键代码如下:

%% 数据读取与标准化 [data, ~, ~] = xlsread('特征数据.xls'); % 兼容旧版本,也可用 readmatrix X = data(:, 2:end); % 假设第一列是样本编号,特征从第二列开始 % 如果数据没有编号列,直接用 data 即可,下面一行请按实际表头调整 % X = data; % 标准化:每个特征零均值、单位方差 [X_std, mu_X, sigma_X] = zscore(X);

readmatrix在R2023b中是官方推荐的数据导入函数,能处理.xls和.xlsx,不依赖Excel插件。这里假设第一列是编号,如果原始文件没有这一列,就去掉这行改成X = data;。zscore按列做标准化,把不同量纲的特征拉到同一尺度,同时返回每个原始特征的均值mu_X和标准差sigma_X,供后面新样本预测时复用。在GMM里,特征尺度会直接影响协方差矩阵的奇异性:比如一个特征范围在0~1,另一个在0~10000,未经标准化直接拟合,协方差矩阵很可能接近奇异,导致EM迭代失败。

3.2 fitgmdist核心参数与聚类输出

标准化之后进入模型拟合,源码中通过参数化编程把K、协方差类型、展示开关都集中定义在文件头部,方便修改。核心调用如下:

%% GMM 拟合 K = 3; % 预设聚类数,可按业务调整 covType = 'full'; % 'full' 或 'diagonal' shareCov = false; % true 或 false regVal = 1e-6; % 正则化项,防止协方差奇异 gmm = fitgmdist(X_std, K, ... 'CovarianceType', covType, ... 'SharedCovariance', shareCov, ... 'RegularizationValue', regVal, ... 'Options', statset('Display', 'final')); % 硬分类标签与软分类概率 [label, ~, posterior] = cluster(gmm, X_std);

fitgmdist的参数需要逐一说明:

参数名可选值作用
K正整数混合成分数量,即聚类数
CovarianceType'full' / 'diagonal'是否只保留对角线协方差
SharedCovariancetrue / false是否所有成分共享协方差
RegularizationValue如1e-6防止协方差奇异
Optionsstatset(...)控制迭代显示和容差

cluster函数返回三个输出,这里用~跳过第二个负对数似然nlogl,保留label和posterior。label是每个样本最大后验概率对应的类别编号,posterior是N×K的概率矩阵,其中每一行和为1。后续绘制聚类图我们用的是label,而异常检测则直接利用posterior。

3.3 聚类结果图的绘制逻辑

一份能直接放进课程设计报告的图,至少要包含聚类散点和聚类中心。源码用gscatter按label绘制不同颜色的散点,再叠加GMM的均值位置,示例如下:

%% 聚类结果图 figure('Color', 'w'); gscatter(X_std(:,1), X_std(:,2), label, 'rgbkcmy', '.', 15); hold on; plot(gmm.mu(:,1), gmm.mu(:,2), 'kx', 'MarkerSize', 12, 'LineWidth', 2); xlabel('Feature 1 (Standardized)'); ylabel('Feature 2 (Standardized)'); title('GMM Clustering Result (K=3)'); legend('Cluster 1', 'Cluster 2', 'Cluster 3', 'Centroid'); grid on;

gscatter是Matlab的Grouped Scatter,要求X_std至少有两列,高维特征时通常会取PCA前两维投影后再画。我一般会先对高维数据做pca,再用前两个主成分替换X_std输入gscatter,这样不会失真太严重。plot里的gmm.mu是K×d矩阵,直接取前两列标出各成分中心。如果特征维度不是2,这张图只能反映二维投影,报告里需要注明是前两维或主成分。

3.4 协方差矩阵影响对比图的批量生成

源码的第二张输出图是“协方差矩阵类型对模型性能的影响图”,它循环四种协方差组合,拟合并记录BIC和迭代时间,最后用bar或plot对比。核心结构如下:

%% 对比四种协方差设置 covSettings = {'full', 'full', 'diagonal', 'diagonal'}; shareSettings = [false, true, false, true]; bicList = zeros(1, 4); for i = 1:4 gmm_temp = fitgmdist(X_std, K, ... 'CovarianceType', covSettings{i}, ... 'SharedCovariance', shareSettings(i), ... 'RegularizationValue', 1e-6, ... 'Options', statset('Display', 'off')); bicList(i) = gmm_temp.BIC; end figure('Color', 'w'); bar(categorical({'full独立','full共享','diag独立','diag共享'}), bicList); ylabel('BIC'); title('Effect of Covariance Type on GMM');

这里categorical把四个组合的名称作为横轴标签,让BIC对比一目了然。从工程角度看,这张图的价值在于:如果BIC差异很小,优先选参数少的diagonal+sharedTrue,因为它泛化更好,训练也更快。如果BIC差异巨大,说明协方差结构对数据集敏感,需要结合业务判断是哪些簇的形状差异导致的。运行这段代码时,如果某个组合不收敛,别急着加正则,先检查X_std是否标准化,以及K是否设得过大。

4. 协方差类型、初始化与聚类数K:四个影响聚类效果的坑

4.1 四种协方差组合的适用场景

上一章的对比图能展示哪种协方差结构在数据上BIC最低,但实际应用中还需要结合样本量和业务语义。下面这张表是我在使用这类Matlab源码时的参考:

数据特征推荐组合原因
样本量大,各簇大小形状不同full + shared=false最能表示簇的真实边界
样本量中等,簇接近球形或椭圆方向一致full + shared=true降低方差,避免过拟合
特征之间有较强相关性,但样本量少diagonal + shared=false每簇独立,却不需要估计协方差中非对角项
样本量极少,特征独立diagonal + shared=true参数最少,最稳定

这个表格直接对应main.m中的covType和shareCov变量。如果你的实验结果是diag+shared=true的BIC最低,不要觉得意外,那说明你的特征之间相关性弱,或者数据量不足以支撑完整协方差矩阵的估计。相反,如果full+false的BIC显著更优,说明簇的形状差异真实存在,这时候强行用diagonal会得到边缘模糊的聚类边界。

4.2 初始化与Replicates的坑

fitgmdist默认使用k-means++的变体初始化,但EM算法对初始值敏感,容易陷入局部最优。即使定了K和协方差结构,一次运行的结果也可能不同。解决办法是设置Replicates参数,让Matlab从多个随机起点拟合,选出最大似然的模型:

gmm = fitgmdist(X_std, 3, ... 'CovarianceType', 'full', ... 'SharedCovariance', false, ... 'Replicates', 10, ... % 10次随机初始化,保留最优 'Options', statset('MaxIter', 500));

Replicates每多一次,运行时间大约翻一倍,但能显著降低局部最优的风险。

注意:当运行fitgmdist报错,提示协方差矩阵不正定时,优先检查K和RegularizationValue,而不是盲目加大Replicates。

另一个更实际的坑是:如果K过大,某个成分可能只有一两个样本,它的协方差矩阵会退化。这时RegularizationValue要适当加大,比如从1e-6提到1e-4,否则fitgmdist直接报错。遇到这种错误,先检查K是否明显大于类簇数,再考虑加正则。

4.3 聚类数K的确定:BIC、轮廓系数与稳定性

上一章对比图只针对固定K,但K的选择才是GMM最关键的决策。我用一个循环同时计算BIC和轮廓系数,综合判断:

K_list = 1:6; silList = zeros(size(K_list)); for i = 1:length(K_list) gmm_temp = fitgmdist(X_std, K_list(i), ... 'CovarianceType', 'full', ... 'SharedCovariance', false, ... 'RegularizationValue', 1e-6, ... 'Replicates', 5); [tmp_label, ~] = cluster(gmm_temp, X_std); if K_list(i) == 1 silList(i) = 0; % 单簇没有轮廓系数 else silList(i) = mean(silhouette(X_std, tmp_label)); end end

轮廓系数衡量簇内紧密度和簇间分离度,Matlab的silhouette函数返回每个点的轮廓值,取平均得到整体指标。BIC最小值并不总是对应业务上可解释的划分,轮廓系数倾向于紧致且分离的簇,两者结合能避免被单一指标带偏。如果K=3时BIC曲线已明显变平,而K=5时轮廓系数最大,我一般会选择K=3,因为简单模型在工程上更稳定。

4.4 先标准化还是先聚类?特征尺度与异常值

很多直接跑源码的初学者会跳过zscore,结果聚类图全被某个量纲大的特征主导。GMM对尺度敏感,因为它依赖欧氏距离计算协方差矩阵。zscore将每个特征变成均值0方差1,是一种公平处理,但也抹掉了特征间的相对重要性。如果业务上知道某特征更重要,可以用自定义权重替代标准化,比如:

w = [1, 3, 1]; % 给第二个特征三倍权重 X_weighted = X .* w; % 乘性加权,等价于调整协方差贡献

这里更推荐的加权方式是对每个特征除以设定的尺度参数,而不是简单乘权重。异常值也是坑:单个离群点会把某个成分的均值拉偏,协方差矩阵被放大,聚类结果整体漂移。我会先画箱线图或使用robustcov检查,如果异常值超过1%,先做截尾处理再进GMM。对于课程设计,这些预处理步骤最好单独放在main.m文件头部并加注释,保证评估老师能一眼看到。

5. 把GMM当密度估计器:异常检测与软分类的进阶用法

5.1 用后验概率做软分类

聚类结果图中label给出的是硬标签,但GMM真正有价值的输出是后验概率。比如新来了一个样本x_new,可以用posterior函数计算它属于每个成分的概率,然后取最大者作为软分类结果。这在用户分群场景中很有用:可能某个用户同时有70%概率属于高活跃群、30%属于中等活跃群,硬分类会丢掉这层信息。代码上就是:

x_new = [0.5, -1.2]; % 新样本,需与训练时同样的标准化 x_new_std = (x_new - mu_X) ./ sigma_X; % 用训练集均值和标准差标准化 [poster_new, ~] = posterior(gmm, x_new_std); [prob, idx] = max(poster_new); fprintf('样本属于第%d个簇,概率为%.2f\n', idx, prob);

注意训练时的mu_X、sigma_X是在zscore前用mean和std计算出来的,在3.1节已经通过zscore的第二个、第三个输出保存。后验概率的可视化可以画成热度图,如果特征大于三维就画两两投影矩阵,这篇文章里不再展开。

5.2 基于对数似然的异常检测阈值

当GMM拟合完成后,它可以作为一个密度估计器来用。每个样本的对数似然log(p(x))越低,说明它越不可能来自这个生成过程,越可能是异常点。main.m里虽然没直接提供异常检测代码,但只需一行调用即可:

loglike = log(pdf(gmm, X_std)); % N x 1 的对数密度值 threshold = prctile(loglike, 5); % 取5%分位点作为异常阈值 outlier_idx = find(loglike < threshold);

这个阈值取5%是比较常见的默认做法,但具体分位点要看业务。如果数据集本身干净,用低于最小值的5%会误杀正常点;如果数据确实包含大量噪点,可以把阈值上移到10%。这里有个验证技巧:把异常点标红画到聚类图上,如果它们都落在椭圆边界外,说明GMM的密度估计和聚类几何是一致的;如果异常点散布在簇内部,则说明数据分布不适合用单一高斯成分建模,需要考虑增加K或改用其他密度估计模型。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询