☰
中文关键词抽取三大方法:TF-IDF、TextRank与深度语义
2026/10/3 18:14:14 网站建设 项目流程

简介:压缩包基于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, topK=10, # 只取前10个结果 withWeight=True, # 输出权重,方便看每个词的得分 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,后面的结果会有大量噪声词。withWeight=True 时返回的是 (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, topK=30, withWeight=True) filtered = [(w, wt) for w, wt in raw if w not in stopwords][:10]

注意不要从网上抄一份大停用词表一上来就全套,很多行业词会被误杀。我一般先按“无实义词 + 高频代词 + 通用动词”做一个最小集合,等跑完 500 条真实文本再迭代补充。自定义词典的入口是 jieba.add_word 或 userdict.txt。

jieba.add_word("灵隐寺", freq=10000, tag="nt")

如果发现“灵隐寺”被拆成“灵隐”和“寺”,加这一行再用 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, topK=10, # 取前10个 withWeight=True, # 返回权重 allowPOS=("ns", "n", "vn", "v"), span=5 # 共现窗口半径 ) 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(model="paraphrase-multilingual-MiniLM-L12-v2") keywords = kw_model.extract_keywords( candidates, keyphrase_ngram_range=(1, 1), # 候选词只取一个中文词 stop_words="zh", # 启用中文停用词 top_n=5, use_maxsum=True, # 让结果更分散 nr_candidates=10 ) 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_words="zh" 是模型自带的中文停用词表,能过滤“这”“就”“冲”这类词,但不会过滤“家人们”,如果你业务里这个也算脏词,要在输出后再做一次过滤。use_maxsum=True 的作用是让返回结果之间的相似度尽量低,避免 top5 全是“奶茶”“新品”“饮品”这种同义替换,代价是计算量变大。

4.3 模型与算力的取舍:什么时候值得上深度方法

深度语义方法不是银弹。我一般在批量处理几千条短文本时,先用 TF-IDF/TextRank 跑一版,如果 P@5 已经在 70% 以上,就不会轻易上 KeyBERT。因为深度方法有两个硬成本:一是模型下载、加载、推理,单条文本耗时是统计方法的几十倍;二是调参维度多,模型选择、语言、候选词来源都会改变结果,线上问题更难排查。

什么时候值得上?我在两种场景会优先考虑深度方法:一是文本极短但语义密度高(评论、Title、搜索结果),统计方法基本等于词频榜;二是关键词要求“同义表达也能召回”,比如用户写“买一送一”,你希望也能召回“促销”或“优惠”。这种语义扩展是统计方法做不到的。成本上,如果机器没 GPU,给 1000 条短文本跑 KeyBERT,CPU 大约要 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("米哈游", freq=10000, tag="n"),另一个是把词表写成 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))

注意代码里我把命中条件放宽到了“子串包含”,这是为了避免人工标了“西湖旅游”,机器抽了“西湖”和“旅游”时,判成两个都对。实际操作中建议做两张表:严格匹配和宽松匹配,分别算 P@K。不要混用,否则你会觉得三种方法都差不多。对短文本,我更看重 Top3 的准确率;对长文档,则结合召回:真实关键词有 5 个,抽取 10 个命中 3 个,召回 0.6。最后把 50 条文本的平均 P@K 作为选型依据,而不是拿一两个样例拍脑袋,这条我踩过太多次,印象很深。

6. 实战技巧:把三种方法的投票融合成一套不容易翻车的抽取流程

前面三种方法各有脾气:TF-IDF 爱找特色词,TextRank 稳但慢,深度方法准但贵。线上项目里,我习惯把三者按排名做投票融合,而不是只选一个。给代码:

def vote_rank(tfidf_list, textrank_list, bert_list, weights=(0.4, 0.4, 0.2), topK=10): 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(), key=lambda x: x[1], reverse=True) 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 个词的语义是否和整篇一致。如果发现某个词在所有方法里都不出现,但人工标注里有,那大概率是分词拆碎了,先加用户词典,而不是换模型。这个流程帮我处理过很多文本分析的需求。关键词抽取没有银弹,先把三种方法跑起来,再用自检流程给它们打分,你手里的“关键词”才会从玄学变成可复现的产出。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询