
简介这份资源面向机器学习初学者与高校课程设计场景提供一套完整的中文文本聚类算法实现方案帮助读者理解无监督学习在文本数据上的落地方式。包内包含基于KMeans、DBSCAN、LDA以及Single Pass策略的四种聚类程序其中Single Pass无需预先设定类别数适合处理流式或类别未知的文本集合。资源共35个文件以py脚本、sample样例、head与master等Git版本控制文件、txt数据与停用词文件为主另有config、md说明及license等辅助内容压缩包约139KB结构紧凑便于直接运行与二次修改。目前已有1622人学习下载说明其在课设与自学群体中具有一定参考价值。读者可借此对比不同聚类算法在中文文本上的效果差异掌握从数据读取、停用词处理到聚类输出的完整流程并基于现有脚本快速完成课程设计或实验报告中的算法验证环节。1. 文本聚类四件套KMeans、DBSCAN、LDA 和 Single_Pass 到底该怎么选做课设最怕的不是写不出代码而是拿到题目不知道从哪下手。文本聚类这个方向标题里一口气塞了 KMeans、DBSCAN、LDA、Single_Pass 四个算法很多人第一反应是「四个都写一遍交差」。但真正做过的人知道这四个东西压根不在一个层面上KMeans 和 DBSCAN 是通用聚类器LDA 是主题模型Single_Pass 是流式增量聚类。它们处理的数据形态、对参数的要求、输出结果的解释方式完全不同。这篇笔记就按一线做课设的思路把 Python 环境下这四个算法的落地路径拆开讲清楚——每个算法怎么跑通、参数怎么调、结果怎么验证、四个凑在一起怎么组织成一份能过答辩的程序。适合正在做文本挖掘课设、需要一套可复现代码框架的人也适合想把聚类真正用到自己数据上的人。2. 数据准备与向量化聚类之前必须做对的三件事2.1 中文文本清洗与 jieba 分词的最小流程不管后面用哪个算法输入都是「文档 → 向量」这条链路。中文文本第一步是清洗去掉 HTML 标签、URL、多余空白和标点。第二步是分词Python 里最常用的还是 jieba。第三步是去停用词。这三步任何一步偷懒后面聚类结果都会变成玄学——明明参数一样换一批数据就完全不可用。import re import jieba def clean_text(text): # 去掉 HTML 标签和 URL text re.sub(r[^], , text) text re.sub(rhttps?://\S, , text) # 只保留中文、英文、数字 text re.sub(r[^\u4e00-\u9fa5a-zA-Z0-9], , text) return text.strip() def tokenize(text, stopwords): text clean_text(text) words jieba.lcut(text) # 过滤停用词和单字 return [w for w in words if w not in stopwords and len(w) 1] # 加载停用词表一行一个词 with open(stopwords.txt, encodingutf-8) as f: stopwords set(line.strip() for line in f) docs [你的第一条文本..., 第二条...] corpus [tokenize(d, stopwords) for d in docs]clean_text里那条正则[^\u4e00-\u9fa5a-zA-Z0-9]是关键它把中文区间之外的所有符号统一替换成空格避免标点粘连影响分词。len(w) 1过滤单字是因为中文单字噪声极大「的」「了」即使不在停用词表里也会干扰。停用词表建议用哈工大或百度那份通用表再根据自己数据补几个领域词。2.2 用 TF-IDF 和 CountVectorizer 把文本变成矩阵分词完得到的是词列表聚类算法要的是数值矩阵。两条路词频矩阵CountVectorizer和 TF-IDF 矩阵。做 KMeans 和 DBSCAN 一般用 TF-IDF因为它压制了高频通用词的权重做 LDA 一般用词频矩阵因为 LDA 的生成过程本身假设的是词频计数。from sklearn.feature_extraction.text import TfidfVectorizer, CountVectorizer # 把分词结果拼回字符串sklearn 的向量化器接受空格分隔的字符串 texts [ .join(tokens) for tokens in corpus] tfidf TfidfVectorizer(max_features2000, min_df2, max_df0.8) X_tfidf tfidf.fit_transform(texts) count_vec CountVectorizer(max_features2000, min_df2, max_df0.8) X_count count_vec.fit_transform(texts)max_features2000控制词表规模课设数据量不大时够用min_df2表示至少出现在两篇文档里的词才保留过滤掉只出现一次的噪声词max_df0.8表示出现在超过 80% 文档里的词丢掉这类词基本没有区分度。这三个参数是文本聚类里最值得先调的一组很多人结果差就是没设min_df和max_df。2.3 降维与稀疏矩阵什么时候该用 TruncatedSVDTF-IDF 矩阵通常是几千维的稀疏矩阵。KMeans 和 DBSCAN 在高维稀疏空间里距离度量会失效——这就是所谓的维度灾难。常见做法是先做 TruncatedSVD本质是 LSA降到 100 到 300 维再送进聚类器。from sklearn.decomposition import TruncatedSVD svd TruncatedSVD(n_components100, random_state42) X_reduced svd.fit_transform(X_tfidf) # 看解释方差比判断保留了多少信息 print(svd.explained_variance_ratio_.sum())n_components100是经验起点如果解释方差比总和低于 0.3说明维度压得太狠往上加如果接近 0.9 还很快说明可以再压。注意 DBSCAN 对维度尤其敏感降维几乎是必须的KMeans 在原始 TF-IDF 上也能跑但降维后通常更快更稳。3. KMeans 与 DBSCAN两种聚类范式的参数怎么调3.1 KMeans 的 K 值选择肘部法和轮廓系数一起看KMeans 最大的坑就是 K 值要人定。课设里最常被问的就是「你怎么确定聚成几类」。标准做法是肘部法inertia 随 K 下降的拐点配合轮廓系数silhouette score两个指标一起看。from sklearn.cluster import KMeans from sklearn.metrics import silhouette_score import matplotlib.pyplot as plt inertias, sil_scores [], [] K_range range(2, 11) for k in K_range: km KMeans(n_clustersk, n_init10, random_state42) labels km.fit_predict(X_reduced) inertias.append(km.inertia_) sil_scores.append(silhouette_score(X_reduced, labels)) # 画双轴图找拐点和高点 fig, ax1 plt.subplots() ax1.plot(list(K_range), inertias, b-o) ax1.set_xlabel(K) ax1.set_ylabel(Inertia, colorb) ax2 ax1.twinx() ax2.plot(list(K_range), sil_scores, r-s) ax2.set_ylabel(Silhouette, colorr) plt.show()n_init10表示用 10 组不同初始质心各跑一次取最优sklearn 新版本默认已经是auto但显式写 10 更保险能避免陷入局部最优。random_state42保证可复现答辩时别人跑你的代码结果一致。判断逻辑inertia 曲线拐点对应的 K和轮廓系数最高的 K如果一致就直接用不一致时优先看轮廓系数因为它衡量的是簇内紧密度和簇间分离度的综合。3.2 DBSCAN 的 eps 和 min_samplesk-距离图定 epsDBSCAN 不需要指定簇数但eps邻域半径和min_samples核心点的最小邻居数这两个参数比 KMeans 的 K 还难调。eps太小全是噪声太大所有点挤成一簇。工程上最靠谱的办法是画 k-距离图。from sklearn.cluster import DBSCAN from sklearn.neighbors import NearestNeighbors import numpy as np # 用第 min_samples 近邻的距离排序画曲线 min_samples 5 nn NearestNeighbors(n_neighborsmin_samples).fit(X_reduced) distances, _ nn.kneighbors(X_reduced) distances np.sort(distances[:, -1]) plt.plot(distances) plt.xlabel(Points sorted by distance) plt.ylabel(f{min_samples}-th nearest neighbor distance) plt.show() # 曲线拐点肘部对应的距离就是 eps 的候选值 db DBSCAN(eps0.5, min_samplesmin_samples, metriceuclidean) labels_db db.fit_predict(X_reduced) n_clusters len(set(labels_db)) - (1 if -1 in labels_db else 0) n_noise list(labels_db).count(-1) print(f簇数: {n_clusters}, 噪声点: {n_noise})k-距离图的读法把每个点到它第min_samples近邻的距离从小到大排序画出来曲线急剧上升的那个拐点就是eps的合理值。min_samples一般取特征维度加一或者从 4 到 10 试。metriceuclidean在降维后的稠密向量上没问题如果直接在稀疏 TF-IDF 上跑要换成cosine。DBSCAN 输出里-1是噪声标签统计簇数时必须把它排除这是新手最容易算错的地方。3.3 两个算法在同一份数据上的结果对比同一份文本KMeans 和 DBSCAN 给出的结构往往不一样。KMeans 强行把所有点分到 K 个簇适合簇大小相近、形状偏球的数据DBSCAN 能发现任意形状的簇还能标出噪声适合簇密度不均、有离群文档的场景。课设里把两者结果并排展示再各挑几个簇的关键词做人工解读比只跑一个算法有说服力得多。对比项KMeansDBSCAN是否需要预设簇数需要K 值不需要对噪声的处理无所有点都归类有标为 -1簇形状假设近似球形任意形状主要参数n_clusterseps, min_samples高维稀疏数据降维后可用必须降维结果稳定性受初始质心影响参数定好后确定4. LDA 与 Single_Pass主题建模和流式聚类的落地4.1 LDA 主题模型用困惑度和主题词做双重验证LDA 严格说不是聚类算法是主题模型。它假设每篇文档是多个主题的混合每个主题是词上的分布。做课设时经常把它和聚类混着用先 LDA 得到文档-主题分布再对这个分布做 KMeans或者直接用每篇文档概率最高的主题当簇标签。from sklearn.decomposition import LatentDirichletAllocation lda LatentDirichletAllocation( n_components5, # 主题数 max_iter50, # 最大迭代 learning_methodbatch, random_state42 ) doc_topic lda.fit_transform(X_count) # 文档-主题分布 # 打印每个主题的 top 词 feature_names count_vec.get_feature_names_out() for topic_idx, topic in enumerate(lda.components_): top_words [feature_names[i] for i in topic.argsort()[:-11:-1]] print(f主题 {topic_idx}: { .join(top_words)})n_components5是主题数和 KMeans 的 K 一样需要试。learning_methodbatch适合小数据全量训练数据大时换online。max_iter50一般够收敛配合perp_tol控制困惑度变化阈值。验证 LDA 质量看两个东西一是困惑度perplexity越低越好二是每个主题的 top 词是否语义连贯——后者更重要因为困惑度低不代表主题可解释。argsort()[:-11:-1]这个切片是取概率最高的 10 个词注意是倒序切片。4.2 Single_Pass 增量聚类新文档来了怎么归堆Single_Pass 是流式场景的算法文档一条条来每条和已有簇的质心比相似度超过阈值就归入该簇并更新质心否则新建一个簇。它不需要预设簇数也不像 DBSCAN 那样要一次性看到全部数据适合新闻流、日志流这类增量场景。import numpy as np from sklearn.metrics.pairwise import cosine_similarity def single_pass(vectors, threshold0.6): clusters [] # 每个簇的质心 labels [] # 每篇文档的簇标签 for vec in vectors: vec vec.reshape(1, -1) if not clusters: clusters.append(vec) labels.append(0) continue # 和所有已有质心算余弦相似度 sims [cosine_similarity(vec, c)[0][0] for c in clusters] best int(np.argmax(sims)) if sims[best] threshold: labels.append(best) # 更新质心新质心 旧质心和当前向量的平均 clusters[best] (clusters[best] vec) / 2 else: clusters.append(vec) labels.append(len(clusters) - 1) return labels, len(clusters) labels_sp, n_sp single_pass(X_reduced, threshold0.6) print(fSingle_Pass 得到 {n_sp} 个簇)threshold0.6是相似度阈值这是 Single_Pass 唯一的核心参数。阈值高簇多而纯阈值低簇少而杂。余弦相似度适合文本向量因为关注方向而非长度。质心更新用简单平均是常见简化严格做法是按簇内文档数做加权平均。Single_Pass 的结果对文档输入顺序敏感这是它的固有特性课设里可以打乱顺序跑几次看簇数波动范围。4.3 四个算法的输出怎么统一成一份可对比的结果四个算法输出格式不一样KMeans 和 DBSCAN 直接给标签LDA 给的是概率分布Single_Pass 给的是自定义标签。要对比就得统一。常见做法是把 LDA 的文档-主题分布取 argmax 当硬标签把 Single_Pass 的标签对齐成整数序列然后统一用轮廓系数、Calinski-Harabasz 指数这些外部无法用因为没真标签但内部可用的指标来横向比。from sklearn.metrics import calinski_harabasz_score results { KMeans: labels_km, DBSCAN: labels_db, LDA-argmax: doc_topic.argmax(axis1), Single_Pass: labels_sp, } for name, labels in results.items(): # DBSCAN 的噪声点要剔除后再算指标 mask np.array(labels) ! -1 if len(set(np.array(labels)[mask])) 1: score calinski_harabasz_score(X_reduced[mask], np.array(labels)[mask]) print(f{name}: CH指数 {score:.2f})Calinski-Harabasz 指数越大表示簇间越分散、簇内越紧凑。注意 DBSCAN 的-1噪声点必须先剔除否则会被当成一个独立簇指标完全失真。LDA 的 argmax 硬标签会丢失主题混合信息这是简化处理的代价课设里说明清楚即可。5. 避坑与排查文本聚类课设里最容易翻车的五个地方5.1 现象所有文档被分到同一个簇原因通常是向量化参数没设好max_df太高导致词表里全是通用词或者min_df太低保留了太多只出现一次的词向量区分度不够。另一个可能是 KMeans 的 K 设成了 1或者 DBSCAN 的eps太大把所有点连成一片。解决先检查min_df是否至少为 2max_df是否在 0.7 到 0.9 之间DBSCAN 重新画 k-距离图确认epsKMeans 确认 K 大于 1。5.2 现象DBSCAN 跑出来全是噪声点eps太小或min_samples太大。在降维后的向量上点间距离的尺度和你想象的不一样凭感觉设eps0.5经常翻车。解决老老实实画 k-距离图取拐点值min_samples从 4 开始试别一上来就设 20。如果数据本身密度极不均匀DBSCAN 可能确实不适合换 HDBSCAN 或退回 KMeans。5.3 现象LDA 主题词全是「的」「了」「是」停用词没去干净或者用了 CountVectorizer 但没设max_df。LDA 对高频通用词特别敏感因为它建模的是词频。解决停用词表要覆盖常见虚词max_df设到 0.5 甚至更低把出现在一半以上文档里的词全踢掉。另外max_features别设太大2000 到 5000 对课设数据量足够。5.4 现象Single_Pass 每次跑簇数都不一样这是算法固有特性不是 bug。Single_Pass 对输入顺序敏感顺序变了质心更新路径就变了。解决如果课设要求结果可复现固定输入顺序并设随机种子如果要做稳定性分析打乱顺序跑 10 次报告簇数的均值和标准差这反而是个加分项。5.5 现象轮廓系数算出来报错或为负报错通常是标签里只有一个簇silhouette_score要求至少两个簇。为负说明样本被分到了比最近邻簇更远的簇聚类结构很差。解决先确认簇数大于 1为负时回头检查向量化参数和降维维度别急着换算法。轮廓系数在稀疏高维数据上本身就不稳定降维后再算会靠谱很多。6. 把四个算法串成一个可演示的课设程序课设最终要交的是一个能跑、能演示、能讲清楚的东西。我的习惯是写一个主入口脚本用命令行参数控制跑哪个算法把数据加载、向量化、聚类、评估、可视化串成流水线。这样答辩时现场切换算法演示比四个独立脚本体面得多。import argparse def main(): parser argparse.ArgumentParser() parser.add_argument(--algo, choices[kmeans, dbscan, lda, single_pass], defaultkmeans) parser.add_argument(--data, defaultdata.txt) parser.add_argument(--k, typeint, default5) parser.add_argument(--eps, typefloat, default0.5) parser.add_argument(--threshold, typefloat, default0.6) args parser.parse_args() # 1. 加载并预处理 corpus load_and_tokenize(args.data) X_tfidf, X_count vectorize(corpus) X_reduced reduce_dim(X_tfidf) # 2. 按参数分发 if args.algo kmeans: labels run_kmeans(X_reduced, args.k) elif args.algo dbscan: labels run_dbscan(X_reduced, args.eps) elif args.algo lda: labels run_lda(X_count) else: labels, _ single_pass(X_reduced, args.threshold) # 3. 评估并输出每个簇的关键词 evaluate(X_reduced, labels) print_cluster_keywords(corpus, labels, top_n10) if __name__ __main__: main()这个骨架的价值在于把「算法」和「流程」解耦了预处理和向量化只写一遍四个算法各自一个函数评估和关键词输出复用。print_cluster_keywords是我强烈建议加的一步——对每个簇统计词频取 top 10人工看一眼就知道聚类有没有意义比任何指标都直观。答辩时评委问「你怎么知道聚得对」把每个簇的关键词一亮比解释轮廓系数管用。再补一个可视化技巧降维到 2 维用 t-SNE 或 PCA 画散点图按标签上色。t-SNE 的perplexity参数对图的样子影响很大课设数据量小的时候设 5 到 30 之间多试几个值挑分得最开的。注意 t-SNE 图只能看相对结构簇间距离没有绝对意义别在图上量距离。最后说个血泪经验课设代码一定要在干净环境里从零跑一遍。我见过太多人本地跑得好好的换台机器就报ModuleNotFoundError或者 jieba 版本不同分词结果有细微差异导致聚类结果对不上。把依赖写进requirements.txt把随机种子固定死把停用词表和数据一起打包。这些琐碎的事才是让课设从「能跑」变成「能交」的关键。希望帮到你。本文还有配套的精品资源点击获取