尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
R语言无监督聚类实战:K-means与层次聚类核心原理及调参指南
1. 为什么无监督聚类值得你花时间搞明白做数据分析的人迟早会撞上这样一个场景手里攥着一份数据有几十个甚至上百个特征列但偏偏没有标签。你没法做分类也没法做回归因为压根不知道“正确答案”长什么样。这时候聚类就是你的第一把钥匙。R 语言里做无监督聚类的工具非常多但真正经得起实战考验、跑得快又解释得清楚的K-means 和层次聚类是绕不开的两座山头。这篇文章面向的是已经会用 R 做基本数据处理、但聚类实操经验不多的朋友。我会把 K-means 和层次聚类的核心原理、R 里的实现细节、参数怎么调、结果怎么评估、踩过的坑怎么填全部拆开讲一遍。每个关键选择我都会解释“为什么这么做”而不是只丢一段代码让你抄。文章里涉及的所有数据集都是模拟生成的不涉及任何真实业务数据你可以直接复制代码跑通。先说清楚这两个算法各自适合什么场景。K-means 适合样本量大、簇形状偏球形、你大致能预估簇数量的情况层次聚类适合样本量中等、你想看数据的层次结构、或者根本不知道该分几类的情况。两者不是替代关系而是互补关系。实际项目里我经常先用层次聚类看整体结构再用 K-means 做精细划分这个组合拳后面会详细讲。2. K-means 的核心逻辑与 R 实现细节2.1 K-means 到底在优化什么K-means 的目标函数非常直白让每个点到它所属簇中心的距离平方和最小。数学上写成这样[ J \sum_{i1}^{n} \sum_{k1}^{K} w_{ik} |x_i - \mu_k|^2 ]其中 (w_{ik}) 等于 1 表示点 (i) 属于簇 (k)否则为 0(\mu_k) 是簇 (k) 的中心。这个目标函数是个 NP-hard 问题所以实际用的是迭代近似算法也就是经典的 Lloyd 算法。Lloyd 算法的流程分三步循环第一步随机选 K 个点作为初始中心第二步把每个点分配给最近的中心第三步重新计算每个簇的中心取均值。重复二三步直到中心不再变化或达到最大迭代次数。这里有个关键点很多人忽略K-means 每次迭代一定不会让目标函数变大但它只能保证收敛到局部最优不保证全局最优。这就是为什么初始化方式如此重要。2.2 R 里做 K-means 的几种方式对比R 基础包自带kmeans()函数这是最常用的入口。但如果你只用它可能会错过一些更好的选择。下面这张表是我在实际项目中总结的对比函数/包初始化方式优点缺点适用场景stats::kmeans()随机/Hartigan-Wong无需额外安装速度快只支持欧氏距离快速探索cluster::pam()围绕中心点支持任意距离抗噪计算量大小样本、有离群点ClusterR::KMeans_rcpp()K-means初始化好支持多线程需额外安装大数据集factoextra::eclust()多种可选可视化方便封装较厚教学演示我个人的习惯是数据量小于 1 万行且特征不多时直接用stats::kmeans()配合nstart25数据量大或者对初始化敏感时换ClusterR包的 K-means 实现。2.3 初始化为什么 nstart 参数不能省stats::kmeans()默认的nstart1意思是只做一次随机初始化。这在簇结构明显的时候没问题但如果数据分布稍微复杂一点单次初始化很可能掉进一个很差的局部最优。我做过一个模拟实验生成三个真实簇的数据用nstart1跑 100 次结果有将近 30 次把两个簇合并了。改成nstart25之后100 次里只有 2 次出现明显偏差。代价是计算时间大约增加 20 倍但对于中等规模数据完全可以接受。# 模拟数据三个簇每个簇 200 个点 set.seed(42) cluster1 - matrix(rnorm(400, mean 0, sd 0.8), ncol 2) cluster2 - matrix(rnorm(400, mean 4, sd 0.8), ncol 2) cluster3 - matrix(rnorm(400, mean 8, sd 0.8), ncol 2) sim_data - rbind(cluster1, cluster2, cluster3) # nstart1 的糟糕结果示例 set.seed(123) km_bad - kmeans(sim_data, centers 3, nstart 1) km_bad$size # 可能输出类似 400 200 0 的分布说明有一个簇空了 # nstart25 的稳定结果 set.seed(123) km_good - kmeans(sim_data, centers 3, nstart 25) km_good$size # 输出接近 200 200 200注意nstart不是越大越好。超过 50 之后边际收益急剧下降但计算时间线性增长。我的经验值是 25 到 50 之间足够。2.4 如何确定 K 值三种方法交叉验证确定簇数量 K 是 K-means 最头疼的问题。常用的方法有三种肘部法、轮廓系数、gap statistic。我一般三个都跑一遍看它们是否指向同一个 K。肘部法看的是组内平方和within-cluster sum of squares, WSS随 K 变化的曲线。WSS 一定随 K 增大而减小我们要找的是下降速度突然变缓的那个“肘部”。但这个肘部有时候很模糊尤其是真实簇结构不明显的时候。轮廓系数衡量的是每个点跟自己簇的紧密程度 vs 跟最近邻簇的分离程度取值在 -1 到 1 之间。越接近 1 说明聚类效果越好。对每个 K 计算平均轮廓系数取最大值对应的 K。Gap statistic 比较的是实际数据的 WSS 和均匀分布下的期望 WSS选 gap 最大的 K。这个方法理论上更严谨但计算量大。# 肘部法 wss - sapply(1:10, function(k) { kmeans(sim_data, centers k, nstart 25)$tot.withinss }) plot(1:10, wss, type b, pch 19, xlab 簇数量 K, ylab 组内平方和 WSS) # 轮廓系数 library(cluster) sil_width - sapply(2:10, function(k) { km - kmeans(sim_data, centers k, nstart 25) ss - silhouette(km$cluster, dist(sim_data)) mean(ss[, 3]) }) plot(2:10, sil_width, type b, pch 19, xlab 簇数量 K, ylab 平均轮廓系数)实测下来对于我这个模拟数据肘部法在 K3 处明显变缓轮廓系数在 K3 处达到峰值gap statistic 也指向 3。三个方法一致的时候你就可以放心用这个 K。2.5 标准化不做这一步可能全盘皆输K-means 用的是欧氏距离这意味着特征的量纲直接影响距离计算。如果一个特征的范围是 0 到 1000另一个是 0 到 1那第一个特征会完全主导距离第二个特征等于白给。标准化z-score是最常用的处理方式每个特征减去均值再除以标准差。这样所有特征都在同一个尺度上均值为 0标准差为 1。sim_data_scaled - scale(sim_data) # 对比标准化前后的聚类结果 km_raw - kmeans(sim_data, centers 3, nstart 25) km_scaled - kmeans(sim_data_scaled, centers 3, nstart 25) # 如果原始特征量纲差异大两个结果会完全不同提示不是所有情况都要标准化。如果所有特征本身就是同一量纲比如都是长度、都是评分标准化反而可能抹掉有意义的差异。判断标准是特征之间的量纲差异是否大到会影响距离计算。3. 层次聚类的原理与实操要点3.1 凝聚层次聚类从下往上的合并逻辑层次聚类分两种凝聚agglomerative和分裂divisive。R 里常用的是凝聚层次聚类逻辑是从每个点自成一簇开始每次合并距离最近的两个簇直到所有点合并成一个大簇。这个合并过程形成一棵树叫树状图dendrogram。你可以在任意高度切一刀得到不同数量的簇。这是层次聚类相比 K-means 最大的优势不需要预先指定 K而且能直观看到簇的嵌套关系。但这里有个关键问题两个簇之间的距离怎么定义这就是 linkage 方法要解决的问题。3.2 Linkage 方法选择single、complete、average、ward四种常见的 linkageSingle linkage两个簇中最近点之间的距离。容易产生链式效应把一条长链上的点都归为一簇。Complete linkage两个簇中最远点之间的距离。倾向于产生紧凑的、大小相近的簇。Average linkage所有点对距离的平均。折中方案但对簇大小敏感。Ward 方法合并后组内平方和增加最小的两个簇。倾向于产生球形、大小相近的簇跟 K-means 的目标函数很像。我个人的经验是Ward 方法在大多数场景下表现最稳定尤其是数值型数据。Single linkage 几乎不用除非你明确知道数据有链式结构。Complete 和 average 在某些特定分布下有用但不如 Ward 通用。# 计算距离矩阵 dist_matrix - dist(sim_data_scaled, method euclidean) # 四种 linkage 对比 hc_single - hclust(dist_matrix, method single) hc_complete - hclust(dist_matrix, method complete) hc_average - hclust(dist_matrix, method average) hc_ward - hclust(dist_matrix, method ward.D2) # 画树状图 par(mfrow c(2, 2)) plot(hc_single, main Single Linkage, labels FALSE) plot(hc_complete, main Complete Linkage, labels FALSE) plot(hc_average, main Average Linkage, labels FALSE) plot(hc_ward, main Ward.D2, labels FALSE)跑完这段代码你会看到Ward 方法的树状图最“平衡”三个主分支的高度差异明显切三刀很自然。Single linkage 的树状图则会出现一条很长的“尾巴”这就是链式效应的直观体现。3.3 距离度量欧氏之外还有什么选择dist()函数支持多种距离euclidean、maximum、manhattan、canberra、binary、minkowski。对于连续数值型数据欧氏距离是默认选择。但如果数据里有离群点manhattan 距离更稳健。对于二元数据0/1 矩阵binary 距离Jaccard 系数更合适。# Manhattan 距离对离群点更稳健 dist_manhattan - dist(sim_data_scaled, method manhattan) hc_manhattan - hclust(dist_manhattan, method ward.D2)注意Ward 方法理论上要求使用欧氏距离因为它的目标函数是基于平方欧氏距离推导的。如果你换成 manhattan 距离Ward 的数学基础就不成立了。这时候应该换用 average 或 complete linkage。3.4 树状图切割怎么切、切几刀切树状图有两种方式按高度切和按簇数量切。cutree()函数两个参数都支持。# 按簇数量切 clusters_k3 - cutree(hc_ward, k 3) # 按高度切 clusters_h - cutree(hc_ward, h 5) # 查看每个簇的大小 table(clusters_k3)按高度切的好处是你可以从树状图上看哪个高度切下去最“干净”——也就是切割线穿过的分支最少。如果一条切割线横穿了好几个密集的分支说明这个高度不合适。我通常的做法是先画树状图目测几个候选高度然后用cutree()分别切出来对比轮廓系数或者业务解释性选最好的那个。4. 两种方法的对比与组合使用策略4.1 什么场景选 K-means什么场景选层次聚类这张表是我在实际项目中做决策的依据维度K-means层次聚类样本量大万级以上中几千以内簇形状球形、大小相近任意形状是否需要预设 K是否结果可复现性依赖初始化种子完全确定计算复杂度O(nKd) 每轮O(n²) 起离群点敏感度高中等取决于 linkage输出结构平坦簇层次树样本量超过 1 万行时层次聚类的距离矩阵会占用大量内存n² 个元素计算也会变得很慢。这时候 K-means 是唯一务实的选择。反过来如果你只有几百个样本又想看数据的层次结构层次聚类更合适。4.2 组合策略层次聚类定 KK-means 精分这是我最常用的组合拳。先用层次聚类跑一遍画树状图目测可能的簇数量范围。然后用肘部法和轮廓系数在候选范围内确定最佳 K。最后用 K-means 做精细划分因为 K-means 在大样本上更快而且能给出每个簇的中心点方便后续解释。# 第一步层次聚类看结构 hc - hclust(dist(sim_data_scaled), method ward.D2) plot(hc, labels FALSE, main 层次聚类树状图) # 第二步根据树状图确定候选 K 范围比如 2 到 6 # 用轮廓系数选最佳 K sil_results - sapply(2:6, function(k) { km - kmeans(sim_data_scaled, centers k, nstart 25) mean(silhouette(km$cluster, dist(sim_data_scaled))[, 3]) }) best_k - which.max(sil_results) 1 # 第三步用最佳 K 跑 K-means final_km - kmeans(sim_data_scaled, centers best_k, nstart 50)这个流程的好处是层次聚类给你全局视角K-means 给你精细结果两者互相验证。如果层次聚类在 K3 处切得很干净K-means 的轮廓系数也在 K3 处最大那你就很有信心了。4.3 结果评估内部指标和外部指标聚类没有标签所以评估只能用内部指标。常用的有轮廓系数-1 到 1越大越好。但计算量大大样本慎用。Calinski-Harabasz 指数簇间方差与簇内方差的比值越大越好。Davies-Bouldin 指数簇内散度与簇间距离的比值越小越好。library(fpc) # Calinski-Harabasz 指数 ch_index - cluster.stats(dist(sim_data_scaled), final_km$cluster)$ch # Davies-Bouldin 指数 db_index - cluster.stats(dist(sim_data_scaled), final_km$cluster)$db如果数据恰好有真实标签比如你拿一个已知分类的数据集做验证可以用外部指标调整兰德指数ARI、归一化互信息NMI。但实际项目中这种情况很少大部分时候还是靠内部指标加业务解释性。5. 常见问题与排查技巧实录5.1 空簇问题K-means 跑出来某个簇是空的空簇是 K-means 的经典问题。原因通常是初始中心选得太近或者 K 设得太大。解决方法增大nstart让算法多试几次初始化。换用 K-means 初始化ClusterR包默认就是。如果空簇反复出现说明 K 可能设大了减少 K 试试。# 检查是否有空簇 km - kmeans(sim_data_scaled, centers 5, nstart 25) any(km$size 0) # 返回 TRUE 说明有空簇5.2 层次聚类树状图看不清标签重叠怎么办样本量超过 100 时树状图的叶子标签会挤成一团。解决方法plot(hc, labels FALSE)直接不显示标签。用dendextend包调整标签大小和角度。只画部分子树比如plot(hc, hang -1)让所有叶子对齐底部。library(dendextend) dend - as.dendrogram(hc_ward) dend - set(dend, labels_cex, 0.5) plot(dend, main 调整标签大小后的树状图)5.3 聚类结果不稳定每次跑 K-means 结果都不一样这是初始化随机性导致的。解决方法设置随机种子set.seed()。增大nstart。如果还是不稳定说明数据结构本身模糊没有明确的簇边界。这时候要考虑降维或者换算法。提示set.seed()只能保证你这次跑的结果可复现不能保证结果就是最优的。真正稳定的做法是增大nstart让算法自己找到更好的局部最优。5.4 高维数据聚类效果差维度灾难怎么破特征超过 20 个时欧氏距离会变得没有区分度——所有点对的距离都差不多。这时候需要先降维PCA线性降维保留方差最大的主成分。t-SNE非线性降维适合可视化但不适合直接用于聚类。UMAP非线性降维比 t-SNE 快保留全局结构更好。# PCA 降维后再聚类 pca_result - prcomp(sim_data_scaled, center TRUE, scale. FALSE) sim_data_pca - pca_result$x[, 1:5] # 取前 5 个主成分 km_pca - kmeans(sim_data_pca, centers 3, nstart 25)我通常先看 PCA 的方差解释比例如果前几个主成分能解释 80% 以上的方差就可以用降维后的数据做聚类。否则降维可能丢失太多信息。5.5 离群点干扰一个极端值带偏整个簇K-means 对离群点非常敏感因为均值本身就不是稳健统计量。一个极端值可以把簇中心拉偏很远。解决方法聚类前先做离群点检测和剔除。换用cluster::pam()它用中心点medoid而不是均值更稳健。用 manhattan 距离替代欧氏距离。library(cluster) pam_result - pam(sim_data_scaled, k 3) pam_result$medoids # 查看中心点5.6 常见问题速查表问题现象可能原因排查方法解决方案空簇初始化差或 K 过大检查km$size增大 nstart减小 K结果不稳定初始化随机性多次运行对比set.seed 增大 nstart树状图标签重叠样本量太大目测labelsFALSE 或 dendextend聚类效果差未标准化或高维检查特征量纲scale() PCA离群点带偏均值不稳健箱线图检查剔除离群点或换 pam轮廓系数低簇结构不明显可视化换算法或降维6. 一个完整的实战流程模板把上面所有内容串起来这是一个可以直接套用的流程# 1. 数据准备与探索 data_scaled - scale(sim_data) # 2. 层次聚类看整体结构 hc - hclust(dist(data_scaled), method ward.D2) plot(hc, labels FALSE, main 层次聚类树状图) # 3. 确定候选 K 范围根据树状图目测 k_range - 2:6 # 4. 肘部法 轮廓系数选 K wss - sapply(k_range, function(k) kmeans(data_scaled, k, nstart 25)$tot.withinss) sil - sapply(k_range, function(k) { km - kmeans(data_scaled, k, nstart 25) mean(silhouette(km$cluster, dist(data_scaled))[, 3]) }) # 5. 综合判断最佳 K par(mfrow c(1, 2)) plot(k_range, wss, type b, main 肘部法) plot(k_range, sil, type b, main 轮廓系数) # 6. 用最佳 K 跑最终 K-means best_k - k_range[which.max(sil)] final_km - kmeans(data_scaled, centers best_k, nstart 50) # 7. 可视化结果 library(ggplot2) df - data.frame(x sim_data[, 1], y sim_data[, 2], cluster factor(final_km$cluster)) ggplot(df, aes(x, y, color cluster)) geom_point(size 2) theme_minimal() labs(title paste(K-means 聚类结果 (K , best_k, ))) # 8. 输出簇中心 final_km$centers这个模板我用了很多次基本覆盖了 80% 的聚类场景。剩下的 20% 需要根据具体数据调整比如换距离度量、换 linkage、加降维步骤。7. 几个容易被忽略的实操心得第一个心得聚类之前一定要画散点图矩阵或者 PCA 图。很多人拿到数据直接跑算法结果跑完才发现数据里有两个明显的离群点或者特征之间高度相关。先可视化能帮你省下大量调试时间。第二个心得标准化不是万能的。如果所有特征本身就是同一量纲标准化反而可能抹掉有意义的差异。判断标准是特征之间的量纲差异是否大到会影响距离计算。比如都是 1 到 5 的评分就不需要标准化。第三个心得K-means 的centers参数可以接受一个矩阵而不是一个数字。这意味着你可以手动指定初始中心。如果你对数据有先验知识比如知道某个簇大概在什么位置可以直接把中心设在那里跳过随机初始化。# 手动指定初始中心 manual_centers - matrix(c(-1, -1, 4, 4, 9, 9), ncol 2, byrow TRUE) km_manual - kmeans(sim_data_scaled, centers manual_centers)第四个心得层次聚类的树状图不一定要从上往下看。有时候从下往上看找到第一个合并高度突然增大的地方那个位置往往就是天然的簇边界。这个方法叫“最大间隙法”比目测肘部更客观。第五个心得如果数据量在 5000 到 20000 之间层次聚类跑得动但有点慢K-means 很快但可能不稳定。这时候可以用一个折中方案先随机抽样 2000 个点做层次聚类确定 K 和初始中心然后用全部数据跑 K-means初始中心用层次聚类的结果。这样既利用了层次聚类的稳定性又利用了 K-means 的速度。这个抽样策略我在一个模拟项目里试过20000 个点、10 个特征的数据全量层次聚类跑了将近 3 分钟抽样后只要 8 秒而且最终 K-means 的结果跟全量层次聚类的簇分配一致率超过 95%。最后再分享一个小技巧factoextra包里的fviz_cluster()函数可以直接把 K-means 或层次聚类的结果画成漂亮的散点图自动处理维度问题超过 2 维时用 PCA 投影。如果你不想自己写可视化代码这个函数能省不少事。library(factoextra) fviz_cluster(final_km, data sim_data_scaled, geom point, ellipse.type convex)聚类这件事算法本身不复杂难的是理解数据、选对方法、调好参数、解释结果。K-means 和层次聚类是两块最基础的积木把它们玩熟了后面学 DBSCAN、GMM、谱聚类都会轻松很多。
RELATED

相关推荐

从XMLHttpRequest到Fetch:前端异步请求的机制、封装与实战避坑指南

从XMLHttpRequest到Fetch:前端异步请求的机制、封装与实战避坑指南

干前端这些年,我有个很深的感受:很多同学 JS 基础背得滚瓜烂熟,但一碰到数据交互就露怯。项目里十个报错,至少有五个和 JS AJAX 这条链路有关。AJAX 不是一个库,也不是某个框架独有的能力,它是浏览器提供的…

📅 2026/10/13 17:26:37
CC2530温湿度系统开发:IAR 8.10版本锁死与Z-Stack 2.5.1a工程实践

CC2530温湿度系统开发:IAR 8.10版本锁死与Z-Stack 2.5.1a工程实践

简介:本资源是一套基于CC2530微控制器的温湿度检测控制系统嵌入式开发工程,面向嵌入式初学者、物联网课程实践者及Zigbee无线传感网络开发者,解决环境参数实时采集、本地处理与无线通信集成等典型IoT工程问题,适用于智能家居、农业…

📅 2026/10/13 17:26:37
H.265/HEVC C/C++工程落地实战:从HM裁剪到x265封装与FFmpeg深度集成

H.265/HEVC C/C++工程落地实战:从HM裁剪到x265封装与FFmpeg深度集成

简介:这是一份面向C/C开发者与音视频技术学习者的H.265(HEVC)开源解码器实现资源,聚焦于底层编解码能力集成与多线程性能优化。libde265从零编写,提供简洁稳定的C语言API,支持WPP(波前并行处理&…

📅 2026/10/13 17:26:37
MORE NEWS

更多资讯

📰

Unity TMP中文字体Asset生成指南:字符集搭建与参数配置

之前有次接手一个Unity项目,一打开UI界面,中文全部变成白色方框。排查了半天,不是贴图丢失,是TextMeshPro没有中文字形。TMP默认字体文件里只有拉丁字符,项目只要显示简体中文,就必须先准备一个包含中文字形…

📰

DICOM转NII:元数据语义重建与坐标系校准实战指南

1. 为什么DICOM转NII不是“点一下就完事”的操作在某高校医学影像实验室带学生做课题时,我见过太多次这样的场景:刚接触影像处理的A同学兴冲冲跑来问:“老师,网上说用dcm2niix就能把DICOM转成NII,我拖了文件夹进去&…

📰

租房数据可视化系统:基于Flask与Layui的Python全栈开发实践

今年这个选题方向确实很值得好好说一说。计算机相关专业的毕业设计,年年都逃不开“要做一个能跑、能讲、能演示的系统”这场大考。租房数据可视化分析系统,用 Python 做全栈开发,后端走 Flask 框架、前端用 Layui 框架,再把房源、…

📰

精简中文字符集,解决Unity TMP缺字与生成慢

做 UI 的应该都有过这种经历:界面写好了,文案也填进去了,一进 Play Mode,TextMeshPro 文本里密密麻麻全是方块和空白。问题不出在文案,而是字体资源里根本没有这些字形。Unity TextMeshPro 的 SDF 渲染方案&#xff0c…

📰

从O(n²)到O(n log n):Barnes-Hut算法与N体模拟器实现指南

简介:一份C实现的Barnes-Hut模拟器源码包,面向需要掌握N体问题高效计算方法的开发者与天体物理爱好者。项目以八叉树结构递归划分三维空间,通过远近法则用节点质心近似远处引力,避免逐对力计算,在大规模粒子系统下显著…

📰

Simulink DEE学习课件:微分方程编辑器与连续系统建模实战指南

简介:面向 Simulink DEE 初学者与动态系统建模人员,这份入门课件着力解决微分方程建模与仿真的上手难题,即使非程序员也能借助图形界面快速构建复杂系统。压缩包整体仅 1.42MB,共 3 个文件:PDF 版详细教程深入讲解 DEE…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬