ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Python中文关键词抽取:TF-IDF、TextRank与BERT三种路线选型指南

Python中文关键词抽取:TF-IDF、TextRank与BERT三种路线选型指南

简介:这份资源面向Python中文文本处理的学习者与课程设计者,围绕关键词抽取这一典型NLP任务,系统整理了TF-IDF、TextRank和Word2Vec词向量聚类三种实现方法,并配有原理说明、流程梳理与代码讲解,适合作为课程设计、毕业设计或NLP入门实践的参考。压缩包共31个文件,约1.78MB,包含4个py源码脚本、14个csv数据与结果文件、8张png运行截图、2个txt词表与词性标注参考、1个docx课程论文及README说明,覆盖从数据、代码到论文与截图的完整链路。已有2116人学习下载。读者可据此掌握三种关键词抽取方法的差异与适用场景,直接复用源码与样例数据跑通实验,并借助论文与截图理解实验设计思路;文中还提示了Word2Vec语料扩充、标题权重、聚类数设置及高频词过滤等可改进细节,便于进一步优化与排错。

1. 中文关键词抽取:三种 Python 路线到底该怎么选

手上有一批中文文本——可能是用户评论、工单记录、论文摘要或者商品标题——老板让你"把关键词提出来"。你打开搜索引擎搜"Python 中文关键词抽取",跳出来一堆方案:jieba、TF-IDF、TextRank、LDA、BERT……每个都有人写教程,但没人告诉你什么场景该用哪个。这个标题讲的正是这件事:用 Python 把中文文本里的关键词抽出来,并且给出三种可落地的实现路线。它解决的不是"能不能抽"的问题,而是"抽得准不准、跑得快不快、维护成本高不高"的工程取舍。适合已经会写基础 Python、手里有真实中文语料、需要快速搭一套关键词管线的从业者。下面我按从轻到重的顺序,把三种方法的原理、代码、参数和踩坑点逐一拆开。

2. 三种关键词抽取路线的原理与选型逻辑

2.1 统计路线:TF-IDF 到底在算什么

TF-IDF 的核心思想很朴素:一个词在当前文档里出现得越多(TF 高),同时在所有文档里出现得越少(IDF 高),它就越可能是这篇文档的关键词。公式拆开看:

  • TF(词频):某词在单篇文档中的出现次数,常见做法是取对数平滑,避免长文档里高频词权重爆炸。
  • IDF(逆文档频率):log(总文档数 / 包含该词的文档数),分母加 1 防止除零。

最终得分 = TF × IDF。这个方法的致命前提是:你需要一个文档集合,而不是单篇文本。只有一篇文本时 IDF 退化成常数,TF-IDF 就变成了纯粹的词频统计,效果大打折扣。很多教程拿一段话跑 TF-IDF 然后说"效果不好",问题就出在这里——不是算法不行,是输入不满足前提。

TF-IDF 的优势在于速度极快、可解释性强、无需训练。缺点是它完全基于词频,不理解语义。"苹果"和"梨"在它眼里毫无关系,同义词无法合并,而且对分词质量极度敏感。

2.2 图模型路线:TextRank 怎么把词排成图

TextRank 借用了 PageRank 的思路:把每个词看作图中的一个节点,如果两个词在滑动窗口内共现,就在它们之间连一条边。然后迭代计算每个节点的重要性——一个词如果被很多重要词"投票"(共现),它自己也重要。

关键参数有三个:

参数含义常用值影响
window共现窗口大小2~5越大越能捕捉远距离关联,但噪声也增多
alpha阻尼系数0.85控制随机跳转概率,一般不改
max_iter最大迭代次数100~200太小不收敛,太大浪费算力

TextRank 不需要外部文档集合,单篇文本就能跑,这是它比 TF-IDF 更实用的地方。但它对短文本效果一般——窗口内共现次数太少,图太稀疏,排序结果不稳定。

2.3 深度学习路线:BERT 词向量做语义关键词

前两种方法都是"词袋"思路,丢掉了上下文。BERT 类模型可以把每个词映射成一个包含上下文信息的稠密向量,然后通过计算词向量与全文向量的相似度来打分。常见做法是:

  1. 用预训练模型对文本编码,取最后一层或倒数第二层的 token 向量。
  2. 对 token 向量做均值池化得到文档向量。
  3. 计算每个 token 向量与文档向量的余弦相似度,作为关键词得分。
  4. 对 token 做词边界还原(因为 BERT 用的是子词切分),合并同一词的子词得分。

这条路效果最好,但代价也最大:需要 GPU 或较长的 CPU 推理时间,模型文件动辄几百 MB,部署复杂度高。适合对精度要求高、有算力预算、且文本语义复杂的场景。

选型建议:如果只有单篇或少量文本、追求快速上线,优先 TextRank;如果有文档集合且需要批量处理,TF-IDF 性价比最高;如果语义歧义严重(比如"银行"是金融机构还是河岸),再上 BERT。

3. 用 jieba 跑通 TF-IDF 与 TextRank 的最小命令

3.1 环境准备与分词基线

先把环境搭好。Python 3.8 及以上都行,核心依赖只有 jieba:

pip install jieba

如果你还需要做词性过滤(比如只保留名词和动词作为候选词),再装一个:

pip install jieba posseg

分词是所有中文关键词抽取的第一步,分词质量直接决定后续效果。jieba 提供三种分词模式:

import jieba import jieba.posseg as pseg text = "自然语言处理是人工智能领域的一个重要方向,中文分词是自然语言处理的基础任务。" # 精确模式:适合文本分析 words = jieba.lcut(text) print("精确模式:", words) # 全模式:把所有可能的词都切出来,适合搜索引擎召回 words_full = jieba.lcut(text, cut_all=True) print("全模式:", words_full) # 搜索引擎模式:在精确模式基础上对长词再切分 words_search = jieba.lcut_for_search(text) print("搜索模式:", words_search) # 词性标注:过滤掉虚词 for word, flag in pseg.cut(text): if flag in ('n', 'vn', 'v'): # 只保留名词、动名词、动词 print(f"{word}/{flag}", end=" ")

逻辑说明:lcut返回列表,cut返回生成器。词性标注用pseg.cut,flag 是词性标签,n代表名词,v代表动词,vn是动名词。实际做关键词抽取时,我一般会过滤掉"的、了、是、在"这类停用词和虚词,只保留实词作为候选。

参数说明:cut_all=True会把"自然语言"切成"自然"、"语言"、"自然语言",召回高但噪声大;lcut_for_search适合做搜索索引,对关键词抽取来说精确模式通常够用。

3.2 TF-IDF 抽取:语料准备与参数调优

TF-IDF 需要多篇文档。假设你有一个docs列表,每个元素是一篇文本:

import jieba.analyse # 模拟语料:至少 5 篇以上,IDF 才有意义 docs = [ "机器学习是人工智能的核心,深度学习是机器学习的一个分支。", "自然语言处理让计算机理解人类语言,中文分词是基础任务。", "Python 是数据科学最常用的编程语言,拥有丰富的机器学习库。", "关键词抽取可以从文本中提取核心信息,常用于搜索和推荐。", "深度学习模型需要大量数据和算力,训练成本较高。", ] # 第一步:用所有文档构建 IDF 语料 # jieba 的 extract_tags 支持传入 idf 频率文件,但更常见的做法是 # 先用所有文档内容喂给 jieba,让它统计词频 corpus = " ".join(docs) jieba.analyse.set_idf_path("idf.txt") # 可选:自定义 IDF 文件 # 第二步:对目标文档抽取关键词 target = "自然语言处理中的关键词抽取方法对比,包括 TF-IDF 和 TextRank。" keywords = jieba.analyse.extract_tags( target, topK=10, # 返回前 10 个关键词 withWeight=True, # 同时返回权重 allowPOS=('n', 'vn', 'v') # 只保留指定词性 ) for word, weight in keywords: print(f"{word}\t{weight:.4f}")

逻辑说明:extract_tags内部会先分词,再计算每个词的 TF-IDF 值,最后按权重排序返回 topK。withWeight=True让你能看到每个词的得分,方便设阈值过滤。allowPOS是词性白名单,能有效去掉"的"、"了"这类无意义高频词。

参数说明:topK控制返回数量,一般 5~20 之间;allowPOS建议至少包含n和vn,如果文本中动作描述重要再加上v。注意 jieba 自带的 IDF 语料是通用领域的,如果你的文本是垂直领域(比如医疗、法律),最好用自己的语料重新统计 IDF,否则领域专有词会被低估。

3.3 TextRank 抽取:窗口大小怎么定

TextRank 的调用方式和 TF-IDF 几乎一样:

import jieba.analyse text = "关键词抽取是自然语言处理的重要任务。TextRank 算法基于图模型,通过词语共现关系计算重要性。与 TF-IDF 不同,TextRank 不需要外部语料。" keywords = jieba.analyse.textrank( text, topK=8, withWeight=True, allowPOS=('n', 'vn', 'v') ) for word, weight in keywords: print(f"{word}\t{weight:.4f}")

逻辑说明:textrank内部构建词共现图,默认窗口大小为 5,迭代 100 次。返回结果按 PageRank 值降序排列。

参数说明:jieba 的textrank接口没有直接暴露 window 参数,它写死在源码里是 5。如果你需要调整窗口,得改 jieba 源码或者自己实现图构建。我一般不改,因为 5 在多数中文文本上表现稳定。真正需要调的是allowPOS——TextRank 对词性更敏感,如果不过滤,虚词会在图中形成高连接节点,把真正的关键词挤下去。

提示:TextRank 在短文本(少于 50 字)上容易翻车,因为共现图太稀疏。如果文本很短,建议直接用 TF-IDF 或者补充外部语料。

4. 避坑与排查:中文关键词抽取的五个血泪教训

4.1 分词错误导致关键词完全跑偏

现象:抽取出的关键词里出现了"的自"、"了机"这种明显不该存在的词。

原因:jieba 默认词典不含你的领域专有词,导致切分错误。比如"机器学习"被切成"机器"和"学习","自然语言"被切成"自然"和"语言"。

解决:加载自定义词典。把领域词写成一个文本文件,每行一个词,然后:

jieba.load_userdict("user_dict.txt")

词典格式是"词语 词频 词性",词频和词性可省略。加载后 jieba 会优先按你的词典切分。另外可以用jieba.suggest_freq动态调整某个词的成词概率:

jieba.suggest_freq(('机器学习',), True) # 强制"机器学习"作为一个词

4.2 IDF 语料不匹配导致领域词被淹没

现象:在医疗文本中,"患者"、"疗效"这些词排不到前面,反而"进行"、"表示"排得很高。

原因:jieba 自带 IDF 是基于通用新闻语料统计的,在垂直领域里词频分布完全不同。

解决:用自己的语料重新计算 IDF。步骤是:收集至少几百篇同领域文档,用 jieba 分词后统计每个词出现在多少篇文档中,然后按 IDF 公式算出每个词的 IDF 值,写成 jieba 要求的格式:

import math import jieba from collections import Counter docs = [...] # 你的领域文档列表 doc_count = len(docs) word_doc_freq = Counter() for doc in docs: words = set(jieba.lcut(doc)) for w in words: word_doc_freq[w] += 1 with open("custom_idf.txt", "w", encoding="utf-8") as f: for word, freq in word_doc_freq.items(): idf = math.log(doc_count / (freq + 1)) f.write(f"{word} {idf:.6f}\n")

然后jieba.analyse.set_idf_path("custom_idf.txt")加载。注意文件格式是"词语 空格 IDF值",每行一个。

4.3 TextRank 在单句上返回空结果

现象:输入一句话,textrank返回空列表或只有一两个词。

原因:共现窗口内没有足够的词对,图几乎是空的,PageRank 迭代无法产生有意义的排序。

解决:短文本场景换用 TF-IDF,或者手动扩大窗口。如果坚持用 TextRank,可以把多段相关文本拼在一起再跑,人为增加共现机会。

4.4 停用词表不全会引入噪声

现象:关键词里混入了"我们"、"可以"、"这个"这类词。

原因:allowPOS过滤了词性,但有些代词和助词被 jieba 标注成了名词或动词。

解决:维护一份停用词表,在抽取结果上再做一层过滤:

stopwords = set() with open("stopwords.txt", encoding="utf-8") as f: for line in f: stopwords.add(line.strip()) keywords = [(w, s) for w, s in keywords if w not in stopwords]

停用词表可以从网上找通用版本,再根据自己领域补充。我一般会把"进行"、"表示"、"问题"、"方法"这类在技术文档中高频但无区分度的词也加进去。

4.5 权重阈值设错导致关键词过多或过少

现象:topK=10返回的词里有一半不相关,或者只返回 3 个就没了。

原因:不同文本的关键词分布差异很大,固定 topK 不灵活。

解决:改用权重阈值。先跑一次看权重分布,然后设一个截断值:

keywords = jieba.analyse.extract_tags(text, topK=50, withWeight=True) # 观察权重分布后设阈值 threshold = 0.1 filtered = [(w, s) for w, s in keywords if s >= threshold]

或者用"权重下降拐点"法:把权重从高到低排列,找到下降最快的那个位置作为截断点。

5. 进阶技巧:用 BERT 词向量做语义关键词抽取

前面三种方法(TF-IDF、TextRank、以及它们的变体)本质上都是基于词频或共现统计的,遇到同义词和多义词就会翻车。比如"苹果发布了新手机"和"苹果是一种水果",统计方法无法区分这两个"苹果"。如果你对精度有要求,可以上 BERT 词向量。

核心思路是:用预训练模型把文本编码成 token 向量,然后计算每个 token 与全文的语义相似度,相似度高的就是关键词。这里用transformers库配合一个中文预训练模型:

from transformers import BertTokenizer, BertModel import torch import numpy as np # 加载中文 BERT 模型和分词器 model_name = "bert-base-chinese" tokenizer = BertTokenizer.from_pretrained(model_name) model = BertModel.from_pretrained(model_name) model.eval() text = "关键词抽取是自然语言处理的重要任务,BERT 模型可以捕捉上下文语义信息。" # 编码 inputs = tokenizer(text, return_tensors="pt", truncation=True, max_length=512) with torch.no_grad(): outputs = model(**inputs) # 取最后一层的 token 向量 hidden_states = outputs.last_hidden_state[0] # shape: (seq_len, hidden_dim) # 计算文档向量(均值池化,跳过 [CLS] 和 [SEP]) token_vectors = hidden_states[1:-1] # 去掉特殊 token doc_vector = token_vectors.mean(dim=0) # 计算每个 token 与文档向量的余弦相似度 def cosine_sim(a, b): return torch.dot(a, b) / (torch.norm(a) * torch.norm(b)) scores = [] tokens = tokenizer.convert_ids_to_tokens(inputs["input_ids"][0][1:-1]) for i, vec in enumerate(token_vectors): score = cosine_sim(vec, doc_vector).item() scores.append((tokens[i], score)) # 按得分排序 scores.sort(key=lambda x: x[1], reverse=True) # 合并子词(BERT 用 ## 表示子词续接) merged = [] for token, score in scores: if token.startswith("##") and merged: merged[-1] = (merged[-1][0] + token[2:], max(merged[-1][1], score)) else: merged.append((token, score)) for word, score in merged[:10]: print(f"{word}\t{score:.4f}")

逻辑说明:last_hidden_state是 BERT 最后一层的输出,每个 token 对应一个 768 维向量。均值池化得到文档向量后,用余弦相似度衡量每个 token 与全文的语义相关度。子词合并是因为 BERT 的 tokenizer 会把一个中文词拆成多个子词,比如"关键词"可能被拆成"关"、"键"、"词",需要还原。

参数说明:max_length=512是 BERT 的最大输入长度,超长文本需要截断或分段。model_name可以换成更大的模型(如bert-large-chinese)或领域预训练模型,但推理时间会成倍增加。如果显存不够,可以用model.half()转半精度,或者用 ONNX 加速推理。

这条路的效果确实比统计方法好,但代价也明显:单条文本推理在 CPU 上可能要几百毫秒到几秒,GPU 上也要几十毫秒。如果你的场景是离线批量处理,可以接受;如果是在线实时抽取,需要做模型蒸馏或换用更小的模型。

我自己的习惯是:先用 TextRank 快速跑一版看效果,如果关键词质量能接受就不折腾 BERT;如果发现同义词、多义词问题严重,再上 BERT 做精排。不要一上来就上最重的方案,很多时候统计方法加一个好的自定义词典就能解决 80% 的问题。希望帮到你。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进