中文关键词抽取三大方法:TF-IDF、TextRank与深度语义

发布时间:2026/10/3 18:14:23
中文关键词抽取三大方法:TF-IDF、TextRank与深度语义 简介压缩包基于Python实现了中文文本关键词抽取的三种经典方法——TF-IDF、TextRank与Word2Vec词向量聚类面向自然语言处理初学者、Python开发者以及需要完成课程设计的学生帮助系统理解关键词抽取的原理、完整流程与代码写法。资源共31个文件包含课程论文Word文档、Python源码、实验CSV数据、停用词表、运行截图与说明文档等压缩包整体仅1.78MB目录按论文、代码、数据、截图分区查阅方便已有2115人学习下载。除核心实现外资源还梳理了实验中的可改进细节例如扩充专业语料训练Word2Vec、对标题关键词赋予初始权重、按文本类别数调整KMeans的n_clusters参数以及过滤文档中的高频无意义词。这些内容既有助于入门者理解三种抽取思路的差异也能为课程设计答辩或进一步优化提供参考。1. 从“词频”到“语义”中文关键词抽取到底在解决什么问题中文文本关键词抽取是 Python 文本挖掘入门第一个绕不开的落地任务。无论是做舆情分析还是 Python 数据分析与可视化拿到一堆新闻、评论或工单后第一步都是把每篇浓缩成几个词。这篇笔记按标题里的“三种方法”往下拆基于 TF-IDF 的词频统计法、基于 TextRank 的图迭代法、基于预训练模型的深度语义法。它们分别解决“哪几个词在文档里最刺眼”“哪几个词跟上下文关系最紧”“哪几个词语义上真的在讲核心”。我会顺着“原理—代码—参数—坑”的顺序写新手能跟着跑出第一版结果熟手也能直接拿走调参清单。2. 方法一基于 TF-IDF 的中文关键词抽取分词、权重与三个必调参数2.1 TF-IDF 的原理为什么“词频”不能单独当关键词依据如果只按词频中文文本里“的”“是”“我们”永远排在最前面因为它们高频且没有含义。TF-IDF 的朴素思想是一个词在当前文档出现得多但放到整个语料里很少出现才说明它“能代表这篇文章”。TF 是词频一个词在文档里出现的次数除以总词数IDF 是逆文档频率总文档数除以包含这个词的文档数再取对数。两者相乘得到的权重就是“刺眼”的程度。中文没有自然空格所以做 TF-IDF 之前必须分词。Python 生态里 jieba 是最常用的中文分词库一般流程是分词 - 过滤停用词 - 统计 TF-IDF - 取前 N 个。注意这种方法是“用统计信息抽关键词”它不关心词的语义只看分布适合长文档缺点是容易抽出一堆术语。需要提醒一点很多人在这一步抄了一段 sklearn 的 TfidfVectorizer 代码直接传中文原文结果向量化出来的是一堆“字”级的特征因为 TfidfVectorizer 默认按空格或标点切分。标准做法要么先 jieba 分词再用空格 join要么就用 jieba.analyse 自带的 TF-IDF 接口后者对中文的默认语料和分词都做了适配。2.2 用 jieba 跑通最小可复现代码从文本到中文关键词列表我用一段景区评论作为示例文本这是身边最常见的文本场景直接复制就能跑。import jieba import jieba.analyse text 杭州西湖风景区就在市中心游客可以免费游览但节假日人非常多。 建议提前预约门票坐地铁一号线直达附近还有灵隐寺和龙井茶园。 keywords jieba.analyse.extract_tags( text, topK10, # 只取前10个结果 withWeightTrue, # 输出权重方便看每个词的得分 allowPOS(ns, n, vn, v, a, nt) # 限制词性避免虚词上榜 ) for word, weight in keywords: print(f{word}\t{weight:.4f})这段代码用 extract_tags 接口它在 jieba 内部完成分词、词性标注、IDF 加权最后按权重从高到低返回。输出类似“西湖”“杭州”“游客”“免费”“预约”“龙井”等。注意“建议”这个词可能因为词性是 vn 而进入候选也可能因为参数里只写了 “vn” 没有写 “v”如果词性标注为 “v”它就会被卡掉这就是调词性参数时最常见的副作用。逻辑说明extract_tags 的 allowPOS 不是硬过滤而是先过滤再取 topK所以你把 topK 设成 30后面的结果会有大量噪声词。withWeightTrue 时返回的是 (word, weight) 的二元组列表如果要生成纯关键词列表只要[w for w, wf in keywords]即可。2.3 让结果更可控的三个必调参数topK、allowPOS 与停用词/自定义词典第一个参数是 topK。对一篇文章来说5~10 个关键词足够如果是批量生成标签可以放宽到 15但太多会让标签重复率上升。不要以为 topK 越大越保险它在关键词抽取里是“顺位截断”不是“打分收紧”。第二个参数是 allowPOS我常用的是 ns/n/vn/v/a/nt也就是地名、名词、动名词、动词、形容词、机构名。但词性受分词器版本影响同一句话在不同环境里的标注可能不一样所以看到结果里出现“了”“的”时先看词性再决定是放宽还是收紧。第三个参数其实是两个动作停用词和自定义词典。extract_tags 没有现成的 stop_words 参数所以我在线上项目里的做法是先抽 30 个候选再用停用词集合过滤stopwords {我们, 自己, 可以, 就是, 这个} raw jieba.analyse.extract_tags(text, topK30, withWeightTrue) filtered [(w, wt) for w, wt in raw if w not in stopwords][:10]注意不要从网上抄一份大停用词表一上来就全套很多行业词会被误杀。我一般先按“无实义词 高频代词 通用动词”做一个最小集合等跑完 500 条真实文本再迭代补充。自定义词典的入口是 jieba.add_word 或 userdict.txt。jieba.add_word(灵隐寺, freq10000, tagnt)如果发现“灵隐寺”被拆成“灵隐”和“寺”加这一行再用 extract_tags结果会立刻改观。副作用是 freq 给太高会让这个词在 TF 阶段拿到过多先验权重导致不管什么文本都抽它。常见做法是只给 tag 和正常频次让分词器认识这个词就行权重交给真实文本里的出现次数来算。新词还有一个更隐蔽的问题IDF 词典里没有它。jieba 自带的 idf.txt 覆盖的是通用语料某个垂直领域的词比如“龙井茶园”在 IDF 词典里不存在时它的逆文档频率会被当成默认值偏小结果排名靠后。想根治就要用领域语料重训 IDF下一篇我再展开先知道这个坑在哪。3. 方法二基于 TextRank 的中文关键词抽取图模型把“共现关系”变成权重3.1 TextRank 与 TF-IDF 的本质区别图迭代和词频统计差在哪TF-IDF 把每个词当成独立的点只看“分布”TextRank 则把词放在一张共现图里。先把文章分词、去停用词然后设定一个滑动窗口窗口内任意两个词算“共现”在图上连一条边。接着开始迭代计算各节点的权重一个词的权重不仅来源于自己出现的次数还来源于邻居词传过来的权重。用一句话概括如果一个词总出现在重要词的旁边那它也重要。这个思想脱胎于 PageRank但把网页换成了词把链接换成了共现窗口。它不再需要整个语料的 IDF所以对单文档、小文本更友好。对中文来说TextRank 的输入和 TF-IDF 一样都要过 jieba 分词但算法核心是图计算。优点是它能抓住“一直围绕某个概念展开”的主题词代价是它容易漏掉低频但点题的关键词因为共现次数太低。3.2 用 jieba.analyse.textrank 跑通代码参数与窗口调整TextRank 在 jieba 里也有现成接口代码比 TF-IDF 更简洁import jieba import jieba.analyse text 人工智能正在赋能制造业很多工厂开始用机器学习做质检。 这个场景里数据质量比算法模型更重要标注数据是最大的瓶颈。 keywords jieba.analyse.textrank( text, topK10, # 取前10个 withWeightTrue, # 返回权重 allowPOS(ns, n, vn, v), span5 # 共现窗口半径 ) for word, weight in keywords: print(f{word}\t{weight:.4f})参数说明这个接口没有停用词参数实际它内部有一个默认停用词表但很小可能漏掉“这个”“很多”“开始”这类词所以抽出来的 raw 结果要再过滤一遍。span 是 TextRank 自己特有的窗口参数表示节点与节点连接时向前/向后看几个词。span 越小图里的边越短更强调相邻共现span 越大边越稀疏更接近全局词频。默认值是 5短文本可以调到 3长文档不要超过 8否则边太多迭代权重会趋向平均。每次返回的 weight 是迭代收敛后的节点分数不是概率跨文本不可比。我看到有人拿 TextRank 和 TF-IDF 的权重直接 1:1 相加这没有意义要先把权重各自做归一化或排名转换。3.3 对比 TF-IDF 的典型场景为什么新闻稿用 TextRank 更合适用同一段话快速对比两个方法。假如文本反复出现“人工智能”“制造业”“数据质量”TF-IDF 会把“人工智能”和“制造业”排前面但也可能把“瓶颈”这种在通用语料里出现得少的词排得很高因为它 IDF 大。TextRank 不会特别喜欢“瓶颈”它只看“瓶颈”周围的邻居——哪怕“瓶颈”只出现一次只要它身边是“数据质量”“模型”“算法”权重也会被抬上去。所以我的经验是新闻稿、政策文件、技术博客这类“通篇围绕一个主题展开”的文本TextRank 的可靠度明显更好它能抽出“人工智能”“制造业”这类主题概念而不是被单个冷门词带跑。反过来如果文本是用户评论、商品描述每段就三四句话共现信息太少TextRank 会退化得像词频统计这时候 TF-IDF 反而更靠谱。一句话词少用 TF-IDF句多段多用 TextRank。4. 方法三基于深度语义的中文关键词抽取让模型读懂上下文4.1 为什么统计方法在口语化短文本上翻车前面两种方法都依赖一个前提文本长度够词和词之间有可统计的“分布规律”。一旦文本只有一条微博——“这也太绝了家人们冲新品奶茶今晚就上线买一送一。”TF-IDF 抽出来的可能是“家人们”“冲”“买一送一”TextRank 因为共现窗口里词太少结果也接近词频榜。但你想要的关键词其实是“新品”“买一送一”甚至“奶茶”这些词的语义和信息密度比“家人们”高得多。统计模型看不到语义它只知道“家人们”出现一次“绝了”出现一次权重一样。深度语义方法先把文档和每个候选词映射成向量再用向量相似度排序。这相当于把“关键词”定义从“这个词在文本里重要”改成了“这个词的语义和整篇文本最接近”。副作用是模型是预训练的黑匣子结果受训练语料影响但通常还是比纯统计方法更接近人对“关键词”的理解。4.2 用 KeyBERT Sentence-BERT 抽取中文关键词的最小实现Python 里最快上手的深度语义抽取是 KeyBERT。它是一个极简库核心逻辑就是“编码文档编码候选词算余弦相似度”。中文环境要先解决分词因为预训练模型不了解汉字序列直接喂原文容易把整句话当成一个 token。我的最小实现如下from keybert import KeyBERT import jieba text 这也太绝了家人们冲新品奶茶今晚就上线买一送一。 candidates .join(jieba.cut(text)) print(candidates) # 这也 太绝了 家人们 冲 新品 奶茶 今晚 就 上线 买一送一 kw_model KeyBERT(modelparaphrase-multilingual-MiniLM-L12-v2) keywords kw_model.extract_keywords( candidates, keyphrase_ngram_range(1, 1), # 候选词只取一个中文词 stop_wordszh, # 启用中文停用词 top_n5, use_maxsumTrue, # 让结果更分散 nr_candidates10 ) for word, score in keywords: print(word, round(score, 4))这段代码做的事情很简单先用 jieba 切词再用空格把词串起来然后交给 KeyBERT 抽取。模型会用 sentence-transformers 把整句话编码成一个向量再把每个候选词也编码成向量最后按余弦相似度返回 topN。第一次运行时模型会从 HuggingFace 下载到本地大约 120MB如果运行环境没有网络这段代码会卡在下载阶段建议提前把模型缓存下来用 cache_folder 参数指定本地路径。参数说明keyphrase_ngram_range 控制候选词由几个词组成中文分词后设 (1,1) 最稳设 (1,2) 可能抽到“新品奶茶”这种很有价值的复合词但也容易把“太绝了家人们”这种粘连短语抽出来。stop_wordszh 是模型自带的中文停用词表能过滤“这”“就”“冲”这类词但不会过滤“家人们”如果你业务里这个也算脏词要在输出后再做一次过滤。use_maxsumTrue 的作用是让返回结果之间的相似度尽量低避免 top5 全是“奶茶”“新品”“饮品”这种同义替换代价是计算量变大。4.3 模型与算力的取舍什么时候值得上深度方法深度语义方法不是银弹。我一般在批量处理几千条短文本时先用 TF-IDF/TextRank 跑一版如果 P5 已经在 70% 以上就不会轻易上 KeyBERT。因为深度方法有两个硬成本一是模型下载、加载、推理单条文本耗时是统计方法的几十倍二是调参维度多模型选择、语言、候选词来源都会改变结果线上问题更难排查。什么时候值得上我在两种场景会优先考虑深度方法一是文本极短但语义密度高评论、Title、搜索结果统计方法基本等于词频榜二是关键词要求“同义表达也能召回”比如用户写“买一送一”你希望也能召回“促销”或“优惠”。这种语义扩展是统计方法做不到的。成本上如果机器没 GPU给 1000 条短文本跑 KeyBERTCPU 大约要 5~10 分钟可以接受如果每天百万级就要想清楚是否需要离线批量算或者换更小的模型比如用 distiluse-base-multilingual-cased 这类轻量多语言模型。5. 避坑排障中文关键词抽取的三个必踩坑与一组自检流程5.1 分词不一致导致同一关键词被拆碎现象同一批文本里“人工智能”有时整体出现有时被拆成“人工”和“智能”关键词列表里稀碎。原因jieba 的动态词库对未登录词和歧义场景处理不稳定尤其是文本来自不同领域时比如“供应链”在金融文本和物流文本里被切分的概率不同。解决准备领域词典 userdict.txt一行一个词人工智能 1000 n、供应链 800 n然后用jieba.load_userdict(userdict.txt)加载。如果只有一个词需要微调也可以用jieba.suggest_freq(人工智能, True)让分词器提高整体切分概率。加完之后再用 extract_tags 跑一遍观察结果是否一致。这一步相当于给分词吃后悔药但别贪多词表太大反而会让新词误分。5.2 停用词表过犹不及把“关键词”都过滤掉了现象有人从网上抄了一份 1000 行的停用词表跑完 topK 一看“问题”“方法”“分析”全被过滤了抽出来的关键词变成“数据”“模型”“结果”而业务真正关心的“问题”却没了。原因通用停用词表是站在全文级去停的它不知道你的行业里“问题”可能就是核心名词比如工单语义里的“问题”代表故障类型。TF-IDF 的 IDF 词典也会对高频名词抬权导致“分析”“方法”这类词长期霸榜。解决不要一上来就全家桶先用最小停用词集合——“的、了、是、在、有、和、就、都、而”这类无实义词再让领域词留在表内。每跑 200 条语料把稳定进入 topK 且确定无意义的词追加进停用词表。要注意加了停用词之后最好重新生成 IDF否则“问题”的 IDF 权重还在即便第一轮被过滤换一篇文章它又回来了。5.3 新词和专有名词识别用户词典的正确打开方式现象新品名、艺人名、地名全抽不出来比如“米哈游”被拆成“米”“哈”“游”。原因分词器的词典是静态的新词和专有名词没有语料先验。解决两个入口一个是直接调用jieba.add_word(米哈游, freq10000, tagn)另一个是把词表写成 userdict.txt 后加载。更稳妥的是在每次启动时都加载同一个 userdict保证线上线下的行为一致。注意 freq 参数会当成词频先验给得过高会让这个词在 TF 阶段刷分影响 TF-IDF 的结果。如果你想让它正常参与 IDF 计算正确做法是先确保分词正确再用领域语料重新整理 IDF 词典而不是只加词不重新统计。5.4 验证方法用人工标注去量化三种方法的效果三种方法都跑完之后你还需要一把尺子否则不知道哪套参数更适合你的业务。最简单有效的自检流程是挑 50 条有代表性的文本人工标出“真正的关键词”然后写一个精确率函数。def precision_at_k(true_set, predicted_list, k): pred predicted_list[:k] if len(predicted_list) k else predicted_list hits sum(1 for w in pred if w in true_set or any(w in t or t in w for t in true_set)) return hits / k if k else 0 true_set {西湖, 旅游, 灵隐寺} pred [西湖, 免费, 游客, 旅游, 灵隐寺] print(precision_at_k(true_set, pred, 5))注意代码里我把命中条件放宽到了“子串包含”这是为了避免人工标了“西湖旅游”机器抽了“西湖”和“旅游”时判成两个都对。实际操作中建议做两张表严格匹配和宽松匹配分别算 PK。不要混用否则你会觉得三种方法都差不多。对短文本我更看重 Top3 的准确率对长文档则结合召回真实关键词有 5 个抽取 10 个命中 3 个召回 0.6。最后把 50 条文本的平均 PK 作为选型依据而不是拿一两个样例拍脑袋这条我踩过太多次印象很深。6. 实战技巧把三种方法的投票融合成一套不容易翻车的抽取流程前面三种方法各有脾气TF-IDF 爱找特色词TextRank 稳但慢深度方法准但贵。线上项目里我习惯把三者按排名做投票融合而不是只选一个。给代码def vote_rank(tfidf_list, textrank_list, bert_list, weights(0.4, 0.4, 0.2), topK10): score {} method_lists [tfidf_list, textrank_list, bert_list] for method, w in zip(method_lists, weights): for rank, word in enumerate(method): if not word or word in stopwords: continue score[word] score.get(word, 0) w / (rank 1) ranked sorted(score.items(), keylambda x: x[1], reverseTrue) return [w for w, s in ranked[:topK]]注意这里的权重不是拍脑袋是用上一章的 50 条人工标注去网格搜索出来的。如果你只想快速试试0.4/0.4/0.2 是合理起点如果算力有限可以先把最后一项深度模型去掉退化成 TF-IDF TextRank 的 0.5/0.5。“排名倒数”这个技巧是为了避免权重不可比因为 TF-IDF 的权重上限和 TextRank 不是一个量级直接用权重相加会导致某一方法霸权。我个人的落地习惯是先在短文本上跑 TextRank 看主题再去 TF-IDF 结果里捞一个最有特色的词做文章标题素材最后感觉不对时用深度方法验证前 N 个词的语义是否和整篇一致。如果发现某个词在所有方法里都不出现但人工标注里有那大概率是分词拆碎了先加用户词典而不是换模型。这个流程帮我处理过很多文本分析的需求。关键词抽取没有银弹先把三种方法跑起来再用自检流程给它们打分你手里的“关键词”才会从玄学变成可复现的产出。希望帮到你。本文还有配套的精品资源点击获取