简介:这个资源提供一套完整、可直接运行的高斯混合模型(GMM)聚类实现,全部用原生Python编写,不依赖scikit-learn等高级库。包含三个核心模块:genSample.py用于生成二维或三维的带标签高斯混合样本,支持自定义簇数量、维度、样本总数;gmm.py封装了标准EM算法逻辑,涵盖E步(计算隐变量后验概率)和M步(更新均值、协方差、混合系数),并输出每轮迭代的对数似然值,便于追踪收敛过程;main.py作为主入口,整合数据加载、模型初始化、迭代训练与结果可视化。配套提供两个示例数据文件(gmm.data和sample.data),以及详细README.md说明参数含义、运行步骤和调试建议。支持手动指定初始参数(如初始均值位置、协方差矩阵形式、先验权重),适合深入理解GMM数学原理、观察EM算法行为、教学演示或课程实验作业。所有代码结构清晰、注释充分,便于逐行调试和原理验证。
1. 为什么我坚持手写GMM——不是为了造轮子,而是为了看清“概率聚类”的心跳
你有没有试过调用sklearn.mixture.GaussianMixture,传入数据、设好n_components,几行代码就拿到聚类结果,但心里却始终悬着一个问题:那个“软分配”到底是怎么算出来的?E步里那个γ(z_{nk})——也就是第k个高斯成分对第n个样本的后验责任(responsibility)——它背后那串指数、行列式、逆矩阵,究竟是怎么一步步把模糊的归属感量化成0到1之间的实数?M步更新均值时,为什么是加权平均?协方差矩阵为什么带权重求和再除以总责任?混合系数π_k为什么等于所有样本对该成分的平均责任?这些不是公式推导书上的符号游戏,而是EM算法每一次呼吸的真实节律。
这套代码包,就是我当年在讲授《机器学习基础》课程时,为本科生亲手打磨的一套“解剖级”教学工具。它不追求速度,不包装接口,甚至刻意回避了NumPy的高级广播技巧——所有矩阵乘法、求逆、行列式计算,都用最直白的for循环+基础线性代数逻辑展开。目的只有一个:让你在调试器里单步执行gmm.py的每一行时,能清晰看到gamma[n][k]这个数字是怎么从log_pi[k] + log_gaussian(x[n], mu[k], cov[k])一步步累加、归一化而来;能看到mu_new[k]是如何被sum(gamma[n][k] * x[n]) / sum(gamma[n][k])一锤定音地拉向数据密集区;更能亲眼见证对数似然值log_likelihood如何在前几轮剧烈震荡后,像潮水退去般缓慢爬升、最终趋于平坦——那条平缓的曲线,就是收敛的指纹。
它适合三类人:第一类是刚学完最大似然估计、正在啃EM算法推导的研究生,你需要一个可触摸的沙盒,把纸面公式变成内存里的变量;第二类是高校教师或助教,需要一套零依赖、结构透明的课堂演示素材,学生可以改一个初始均值,立刻看到聚类路径如何偏移;第三类是算法工程师,在模型效果异常时,想绕过黑箱,直接检查E步输出的责任矩阵是否合理、M步更新的协方差是否出现病态(比如行列式趋近于零)。这不是生产环境的替代品,而是一把手术刀——当你需要确认“模型到底在想什么”时,它比任何可视化工具都更锋利。
关键词里反复出现的GMM聚类、EM算法、Python实现、高斯混合模型,在这里不是标签,而是四个锚点:GMM聚类定义了问题域——我们处理的是概率性的、重叠的簇;EM算法框定了求解范式——通过交替优化隐变量与参数来逼近极大似然;Python实现意味着完全可控——没有Cython加速,没有预编译,所有中间状态均可打印;高斯混合模型则锁定了数学内核——每个簇由一个多元高斯分布刻画,其参数(均值、协方差、权重)正是我们要迭代求解的对象。接下来,我会带你从模拟数据的“人造土壤”开始,亲手种下这颗模型,再一帧一帧播放它的生长过程。
2. 整体设计思路拆解:为什么模块要这样切分?为什么不用sklearn?
2.1 三层架构:生成—求解—驱动,各司其职不越界
整个资源包采用清晰的三层职责分离,这不是为了炫技,而是为了精准控制每一个教学/调试触点:
-
数据生成层(genSample.py):它只做一件事——按需制造“已知真相”的数据。你指定3个簇、2维空间、每簇500个点,它就返回一个
(1500, 2)的numpy.ndarray和一个长度为1500的labels数组,其中labels[i]明确告诉你第i个点本应属于哪个高斯成分。关键在于,它支持两种协方差模式:'spherical'(球形,即各向同性,协方差为标量乘单位阵)和'full'(满秩,任意正定矩阵)。这直接对应GMM中两种常见简化假设,也决定了后续M步更新协方差的计算复杂度。我特意没让它生成“完美分离”的数据——而是让簇间有可控重叠(通过调整簇心距离和协方差大小),因为真实世界的数据从来不会给你清晰的边界,而EM算法的魅力,恰恰体现在它如何在这种模糊地带给出最优的概率解释。 -
核心求解层(gmm.py):这是心脏。它不碰数据IO,不画图,不打印日志,只专注EM循环本身。入口函数
fit()接收数据X、簇数K、最大迭代次数max_iter等纯算法参数,并返回一个包含所有中间状态的字典:{'mu': [...], 'cov': [...], 'pi': [...], 'gamma': [...], 'log_likelihoods': [...]}。这里的设计哲学是“最小接口,最大可见性”。例如,gamma被完整保留下来——它是一个(N, K)的二维数组,每一行就是一个样本对K个簇的责任分布。你可以随时取出gamma[0],看看第一个点认为自己属于各簇的概率分别是多少;也可以计算np.argmax(gamma, axis=1)得到硬聚类结果,再与genSample.py生成的真标签对比,量化聚类准确率。这种“中间态全暴露”的设计,是理解EM行为不可替代的窗口。 -
驱动整合层(main.py):它像一位严谨的实验员,负责串联全流程:加载数据(支持
.data文本文件或调用genSample.py生成)、设置初始参数(可随机初始化,也可手动指定mu_init,cov_init,pi_init)、调用gmm.fit()、监控收敛(判断连续两轮log_likelihood变化小于tol=1e-3则停止)、最后调用可视化函数。它的存在,让整个流程可复现、可配置、可对比——比如你可以跑两次,一次用随机初值,一次把初始均值故意设在错误位置,然后并排看它们的收敛曲线和最终簇心漂移路径,这种对比带来的洞见,远超任何理论讲解。
2.2 拒绝sklearn:不是技术傲慢,而是教学刚需
有人会问:既然sklearn的GMM这么成熟,为什么还要手写?答案很实在:sklearn封装了太多“看不见”的优化,而这恰恰是教学最大的敌人。
举三个典型例子:
1. 初始化策略:sklearn默认用K-means++初始化均值,这本身就是一个启发式算法。当你想纯粹观察EM在不同初值下的鲁棒性时,你无法关闭它,也无法精确控制初始mu的位置。而我们的main.py允许你传入mu_init=[[0,0], [5,5], [-3,4]],让算法从你指定的三个点出发,这是验证“初值敏感性”的基石。
2. 协方差约束:sklearn的covariance_type参数虽支持'full'/'tied'/'diag'/'spherical',但其内部实现对奇异协方差矩阵做了自动正则化(如加一个小的1e-6 * I),这掩盖了EM在病态数据上可能遇到的真实数值问题。我们的gmm.py在计算协方差时,如果遇到np.linalg.det(cov_k) < 1e-10,会直接抛出ValueError("Covariance matrix is singular!"),逼你去思考:是数据维度太高?还是某个簇的样本太少?还是初始值太差?这种“报错即教学”的设计,比静默修复更有价值。
3. 收敛监控粒度:sklearn的verbose参数只能告诉你“第几轮迭代”,但不输出具体的log_likelihood值。而我们的实现,每轮迭代后都计算并追加到log_likelihoods列表中。你可以用plt.plot(log_likelihoods)画出一条真实的收敛曲线,甚至计算np.diff(log_likelihoods)看每一步的增益,从而直观理解“为什么EM保证单调不减”。
所以,这不是在重复造轮子,而是在铸造一把专门用于“拆解轮子”的扳手。当你需要理解轮子为何转动、轴承如何受力、齿轮怎样咬合时,预制的整车永远不如散落的零件来得透彻。
3. 核心细节解析与实操要点:E步与M步的数学落地
3.1 E步:计算后验责任γ(z_{nk})——一场精密的概率分配
E步的核心任务,是给定当前参数θ^{(t)} = {μ_k^{(t)}, Σ_k^{(t)}, π_k^{(t)}},计算每个样本x_n属于每个簇k的后验概率γ(z_{nk}) = p(z_{nk}=1 | x_n, θ^{(t)})。根据贝叶斯定理,这等于:
γ(z_{nk}) = [π_k^{(t)} * N(x_n | μ_k^{(t)}, Σ_k^{(t)})] / [Σ_j π_j^{(t)} * N(x_n | μ_j^{(t)}, Σ_j^{(t)})]
其中N(x | μ, Σ)是d维高斯概率密度函数:N(x|μ,Σ) = (2π)^{-d/2} |Σ|^{-1/2} exp{ -1/2 (x-μ)^T Σ^{-1} (x-μ) }
在gmm.py的_e_step()函数中,这个计算被拆解为四步,每一步都服务于数值稳定性:
-
预计算对数项:直接计算
π_k * N(x_n|μ_k,Σ_k)会导致极小值下溢(underflow)。因此,我们先计算对数形式:log_num[n][k] = log(π_k) + log(N(x_n|μ_k,Σ_k))。其中log(N(...))的计算又细分为:
-log_det_cov = np.log(np.linalg.det(cov_k))(协方差行列式的对数)
-inv_cov = np.linalg.inv(cov_k)(协方差逆矩阵)
-diff = x_n - mu_k(样本与均值的差向量)
-quad_form = np.dot(diff.T, np.dot(inv_cov, diff))(马氏距离的二次型)
- 最终log_N = -0.5 * (d * np.log(2*np.pi) + log_det_cov + quad_form) -
LogSumExp技巧:分母是
Σ_j exp(log_num[n][j]),直接计算exp()仍会溢出。标准解法是提取最大值:log_denom[n] = log_sum_exp(log_num[n]) = max_val + np.log(np.sum(np.exp(log_num[n] - max_val)))。这确保了指数运算的输入都在安全范围内(log_num[n][j] - max_val ≤ 0)。 -
计算对数责任:
log_gamma[n][k] = log_num[n][k] - log_denom[n]。此时log_gamma的每一行,其exp()后的和严格等于1。 -
转换为浮点责任:
gamma[n][k] = np.exp(log_gamma[n][k])。至此,gamma矩阵完成,它是一个(N, K)的概率矩阵,每行和为1。
提示:在调试时,务必检查
gamma的合法性。一个简单的断言assert np.allclose(gamma.sum(axis=1), 1.0, atol=1e-10)能帮你快速捕获E步中的数值错误。我曾在一个早期版本中忘记在log_sum_exp里处理全负无穷的情况,导致某轮迭代后gamma出现NaN,这个断言第一时间揪出了问题。
3.2 M步:更新参数θ^{(t+1)}——用责任加权的统计重构
M步的目标,是最大化Q函数,它等价于用E步计算出的责任γ(z_{nk})作为“软标签”,对每个簇k进行加权统计。
-
更新混合系数π_k:
π_k^{(t+1)} = (1/N) * Σ_n γ(z_{nk})。这非常直观——π_k就是簇k被分配到的所有样本的平均责任,即该簇在整体混合中的“占比”。在代码中,就是pi_new[k] = np.mean(gamma[:, k])。 -
更新均值μ_k:
μ_k^{(t+1)} = [Σ_n γ(z_{nk}) * x_n] / [Σ_n γ(z_{nk})]。分子是x_n按责任加权的和,分母是总责任(即有效样本数)。这本质上是加权质心。代码实现为:
python weighted_sum = np.zeros(d) for n in range(N): weighted_sum += gamma[n, k] * X[n] mu_new[k] = weighted_sum / np.sum(gamma[:, k]) -
更新协方差Σ_k:
Σ_k^{(t+1)} = [Σ_n γ(z_{nk}) * (x_n - μ_k^{(t+1)}) (x_n - μ_k^{(t+1)})^T] / [Σ_n γ(z_{nk})]。这是加权的协方差矩阵,权重同样是γ(z_{nk})。注意,这里必须使用更新后的μ_k^{(t+1)},而非旧的μ_k^{(t)},否则会破坏EM的收敛保证。代码中,我们先计算外积np.outer(diff, diff),再加权累加。
注意:协方差更新是数值陷阱的高发区。当某个簇的总责任
np.sum(gamma[:, k])极小时(比如<1),分母接近零,会导致Σ_k爆炸。我们的代码在_m_step()末尾加入了保护:
python if np.sum(gamma[:, k]) < 1e-8: raise ValueError(f"Cluster {k} has near-zero responsibility ({np.sum(gamma[:, k]):.2e}). Try different initialization.")
这个检查强制你面对数据或初值的问题,而不是让模型默默产生无意义的结果。
3.3 对数似然监控:收敛的唯一可信信标
EM算法的理论保证是:每次迭代,完整数据的对数似然log p(X|θ)单调不减。因此,监控log_likelihood是判断收敛最可靠的方式。其计算公式为:
log p(X|θ) = Σ_n log [ Σ_k π_k * N(x_n | μ_k, Σ_k) ]
这看起来和E步分母的计算一模一样!事实上,log_likelihood正是E步中log_denom数组所有元素的和:log_likelihood = np.sum(log_denom)。
在main.py中,我们每轮迭代后都计算并记录这个值。一个健康的收敛过程应该是:前几轮log_likelihood快速上升(有时甚至因初值不佳而短暂下降,这是正常现象),随后上升速度越来越慢,最终连续几轮变化小于tol(默认1e-3)即停止。你可以用以下代码快速绘制它:
import matplotlib.pyplot as plt
plt.plot(log_likelihoods)
plt.xlabel('Iteration')
plt.ylabel('Log Likelihood')
plt.title('EM Algorithm Convergence')
plt.grid(True)
plt.show()
这条曲线,就是EM算法的心电图。它不撒谎——如果曲线震荡剧烈或长期停滞,说明你的数据不适合GMM,或者初值选得太差,或者簇数K设错了。这比任何外部评估指标(如轮廓系数)都更能反映模型内在的拟合质量。
4. 实操过程与核心环节实现:从零运行到深度调试
4.1 环境准备与依赖安装:轻量纯净,开箱即用
整个项目仅依赖两个基础库:numpy用于数值计算,matplotlib用于可视化。requirements.txt内容极其简单:
numpy==1.24.3
matplotlib==3.7.1
安装只需一行命令:
pip install -r requirements.txt
为什么坚持如此精简?因为scipy虽然提供了scipy.linalg.sqrtm等高级矩阵函数,但它会引入额外的编译依赖(如BLAS/LAPACK),在某些教学机房或老旧Linux发行版上可能安装失败。而我们的所有矩阵运算(求逆、行列式、特征值分解)都基于numpy.linalg,它在绝大多数Python环境中都是开箱即用的。matplotlib也仅用于最后的2D/3D散点图,如果你只需要后台训练和数值分析,甚至可以注释掉所有import matplotlib和绘图代码,完全不影响核心算法运行。
实操心得:我在一次嵌入式AI课程中,曾让学生在树莓派4B上运行此代码包。由于树莓派ARM架构下
scipy编译耗时极长,而numpy早已预装,这套轻量依赖让我们节省了整整一节课的环境搭建时间,学生得以把全部精力聚焦在算法理解上。
4.2 数据生成实战:用genSample.py定制你的“理想实验场”
genSample.py是你的数据工厂。它的核心函数generate_gmm_data(n_samples=1000, n_components=3, n_features=2, ...)接受一系列直观参数:
n_samples: 总样本数,如1500n_components: 簇数K,如3n_features: 维度d,支持2(平面)或3(立体)means: 初始簇心坐标列表,如[[0, 0], [5, 5], [-3, 4]]。若为None,则随机生成covariances: 协方差矩阵列表,每个是(d,d)数组。若为None,则按covariance_type生成covariance_type:'spherical'(标量)或'full'(满秩)weights: 各簇先验权重,如[0.4, 0.35, 0.25]。若为None,则均匀分配
下面是一个完整的生成示例,创建一个具有挑战性的2D数据集:
# 在Python交互环境或main.py中
from genSample import generate_gmm_data
import numpy as np
# 创建3个簇:两个靠近(易混淆),一个远离
means = [[0, 0], [1.5, 1.5], [8, 2]]
# 使用'spherical'协方差,半径分别为0.8, 1.2, 1.0
covariances = [0.64 * np.eye(2), 1.44 * np.eye(2), 1.0 * np.eye(2)]
weights = [0.4, 0.4, 0.2]
X, y_true = generate_gmm_data(
n_samples=1200,
n_components=3,
n_features=2,
means=means,
covariances=covariances,
weights=weights,
random_state=42 # 确保可复现
)
print(f"Generated data shape: {X.shape}")
print(f"True labels: {np.unique(y_true)}")
print(f"Label distribution: {np.bincount(y_true)}")
运行后,你会得到一个(1200, 2)的X和一个(1200,)的y_true。y_true是整数标签(0,1,2),它由genSample.py内部根据每个点的真实后验概率p(x_n|z_{nk})π_k采样生成,因此它代表了数据的“地面实况”。这个y_true,是你后续评估聚类效果(如计算Adjusted Rand Index)的黄金标准。
常见问题:为什么生成的数据文件
sample.data里只有坐标,没有标签?因为sample.data是为main.py的“加载已有数据”模式准备的,它模拟真实场景——你只有X,没有y_true。而y_true只存在于genSample.py的内存中,供你做内部验证。如果你想把带标签的数据保存下来用于其他实验,只需在生成后添加:
python np.savetxt('labeled_sample.data', np.column_stack([X, y_true]), fmt='%.6f')
4.3 模型训练全流程:main.py的逐行解析与参数调优
main.py是整个流程的指挥中心。我们以一个典型的2D训练为例,逐步解析其关键步骤:
Step 1: 数据加载
# 支持两种方式:加载文件 或 生成新数据
if args.mode == 'load':
X = np.loadtxt(args.data_file) # 如 gmm.data
elif args.mode == 'generate':
X, y_true = generate_gmm_data(...) # 如上节所示
Step 2: 初始化参数
这是影响收敛速度和最终结果的关键。main.py提供了三种策略:
- init='random':均值在数据范围随机,协方差为单位阵缩放,权重均匀。
- init='kmeans':先用简易K-means(非sklearn)粗略聚类,用簇心和簇内散度初始化。
- init='manual':用户传入mu_init, cov_init, pi_init。
对于教学,我强烈推荐init='manual'。例如,故意把初始均值设在错误位置:
mu_init = [[-5, -5], [0, 0], [10, 10]] # 全部偏离真实簇心
然后运行,观察EM如何“挣扎”着把它们拉回正确区域。这种“人为制造困难”的实验,比任何理论都更能建立直觉。
Step 3: 调用GMM训练
from gmm import GMM
gmm = GMM(n_components=args.n_components, max_iter=args.max_iter, tol=args.tol)
results = gmm.fit(X, mu_init=mu_init, cov_init=cov_init, pi_init=pi_init)
results字典包含了所有宝藏:results['mu']是最终均值列表,results['cov']是协方差列表,results['gamma']是最终责任矩阵,results['log_likelihoods']是收敛曲线。
Step 4: 可视化与评估
对于2D数据,main.py会自动生成三张图:
- 左图:原始数据散点图,颜色按y_true(如果可用)。
- 中图:GMM聚类结果,颜色按np.argmax(results['gamma'], axis=1)(硬聚类)。
- 右图:收敛曲线log_likelihoods。
你可以通过命令行参数精细控制:
# 生成数据并训练,手动初始化,保存结果
python main.py --mode generate --n_components 3 --n_samples 1200 --init manual \
--mu_init "0,0;1.5,1.5;8,2" --cov_init "0.64,0;0,0.64;1.44,0;0,1.44;1.0,0;0,1.0" \
--output_dir ./results/
# 加载现有数据文件训练
python main.py --mode load --data_file gmm.data --n_components 2 --max_iter 100
实操心得:在调试时,我习惯在
gmm.py的fit()函数内部插入print(f"Iter {iter}: LogLik = {log_likelihood:.4f}")。虽然main.py已经打印了收敛曲线,但实时日志能让我在终端里一眼看到每一轮的进展,尤其当迭代卡住时,能立刻定位是哪一轮出了问题。这个“土办法”,比任何IDE调试器都来得直接。
4.4 高级技巧:3D可视化与协方差椭圆绘制
对于2D数据,main.py不仅画散点图,还绘制了每个簇的协方差椭圆,这是理解GMM几何意义的神来之笔。
椭圆的绘制基于协方差矩阵的特征分解:
- 计算cov_k的特征值λ1, λ2和特征向量v1, v2
- 椭圆的长轴长度正比于√λ1,短轴正比于√λ2
- 椭圆的方向由特征向量v1, v2决定
代码片段如下:
def plot_cov_ellipse(ax, cov, mean, n_std=2.0, **kwargs):
"""绘制2D协方差椭圆"""
vals, vecs = np.linalg.eigh(cov) # 特征值和特征向量
order = vals.argsort()[::-1] # 降序排列
vals, vecs = vals[order], vecs[:, order]
theta = np.degrees(np.arctan2(*vecs[:, 0][::-1])) # 椭圆旋转角
width, height = 2 * n_std * np.sqrt(vals) # 长短轴长度
ellip = Ellipse(xy=mean, width=width, height=height, angle=theta, **kwargs)
ax.add_patch(ellip)
调用它:
for k in range(K):
plot_cov_ellipse(ax, results['cov'][k], results['mu'][k],
n_std=2, alpha=0.3, facecolor='none', edgecolor='red')
这个椭圆,就是该高斯成分“影响力范围”的直观体现——它覆盖了约95%(2σ)的该成分概率质量。当你看到两个椭圆大面积重叠时,就能立刻理解为什么E步会给跨簇样本分配显著的责任。
对于3D数据,main.py同样支持。它使用mpl_toolkits.mplot3d绘制3D散点图,并用半透明球体(plot_surface)近似3D协方差椭球。虽然不如2D椭圆精确,但足以展示簇的三维形态和相对位置。
5. 常见问题与排查技巧实录:那些年踩过的坑与独家解法
5.1 “NaN”与“Infinite”:数值不稳定的第一信号
现象:运行几轮后,log_likelihood变成nan,gamma矩阵出现nan或inf,程序崩溃。
根本原因:高斯密度函数N(x|μ,Σ)的计算中,exp(-0.5 * quad_form)部分,当quad_form极大(如>800)时,exp(-400)在双精度浮点下就是0,导致分子为0;而分母若也接近0,就会出现0/0或inf/inf,结果为nan。
排查步骤:
1. 在_e_step()中,log_num[n][k]计算后,立即检查:if np.any(np.isnan(log_num)) or np.any(np.isinf(log_num)):,打印n, k, quad_form。
2. 定位到具体哪个x_n和哪个k导致quad_form过大。通常是因为x_n离mu_k太远,或cov_k太小(行列式接近零)。
独家解法:
- 动态截断:在计算quad_form前,加一个保护:
python quad_form = np.dot(diff.T, np.dot(inv_cov, diff)) if quad_form > 800: # 防止exp(-400)下溢 log_N = -800.0 # 直接设为一个极小的常数 else: log_N = -0.5 * (d * np.log(2*np.pi) + log_det_cov + quad_form)
- 协方差正则化:在_m_step()更新cov_k后,强制加入微小扰动:
python cov_new[k] += 1e-6 * np.eye(d) # 防止奇异
我的经验:90%的
nan问题源于初始协方差矩阵cov_init设得太小(如1e-8 * I)。在main.py中,我默认将spherical协方差初始化为np.var(X, axis=0).mean() * 0.5,即数据总方差的一半,这是一个经验性的、稳健的起点。
5.2 “收敛太慢”或“不收敛”:初值与簇数的双重陷阱
现象:迭代上百轮,log_likelihood仍在缓慢爬升,或在某个平台停滞不前。
排查清单:
- 检查初值:用--init manual传入明显错误的mu_init,看是否比随机初始化更差。如果是,说明算法对初值敏感,需要更好的初始化策略(如kmeans)。
- 检查簇数K:用sklearn.metrics.calinski_harabasz_score或silhouette_score(在main.py中已预留接口)计算不同K值下的分数,寻找拐点。GMM的log_likelihood会随K增大而单调增加,不能直接用于选K,必须用BIC或AIC。
- 检查数据维度:高维数据(d>10)下,|Σ|和Σ^{-1}计算误差放大。尝试先用PCA降到5维再运行。
独家技巧:BIC准则自动选K
我们在main.py中内置了BIC计算:
def compute_bic(log_likelihood, n_params, n_samples):
return -2 * log_likelihood + n_params * np.log(n_samples)
# n_params for GMM: K-1 (pi) + K*d (mu) + K*d*(d+1)//2 (cov, full)
n_params = (K-1) + K*d + K*d*(d+1)//2
bic = compute_bic(results['log_likelihoods'][-1], n_params, len(X))
运行多个K值(如2,3,4,5),取BIC最小者为最优K。这比凭感觉猜测可靠得多。
5.3 “聚类结果怪异”:责任矩阵gamma的深度诊断
现象:散点图上,明明视觉上分离很好的两个簇,却被GMM合并为一个;或一个紧凑簇被强行拆成两个。
终极诊断法:直接查看gamma
# 打印前5个样本的责任
print("First 5 samples' responsibilities:")
print(results['gamma'][:5])
# 计算每个簇的“主导样本数”
dominant_counts = np.sum(np.argmax(results['gamma'], axis=1) == np.arange(K), axis=0)
print(f"Dominant sample count per cluster: {dominant_counts}")
如果某个dominant_counts[k]远小于len(X)/K,说明该簇未被有效激活。此时应:
- 检查pi_k是否过小(<0.05),若是,可能是M步更新时该簇责任不足;
- 检查mu_k是否离所有x_n都很远(计算np.min(np.linalg.norm(X - mu_k, axis=1))),若是,说明它被“遗弃”了;
- 尝试在下一轮训练中,用--init kmeans,让初始簇心更贴近数据密集区。
最后分享一个小技巧:在
main.py中,我添加了一个--debug标志。启用后,它会在每轮迭代后保存gamma矩阵到./debug/iter_001_gamma.npy。你可以用np.load()加载这些文件,用matplotlib.animation制作一个gamma矩阵随时间演化的热力图动画。看着那些概率块如何从均匀分布,逐渐凝聚成清晰的对角优势,你会真正感受到EM算法那无声而磅礴的生命力——它不是在“计算”,而是在“演化”。
简介:这个资源提供一套完整、可直接运行的高斯混合模型(GMM)聚类实现,全部用原生Python编写,不依赖scikit-learn等高级库。包含三个核心模块:genSample.py用于生成二维或三维的带标签高斯混合样本,支持自定义簇数量、维度、样本总数;gmm.py封装了标准EM算法逻辑,涵盖E步(计算隐变量后验概率)和M步(更新均值、协方差、混合系数),并输出每轮迭代的对数似然值,便于追踪收敛过程;main.py作为主入口,整合数据加载、模型初始化、迭代训练与结果可视化。配套提供两个示例数据文件(gmm.data和sample.data),以及详细README.md说明参数含义、运行步骤和调试建议。支持手动指定初始参数(如初始均值位置、协方差矩阵形式、先验权重),适合深入理解GMM数学原理、观察EM算法行为、教学演示或课程实验作业。所有代码结构清晰、注释充分,便于逐行调试和原理验证。
508



被折叠的 条评论
为什么被折叠?



