最近做了一阵子微信公众号文章分析的工作,其中一个比较有意思的小项目是把手头积累的一大批微信文章,用 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_features=5000,只保留词频最高的 5000 个词作为特征维度。这个参数很值得讨论——如果太大,矩阵会稀疏到聚类中心难以稳定;如果太小,又会丢失低频但高区分度的词。我做了一组实验对比:特征数从 2000 调到 8000,轮廓系数差别不大,但 5000 的时候各簇的区分度人眼看起来最舒服,因此最终选了 5000。
还开了sublinear_tf=True,这是把词频做1+log(tf)变换,避免某个词在一篇文章里出现次数过多导致权重虚高。在公众号文章里“我们”“自己”这类词出现几百次很常见,如果不做这个变换,这些词的权重会压过真正有主题意义的词。min_df=2表示至少在 2 篇文章中出现过的词才保留,max_df=0.8表示在超过 80% 的文章中都出现的词视为噪声过滤掉。
3. 聚类可行性判断与降维处理:先别急着跑 K-Means
3.1 为什么先做 PCA 和 t-SNE 可视化
很多初学者拿到 TF-IDF 矩阵后直接KMeans(n_clusters=5)一把梭,做完也不知道效果怎么样。我的做法是先用 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_rate=200也是经验值。可视化用的是matplotlib,不同的真实类别用不同颜色标出来(这里我拿公众号的原始分类做参考标签),从图上能直观看到哪些类别之间距离近、哪些类别重叠严重。
4. K 值的确定:手肘法结合轮廓系数的实操
4.1 手肘法的具体计算
K-均值聚类最大的超参数就是 K,也就是最终分成几类。K 选少了,内容差异很大的文章会被硬塞进同一个簇;K 选多了,明明主题一致的文章被拆散成多个碎片簇。我用两种方法交叉验证。
第一种是手肘法。核心逻辑是对比不同 K 值下的簇内误差平方和 SSE(也叫 inertia)。K 从 2 取到 12,每个 K 值下跑 3 次取平均 SSE,然后画出折线图。理论上 SSE 会随着 K 增大而持续下降,但下降速度会在某个点变缓,这个拐点就是合理的 K。我这次跑完发现 K=6 的时候折线出现明显的拐点,K 从 5 增加到 6 时 SSE 降幅很大,从 6 增加到 7 时降幅明显缩小,所以初步把 K 定在 6。
4.2 轮廓系数验证与最终确定
第二种是轮廓系数(Silhouette Coefficient)。每个样本的轮廓系数综合了“自身与同簇其他样本的平均距离”和“自身与最近的其他簇样本的平均距离”,取值范围在 -1 到 1 之间,越接近 1 说明聚类效果越好,越接近 -1 说明样本被分错了簇。把所有样本的轮廓系数取平均,得到整体聚类质量的评价指标。
我在 K=6 时计算平均轮廓系数约为 0.23,说实话这个数值不算高——文本聚类的轮廓系数普遍低于数值型数据的聚类,能到 0.2 以上就算可以接受。我对比了 K=5 和 K=7 的结果,K=5 的轮廓系数约 0.21,K=7 约 0.20,和 K=6 相比都没有显著优势。结合手肘法的拐点位置,最终确定 K=6,既保留了类别间的区分度,又不至于过碎。
另外我还做了一步辅助验证:把 K=6 的聚类结果按公众号原始分类做交叉表,看看每个簇里主要集中了哪些公众号类别。这一步是为了确认聚类结果和业务认知基本一致,如果聚类结果和原始分类完全不搭边,那说明特征工程或者 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( tokenizer=chinese_tokenizer, max_features=5000, max_df=0.8, min_df=2, sublinear_tf=True, stop_words=stopwords ) tfidf_matrix = vectorizer.fit_transform(texts) km = KMeans( n_clusters=6, init='k-means++', n_init=10, max_iter=300, random_state=42 ) cluster_labels = km.fit_predict(tfidf_matrix)这里有几个参数值得展开聊聊。init='k-means++'是用一种更聪明的中心点初始化方式,避免随机初始化导致聚类结果特别差;n_init=10表示每次用不同随机种子跑 10 遍,取最优结果;max_iter=300是单次迭代的最大次数。K-Means 本身是个启发式算法,对初始值敏感,如果不设置random_state,多次运行结果可能差异很大。我在项目里固定random_state=42,确保每次跑出来的簇编号一致,方便复现和调参。
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 | 轮廓系数 | 簇内平均距离 | 备注 |
|---|---|---|---|---|
| 4 | 456.2 | 0.19 | 2.34 | 簇内太混,主题不清 |
| 5 | 398.7 | 0.21 | 2.16 | 财经和科技新闻部分混杂 |
| 6 | 341.5 | 0.23 | 1.98 | 主题边界相对清晰 |
| 7 | 317.8 | 0.20 | 1.86 | 情感类被拆成两个簇 |
| 8 | 296.3 | 0.18 | 1.75 | 碎片簇增多 |
从表里能看出,K=6 在轮廓系数上最优,虽然簇内平均距离不是最小的,但业务解释性最好。这告诉我们一个道理:聚类效果不能只看某一个指标,还要结合业务侧的可解释性综合判断,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_init=10多次运行取最优。如果业务上允许,建议多跑几次,观察哪些文章在多次结果里被稳定分配到同一个簇,哪些文章经常“跳簇”,后者往往是边界模糊的样本文,后续可以单独处理。
7.3 聚类簇数不好解释
有时候指标上 K=5 最好,但业务上觉得分成 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 可能偏大。在我这个数据集上,K=8 时出现了一个只有 40 多篇文章的碎片簇,而 K=6 时最少的一个簇也有 200 多篇,分布更均匀,这也从侧面支持了 K=6 的选择。
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 以上就可以交给业务侧判断了。
最后分享一个细节:前期我在停用词表上花了两个多小时,当时觉得这是笨功夫,后来做特征分析时发现,正是这些笨功夫让每个簇的主题词干净到可以直接拿到运营会议上当决策依据。如果这个项目对你有启发,建议你从自己的公众号文章数据开始跑一遍,哪怕只有几百篇文章,也会对“数据如何变成业务判断”有全新的体会。