2026/9/16 21:21:34

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

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_df2词至少要在2条关键词里出现才保留结果一批低频长尾词直接被丢弃特征聚类出来一片惨淡。后来调成min_df1保留全量特征用稀疏矩阵存储内存完全扛得住。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值下的簇内误差平方和SSESSE会随着K增加而下降——因为簇多了每个词离自己的质心自然更近。但下降速度会越来越慢在某个点之后增加K带来的收益锐减这个拐点像手肘一样就是比较理想的K。说句实在话肘部法则在关键词数据上经常不给面子。真实搜索词数据的SSE曲线十分平滑拐点不分明让你怀疑每个K都合理也都不合理。所以我的用法是把它当区间筛选工具而不是精确的选参工具——先看曲线找“肘部可能出现”的范围比如K8到K15之间把候选范围锁定在这个区间。3.2 轮廓系数关键词场景别用理想值卡轮廓系数衡量的是簇内紧凑度和簇间分离度的综合效果范围从-1到1越接近1越好。逻辑上没错但在关键词场景里我见过的轮廓系数普遍偏低0.15到0.3之间浮动是常态远达不到文本聚类教程里展示的0.5。原因不复杂搜索词是自然语言语言本来就有模糊性。“氨基酸洗面奶”和“氨基酸身体乳”都含有“氨基酸”词面上离得近但一个是洁面场景一个是沐浴场景反过来“洗面奶”和“洁面乳”语义完全同指但词面零重合。这种词面和语义的错位导致关键词向量的簇结构天然就不是紧凑球形的轮廓系数自然上不去。所以我的经验是轮廓系数只用来做横向对比。在同样的数据、同样的向量化参数下K12的轮廓系数明显高于K9和K15这个信号就足够支持选K12。至于0.2还是0.35根本不重要别拿阈值卡自己。3.3 业务约束K值最终要匹配业务节奏算法说多少不重要重要的是业务上能不能消化。这一点新手特别容易忽略一上来跑个K50结果拿到50个主题簇完全不知道怎么用。我的取舍逻辑是人工能有效管理的主题数量约等于内容团队一个月能产出的专题数。当时内容团队的计划是一个季度做15篇专题文章广告账户要控制在10个以内的ad group那么聚类数定在10到20之间最合适。算法给出的最优K再漂亮如果超出业务承载能力最终结果也只能躺在Excel里吃灰。跑K-means的时候我把K从5到30挨个跑了一遍同时看了SSE曲线、轮廓系数、以及几个代表K值下的聚类抽样结果最后折中选了15。这个K值下每个簇的词数还算均衡没有出现某个簇只有两三个词的极端情况人工浏览每个簇的代表词也能看出清晰主题。4. 从脚本到结果完整聚类流程与参数注解理论说够了上实操。下面是我在实际项目里用的完整脚本数据集结构很简单CSV文件第一列keyword是关键词文本其他列忽略。用到的主要是jieba、scikit-learn、pandas三个库。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_df1, max_df0.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_clustersk, initk-means, n_init10, max_iter300, random_state42) labels km.fit_predict(X) sse.append(km.inertia_) sil silhouette_score(X, labels, sample_size5000, random_state42) silhouette_scores.append(sil) print(fK{k}, SSE{km.inertia_:.2f}, Silhouette{sil:.4f}) # 5. 选定K重新训练 best_k 15 km KMeans(n_clustersbest_k, initk-means, n_init20, max_iter300, random_state42) 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, indexFalse, encodingutf-8-sig)4.1 参数设置的理由initk-means是必选项原因很简单普通随机初始化在关键词这种高维稀疏数据上特别容易掉进局部最优跑出来的聚类结果每次都不同甚至有一次某个簇里只有一个词。k-means通过让初始质心尽量分散大幅降低这个风险。加上n_init10甚至n_init20相当于从多个初始点出发各自跑一遍最后保留SSE最小的那组集群的稳定性会明显好很多。random_state42是为了可复现。你可能觉得自己不需要但实际工作中你会反复调整参数、对比不同版本的聚类结果如果每次跑出来的标签都不一样你永远不知道改善是来自参数变化还是随机波动。固定随机种子这个坑就没了。silhouette_score计算时我加了sample_size5000。三万个词两两算距离做轮廓系数非常慢全部算一遍要等很久。抽样算不会对走势判断造成实质影响但能省下大量时间。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里的一个隐藏功能直接predictK-means本质上有监督学习的外壳——先按无监督方式学出质心之后对新样本可以直接走predict分配最近的质心。这个特性是长尾词处理路线的关键。# 假设 short_df 是短词DataFramelong_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个簇对其中最大的几个簇分别再做一次K3或K4的聚类把大簇拆成子话题。这样既保留了主题骨架又不会把长尾词的细分意图揉碎。6. 聚类完怎么读结果质心、主题命名与落地聚类跑完只是第一步怎么把结果变成业务可用的东西才是关键。这一步做不好前面所有代码都是白跑。6.1 从质心和类内词还原主题拿到聚类结果后我的动作是这样的对每个簇看一眼质心top10词和簇内出现频率最高的top20词两个列表交叉对照基本就能判断这组的主题方向。比如质心词里有“洗面奶”“洁面”“氨基酸”簇内高频词里有“氨基酸洗面奶”“温和洁面”“洗面奶推荐”这个簇的主题可以命名为“洁面产品”。只依赖质心词容易误判只依赖高频词容易忽略异常词两个都看最稳。6.2 簇的“含金量”指标我会额外算一个指标每个簇内词的平均搜索量。这个指标不参与聚类训练但用在结果评估上非常有用——它帮助你判断不同主题簇的商业价值分布。有的簇可能聚出上千个词但平均搜索量低得可怜有的簇虽然词数只有一两百但平均搜索量高是这个品类的主阵地。拿这个排序内容排期和广告预算分配就一目了然。6.3 抽样人工复核不要迷信聚类结果每次聚完类我会从每个簇里随机抽20个词人工快速过一眼确认“这20个词是不是同一个主题”。这个词看起来属于哪一类”如果超过三成和该簇主题不一致我就知道这轮聚类参数有问题——要么K值不合适要么需要调整向量化参数要么该分层的没分层。人工复核这件事不能省算法给的是参考结构不是标准答案。6.4 聚类结果落到业务动作最后说说聚类结果在业务上的三种实际用法。一是搭内容架构。把聚类出的15个簇当作站内的15个内容频道簇内词对应选题词比如“洁面产品”簇里再按“氨基酸”“皂基”“敏感肌”细分目录每一层都能直接对到内容生产任务上。二是做广告账户结构。广告账户逻辑天然是分层的账户-广告组-关键词。聚类得到的每个簇就是一个天然广告组簇内词全部放进这个广告组里写统一文案和落地页。以前广告组划分全靠人工想思路不统一还慢现在直接按聚类来逻辑清晰效率高。三是给站内搜索结果做导流。搜索词聚完类之后站内搜索的“无结果词”或“少结果词”可以映射到最近的主题簇直接给用户推荐该簇下的核心内容比较常见的做法是拿聚类模型做相关推荐楼层。最后分享一点个人体会K-means在关键词聚类这件事上价值不在算法有多高级而在于它足够简单、结果可解释、工程链路短——从数据到结果一个人半天就能跑通。但它的上限也摆在那里它永远只能发现“词面上”相似的主题抓不住深层语义关联。如果哪天需求升级到要识别“不同写法同一意图”这种深层语义关系该上词向量加聚类、还是直接上预训练模型到时候再另说。对我自己来说这套流程用下来的最大收获不是聚类本身而是重新理解了“把文本变成向量再算距离”这一整套方法论。它换个场景还能用在很多地方用户问题归类、评论主题聚合、甚至竞品词库对比。聚类只是工具真正值钱的是你想清楚“业务上到底需要什么样的分组分完组之后要干什么”。想清楚这两个问题K-means这个最朴素的算法往往就是最合适的解法。