ARTICLE DETAIL

资讯详情

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

K-means vs DBSCAN:无监督聚类算法选型与Python实战

K-means vs DBSCAN:无监督聚类算法选型与Python实战 1. 从业务需求说起什么时候需要无监督聚类1.1 没有标签的数据怎么找出结构做数据分析或者机器学习项目前期最头疼的问题通常不是模型选哪个而是手里拿到的数据到底长什么样。很多场景下数据是没有标签的一批用户的行为记录、一堆社区服务需求问卷、一批地理坐标点、一组商品的购买流水没有人告诉你这些样本应该分成几类每一类叫什么名字。这时候无监督学习就派上了用场而聚类是无监督学习里最直观、最常用的一类方法。聚类的目标说白了就一句话把相似的样本归到一起让同一个簇内的样本尽可能相似不同簇之间的样本差异尽可能明显。这个思想非常朴素但落到实际数据上就会牵扯出一堆问题相似怎么度量距离用欧式还是余弦分成几个簇合适有些簇是圆形的有些是月牙形的有些甚至中间是断开的一种算法能不能全部搞定这就要回到算法本身的建模假设上来。K-means和DBSCAN是我在实际项目里用得最多的两种聚类算法它们的思路截然不同K-means假设簇是球形的、大小相近的用质心来代表簇的中心DBSCAN假设簇是密集区域被稀疏区域分隔开的用密度的眼光来看数据。这两种假设决定了各自的适用场景和局限也决定了你什么时候该用哪个。这篇文章就把这两条路线从头到尾讲透配套完整可跑的Python代码帮你看完就能在真实数据上动手。1.2 K-means和DBSCAN各自擅长解决什么问题先给一个粗略的判断标准后面会展开细讲。K-means适合数据量大、样本大致呈凸形分布、簇的数量可以提前估出来的场景。比如电商用户按消费频次和客单价做分群只要数据清洗到位、量纲处理好了K-means跑得又快又稳结果也好解释。DBSCAN适合簇形状不规则、存在大量离群点、事先不知道簇数量的场景。比如基于地理坐标的社区服务需求分类网点分布可能是任意形状的而且天然有很多噪声点DBSCAN能在聚类的同时把这些噪声标出来。光知道结论不够下面我把两种算法的工作原理、参数背后的逻辑、Python实现细节和实战中的坑逐个拆开你就能理解这个结论是怎么来的了。2. K-means原理拆解K值怎么定、距离怎么算、为什么必须标准化2.1 K-means三步循环和目标函数K-means的核心逻辑可以压缩成一个三步循环每一步都不复杂但组合起来就能完成聚类随机或用k-means策略初始化K个质心。分配把每一个样本划分到距离最近的质心所在的簇。更新重新计算每个簇内所有样本的均值把这个均值作为新的质心。然后重复第2步和第3步直到质心位置基本不再变化或者达到最大迭代次数。这个流程本质上是坐标下降法在优化一个目标函数目标函数叫簇内平方和WCSS也叫inertia[ J \sum_{i1}^{K} \sum_{x\in C_i} | x - \mu_i |^2 ]其中 (\mu_i) 是第 (i) 个簇的质心(| x - \mu_i |) 是样本到质心的欧式距离。K-means每轮迭代都在尽力降低这个损失直到收敛到局部最优。注意是局部最优不是全局最优这也就是为什么同一份数据用不同随机种子跑结果可能不一样。如果你接触过EM算法会发现K-means和EM有很强的相似性分配步骤相当于E步估计每个样本属于哪个簇更新质心相当于M步用簇内样本重新估计质心参数。理解了这层对应关系后面再看高斯混合模型GMM之类的进阶方法就不会觉得突兀了。2.2 K值选择肘部法则和轮廓系数的配合使用K-means最让人头疼的问题永远是K到底取多少。实际项目里没人会告诉你应该分成几类这个值基本都是试出来的。我常用的方法是肘部法则加轮廓系数一起看。肘部法则的做法是跑一组不同K值的模型记录每个K对应的inertia然后画折线图。随着K增大inertia总是下降的因为簇越多每个样本离自己质心的距离就越近。关键看下降速度K从1增加到某个值之前inertia下降很快再往后下降变缓曲线会出现一个类似手肘的拐点这个拐点对应的K就是候选值。轮廓系数则是从簇内紧密度和簇间分离度两个维度评价聚类质量。每个样本的轮廓系数在-1到1之间越接近1说明这个样本被分得越好。把全体样本的轮廓系数取平均得到一个整体评分取评分最高的K。轮廓系数的好处是不需要换K重新算质心直接基于现有聚类结果计算即可所以非常适合和肘部法则搭配验证。我的实际习惯是先用肘部法则圈定K的大致范围再用轮廓系数在范围内细选最后一定还要结合业务含义确认。比如做用户分群K4在指标上很漂亮但是分出来的四个群业务上没法解释这个K就不可用。指标永远是辅助业务可解释性才是最终目标。提示K-means对K值很敏感K选大了容易把一个大簇硬切成两半K选小了又会把两个相近的簇强行合并所以这一步不要省也不要只看一个指标。2.3 量纲不统一会毁掉距离计算这一点我放在K-means原理里讲是因为它直接决定算法成败而且新手最容易忽略。K-means用欧式距离衡量样本相似度欧式距离对每一维特征的尺度都非常敏感。举个极端例子用户数据里年消费金额的取值范围是0到数万购买次数的取值范围是0到几十。如果不做任何处理直接算距离消费金额这一维会完全主导距离计算购买次数几乎变成摆设聚类结果基本等效于只按消费金额排序分段这是典型的错误用法。标准做法是用StandardScaler做z-score标准化把每个特征变成均值0、方差1from sklearn.preprocessing import StandardScaler scaler StandardScaler() X_scaled scaler.fit_transform(X) # 标准化之后再去做聚类有些场景也可以用MinMaxScaler把数据缩放到[0, 1]区间具体看数据分布形态。如果特征里存在极严重的偏态先做log变换再标准化效果通常更好。注意scaler一定要用拟合训练数据的参数去变换后续新数据不要在全部数据上fit之后再拿同一批数据做评估那样信息会泄露影响对泛化能力的判断。3. DBSCAN原理拆解密度、邻域半径和MinPts怎么联动3.1 核心点、边界点、噪声点的定义DBSCAN的全称是Density-Based Spatial Clustering of Applications with Noise密度聚类。它不关心质心也不要求簇是凸的而是换了一套完全不同的判定逻辑如果某个区域样本足够稠密就把它们连成一个簇稀疏区域的样本直接标记为噪声。具体判定依赖两个参数邻域半径eps和最小样本数min_samples通常也写作MinPts。基于这两个参数DBSCAN把每个样本分成三类核心点以该点为圆心、eps为半径的邻域内样本数量大于等于min_samples。核心点是簇的骨架。边界点不是核心点但落在某个核心点的eps邻域内。边界点属于某个簇但自身密度不够。噪声点既不是核心点也不在任何一个核心点的邻域内标记为-1。聚类过程可以想象成人传人的扩展过程从一个核心点出发把它eps范围内的所有样本拉进来这里面如果有其他核心点就继续扩展它们的邻域直到没有新的核心点为止。这个扩展过程不要求簇是圆形只要是密度相连的区域哪怕绕成一条蛇形也能被归为同一个簇。3.2 eps和min_samples的调参方法KNN距离曲线DBSCAN调参比K-means更考验经验尤其是eps它对结果的影响极大。eps太大所有点都可能被连成一个簇eps太小一个完整的簇会被拆成碎片大量样本变成噪声。一个相对靠谱的确定eps方法是用KNN距离曲线。思路是先取min_samples为K然后计算每个样本到它第K个最近邻居的距离把这些距离排序画一条曲线。曲线在某个位置出现明显拐点的距离值就是合适的eps。因为曲线比较陡峭处说明样本密度变化剧烈超过这个距离后邻居数量会快速增长自然形成簇的边界。import numpy as np from sklearn.neighbors import NearestNeighbors # 假设 X_scaled 是标准化后的数据 min_samples 5 nn NearestNeighbors(n_neighborsmin_samples) nn.fit(X_scaled) distances, _ nn.kneighbors(X_scaled) # 每个样本到第 min_samples 个最近邻居的距离排序后画曲线 kneighbor_dist np.sort(distances[:, -1]) import matplotlib.pyplot as plt plt.plot(kneighbor_dist) plt.xlabel(样本序号按距离排序) plt.ylabel(第 %d 个最近邻居的距离 % min_samples) plt.show()min_samples的选择相对简单一些经验值一般取特征维度的两倍或者按数据量级取5到10。min_samples越大聚类的抗噪性越强但容易把本来密度不高的小簇直接抹掉。实际使用中我习惯先定min_samples再去KNN曲线里找eps然后少量调整反复看聚类结果的质量。3.3 密度的视角能处理什么、怕什么DBSCAN最大的优势是能处理任意形状的簇以及自动识别噪声点。月牙形、环形的数据在K-means那里会很难看但DBSCAN可以把它们沿着密度连通性完整地连起来。这在真实项目里非常实用因为真实数据的簇很少是完美的球形。不过DBSCAN也有两个明显的软肋。第一它对参数太敏感。同一份数据eps差个0.05结果可能从一个簇加一堆噪声变成四个簇。这种脆性意味着你需要对数据分布有比较好的先验认识否则调参会变成玄学。第二它对密度差异很大的数据不太友好。如果数据集中同时存在一个非常稠密的簇和一个相对稀疏但也是真实簇的区域DBSCAN的单一全局eps很难同时照顾两边。eps取大了稠密簇会被合并eps取小了稀疏簇全变成噪声。这种情况下可以考虑OPTICS算法或者先把数据分区域再分别聚类。4. Python全流程实现从环境准备到聚类落地4.1 环境准备虚拟环境与依赖安装动手跑代码之前先把环境整理干净。我强烈建议不要直接往系统Python里pip install一堆包不同项目的依赖版本很可能会冲突。用内置的venv就能解决问题# 创建虚拟环境 python -m venv cluster_env # 激活 # Linux / macOS source cluster_env/bin/activate # Windows cluster_env\Scripts\activate # 安装依赖 pip install numpy pandas scikit-learn matplotlib安装完成后建议顺手检查一下版本scikit-learn版本差异偶尔会造成部分API行为变化尤其注意新版本对部分参数弃用的警告import sklearn print(sklearn.__version__) # scikit-learn 1.0 在本文涉及的功能上都正常如果你用的是Anaconda之类的发行版也可以用conda create -n cluster_env python3.10来创建环境原理一样。环境这步看起来无关紧要但能帮你省掉大量后面莫名其妙的报错时间。4.2 构造和预处理数据集为了把两种算法的差异展示清楚我用两类经典数据集一类是球形簇make_blobs一类是月牙形簇make_moons后者专门用来让K-means翻车。import numpy as np import matplotlib.pyplot as plt from sklearn.datasets import make_blobs, make_moons from sklearn.preprocessing import StandardScaler # 数据集1球形簇 X_blob, y_blob make_blobs(n_samples800, centers4, cluster_std0.8, random_state42) # 数据集2月牙形簇加少量噪声 X_moon, y_moon make_moons(n_samples500, noise0.05, random_state42)虽然是无监督任务make_blobs里的y_blob其实是有标签的我们用它只是为了在评估聚类效果时做对照不是给算法偷看答案。实际项目中也可以用类似思路先用有标签的历史数据验证聚类算法表现再应用到真正的无标签数据上。预处理环节要做的两件事一是标准化前面已经讲过原因二是可视化检查聚类前先把数据分布图画出如果发现特征有明显的偏态分布考虑先变换。下面统一生成标准化版本scaler_blob StandardScaler() X_blob_scaled scaler_blob.fit_transform(X_blob) scaler_moon StandardScaler() X_moon_scaled scaler_moon.fit_transform(X_moon)4.3 K-means完整实现包括最优K的确认先实现一个函数来对不同K值跑K-means同时计算inertia和轮廓系数from sklearn.cluster import KMeans from sklearn.metrics import silhouette_score def evaluate_kmeans(X, k_rangerange(2, 11)): inertia_list [] silhouette_list [] models {} for k in k_range: model KMeans(n_clustersk, initk-means, n_init10, random_state42) model.fit(X) models[k] model inertia_list.append(model.inertia_) silhouette_list.append(silhouette_score(X, model.labels_)) return inertia_list, silhouette_list, models inertia_blob, sil_blob, models_blob evaluate_kmeans(X_blob_scaled) # 画出肘部曲线和轮廓系数曲线 fig, axes plt.subplots(1, 2, figsize(12, 4)) axes[0].plot(range(2, 11), inertia_blob, markero) axes[0].set_title(肘部法则 - Inertia) axes[1].plot(range(2, 11), sil_blob, markero) axes[1].set_title(轮廓系数) plt.show()跑完可以明显看到球形数据在K4附近出现拐点轮廓系数也在K4时达到峰值符合make_blobs的真实结构。拿到最优K之后就可以拟合最终模型并把聚类结果和真实标签做对照可视化best_k 4 final_kmeans models_blob[best_k] plt.scatter(X_blob_scaled[:, 0], X_blob_scaled[:, 1], cfinal_kmeans.labels_, cmapviridis, alpha0.7) plt.scatter(final_kmeans.cluster_centers_[:, 0], final_kmeans.cluster_centers_[:, 1], cred, markerx, s100, label质心) plt.legend() plt.show()同样的代码换到make_moons数据上你就发现问题了K-means无论如何调整K值都不可能把两个月牙完整分开它会部分把一侧切成两段然后把两片区域硬凑在一起。这不是实现细节的问题而是K-means的模型假设决定的后面我会在选型部分细说。4.4 DBSCAN完整实现与调参可视化DBSCAN的代码本身很简洁难点全在参数上from sklearn.cluster import DBSCAN def fit_dbscan(X, eps, min_samples): model DBSCAN(epseps, min_samplesmin_samples) model.fit(X) return model # 先用KNN距离曲线确定eps的候选值 min_samples_candidate 5 nn NearestNeighbors(n_neighborsmin_samples_candidate) nn.fit(X_moon_scaled) distances, _ nn.kneighbors(X_moon_scaled) kneighbor_dist np.sort(distances[:, -1]) plt.plot(kneighbor_dist) plt.xlabel(样本序号) plt.ylabel(%d-NN距离 % min_samples_candidate) plt.show()从曲线上找到一个明显拐点的位置比如这里大概是0.25到0.3附近。然后跑DBSCANdbscan_moon fit_dbscan(X_moon_scaled, eps0.28, min_samples5) n_clusters len(set(dbscan_moon.labels_)) - (1 if -1 in dbscan_moon.labels_ else 0) n_noise list(dbscan_moon.labels_).count(-1) print(f簇数量: {n_clusters}, 噪声点数量: {n_noise}) plt.scatter(X_moon_scaled[:, 0], X_moon_scaled[:, 1], cdbscan_moon.labels_, cmapviridis, alpha0.7) plt.show()你会发现DBSCAN很好地认出了两个月牙簇同时把少数远离簇主体的点标成了噪声颜色与其他簇都不同标签为-1。同样在make_blobs上DBSCAN也能识别出4个球形簇效果与K-means相当这印证了DBSCAN在常规簇形状上并不会输给K-means只是参数需要多花些心思来调。4.5 用轮廓系数评估聚类结果注意它不能当唯一标准无论跑K-means还是DBSCAN我建议都用silhouette_score量化评估一下from sklearn.metrics import silhouette_score # 排除噪声点做轮廓系数评估 mask dbscan_moon.labels_ ! -1 # 轮廓系数不支持噪声标签 score silhouette_score(X_moon_scaled[mask], dbscan_moon.labels_[mask]) print(DBSCAN在月牙数据上的轮廓系数:, score)要注意一个细节轮廓系数要求每个样本都有簇标签DBSCAN的噪声点-1需要先剔除再计算否则会报错或者给出误导性的低分。即便算出来分数不错也不要只看这一个指标。真实场景里还要看簇内样本数量是否合理、业务含义是否清晰、聚类结果是否稳定等综合判断才能下结论。5. 实战踩坑记录与算法选型建议5.1 K-means的翻车现场异常值、初始化和簇形状我实际项目里第一次用K-means做客户分群结果惨不忍睹。数据里有几个消费金额特别高的异常客户K-means硬是给它们单独分了一个簇其他几千个用户被压缩到两个大簇里业务上完全没法用。原因就是K-means对异常值极其敏感异常样本在线性距离计算中会被强烈吸引到某个质心里甚至直接把质心拽走。解决办法分几个层次数据预处理阶段先用IQR或业务规则把明显异常值剔除或者改用对异常值更鲁棒的聚类方法如果因为有特殊价值的群体不想删除它们可以专门设置一个高端客户类别不在整体聚类里处理。另外两个K-means的常见问题也一并总结初始质心选择虽然k-means已经大幅改善了随机初始化的问题但极端情况下仍然可能陷入局部最优。我用n_init10到20做多次初始化然后取inertia最小的结果稳定很多。簇形状失配K-means假设每个簇是Voronoi划分出来的凸区域实际遇到月牙、环形、狭长带状的分布就直接失灵别硬撑换密度聚类。5.2 DBSCAN的翻车现场eps的脆弱性和连锁合并DBSCAN最典型的坑是对eps的微小变化反应剧烈。我曾经在一个POI聚类项目里eps从0.3调到0.35结果簇数量从5个直接变成12个因为超过某个密度阈值后一个本来被稀疏区隔开的簇被连锁反应般连到了一起多个簇被误合并。自那以后我总结了一套相对可靠的调参流程先用KNN距离曲线圈定eps的大致区间在这个区间内按0.05步长跑网格搜索用簇数量噪声比例业务可解释性三个维度综合评分选一个最稳定的区域而不是只挑单点最优。比如只要eps在0.25到0.30之间结果都稳定在指定簇数说明这个参数区域是可信的如果eps稍微一变结果就剧烈抖动说明数据本身的簇结构不清晰这时候无论怎么调参数都很难得到稳定的业务结论应该回头检查特征工程。DBSCAN在高维数据上的表现也更差。当特征维度变高距离度量会趋于钝化样本间距离差异变小eps很难找到有效分界。这种情况下我通常先用PCA把维度压到两到三维或者用UMAP降维后再聚类效果往往会有明显改善。5.3 选型判断流程一张表看懂什么时候用哪个为了让选型更直观我把自己常用的判断路径整理成了表格方便大家保存参考判断维度选K-means选DBSCAN簇的形状凸形、球形、大小相近任意形状月牙、环状、长条异常点/噪声需要提前清洗算法本身不识别天然识别为-1不需要预处理剔除簇的数量需要事先确定或用肘部法/轮廓系数估计不用预设由密度自动决定数据量百万级照样很快复杂度近似O(n)不使用空间索引时偏慢接近O(n^2)高维数据距离钝化问题较轻仍能凑合跑严重依赖密度高维下eps难定义参数可解释性只有K方便向业务方解释epsmin_samples业务方理解成本高这个表格不能当死规则用但它覆盖了我踩坑经验的80%。简单说不确定簇数量或者数据里有大量噪声优先DBSCAN数据量非常大、簇的形状又比较规整优先K-means速度优势明显如果业务更关注哪些点算正常、哪些点是异常DBSCAN天然提供这个视角如果业务需要把每个簇解释成一个典型用户画像K-means的质心直接给你一组特征均值解释起来很顺手。5.4 我在真实项目里的组合用法最后分享一个比较进阶的组合思路也是我最近几个项目里用得比较多的流程先用DBSCAN把数据里的噪声点识别并剔除得到一个相对干净的样本集再用K-means对剔除噪声后的数据做细分。这个流程的好处在于两类算法互补。DBSCAN擅长处理噪声和任意形状但它的簇结果通常比较粗糙直接拿它的簇作为最终分组不太方便做深度画像因为DBSCAN没有质心概念。K-means正好相反需要干净的输入数据但能给出清晰可解释的质心和明确的簇个数。两者一前一后配合往往能得到既稳定又解释得通的结果。另一个实战体会是聚类做完之后不要急着交付花时间做一个稳定性检查用不同的随机种子重新跑K-means多次看看哪些样本的簇归属经常变化。稳定的簇才是可信的边缘样本的归属本来就是模糊的如果边缘样本占的比例太高说明这个聚类解决方案本身就不够靠谱需要回到特征工程步骤调整。实际项目中聚类从来不是终点它更像是帮你快速理解数据的一个透镜。K-means和DBSCAN的差别也不只是算法上的优劣而是你选择用什么样的视角看待数据。理解了这两套视角的底层逻辑再遇到任何无监督分析任务选型这件事就不会让你纠结太久了。
返回列表