
最近做了一阵子微信公众号文章分析的工作其中一个比较有意思的小项目是把手头积累的一大批微信文章用 K-均值聚类算法自动分了类。先说一下背景我一直在做新媒体内容方向的数据分析经常需要从几十上百篇公众号文章里快速判断大家都在写什么主题、哪些方向的内容比较集中光靠肉眼一篇篇看标题和摘要实在太低效了。于是就用 K-均值聚类做了个自动分组的工具效果比我预期要稳这篇文章就把完整思路、流程和踩过的坑梳理一遍。这个项目适合谁参考如果你是做内容运营、新媒体数据分析或者刚接触自然语言处理和聚类算法的开发者这篇东西应该能给你省不少试错时间。核心就是把“微信文章”变成“可计算的向量”再通过“K-均值聚类”让相似的文本自动归堆最后从每个簇里提炼出主题关键词实现批量文本的自动化分类和理解。1. 数据采集与预处理微信文章文本清洗的细节1.1 语料来源与采集限制先说数据来源。微信公众号文章没有一个公开的官方API让你一次性拉取全量文章实际操作中通常有几个渠道公司内部的内容库、第三方数据服务平台的导出文件、公众号后台的素材管理列表或者是自己运营的账号后台数据。我这次用的是某个第三方平台导出的历史文章列表包含标题、摘要、发布时间、正文全文共约 1800 篇文章涉及几十个不同类型的公众号。采集这块有个容易忽视的问题第三方导出的字段五花八门有的带HTML标签有的把图片的alt文字也塞进正文字段还有的正文里混着“阅读原文”跳转链接和一堆小程序卡片信息。所以第一步不是急着写聚类代码而是先把文本清洗干净否则后面算出来的相似度会被这些噪声干扰得很严重。1.2 文本清洗的标准流程清洗过程我按以下顺序处理去掉HTML标签和Markdown符号用正则匹配!--.*?--或[^]这类模式直接替换成空格把全文统一转成英文标点、半角符号避免全角括号和半角括号混在一起影响分词效果去掉“点击上方蓝字关注”“声明本文仅代表作者观点”“广告”这类高频非内容文本这些词在每篇文章里都出现会让聚类结果明显偏向这些噪声词过滤掉URL、邮箱地址、电话号码、纯数字编号因为这些对主题聚类没有帮助提取标题摘要作为聚类分析的输入字段正文太长太冗余标题和摘要是作者对文章主题最浓缩的表达聚类效果反而更好。清洗这一步听起来简单但做完之后你会发现文本长度分布发生了明显变化有些原来 5000 字的正文清洗后剩下 3000 多字因为里面有大量模板信息。我在实际项目里还加了一步把清洗后的文本按长度过滤低于 50 个字符的样本直接丢弃这些多半是纯转发或者只有一张图片的“伪文章”留着会干扰聚类中心的计算。2. 文本向量化方案TF-IDF 与分词细节2.1 分词工具的选择与处理K-均值聚类做不了原始文本必须先把文本转成数值向量。中文文本和英文不同词与词之间没有天然空格必须先分词。我试过几种方式最终用的是jieba分词配合自定义词典和停用词表。分词这一步影响极大比后面选什么聚类算法都重要。举个例子“机器学习工程师”这个短语如果默认分词得到“机器”“学习”“工程师”那么和“深度学习平台”里的“学习”就会产生语义混淆。我在自定义词典里加入了互联网行业和自媒体领域的专有名词比如“公众号”“短视频”“私域流量“”用户增长”等让它们作为整体被切分。自定义词典的格式很简单每行一个词可以带词频和词性。停用词表也很关键。我用的是网上开源的通用中文停用词表再人工补充了一批和公众号文章强相关的噪声词包括“点击”“关注”“领取”“二维码”“文章”“作者”“编辑”“阅读原文”等。如果停用词表不够完整聚类结果里会出现一个簇全是“点击上方蓝字”这就没什么意义了。2.2 TF-IDF 向量化的参数选择分完词之后进入向量化。文本聚类的常用向量化方式有 TF-IDF、Word2Vec、BERT 句向量等。我用 TF-IDF 作为主干方案原因有二一是 interpretability 好每个维度对应的是一个具体的词聚类后可以根据维度权重反推出每个簇的主题词二是计算成本低1800 篇文档的 TF-IDF 矩阵在普通笔记本上秒级就能跑完。具体参数上TfidfVectorizer里我设置了max_features5000只保留词频最高的 5000 个词作为特征维度。这个参数很值得讨论——如果太大矩阵会稀疏到聚类中心难以稳定如果太小又会丢失低频但高区分度的词。我做了一组实验对比特征数从 2000 调到 8000轮廓系数差别不大但 5000 的时候各簇的区分度人眼看起来最舒服因此最终选了 5000。还开了sublinear_tfTrue这是把词频做1log(tf)变换避免某个词在一篇文章里出现次数过多导致权重虚高。在公众号文章里“我们”“自己”这类词出现几百次很常见如果不做这个变换这些词的权重会压过真正有主题意义的词。min_df2表示至少在 2 篇文章中出现过的词才保留max_df0.8表示在超过 80% 的文章中都出现的词视为噪声过滤掉。3. 聚类可行性判断与降维处理先别急着跑 K-Means3.1 为什么先做 PCA 和 t-SNE 可视化很多初学者拿到 TF-IDF 矩阵后直接KMeans(n_clusters5)一把梭做完也不知道效果怎么样。我的做法是先用 PCA 降到 2 维或者 3 维再用 t-SNE 降到 2 维做可视化看看数据大概长什么样、有没有明显的簇状分布。这一步不是可有可无它能帮你判断“这批数据到底适不适合做聚类”。如果 t-SNE 图上所有点混成一团没有任何分离趋势那说明这批文章的文本相似度普遍偏高或者特征选择有问题直接做 K-Means 得到的结果大概率是人为硬切的簇间语义差异很小。如果图上能隐约看到几坨点群哪怕边界模糊说明文本之间确实存在主题差异聚类的可行性就有了保证。3.2 降维的两种方式对比我在项目里把 PCA 和 t-SNE 都试了一遍。PCA 是线性降维计算快适合先看全局方差分布t-SNE 是非线性降维能更好地展示局部结构但计算慢、随机性较大同一份数据多次运行结果会略有不同。实际操作中我先看 PCA 的前两个主成分大概能解释 15%~20% 的方差就不错了这个数值不要指望太高因为文本数据的有效信息分布在几千个维度里。t-SNE 的参数里perplexity我设为 30这是默认值对 1800 个样本来说比较合适。learning_rate200也是经验值。可视化用的是matplotlib不同的真实类别用不同颜色标出来这里我拿公众号的原始分类做参考标签从图上能直观看到哪些类别之间距离近、哪些类别重叠严重。4. K 值的确定手肘法结合轮廓系数的实操4.1 手肘法的具体计算K-均值聚类最大的超参数就是 K也就是最终分成几类。K 选少了内容差异很大的文章会被硬塞进同一个簇K 选多了明明主题一致的文章被拆散成多个碎片簇。我用两种方法交叉验证。第一种是手肘法。核心逻辑是对比不同 K 值下的簇内误差平方和 SSE也叫 inertia。K 从 2 取到 12每个 K 值下跑 3 次取平均 SSE然后画出折线图。理论上 SSE 会随着 K 增大而持续下降但下降速度会在某个点变缓这个拐点就是合理的 K。我这次跑完发现 K6 的时候折线出现明显的拐点K 从 5 增加到 6 时 SSE 降幅很大从 6 增加到 7 时降幅明显缩小所以初步把 K 定在 6。4.2 轮廓系数验证与最终确定第二种是轮廓系数Silhouette Coefficient。每个样本的轮廓系数综合了“自身与同簇其他样本的平均距离”和“自身与最近的其他簇样本的平均距离”取值范围在 -1 到 1 之间越接近 1 说明聚类效果越好越接近 -1 说明样本被分错了簇。把所有样本的轮廓系数取平均得到整体聚类质量的评价指标。我在 K6 时计算平均轮廓系数约为 0.23说实话这个数值不算高——文本聚类的轮廓系数普遍低于数值型数据的聚类能到 0.2 以上就算可以接受。我对比了 K5 和 K7 的结果K5 的轮廓系数约 0.21K7 约 0.20和 K6 相比都没有显著优势。结合手肘法的拐点位置最终确定 K6既保留了类别间的区分度又不至于过碎。另外我还做了一步辅助验证把 K6 的聚类结果按公众号原始分类做交叉表看看每个簇里主要集中了哪些公众号类别。这一步是为了确认聚类结果和业务认知基本一致如果聚类结果和原始分类完全不搭边那说明特征工程或者 K 的选择有问题。我这边交叉表的结果显示来自科技类公众号的文章大量集中在簇 0 和簇 3情感类公众号的文章集中在簇 2生活类集中在簇 4说明聚类结果和人工分类高度吻合。5. K-Means 聚类的完整实现与关键参数调试5.1 核心代码实现K-均值聚类的实现我直接用sklearn.cluster.KMeans代码量不大核心逻辑如下from sklearn.cluster import KMeans from sklearn.feature_extraction.text import TfidfVectorizer import jieba import re # 假设 texts 是清洗后的标题摘要列表 def chinese_tokenizer(text): return [w for w in jieba.cut(text) if len(w.strip()) 1] vectorizer TfidfVectorizer( tokenizerchinese_tokenizer, max_features5000, max_df0.8, min_df2, sublinear_tfTrue, stop_wordsstopwords ) tfidf_matrix vectorizer.fit_transform(texts) km KMeans( n_clusters6, initk-means, n_init10, max_iter300, random_state42 ) cluster_labels km.fit_predict(tfidf_matrix)这里有几个参数值得展开聊聊。initk-means是用一种更聪明的中心点初始化方式避免随机初始化导致聚类结果特别差n_init10表示每次用不同随机种子跑 10 遍取最优结果max_iter300是单次迭代的最大次数。K-Means 本身是个启发式算法对初始值敏感如果不设置random_state多次运行结果可能差异很大。我在项目里固定random_state42确保每次跑出来的簇编号一致方便复现和调参。5.2 簇内主题词提取与簇的解读聚类跑完之后最难的一步不是看每个文章被分到了哪个簇而是理解“这个簇到底代表了什么主题”。我的做法是提取每个簇的高权重词即每个簇中心向量上绝对值最大的前 10-15 个词。由于中心向量的维度对应的就是词汇表中的词直接按权重排序就能得到主题词。import numpy as np feature_names vectorizer.get_feature_names_out() order_centroids km.cluster_centers_.argsort()[:, ::-1] for i in range(6): print(f簇 {i} 关键词:, [feature_names[idx] for idx in order_centroids[i, :10]])实际运行后6 个簇的主题词分别呈现出明显差异簇 0 集中在“科技、企业、市场、数据、产品”簇 1 集中在“健康、饮食、营养、身体”簇 2 集中在“情感、关系、心理、生活”簇 3 集中在“财经、投资、行业、分析”簇 4 集中在“育儿、教育、家长、孩子”簇 5 则相对混合主要是“娱乐、影视、明星”。根据这些关键词我给每个簇打了主题标签后续做内容分析就方便得多。6. 聚类效果的评估除了损失函数还要看业务可解释性6.1 内部评价指标的多维度对比很多人跑完 K-Means 只盯着 inertia 下降了多少但 inertia 对 K 值几乎是单调递减的单独看意义不大。我习惯同时看三个指标inertia、轮廓系数、簇内平均距离。这三个指标放在一个表格里对比才能比较全面地了解聚类质量。K 值inertia轮廓系数簇内平均距离备注4456.20.192.34簇内太混主题不清5398.70.212.16财经和科技新闻部分混杂6341.50.231.98主题边界相对清晰7317.80.201.86情感类被拆成两个簇8296.30.181.75碎片簇增多从表里能看出K6 在轮廓系数上最优虽然簇内平均距离不是最小的但业务解释性最好。这告诉我们一个道理聚类效果不能只看某一个指标还要结合业务侧的可解释性综合判断K 不是越大越好。6.2 外部评价的标签交叉验证如果手里有微信公众号自带的分类信息可以做交叉验证。我通过手工抽查每个簇里文章标题计算了一个简单准确率——随机从每个簇里抽 20 篇文章人工看标题判断和簇主题是否吻合。结果是簇 1 和簇 4 的准确率最高接近 90%簇 5 偏低只有 70% 左右这和簇 5 本身内容分散有关。我不建议在不理解业务背景的情况下盲目追求某个量化指标因为聚类毕竟是无监督学习最终目的是帮助人做判断而不是替代人做判断。7. 常见问题与排查整理7.1 零散词干扰严重第一次跑聚类结果出来几乎每个簇里都有“我们”“自己”“可以”“进行”“一个”这些词排在权重前列这是因为泛化词在文本中出现太频繁TF-IDF 的 IDF 部分没能完全压住它们。解决方法是扩充停用词表把常见的虚词、代词、副词全部列入。另一个更激进的做法是只用 TF-IDF 大于某个阈值的词作为特征但我个人觉得先加停用词更可控。7.2 聚类结果每次运行都不一样这就是 K-Means 的随机初始化问题。文本数据维度高、分布复杂不同的初始中心点可能收敛到不同的局部最优解。解决方案就是我前面提到的固定random_state同时用n_init10多次运行取最优。如果业务上允许建议多跑几次观察哪些文章在多次结果里被稳定分配到同一个簇哪些文章经常“跳簇”后者往往是边界模糊的样本文后续可以单独处理。7.3 聚类簇数不好解释有时候指标上 K5 最好但业务上觉得分成 6 类更方便运营决策这很正常。聚类分析不是纯数学游戏最终是要服务于业务理解的。我的建议是把指标结果作为参考把业务需求作为主导两者结合找到大家都接受的方案。7.4 K-Means 只适合凸簇的问题K-Means 假设簇是凸的也就是圆形或者类圆形。如果数据的真实簇形状是长条形或者环形K-Means 表现会很差。我在项目里发现财经和科技类文章部分重叠有些文章的摘要风格特别像聚类时被分到了同一个簇。如果遇到这种情况可以考虑换成基于密度的聚类算法比如 HDBSCAN但对文本向量来说K-Means 通常是个不错的起点。8. 几个让聚类效果明显变好的小技巧8.1 用标题加摘要别用正文全文这是我在项目里最想分享的一点。第一次实验我用了全文正文做 TF-IDF聚类结果很差簇与簇之间的差异很模糊。后来改成标题摘要后效果立竿见影。原因也很简单公众号正文为了阅读体验经常插入大量无关内容、口语化表达和举例论证噪声比例很高而标题和摘要是作者精心提炼过的核心语义信息密度和主题一致性都要高得多。8.2 用 Wi-Fi 算法求 K 的补充验证手肘法和轮廓系数之外我补充了一个简单方法计算不同 K 值下每个簇的平均样本数如果某个 K 值下有一个簇的样本数过少比如少于总数的 3%说明 K 可能偏大。在我这个数据集上K8 时出现了一个只有 40 多篇文章的碎片簇而 K6 时最少的一个簇也有 200 多篇分布更均匀这也从侧面支持了 K6 的选择。8.3 文本长度对聚类结果的影响公众号文章长度差异极大有的摘要只有一句话有的摘要是一大段话。TF-IDF 在计算时需要做 L2 归一化这个 sklearn 默认已经做了但长度差异仍然存在。我实验过用截断的方式统一文本长度效果反而不好因为截断会丢失关键信息。后来干脆就不管长度差异让 TF-IDF 的自然权重来处理效果反而稳定。9. 结合具体业务场景的应用价值与扩展思路9.1 竞品内容分析场景聚类结果落地到业务上最直接的一个场景是竞品分析。假设你在运营一个科技类公众号需要了解同领域头部账号最近都在写什么方向。把近一个月内几十个竞品账号的所有文章抓下来做聚类你就能快速得到几大类主题比如“AI 产品测评”“企业数字化”“芯片行业解读”“开源项目推荐”再统计每个簇的文章数量占比和阅读量均值就能看出哪些主题是关注度高但竞品内容少的蓝海方向。9.2 热点选题发现场景另一个落地场景是热点选题。微信公众号文章的发布时间字段很关键把时间信息加进来按天或者按周对聚类结果做一个时间序列统计能看到某个主题簇的文章数量是否在某段时间内急剧上升。如果某个簇的文章数量在三天内从个位数涨到三四十篇说明这个方向出现了热点值得快速跟进。我还试过在聚类结果的基础上做二次挖掘比如对每个簇内文章计算阅读量均值和点赞率找到“高关注主题”和“高互动主题”这两个关键词对内容团队的选题决策有很高的参考价值。聚类结果的正确用法不是替代人的判断而是把信息浓缩到人能够快速消化的范围。9.3 结合其他模型进一步深化如果需要更细粒度的文本理解可以在 K-Means 聚类结果的基础上做二次建模。比如先用 K-Means 给文本打上粗粒度主题标签再用有监督的分类模型构建一个主题分类器后续新文章就能通过分类器自动打标签形成一个持续的自动分类流程。这个思路在内容中台的搭建中很常见我目前也在往这个方向迭代。这套流程跑下来最大的收获是聚类分析不只是一个 sklearn 的 API 调用前面文本清洗和特征工程的工作量占了整个项目的 70%。数据不干净算法再先进也白搭清洗做到位了K-Means 这个几十年前的算法照样能解决现代文本分析的问题。指标上不要被轮廓系数的绝对值迷惑文本聚类能稳定在 0.2 以上就可以交给业务侧判断了。最后分享一个细节前期我在停用词表上花了两个多小时当时觉得这是笨功夫后来做特征分析时发现正是这些笨功夫让每个簇的主题词干净到可以直接拿到运营会议上当决策依据。如果这个项目对你有启发建议你从自己的公众号文章数据开始跑一遍哪怕只有几百篇文章也会对“数据如何变成业务判断”有全新的体会。