文本聚类算法选型指南:KMeans、DBSCAN、LDA与Single_Pass实战对比
发布时间:2026/10/25 19:12:11 锦皓数字建站

简介这份资源面向机器学习初学者与高校课程设计场景围绕中文文本聚类任务提供KMeans、DBSCAN、LDA与Single_Pass四种算法的Python实现。其中KMeans与DBSCAN分别完成基于划分和密度的无监督中文文本聚类LDA用于主题模型下的文本聚类Single_Pass则采用单遍策略无需预先设定类别数适合处理流式或类别未知的语料。压缩包共35个文件以py脚本、sample样例、head与txt数据文件为主另含config、md说明及版本控制相关文件整体约139KB结构按算法模块分目录组织便于对照阅读与二次修改。目前已有1622人学习下载。读者可借此理解不同聚类策略的适用条件与实现差异直接运行脚本观察分词、停用词过滤与聚类效果并在此基础上完成课设报告或扩展实验是一份轻量且完整的文本聚类实践参考。1. 文本聚类四件套KMeans、DBSCAN、LDA 和 Single_Pass 到底该怎么选做课设最怕的不是写不出代码而是把四种算法硬凑在一个main.py里跑完发现 KMeans 的簇大小均匀得像切蛋糕DBSCAN 把一半样本判成噪声LDA 的主题词全是“的、了、是”Single_Pass 又因为阈值没调好把整篇文档聚成一个类。文本聚类的核心矛盾从来不是“算法够不够多”而是“你的数据形态配不配这个算法”。KMeans 假设簇是球形且大小相近DBSCAN 假设簇密度可达且能容忍噪声LDA 假设文档是主题的混合分布Single_Pass 假设流式到达且单遍扫描。这四种假设对应四类完全不同的文本场景短文本分类、长文档去重、主题发现、增量聚类。课设里把它们放在一起真正要回答的是给定一份中文语料从分词、向量化到聚类评估每一步的参数怎么定、结果怎么读、翻车了怎么救。下面按“先立住理论、再动手复现、最后避坑”的顺序拆开讲适合正在做课设但不想只交一份能跑就行的代码的人。2. 从原始文本到向量空间分词、停用词与 TF-IDF 的工程细节2.1 中文分词与停用词表jieba 的三种模式和自定义词典文本聚类的第一步不是选算法而是把中文句子切成词。jieba 提供精确模式、全模式和搜索引擎模式课设里最常用的是精确模式但遇到专业术语或人名时精确模式会把“文本聚类”切成“文本”和“聚类”导致后续 TF-IDF 丢失关键特征。常见做法是加载自定义词典把领域词提前塞进去。import jieba import jieba.analyse # 加载自定义词典每行格式词语 词频 词性词频和词性可省略 jieba.load_userdict(user_dict.txt) # 加载停用词表一行一个词 with open(stopwords.txt, r, encodingutf-8) as f: stopwords set([line.strip() for line in f]) def tokenize(text): # 精确模式切分过滤停用词和单字 words jieba.lcut(text, cut_allFalse) return [w for w in words if w not in stopwords and len(w) 1] # 测试 text 文本聚类算法在课设中需要处理中文分词和停用词过滤 print(tokenize(text))逻辑说明jieba.lcut返回列表cut_allFalse表示精确模式。过滤单字是因为中文单字区分度低保留会引入噪声。参数说明user_dict.txt的格式是“词语 词频 词性”词频越高越不容易被切开停用词表建议用哈工大停用词表加自定义补充比如“课设”“本文”“如下”这类词在聚类中无区分度。注意不要用jieba.analyse.extract_tags代替分词它返回的是关键词不是完整词序列后续 TF-IDF 会丢词频信息。2.2 TF-IDF 向量化max_features 和 ngram_range 怎么定分词后得到的是词列表必须转成数值向量。TF-IDF 是最稳的基线TfidfVectorizer的几个参数直接决定聚类效果。max_features控制词表大小太大导致维度灾难太小丢失关键特征ngram_range决定是否保留二元词组对短文本有用但对长文档会爆炸。from sklearn.feature_extraction.text import TfidfVectorizer # 假设 corpus 是分词后用空格拼接的字符串列表 corpus [ .join(tokenize(doc)) for doc in raw_docs] vectorizer TfidfVectorizer( max_features3000, # 保留 TF-IDF 最高的 3000 个词 ngram_range(1, 2), # 保留一元和二元词组 min_df2, # 至少出现在 2 篇文档中才保留 max_df0.8, # 出现在超过 80% 文档中的词丢弃 sublinear_tfTrue # 用 1log(tf) 代替 tf抑制高频词 ) X vectorizer.fit_transform(corpus) print(X.shape) # (文档数, 特征数)逻辑说明min_df2过滤只出现一次的词这些词往往是噪声或拼写错误max_df0.8过滤“的”“是”这类几乎每篇都有的词即使停用词表没覆盖也能兜底。sublinear_tfTrue对长文档更友好因为长文档词频天然高取对数后差距缩小。参数说明课设语料通常几百到几千篇max_features设在 2000 到 5000 之间比较稳如果语料是短文本如标题、评论ngram_range(1,2)能提升区分度但特征数会翻倍需要同步调低max_features。注意fit_transform只能在训练集上调用如果做增量聚类新文档要用transform而不是重新fit。2.3 降维与稀疏矩阵TruncatedSVD 还是直接上稠密TF-IDF 矩阵是稀疏的KMeans 和 DBSCAN 都能处理稀疏输入但 LDA 和 Single_Pass 的实现方式不同。LDA 在 sklearn 中要求词频矩阵CountVectorizer不是 TF-IDFSingle_Pass 通常用余弦相似度稀疏矩阵直接算即可。如果特征数超过一万建议用TruncatedSVD降到 100 到 300 维既加速又去噪。from sklearn.decomposition import TruncatedSVD svd TruncatedSVD(n_components200, random_state42) X_reduced svd.fit_transform(X) print(解释方差比:, svd.explained_variance_ratio_.sum())逻辑说明TruncatedSVD不中心化适合稀疏矩阵。explained_variance_ratio_.sum()看保留了多少信息一般 0.3 以上就够聚类用。参数说明n_components设 100 到 300太小丢信息太大降维没意义。注意降维后的向量不再稀疏DBSCAN 在稠密向量上计算距离更慢如果语料超过五千篇优先用稀疏矩阵直接聚类。3. KMeans 与 DBSCAN划分聚类和密度聚类的参数怎么调3.1 KMeans 的 K 值选择肘部法、轮廓系数和业务约束KMeans 最大的坑是 K 值靠猜。课设里常见做法是跑肘部法看 SSE 拐点再结合轮廓系数验证。但文本聚类往往没有明确类别数这时候业务约束比数学指标更重要——比如课设要求聚成 5 类那就直接设 5别纠结。from sklearn.cluster import KMeans from sklearn.metrics import silhouette_score import matplotlib.pyplot as plt sse [] sil_scores [] K_range range(2, 11) for k in K_range: kmeans KMeans(n_clustersk, initk-means, n_init10, random_state42) labels kmeans.fit_predict(X_reduced) sse.append(kmeans.inertia_) sil_scores.append(silhouette_score(X_reduced, labels)) # 画肘部图 plt.plot(K_range, sse, bx-) plt.xlabel(K) plt.ylabel(SSE) plt.show() # 打印轮廓系数 for k, s in zip(K_range, sil_scores): print(fK{k}, silhouette{s:.4f})逻辑说明initk-means比随机初始化更稳n_init10表示跑 10 次取最优避免局部最优。inertia_是簇内平方和肘部法看它下降变缓的点。轮廓系数越接近 1 越好但文本聚类通常只有 0.1 到 0.3不要期望太高。参数说明n_init在 sklearn 1.4 之后默认是 auto显式设 10 更可控。注意KMeans 对异常值敏感如果语料里有几篇超长文档会拉偏质心建议先做长度过滤或截断。3.2 DBSCAN 的 eps 和 min_samples用 k-距离图找拐点DBSCAN 不需要指定簇数但eps和min_samples比 K 值更难调。eps太小全成噪声太大全聚成一类。常见做法是画 k-距离图找曲线拐点作为eps。from sklearn.cluster import DBSCAN from sklearn.neighbors import NearestNeighbors import numpy as np # 用 k-距离图找 eps k 5 # 对应 min_samples nbrs NearestNeighbors(n_neighborsk).fit(X_reduced) distances, indices nbrs.kneighbors(X_reduced) distances np.sort(distances[:, k-1], axis0) plt.plot(distances) plt.xlabel(样本排序) plt.ylabel(f{k}-距离) plt.show() # 根据拐点设 eps比如 0.5 dbscan DBSCAN(eps0.5, min_samples5, metriccosine) labels dbscan.fit_predict(X_reduced) print(簇数:, len(set(labels)) - (1 if -1 in labels else 0)) print(噪声数:, list(labels).count(-1))逻辑说明k-距离图里曲线急剧上升的拐点就是合适的eps。metriccosine对文本向量更合适因为 TF-IDF 向量的欧氏距离受长度影响大。参数说明min_samples一般设 3 到 10语料越大设越大eps在 0.3 到 0.8 之间试具体看 k-距离图。注意DBSCAN 在稀疏高维数据上容易失效如果噪声比例超过 50%要么降维要么换算法。3.3 两种聚类结果的解读簇内关键词和噪声样本聚类跑完不是看标签就完事要能解释每个簇在讲什么。常见做法是取每个簇的质心找 TF-IDF 权重最高的词作为簇关键词。DBSCAN 的噪声样本单独看往往是短文本或混合主题文档。def get_cluster_keywords(vectorizer, kmeans, n_top10): order_centroids kmeans.cluster_centers_.argsort()[:, ::-1] terms vectorizer.get_feature_names_out() for i in range(kmeans.n_clusters): print(f簇 {i}:, [terms[ind] for ind in order_centroids[i, :n_top]]) get_cluster_keywords(vectorizer, kmeans)逻辑说明argsort()[:, ::-1]对质心权重降序排列取前n_top个词。参数说明n_top设 10 到 20太少看不出主题太多混入噪声词。注意如果簇关键词全是“的”“了”说明停用词没过滤干净或max_df设太高。4. LDA 与 Single_Pass主题模型和增量聚类的落地方式4.1 LDA 主题数确定困惑度、一致性和人工判断LDA 需要指定主题数n_topics常用困惑度perplexity和一致性coherence来选。困惑度越低越好但实际中一致性更贴近人工判断。课设里可以跑多个主题数看每个主题的词是否可解释。from sklearn.decomposition import LatentDirichletAllocation from sklearn.feature_extraction.text import CountVectorizer # LDA 需要词频矩阵不是 TF-IDF count_vectorizer CountVectorizer(max_features3000, min_df2, max_df0.8) X_counts count_vectorizer.fit_transform(corpus) lda LatentDirichletAllocation( n_components5, # 主题数 max_iter50, # 最大迭代次数 learning_methodbatch, random_state42 ) lda.fit(X_counts) # 打印每个主题的 top 词 terms count_vectorizer.get_feature_names_out() for topic_idx, topic in enumerate(lda.components_): top_terms [terms[i] for i in topic.argsort()[:-11:-1]] print(f主题 {topic_idx}:, top_terms)逻辑说明learning_methodbatch适合小语料online适合大语料。components_是主题-词分布取每行最大的词作为主题词。参数说明n_components设 3 到 10max_iter设 50 到 100太小不收敛太大过拟合。注意LDA 对短文本效果差如果语料是短评论考虑用Biterm或直接上 BERT 句向量。4.2 Single_Pass 的阈值设定余弦相似度与动态更新Single_Pass 是流式聚类每来一篇文档就和已有簇的质心比相似度超过阈值就加入否则新建簇。阈值是唯一的关键参数通常用余弦相似度阈值设 0.6 到 0.8。import numpy as np from sklearn.metrics.pairwise import cosine_similarity class SinglePass: def __init__(self, threshold0.7): self.threshold threshold self.centroids [] # 每个簇的质心向量 self.cluster_docs [] # 每个簇的文档索引 def fit_predict(self, X): labels [] for i in range(X.shape[0]): vec X[i] if not self.centroids: self.centroids.append(vec) self.cluster_docs.append([i]) labels.append(0) continue sims [cosine_similarity(vec, c)[0][0] for c in self.centroids] max_sim max(sims) max_idx sims.index(max_sim) if max_sim self.threshold: labels.append(max_idx) self.cluster_docs[max_idx].append(i) # 更新质心取簇内所有文档的均值 cluster_matrix X[self.cluster_docs[max_idx]] self.centroids[max_idx] np.asarray(cluster_matrix.mean(axis0)).flatten() else: self.centroids.append(vec) self.cluster_docs.append([i]) labels.append(len(self.centroids) - 1) return labels sp SinglePass(threshold0.7) labels sp.fit_predict(X_reduced) print(簇数:, len(set(labels)))逻辑说明每次加入新文档后更新质心保证簇的表示随数据动态调整。cosine_similarity返回矩阵取[0][0]得标量。参数说明threshold越高簇越纯但数量越多越低越容易合并。注意Single_Pass 对输入顺序敏感同一批文档打乱顺序可能得到不同簇数课设里可以固定随机种子或多次跑取平均。4.3 四种算法的评估轮廓系数、Calinski-Harabasz 和人工抽查聚类没有标签评估只能靠内部指标加人工。轮廓系数适合 KMeans 和 DBSCANCalinski-Harabasz 适合比较不同算法的簇分离度。LDA 用困惑度Single_Pass 看簇大小分布。from sklearn.metrics import calinski_harabasz_score # 假设 labels_kmeans 和 labels_dbscan 已经得到 print(KMeans CH:, calinski_harabasz_score(X_reduced, labels_kmeans)) print(DBSCAN CH:, calinski_harabasz_score(X_reduced, labels_dbscan)) # 人工抽查每个簇随机抽 3 篇文档标题 import random for cluster_id in set(labels_kmeans): idxs [i for i, l in enumerate(labels_kmeans) if l cluster_id] samples random.sample(idxs, min(3, len(idxs))) print(f簇 {cluster_id}:, [raw_docs[i][:30] for i in samples])逻辑说明CH 值越大簇间越分离但不同算法之间可比性有限主要看同一算法不同参数的对比。人工抽查是最终标准如果簇内文档主题明显不一致指标再高也没用。参数说明random.sample的k不能超过列表长度用min兜底。注意DBSCAN 的噪声标签 -1 不参与指标计算要先过滤。5. 避坑与排查文本聚类课设里最容易翻车的 5 个点5.1 现象所有文档聚成一类轮廓系数接近 0原因TF-IDF 的max_df设太高或停用词没过滤导致所有文档向量几乎一样或者 KMeans 的 K 设成 1。解决检查max_df是否超过 0.9停用词表是否加载成功打印X.shape看特征数是否过少。如果特征数少于 100说明分词或过滤太狠。5.2 现象DBSCAN 噪声比例超过 80%原因eps太小或min_samples太大导致大部分点密度不可达。解决重画 k-距离图把eps调到拐点附近如果降维后的向量维度太高先降到 50 维再跑 DBSCAN。另外TF-IDF 向量用欧氏距离会受长度影响改用余弦距离。5.3 现象LDA 主题词全是“的、了、是”原因用了 TF-IDF 矩阵而不是词频矩阵或者max_df没设停用词没过滤。解决LDA 必须用CountVectorizer并且设置max_df0.8和stop_words。如果还是不行检查分词结果里是否混入了标点符号。5.4 现象Single_Pass 簇数随输入顺序剧烈变化原因阈值设得太接近相似度分布的中位数导致边界文档摇摆。解决把阈值调到 0.75 以上或者对输入文档按长度排序后再跑。另一种做法是跑多次取簇数的众数但课设里直接固定顺序更简单。5.5 现象KMeans 跑得特别慢内存爆掉原因TF-IDF 特征数过万且没降维KMeans 在稀疏矩阵上迭代计算量大。解决用TruncatedSVD降到 200 维或者把max_features降到 2000。如果语料超过一万篇考虑用MiniBatchKMeans它每次只用一个批次更新质心速度快很多但精度略降。6. 进阶技巧用轮廓系数热力图快速锁定 KMeans 和 DBSCAN 的最优参数课设答辩时老师最爱问“你怎么证明参数选得对”。与其口头解释不如画一张参数热力图把 KMeans 的 K 值和 DBSCAN 的 eps 同时扫一遍用轮廓系数着色一眼看出稳定区域。下面这段代码可以直接抄跑完保存成图片贴进报告。import numpy as np import matplotlib.pyplot as plt from sklearn.cluster import KMeans, DBSCAN from sklearn.metrics import silhouette_score # 假设 X_reduced 是降维后的向量 k_range range(2, 11) eps_range np.arange(0.3, 0.9, 0.1) kmeans_scores [] dbscan_scores [] for k in k_range: km KMeans(n_clustersk, initk-means, n_init10, random_state42) labels km.fit_predict(X_reduced) kmeans_scores.append(silhouette_score(X_reduced, labels)) for eps in eps_range: db DBSCAN(epseps, min_samples5, metriccosine) labels db.fit_predict(X_reduced) # 过滤噪声后计算轮廓系数 mask labels ! -1 if len(set(labels[mask])) 1: dbscan_scores.append(silhouette_score(X_reduced[mask], labels[mask])) else: dbscan_scores.append(-1) # 画图 fig, axes plt.subplots(1, 2, figsize(12, 4)) axes[0].plot(list(k_range), kmeans_scores, bo-) axes[0].set_xlabel(K) axes[0].set_ylabel(轮廓系数) axes[0].set_title(KMeans 参数扫描) axes[1].plot(eps_range, dbscan_scores, ro-) axes[1].set_xlabel(eps) axes[1].set_ylabel(轮廓系数) axes[1].set_title(DBSCAN 参数扫描) plt.tight_layout() plt.savefig(cluster_param_scan.png, dpi150) plt.show()逻辑说明KMeans 的轮廓系数通常在 K3 到 6 之间出现峰值选峰值对应的 K。DBSCAN 的轮廓系数在过滤噪声后计算如果所有 eps 都低于 0.1说明数据本身不适合密度聚类换 KMeans 或 LDA。参数说明eps_range从 0.3 到 0.9 步长 0.1实际跑的时候可以缩小到 0.05 步长。min_samples固定 5如果语料小于 500 篇可以设 3。注意DBSCAN 的轮廓系数在噪声比例高时会虚高因为噪声被排除了所以还要看簇数和噪声数。我自己的习惯是先跑参数扫描图把 KMeans 的 K 和 DBSCAN 的 eps 定下来再跑 LDA 看主题词最后用 Single_Pass 模拟流式场景。课设报告里不要只贴最终结果把参数扫描图放上去老师一看就知道你不是瞎调的。另外所有随机种子固定成 42不然每次跑结果不一样答辩现场翻车就尴尬了。希望帮到你。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。