news 2026/9/16 21:21:23

K-means关键词聚类实战:从分词、TF-IDF到长尾词处理

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
K-means关键词聚类实战:从分词、TF-IDF到长尾词处理

做了几年搜索流量相关的工作,每天面对最多的就是关键词表——几万行、几十万行地从搜索词报告、网站后台、第三方工具里导出来,堆在Excel里根本看不完。人工分组看几百个词还行,量一上来就完全失控。后来试了一圈聚类算法,最后真正在我日常工作里落地并且持续复用的,反而是最基础的K-means。这篇就聊聊我在关键词聚类里用K-means的完整思路、具体代码和一些踩过的坑,特别是长尾关键词混进去之后出现的各种奇怪现象,希望能给同样在跟关键词死磕的朋友一点参考。

K-means做关键词聚类这件事,网上教程一搜一大把,但大部分都是拿新闻语料或者百科条目做演示,换了真正的搜索词数据,各种问题就冒出来了:中文分词把词切得稀碎、长尾词向量稀疏到跟谁都不像、K值选不好整批聚类结果像随机分组。这些实操层面的坑,才是写这篇东西的真正原因。

1. 为什么是K-means:关键词聚类的场景还原

先说清楚我遇到的到底是个什么问题。当时手头有一个搜索词库,大概三万多条,是从多个渠道汇总来的:站内搜索记录、搜索词报告、还有第三方关键词工具批量挖出来的一堆词。这些词来源杂、格式乱、长短差异巨大,有“祛痘”这种两个字的,也有“祛痘印的洗面奶哪个牌子好用”这种长尾到不能再长的。

我的需求其实很简单:把这三万个词按“语义主题”分成若干组,每组对应一个主题方向,方便后续做内容规划和广告账户结构梳理。人工做肯定不现实,三万个词就算每天看一千个也得看一个月,而且看到后面前面忘光了,一致性没法保证。这时候聚类是唯一可行的路子。

那为什么是K-means,而不是LDA主题模型、高斯混合模型或者别的什么?

第一,K-means给每个词一个硬性的簇标签,结果就是一个词归且仅归一类。这对后续操作太重要了——我要拿聚类结果去做广告分组、做内容选题,每个词必须落在明确的某个桶里。LDA给的是主题分布,“这个词60%属于A主题、30%属于B主题、10%属于C主题”,听着很优雅,实际操作的时候你还得加一层argmax或阈值判断,多一步不说,短文本上的分布本来就估不准。

第二,K-means的质心可直接解释。每个簇训练完都有一个质心向量,这个向量里的高权重维度就是这组词的核心特征。换句话说,我不光能得到“哪些词是一个类”,还能直接从质心里看出“这个类大概是什么主题”,这对后续给簇命名的帮助非常大。

第三,也是最重要的一点:关键词是短文本。一条搜索词平均三到八个字,没有上下文语境,语义完全靠词本身承载。LDA这类概率主题模型在长文档上效果好,但在短文本上往往因为共现信息太少而变得不稳定。K-means配合TF-IDF向量,本质上是在算词面的相似度,对短文本来说反而更直接、更诚实——因为搜索词里能用的信息本来就只有词面。

当然K-means的毛病我也清楚:K值要先定、初始质心敏感、对非凸簇效果不好、容易被离群点带偏。但在关键词这个场景里,这些缺点都有对策,后面会一个个说。

2. 把关键词变成K-means能算的东西:分词与向量化

K-means不接受文本输入,它只知道数字向量。所以第一步是把每条关键词变成一个向量。这一步做得糙,后面聚类效果直接崩盘,值得花点心思。

2.1 中文分词:宁可切碎,不要切错

英文关键词处理起来简单,按空格切分就行。中文不行,必须分词。我用的是jieba,原因就一个:生态成熟,文档多,遇到问题好查。

jieba分词我试过三种模式,最后用的是默认的精确模式,但做了一步特殊处理:对分词结果里的词性和长度做了过滤,而不是用现成的停用词表一刀切。为什么?因为搜索词太短了,总共就几个字,如果按通用中文停用词表把“什么”“怎么”“哪个”“多少”全删了,很多长尾词会变成一个光秃秃的核心词,反而丢失了修饰层面的信息。

举个例子:“祛痘印的洗面奶哪个牌子好”。分词结果是“祛痘印 / 的 / 洗面奶 / 哪个 / 牌子 / 好”。如果按照通用停用词表把“的”“哪个”“好”全删掉,剩下“祛痘印 / 洗面奶 / 牌子”,语义方向确实没问题,但“哪个牌子好”这个重要的购买意向信息就没了。所以我的做法是:只过滤单字词和纯数字,保留所有长度大于等于2的词,让TF-IDF自己去决定哪些词重要、哪些不重要。

实际测试下来,jieba对新词和专业词的分词效果一般,比如“早C晚A”会被切成“早 / C / 晚 / A”,“刷酸”可能被切成“刷 / 酸”。这种词出现频率低,对聚类整体结果影响有限,但如果你的关键词集合里某个核心术语特别集中,建议加自定义词典:jieba.add_word("早C晚A"),简单粗暴但有效。

2.2 TF-IDF:给泛词降权,让主题词浮出来

分词之后,每条关键词变成了一个词的序列。接下来要把这些词的序列转成向量。我对比过CountVectorizer和TfidfVectorizer,最终确定用TF-IDF。

纯粹的词频向量(CountVectorizer)有个问题:搜索词里的高频词通常是“怎么”“如何”“多少钱”“推荐”这类泛意图词。它们在每个簇里都可能出现,对区分主题毫无帮助,但词频高,算距离的时候反而把真正的主题词给稀释了。

TF-IDF的处理逻辑正好补上这个短板:一个词如果在所有文档里都出现,IDF就低,权重被压下去。泛意图词在所有搜索词里出现频率高,天然被降权;而某个主题特有的词比如“泥膜”“水杨酸”“氨基酸”只在相关文档里出现,IDF高,权重抬上来。聚类时真正起区分作用的就成了这些主题词。

参数上我用了一个小调整:ngram_range=(1, 2)。这个设置会把“水杨酸 + 洗面奶”这种相邻双词也作为一个特征。实测下来对长尾词特别有效——单字词语义词序信息有限,加上bigram之后,“祛痘 洗面奶”和“洗面奶 祛痘”会被识别为不同特征,虽然K-means本身不关心词序,但特征多了之后向量表达更丰富。

还有一点要注意:min_df别设太高。我一开始图省事设了min_df=2(词至少要在2条关键词里出现才保留),结果一批低频长尾词直接被丢弃特征,聚类出来一片惨淡。后来调成min_df=1,保留全量特征,用稀疏矩阵存储,内存完全扛得住。

2.3 维度爆炸问题:要不要降维

三万个词分词后特征维度轻松到好几万。很多人看到这个维度第一反应是做PCA降维,但我的实测结论是:在关键词聚类这个场景里,可以先不做降维,直接拿稀疏矩阵跑K-means

原因是多方面的。其一,sklearn的KMeans实现本身对稀疏矩阵有优化,能正确处理稀疏表示,不会因为高维就变慢。其二,TF-IDF向量本身极其稀疏,两条词之间可能只有一两个维度有交集,高维稀疏下K-means的收敛反而比较稳定。其三,降维之后的向量每一个维度都是原始特征的线性组合,质心不再可解释——我前面说过,质心可解释是选K-means的核心原因之一,不能为了降维牺牲掉。

当然有一种情况我会考虑降维:当轮廓系数怎么调都低得离谱,聚类结果完全看不出任何语义分组时。这时候用TruncatedSVD降到一个200到300维的稠密向量再聚类,常常能救回来。思路是:高维稀疏可能把语义相关的词因为字面不重叠而强行拆开,降维之后潜在语义维度被压缩出来,相关词会在低维空间凑近。但这是备选方案,不是默认路径。

3. 先别急着跑模型:K值怎么定才靠谱

K-means跑起来很容易,几行代码就完事。真正纠结的是K值。这个值直接决定你最后拿到的是10个主题还是30个主题,而不同数量的主题对业务的意义完全不同。我的经验是三管齐下:肘部法则给候选区间,轮廓系数辅助判断,业务约束做最终裁决。

3.1 肘部法则:看SSE的拐点

肘部法则的核心逻辑不复杂:把K从小往大遍历,记录每个K值下的簇内误差平方和(SSE),SSE会随着K增加而下降——因为簇多了,每个词离自己的质心自然更近。但下降速度会越来越慢,在某个点之后增加K带来的收益锐减,这个拐点像手肘一样,就是比较理想的K。

说句实在话,肘部法则在关键词数据上经常不给面子。真实搜索词数据的SSE曲线十分平滑,拐点不分明,让你怀疑每个K都合理也都不合理。所以我的用法是把它当区间筛选工具,而不是精确的选参工具——先看曲线找“肘部可能出现”的范围,比如K=8到K=15之间,把候选范围锁定在这个区间。

3.2 轮廓系数:关键词场景别用理想值卡

轮廓系数衡量的是簇内紧凑度和簇间分离度的综合效果,范围从-1到1,越接近1越好。逻辑上没错,但在关键词场景里我见过的轮廓系数普遍偏低,0.15到0.3之间浮动是常态,远达不到文本聚类教程里展示的0.5+。

原因不复杂:搜索词是自然语言,语言本来就有模糊性。“氨基酸洗面奶”和“氨基酸身体乳”都含有“氨基酸”,词面上离得近,但一个是洁面场景一个是沐浴场景;反过来“洗面奶”和“洁面乳”语义完全同指,但词面零重合。这种词面和语义的错位导致关键词向量的簇结构天然就不是紧凑球形的,轮廓系数自然上不去。

所以我的经验是:轮廓系数只用来做横向对比。在同样的数据、同样的向量化参数下,K=12的轮廓系数明显高于K=9和K=15,这个信号就足够支持选K=12。至于0.2还是0.35,根本不重要,别拿阈值卡自己。

3.3 业务约束:K值最终要匹配业务节奏

算法说多少不重要,重要的是业务上能不能消化。这一点新手特别容易忽略,一上来跑个K=50,结果拿到50个主题簇完全不知道怎么用。

我的取舍逻辑是:人工能有效管理的主题数量约等于内容团队一个月能产出的专题数。当时内容团队的计划是一个季度做15篇专题文章,广告账户要控制在10个以内的ad group,那么聚类数定在10到20之间最合适。算法给出的最优K再漂亮,如果超出业务承载能力,最终结果也只能躺在Excel里吃灰。

跑K-means的时候我把K从5到30挨个跑了一遍,同时看了SSE曲线、轮廓系数、以及几个代表K值下的聚类抽样结果,最后折中选了15。这个K值下每个簇的词数还算均衡,没有出现某个簇只有两三个词的极端情况,人工浏览每个簇的代表词也能看出清晰主题。

4. 从脚本到结果:完整聚类流程与参数注解

理论说够了,上实操。下面是我在实际项目里用的完整脚本,数据集结构很简单:CSV文件,第一列keyword是关键词文本,其他列忽略。用到的主要是jiebascikit-learnpandas三个库。

import pandas as pd import jieba import re from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.cluster import KMeans from sklearn.metrics import silhouette_score # 1. 读取数据 df = pd.read_csv("keywords.csv") keywords = df["keyword"].dropna().str.strip().tolist() print(f"共读取 {len(keywords)} 条关键词") # 2. 分词 def tokenize(text): # 去掉多余符号,保留中文、英文、数字 text = re.sub(r"[^\u4e00-\u9fa5a-zA-Z0-9]", " ", text) words = jieba.lcut(text) # 过滤单字和纯数字,保留长度>=2的词 return " ".join([w for w in words if len(w) >= 2 and not w.isdigit()]) df["tokenized"] = df["keyword"].apply(tokenize) # 3. TF-IDF向量化 vectorizer = TfidfVectorizer(ngram_range=(1, 2), min_df=1, max_df=0.8) X = vectorizer.fit_transform(df["tokenized"]) print(f"特征维度: {X.shape[1]}") # 4. K值选择:跑一个候选区间,输出SSE和轮廓系数 import numpy as np sse = [] silhouette_scores = [] k_range = range(5, 31) for k in k_range: km = KMeans(n_clusters=k, init="k-means++", n_init=10, max_iter=300, random_state=42) labels = km.fit_predict(X) sse.append(km.inertia_) sil = silhouette_score(X, labels, sample_size=5000, random_state=42) silhouette_scores.append(sil) print(f"K={k}, SSE={km.inertia_:.2f}, Silhouette={sil:.4f}") # 5. 选定K,重新训练 best_k = 15 km = KMeans(n_clusters=best_k, init="k-means++", n_init=20, max_iter=300, random_state=42) df["cluster"] = km.fit_predict(X) # 6. 输出每个簇的质心代表性词汇 order_centroids = km.cluster_centers_.argsort()[:, ::-1] terms = vectorizer.get_feature_names_out() cluster_summary = {} for i in range(best_k): top_terms = [terms[ind] for ind in order_centroids[i, :10]] cluster_summary[i] = top_terms print(f"簇 {i}: {', '.join(top_terms)}") # 7. 保存结果 df.to_csv("keywords_with_cluster.csv", index=False, encoding="utf-8-sig")

4.1 参数设置的理由

init="k-means++"是必选项,原因很简单:普通随机初始化在关键词这种高维稀疏数据上特别容易掉进局部最优,跑出来的聚类结果每次都不同,甚至有一次某个簇里只有一个词。k-means++通过让初始质心尽量分散,大幅降低这个风险。加上n_init=10甚至n_init=20,相当于从多个初始点出发各自跑一遍,最后保留SSE最小的那组,集群的稳定性会明显好很多。

random_state=42是为了可复现。你可能觉得自己不需要,但实际工作中你会反复调整参数、对比不同版本的聚类结果,如果每次跑出来的标签都不一样,你永远不知道改善是来自参数变化还是随机波动。固定随机种子,这个坑就没了。

silhouette_score计算时我加了sample_size=5000。三万个词两两算距离做轮廓系数非常慢,全部算一遍要等很久。抽样算不会对走势判断造成实质影响,但能省下大量时间。

4.2 输出结果里最容易忽略的信息

脚本跑完,大多数人直接看簇里的词,但我提醒一句:质心词和簇内高频词是两个概念,两者都值得看

质心词的逻辑是:找到当前质心向量里权重最大的那10个维度,也就是TF-IDF得分最高的特征。这些词代表了这个簇“在向量空间中最核心的特征”。而簇内高频词是统计每个簇里实际出现的词频,更接近人的直觉。经常有这种情况:质心词看起来有点奇怪(因为TF-IDF会把一些写法少见但IDF极高的词顶上来),但簇内高频词一看就明白这组是什么主题。所以我的实操是:质心词用于快速扫描,簇内高频词用于最终命名和人工复核。

5. 长尾词带来的麻烦:稀疏向量与聚类漂移

如果你只是拿K-means跑一个普通关键词表,前面那些基本够用了。但到了真正面对长尾关键词的时候,各种反直觉的现象就开始出现。这里说的长尾词特指那种七八个字甚至十几个字、搜索量很低但意图很具体的关键词,比如“夏天油皮适合用什么成分的洗面奶”“敏感肌去红血丝的精华哪个牌子好”。

5.1 问题一:长尾词变成“孤儿点”

长尾词的TF-IDF向量极其稀疏——它分词之后可能有五六个词,但每个词出现的文档频率都很低,整个向量和任何其他词的共同非零维度都几乎没有。K-means里这种点会成为聚类时的麻烦:它距离每个初始质心都差不多远,最终归属基本看运气,被硬塞进某个簇之后又会把那个簇的质心拖向一个奇怪的方向。

这是我第一次跑完聚类之后最直观的感受:几个簇还算干净,但总有一两个簇长得像“垃圾回收站”,里面什么都有,全是各个方向上漂移过来的长尾词。

5.2 问题二:核心信息被修饰成分稀释

长尾词天然包含核心词+修饰词+意图词。“油皮洗面奶”和“干皮用什么洗面奶好”这两个词词面上只有“洗面奶”重叠,剩下的“油皮”“干皮”“什么”“好”全是差异。TF-IDF向量化之后,核心词“洗面奶”因为出现在大量文档里IDF被压低,反而修饰词因为出现的文档少IDF更高,权重大。结果就是同一主题下的长尾词,被各自的修饰词拉向不同的方向,聚类结果散成一地。

我的应对方案是按词长分层处理。具体操作是:先按关键词长度分成短词集合(比如小于等于6个字)和长尾集合(大于6个字),短词直接进K-means聚类,长尾词不走聚类训练,而是用训练好的K-means模型直接预测归属——km.predict(vectorizer.transform(长尾词列表))

这个做法的逻辑很简单:核心主题结构应该由“语义骨架清晰”的短词来确定,长尾词是基于这些主题骨架的修饰延展,让它进来参与训练只会把骨架拉歪,但让它归到最相似的现有主题上却很合适。

5.3 sklearn里的一个隐藏功能:直接predict

K-means本质上有监督学习的外壳——先按无监督方式学出质心,之后对新样本可以直接走predict分配最近的质心。这个特性是长尾词处理路线的关键。

# 假设 short_df 是短词DataFrame,long_df 是长尾词DataFrame short_labels = km.fit_predict(vectorizer.fit_transform(short_df["tokenized"])) # 注意:fit_transform用的是short_df的数据,直接用同一个vectorizer去transform长尾词 long_df["cluster"] = km.predict(vectorizer.transform(long_df["tokenized"]))

这里有个细节很容易错:短词和长尾词必须用同一个TfidfVectorizer对象,也就是先在短词上fit拟合出特征空间,再对长尾词只做transform。如果长尾词单独重新设计vectorizer去fit_transform,特征空间的列名和位置就对不上,predict直接报错或者给出荒谬结果。

5.4 如果长尾词体量特别大:二次聚类

上面说的“直接predict”方案有个隐含前提:核心词的聚类结果质量足够好。但如果长尾词量级是核心词的几十倍,或者长尾词里本身还能细分出不同的子主题,那么直接predict会导致某一整个子类别的长尾词被绑在一起贴到同一个核心簇上,主题内差异被掩盖。

这种情况我会做二次聚类:第一次K-means分粗主题,每个簇内部再做一次K-means分细主题。比如第一次分出15个簇,对其中最大的几个簇分别再做一次K=3或K=4的聚类,把大簇拆成子话题。这样既保留了主题骨架,又不会把长尾词的细分意图揉碎。

6. 聚类完怎么读结果:质心、主题命名与落地

聚类跑完只是第一步,怎么把结果变成业务可用的东西才是关键。这一步做不好,前面所有代码都是白跑。

6.1 从质心和类内词还原主题

拿到聚类结果后,我的动作是这样的:对每个簇,看一眼质心top10词和簇内出现频率最高的top20词,两个列表交叉对照,基本就能判断这组的主题方向。比如质心词里有“洗面奶”“洁面”“氨基酸”,簇内高频词里有“氨基酸洗面奶”“温和洁面”“洗面奶推荐”,这个簇的主题可以命名为“洁面产品”。只依赖质心词容易误判,只依赖高频词容易忽略异常词,两个都看最稳。

6.2 簇的“含金量”指标

我会额外算一个指标:每个簇内词的平均搜索量。这个指标不参与聚类训练,但用在结果评估上非常有用——它帮助你判断不同主题簇的商业价值分布。有的簇可能聚出上千个词,但平均搜索量低得可怜;有的簇虽然词数只有一两百,但平均搜索量高,是这个品类的主阵地。拿这个排序,内容排期和广告预算分配就一目了然。

6.3 抽样人工复核:不要迷信聚类结果

每次聚完类,我会从每个簇里随机抽20个词,人工快速过一眼,确认“这20个词是不是同一个主题”。这个词看起来属于哪一类?”如果超过三成和该簇主题不一致,我就知道这轮聚类参数有问题——要么K值不合适,要么需要调整向量化参数,要么该分层的没分层。人工复核这件事不能省,算法给的是参考结构,不是标准答案。

6.4 聚类结果落到业务动作

最后说说聚类结果在业务上的三种实际用法。

一是搭内容架构。把聚类出的15个簇当作站内的15个内容频道,簇内词对应选题词,比如“洁面产品”簇里再按“氨基酸”“皂基”“敏感肌”细分目录,每一层都能直接对到内容生产任务上。

二是做广告账户结构。广告账户逻辑天然是分层的:账户-广告组-关键词。聚类得到的每个簇就是一个天然广告组,簇内词全部放进这个广告组里写统一文案和落地页。以前广告组划分全靠人工想,思路不统一还慢,现在直接按聚类来,逻辑清晰效率高。

三是给站内搜索结果做导流。搜索词聚完类之后,站内搜索的“无结果词”或“少结果词”可以映射到最近的主题簇,直接给用户推荐该簇下的核心内容,比较常见的做法是拿聚类模型做相关推荐楼层。

最后分享一点个人体会

K-means在关键词聚类这件事上,价值不在算法有多高级,而在于它足够简单、结果可解释、工程链路短——从数据到结果,一个人半天就能跑通。但它的上限也摆在那里:它永远只能发现“词面上”相似的主题,抓不住深层语义关联。如果哪天需求升级到要识别“不同写法同一意图”这种深层语义关系,该上词向量加聚类、还是直接上预训练模型,到时候再另说。

对我自己来说,这套流程用下来的最大收获不是聚类本身,而是重新理解了“把文本变成向量再算距离”这一整套方法论。它换个场景还能用在很多地方:用户问题归类、评论主题聚合、甚至竞品词库对比。聚类只是工具,真正值钱的是你想清楚“业务上到底需要什么样的分组,分完组之后要干什么”。想清楚这两个问题,K-means这个最朴素的算法,往往就是最合适的解法。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/16 21:21:00

龙岩汽车遥控钥匙失灵:按顺序排查能省下一把新钥匙的钱

# 龙岩汽车遥控钥匙失灵:按顺序排查能省下一把新钥匙的钱车停在楼下,按遥控没反应。第一反应往往是完了,钥匙坏了,得去配一把。先别急。在龙岩,遥控钥匙失灵打电话来问的人里,有一部分最后根本没换钥匙&…

作者头像 李华
网站建设 2026/9/16 21:18:31

Simulink在混合交直流微电网仿真中的应用与实践

1. 微电网仿真入门:为什么选择Simulink?十年前我第一次接触微电网仿真时,面对各种专业软件眼花缭乱。直到发现Simulink这个神器,才真正找到了工程师的"瑞士军刀"。不同于其他专业电力仿真软件需要复杂的参数设置&#x…

作者头像 李华
网站建设 2026/9/16 21:17:57

ODS架构实战:手把手构建Agent调度-决策-技能三层骨架

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/16 21:16:46

Wi-Fi NDP Sounding机制:波束成形、CSI反馈与性能调优全解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/16 21:14:41

x5sec滑块逆向实战:slidedata参数分析与自动化过码方案设计

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华