中文关键词提取实战:从分词、停用词到TF-IDF与TextRank 做内容分析的时候给一段中文文本提取关键词几乎是绕不开的活儿。我最早接手这方面工作时想法特别简单先把文本用分词工具切开然后数一数哪些词出现得最多取前几个当关键词。结果第一次跑出来的全是“我们”“可以”“进行”“这个”之类的词跟业务内容完全对不上。后来我才意识到中文关键词提取真正的难点并不在“数词频”这一步而是分词、停用词、词性过滤、短语识别这一整串细节。这些细节处理不好后面算法再花哨也白搭。这篇文章就把我后来在实际项目中打磨的这套中文关键词提取方案完整讲一遍从原理到代码再到调参和避坑让不同基础的读者都能直接拿去用。1. 中文文本关键词提取的特殊性先搞懂分词和语言特性1.1 为什么不直接按空格切词中文句子没有空格分词这是和英文最大的差异。英文可以靠空格拿到稳定的词序列中文必须先做分词。分词结果是整个流程的上限如果“智能手机”被切成“智能”“手机”后续的关键词提取就会得到一堆“智能”之类的泛化词而“智能手机”这个更贴合业务语义的词反而丢了。所以做中文关键词提取第一步不是选算法而是先把分词策略想清楚。我目前用得比较多的是开源的中文分词库 jieba它基于前缀词典和 HMM 隐马尔可夫模型日常普通文本基本都能覆盖。更细的场景下也可以考虑其他自带标注能力的分词工具但大多数项目中 jieba 已经够用。这里最需要注意的是分词粒度选择jieba 默认的“精确模式”比较适合关键词提取而“全模式”会把“北京大学”切成“北京”“大学”“北京大学”这种重叠块噪声很大“搜索引擎模式”会把词切得更碎对提取任务也容易制造干扰。所以实际使用时我会建议固定用精确模式不要轻易切换模式。另一个很容易踩的坑是文本里混着英文单词、数字、网络缩略语的时候分词器经常把“AI助手”“citywalk”“5G套餐”这类混合词切碎。比如“citywalk”可能被拆成“city”和“walk”“多巴胺穿搭”可能被切得很零散。处理办法是在分词前后补一份自定义词典把常用热词、混拼词预先添加进去。这个细节我后面在工程踩坑部分会单独展开。1.2 停用词与词性过滤是绕不开的预处理分词之后紧接着就是停用词过滤和词性过滤这两步如果不做所有统计算法都会被功能词淹没。中文里的功能词和泛义动词特别多“进行”“部分”“相关”“以及”“其中”这类词在文本里高频出现但不承载实际内容。如果不过滤它们能稳稳占住关键词榜前排让结果没法看。我早期项目里用一个通用停用词表以为这样就够了后来发现文本里大量出现“问题”“情况”“方面”“用户”这类半功能词通用表压根没覆盖。这些词不算纯停用词但在某些领域语料里就是高频、无明显区分度必须结合业务数据单独处理。所以现在我的习惯是通用停用词表只作为起点跑完一轮统计后把高频但低价值的词抽出来人工看一眼再补充进领域停用词表。词性过滤同样重要。中文里名词含专名、地名、机构名和动名词承担了大部分主题信息形容词和动词是否保留要看场景。比如穿搭类文本里“多巴胺配色”“复古”“甜酷”这类形容词就是核心关键词但在产品说明书、新闻稿里形容词作用就小很多。jieb a 自带词性标注通过 allowPOS 参数就能控制候选词范围这个参数设置得好效果比换算法都明显。我试过在同一批语料里保留名词和保留“名词形容词”两种配置结果差异非常大后面踩坑清单里会细说。2. 三条主流路线对比TF-IDF、TextRank 与 RAKE 各适合什么场景中文关键词提取的经典方案基本绕不开三个TF-IDF、TextRank、RAKE。它们原理不同适用场景也不同。很多人一上来就纠结哪个“最好”但实际项目中往往是组合使用。我先说清楚各自的逻辑和边界再给一个选型参照。2.1 TF-IDF批量语料下最稳的统计基线TF-IDF 是我个人最常用的基线方案。它的核心思想是一个词在某一篇文档里出现次数多说明它对这篇文档重要但如果这个词在所有文档里都高频出现那它的区分度就低需要用 IDF 把它压下去。公式大致是TF 词在文档中出现的次数 / 文档总词数IDF log((总文档数 1) / (包含该词的文档数 1)) 1TF-IDF TF × IDF那个“1”是平滑项防止分母为零也避免新词被算出无穷大的权重。理解公式后你就会发现TF-IDF 天然依赖一个“文档集合”没有批量文档就没有 IDF所以单独拿一小段几百字的文章去算 TF-IDF结果基本没有意义因为所有词在同一篇文档里的文档频率都是 1区分度全丢了。因此 TF-IDF 最适合的场景是批量文档比如几千条新闻、几千条商品简介、一批用户评论。它跑出来的结果稳定性高计算速度快作为后续规则筛选和人工抽检的基础非常合适。它的短板也很明显对单篇短文本无能为力且对同义词、近义词完全没有感知这些都是正常现象。2.2 TextRank单篇长文本的图排序思路TextRank 是从 PageRank 迁移过来的思路它把文本里的每个词当作图里的节点在固定大小的“共现窗口”内让相邻词互相投票。一个词出现在重要词附近的次数越多它自己的重要性也越高迭代收敛后就会得到一个全局权重排序。和 TF-IDF 最大的差别是TextRank 不需要文档集合单篇文章就能跑。所以当你手里只有一篇论文、一篇新闻报道、一份产品介绍但又想知道这篇文本里哪些词最核心TextRank 是比 TF-IDF 更合适的选择。它本质上利用了文本内部的局部结构比如“芯片”和“制程”在一段话里反复靠近出现那它们互相增强的权重就会很高。具体实现时有两个参数影响很大一个是共现窗口大小我一般设成 5窗口太小则词与词的连接太少图太稀疏窗口太大会引入过多无关关联。另一个是词性范围默认只跑名词和动名词会干净很多否则“进行”“可以”这类词也会因高频连入图里被推到前排。工程上可以直接调工具包但理解这个投票机制有助于你判断什么时候该调整参数而不是闷头改来改去。2.3 RAKE 适应中文时的改造思路RAKE 的思路和上面两个都不太一样它先把文本切分成多个候选短语再对每个短语里的词统计“共现次数”和“共现词数”按一套经验公式给短语打分。好处是输出的是短语级别关键词比如“多巴胺穿搭”“咖啡消费趋势”“手机影像能力”而不是单个词。但 RAKE 原本是给英文设计的默认靠标点符号和停用词去切短语中文直接套用效果并不好因为中文的短语边界不是靠空格和停用词就能锁定的。我实践下来的改造方案是先用 jieba 分词并标注词性然后把连续出现的名词、动名词片段拼接成一个候选短语再套 RAKE 的共现评分逻辑。这样既能获得短语级结果又避开了分词边界不稳定带来的问题。这套改造在短文本、评论类数据上表现不错缺点是规则依赖比较重泛化时需要不断维护词性规则和停用表。三种方法我用下面这个表来快速对比方法是否依赖文档集合输出粒度适合场景工程成本TF-IDF依赖需要批量文档词批量文档、内容库、分类标签低TextRank不依赖词单篇长文本、新闻、论文摘要中RAKE 中文改造不依赖短语短文本、评论、标签生成中高3. Python 实战从分词到输出一个可用的关键词序列3.1 技术选型与环境准备这一节我直接给一套能跑的 Python 实现。环境依赖主要是三个jieba、scikit-learn、pandas。scikit-learn 用来做 TF-IDF 矩阵计算jieba 负责分词和词性标注pandas 只是为了方便读入批量文本可以按需替换。很多教程会直接调用 jieba.analyse.extract_tags确实一行就能出结果。但我在项目中更推荐基于 sklearn 手动搭一遍流程原因很简单直接调内置函数时停用词表、词性范围、IDF 平滑方式都比较难精细控制而自己接一遍后每一步都能自由调整后面接业务规则也更顺手。3.2 用 TF-IDF 实现批量文档关键词提取假设我们有这样一批语料都是一些中性话题的文本我把它们放进一个列表里当作批量文档import jieba from sklearn.feature_extraction.text import TfidfVectorizer corpus [ 周末在滨江步道体验了一把citywalk沿途看到很多多巴胺配色的建筑穿搭博主推荐的路线确实出片, 咖啡消费趋势正在悄悄变化手冲和冷萃成为年轻人最喜欢的两个品类品牌方开始围绕咖啡豆产地讲产地故事, 在线读书会组织者发现碎片时间听书的用户增长很快通勤场景已经成了音频内容最核心的消费场景, 新款智能手机主打影像能力长焦镜头在演唱会场景中的表现成为测评博主最关注的指标, 某互联网公司发布了内部开源协作平台开发者可以在上面直接复用项目模板和代码片段节省了很多重复工作, 本地马拉松赛事报名系统一度拥挤参赛者讨论最多的是补给站设置和完赛奖牌设计, ] def tokenize(text): # 自定义分词器只保留长度大于1的词去掉空格和单字 return [w for w in jieba.cut(text) if w.strip() and len(w.strip()) 1] vectorizer TfidfVectorizer(tokenizertokenize, token_patternNone, min_df1) tfidf_matrix vectorizer.fit_transform(corpus) feature_names vectorizer.get_feature_names_out() def top_keywords_tfidf(doc_index, top_k8): row tfidf_matrix[doc_index].tocoo() scores sorted(zip(row.col, row.data), keylambda x: x[1], reverseTrue)[:top_k] return [(feature_names[idx], round(score, 4)) for idx, score in scores] for i in range(len(corpus)): print(f第{i1}篇:, top_keywords_tfidf(i))这里最关键的一行是tokenizertokenize。如果不自定义分词器sklearn 默认会拿英文正则切词对中文只能按字符或空格处理结果基本不可用。我另外设置了min_df1意思是只要在至少 1 篇文档里出现就保留进词表对于小批量语料这样信息损失最少。实际跑出来的结果大概是第一篇会优先输出“citywalk”“多巴胺”“穿搭”“博主”“建筑”这些词第二篇则会把“咖啡”“手冲”“冷萃”“品类”“消费趋势”排到前列。需要注意一点这里的 IDF 是基于这 6 篇文档计算的换成更大的语料库时结果会随之改变这是 TF-IDF 的固有特性。所以工程上如果打算长期给新文章打关键词最好先把大批历史文档跑一遍把 IDF 词典保存下来再给新文章打分而不是每来一篇新文本就重新全量训练。3.3 用 TextRank 作为单篇文本的补充如果只想给单篇文章提取关键词TF-IDF 就不太合适了这时候交给 TextRank。jieba 自带了一个方便的实现直接调就行import jieba.analyse text 新款智能手机主打影像能力长焦镜头在演唱会场景中的表现成为测评博主最关注的指标。 keywords jieba.analyse.textrank( text, topK8, withWeightTrue, allowPOS(n, ns, nt, nz, vn) ) for word, weight in keywords: print(word, weight)withWeightTrue会返回每个词的权重方便后面做融合排序。allowPOS参数里我保留了常见的名词性标签vn是动名词比如“测评”“拍摄”这类在文本里很有信息量的词。实际输出里“影像”“长焦”“演唱会”“测评”“博主”这些词会排得比较靠前。jieba 的 textrank 实现里默认共现窗口是 5词性过滤规则也可以调整。我想多说一句别看这个接口简单使用时一定要清楚它背后的图排序机制。TextRank 对“文本内部共现关系”的依赖很强如果一篇文本从头到尾都是流水账句子之间关联很弱那 TextRank 的输出质量也会明显下降。短文本尤其容易出现这种情况后面我会专门讲短文本要怎么处理。3.4 多方法融合打分简单却有效的工程做法在实际项目里我不太喜欢只依赖单一方法因为 TF-IDF 的文档频率和 TextRank 的局部结构各有优势融合起来往往更稳定。一个很朴素的融合方式是把两种方法各自输出的排名映射成分数再相加。具体做法如下from collections import defaultdict def rank_to_score(keywords): # keywords: [(word, weight), ...] score {} for pos, (word, weight) in enumerate(keywords): score[word] 1.0 / (pos 1) return score tfidf_scores rank_to_score(top_keywords_tfidf(3, top_k10)) textrank_scores rank_to_score(jieba.analyse.textrank(corpus[3], topK10, withWeightTrue)) merged defaultdict(float) for word, score in tfidf_scores.items(): merged[word] score for word, score in textrank_scores.items(): merged[word] score final sorted(merged.items(), keylambda x: x[1], reverseTrue)[:8] print(final)用排名位置代替原始权重的好处是TF-IDF 的权重和 TextRank 的权重量纲差很多直接相加会被大数值那方带偏而排名是相对序更均衡。我见过不少团队在融合时纠结权重系数实际上没有标准答案更多要靠人工抽检来判断哪边该多一点。这个简单融合规则虽然不 fancy但工程性价比非常高。4. 工程落地中的踩坑清单与调优经验4.1 停用词表要领域化而不是拿通用表凑合停用词表这个问题我在文章前面已经提过几次现在展开具体说。网上很多开源停用词表都只覆盖“的了啊吧个”这类高频虚词拿到真实业务语料里根本不够用。比如很多内容型产品里“进行”“内容”“方式”“相关”“问题”都是高频词但它们对关键词的区分度很低。如果你不做领域化处理这些词就会长期占着榜单位置把真正的热点词挤下去。我现在的做法是分三步构造领域停用词表第一步先跑一个通用停用词表看结果第二步把短期语料里 Top 100 的高频词统计出来去掉词性为名词且具有明显业务含义的词第三步把剩下的高频但无语义贡献的词人工抽检一遍放进自定义停用词表。刚开始这个表可能只有几十个词但积累三个月后会变得非常贴合业务语料后面的术语挖掘、关键词质量提升都受益。还要注意一个度的问题停用词表不是越大越好。有些人为了追求“干净”把大量中频词也塞进去结果真正的领域术语不小心被删掉了。我见过一个项目把“模型”“平台”“系统”这种词都加进停用词表后来做技术内容分析时刚好把最核心的三个词全过滤了结果输出完全失真。所以停用词表的构建要保守优先处理真正无信息量的高频词。4.2 词性过滤的粒度直接影响结果可读性词性过滤值得单独讲一遍因为它的影响常被低估。同样是“跑步”这个词在不同词性体系里可能是动词也可能是名词如果不加以限制动词会大量涌入结果。试想一篇体育新闻里“参加”“举办”“出现”“进行”这些动词高频且无语义代表性而“马拉松”“补给站”“完赛奖牌”才是读者真正关心的内容。在 jieba 的词性体系里我常用的保留标签有n普通名词、ns地名、nt机构名、nz专名、vn动名词。对于新闻类、产品类语料用这套组合效果比较好。穿搭类、生活方式类语料则建议额外保留a形容词否则“多巴胺”“复古”“甜酷”这类核心描述词会全部丢掉。评论类短文本里动词可能也有价值比如“踩雷”“回购”“拉黑”这类行为词但这就要看具体业务目标了没有固定答案。这里我强烈建议做一次小规模对比实验同样一批语料分别用“仅名词”和“名词形容词动名词”跑一遍把你觉得输出更符合预期的那版配置写进配置文件而不是固化在代码里。这样后面语料变化时运营或算法同学可以快速调整不用动代码。4.3 短文本场景从关键词提取退化为标签抽取短文本是关键词提取里的老大难。我最早处理用户评论时一条评论就五六行字TF-IDF 基本没法跑TextRank 也因为窗口内共现词太少而表现平平。比如一条评论是“这家餐厅的咖喱鸡味道不错服务也热情”把它分成词后有语义特征的候选词可能只有“咖喱鸡”“味道”“服务”。这种情况下你要的不是“关键词排序”而是“从这几个词里挑出两三个能当标签的词”。我的处理思路有两种。第一种是“预计算 IDF”先用一个大语料库比如全量历史的评论或文章计算好每个词的 IDF然后单条短文本进来时直接用词频乘以预计算的 IDF就可以得到一个相对正常的分数。第二种是“短语块抽取”把连续的高价值名词块整体抽出不做排序只做“是否包含”的判断。比如上面那条评论我可以直接输出“咖喱鸡”作为标签候选。如果用 RAKE 中文改造思路短文本效果会好一些因为短语块天然比单词更能承载语义。但要记住短文本里没有足够上下文任何算法都无法凭空还原主题所以降低预期很重要。实际项目里我会把短文本关键词提取当做一个“标签召回”环节而不是“关键词排序”环节后续靠规则过滤或人工抽检来把关。4.4 新词与领域专词的补充策略另一个我反复踩的坑是领域专词和新词。网络热词每天都有新的产品名词、地名、活动名也是层出不穷。比如“citywalk”“多巴胺”“搭子”这类词如果分词词典里没有就会被切成碎片关键词提取结果自然丢了最关键的信息。而且这类词的 IDF 通常很高一旦被正确切出立刻能冲到关键词榜前列影响非常大。解决办法不是指望分词器自动识别而是主动维护一份自定义词典。jieba 加载用户词典很简单import jieba jieba.load_userdict(user_dict.txt)user_dict.txt 的格式是每行一个词后面可以跟词频和词性比如citywalk 10 nz 多巴胺 10 n 完赛奖牌 5 n词典内容可以从业务高频词表、搜索日志、用户画像词表里汇总整理。另一个相对进阶的方法是定期拿一段时间的语料做新词挖掘比如统计相邻字对的共现频率和凝聚度把超过阈值的字对抽出来作为候选新词人工审核后再加进词典。这个方法不复杂但非常实用能让整个关键词提取系统跟得上内容的更新速度。记住一个原则关键词提取的效果上限由分词结果决定。词典不完善后面所有算法都只是在可能已经错误切分的序列上做补救效率极低。5. 效果评估与从关键词到业务应用的延伸5.1 无标注语料下怎么判断提取质量关键词提取不像分类任务那样有明确标注集很多人做完以后不知道该怎么评价效果。如果没有标注数据我会用几个代理指标来观察质量第一个是可读率随机抽 30 到 50 条结果人工判断这些关键词是否存在明显无语义、错切分、语义偏题的情况第二个是去重率看 Top N 里面有没有大量同义词或重复概念的词比如“咖啡”和“咖啡豆”有时候会同时出现需要业务决定是否保留第三个是覆盖率拿一批文档的关键词去和已知的主题或类目做匹配能匹配上的比例越高说明提取结果和业务方向越一致。有标注数据时评估就更直接了。可以给一批文档标注出人工认为最关键的词然后去和算法输出的 Top N 做比对计算精确率、召回率和 F1。精确率是“算法提的词里有多少是人工认可的”召回率是“人工标注的词里算法抓到了多少”。不过标注成本比较高而且每个人的主观判断差异很大所以我一般只在核心场景做小规模标注日常还是靠人工抽检。5.2 关键词不只是结果搜索索引、文章聚类与推荐很多人觉得关键词提取做完交个 Top N 列表就结束了。但实际业务里关键词的价值远不止这点输出。我常把它当成一个基础组件再往上做三件事第一文档聚类和话题聚合把几万篇文章的关键词共现关系算出来就能把主题相近的内容分到一组哪怕它们用词风格完全不同第二搜索引导和标签推荐根据用户已阅读文章的关键词推荐给他可能感兴趣的其他文章或标签第三内容摘要辅助从一篇长文中抽出核心关键词再结合原文关键句基本就能形成一篇可读的内容摘要。这里我分享一个做内容相关推荐的实际经验用关键词共现去代替整篇文档的向量相似度效果比单纯用 TF-IDF 向量算余弦相似度要好。原因很简单关键词已经做过一波降噪保留的是文章里信息量最大的词噪声比原始向量少很多。实现上就是把文档关键词映射成稀疏向量然后按余弦相似度找近邻成本很低却能解决很多推荐冷启动的问题。我在实际使用中发现关键词提取的稳定性比你想象的更重要。无论用不用融合算法、要不要上深度模型一个经过充分调优的分词器加一套合理的过滤规则往往就能跑赢那些听起来很高端的复杂模型。关键是每一步都要有明确的业务目标不能为了“换算法”而换算法。最后再分享一个小技巧把 stopwords、自定义词典、词性配置、融合权重这些参数都抽出来放到配置文件里不要散落在脚本各处。内容语料一变直接调配置重新跑一轮就行省下的时间足够你多做好几轮人工抽检了。中文文本关键词提取没有一劳永逸的方案它就是一条反复调参、持续沉淀词典和规则的路走顺了以后你会发现自己对数据特点的理解比任何单独方法都值钱。