资讯动态

中文关键词提取实战:TF-IDF与TextRank完整实现与调优指南

发布时间:2026/10/14 8:35:05 来源:尧图企业网站定制
做自然语言处理项目十次里有八次要先过关键词提取这一关。无论是做舆情系统、搜索召回、文本分类预处理还是做自动摘要关键词都是最基础也最实用的信息载体。中文文本的关键词提取看似简单实际上手就是一道坎英文单词天然按空格切分中文呢“自然语言处理”应该是一个词还是三个词“的、了、是”这类虚词怎么处理才算干净这些问题的答案会直接影响后续所有算法效果的上限。如果你正准备在自己的项目里实现一个中文关键词提取模块或者想搞清楚 TF-IDF、TextRank 这些算法在中文场景下的真实表现这篇实战记录应该能帮你少走不少弯路。我会从核心思路讲起对比几种主流方案再给出可以直接跑的完整代码最后把我这些年踩过的坑和排查思路一并整理出来。内容按从浅到深的顺序组织既有原理也有工程细节适合刚接触 NLP 的读者也适合想系统落地关键词模块的工程人员。1. 中文关键词提取的核心思路1.1 真实项目中为什么需要关键词提取很多人会直觉地认为关键词提取就是“把文章里出现次数多的词挑出来”。但真实项目里需求往往是多样的。我在一个客服工单分类系统里需要从用户反馈中抽出“充电慢”“屏幕闪烁”“自动重启”这类症状词用来做问题定位在另一个资讯聚合项目里需要提取“新能源汽车市场趋势”这类话题短语供推荐系统使用还有一个搜索相关的模块要用关键词做同义改写和索引扩充。同样是“关键词提取”不同场景对粒度、数量和召回率的要求完全不同。所以做这个模块之前先问自己三个问题关键词是给机器用的还是给人看的是只取单个名词还是需要能表达完整语义的短语核心诉求是准确还是召回这会直接影响你选什么算法。人和机器的需求差异很典型——给人看的关键词需要符合阅读直觉通常两三个词或四字短语更好给机器建模用的关键词哪怕粒度碎一点只要词表覆盖率高就行。想明白需求后面才不会被“哪个算法更高级”这种问题困住。1.2 中文与英文关键词提取的本质差异先过分词这道坎英文文本做关键词提取预处理相对轻量按空格和标点切分再做词形还原和停用词过滤基本就够。中文没有天然分隔符必须先分词。分词本身就是一个完整的研究方向它会直接影响关键词提取的结果。比如“研究生命科学”“研究/生命/科学”和“研究生/命/科学”完全是两种结果。多数场景下我们会优先选择工程上更稳的分词工具后面会细说但分词产生的误差一定会传导到关键词提取阶段所以排查问题时我永远先怀疑分词而不是先怀疑关键词算法。另一个差异是中文里虚词使用频率极高“的、了、在、是、和”这类词在统计权重时会产生大量噪音必须通过停用词表过滤。此外中文字与字之间没有形态变化不存在英文里的时态、单复数问题做词形还原这步可以省掉但相应地同一个概念的语义变体需要靠同义词映射或词向量来兜底。这些差异决定了中文关键词提取的技术栈整体上是“分词 词性过滤 权重计算”的组合而不是单纯的词频统计。2. 三种主流算法选型统计信心、图排序与向量化2.1 TF-IDF最朴素但最可靠的统计方案TF-IDF 的核心直觉一句话能说清如果一个词在本篇文档里出现得多但在整个语料里出现得少那它对这篇文档就有代表性。公式拆开看词频叫 (TF)在有分词结果后统计某词在当前文档里出现的次数再做归一化一般除以文档总词数避免长文档天然占便宜。逆文档频率叫 (IDF)写作 (IDF \log(\frac{N}{n_i1})1)其中 (N) 是语料文档总数(n_i) 是包含该词 (i) 的文档数。(TF) 乘以 (IDF) 就是最终权重。这里有个关键参数需要理解为什么 (IDF) 要用对数因为文档数可能相差几个数量级直接相除会让数值分布太极端取对数能把这种差异压缩到可比较的区间。这个平滑处理是统计方案里最容易被忽视、又最影响实际效果的细节。TF-IDF 的选型理由很直接计算简单、可解释性强、对长文本效果好。它最大的缺点是依赖一个质量足够好的外部语料来计算 IDF——语料越有代表性IDF 越可靠。如果只有几十篇文档算出来的 IDF 没有统计意义。这种场景下 TextRank 会更合适。基于常见实践经验我给一个参考优先用公开语料预计算好的 IDF 词典比如 jieba 自带的 idf.txt如果你处理的是垂直领域文本再把领域语料和通用词典合并重算。这个做法多数情况下效果最稳。2.2 TextRank不需要外部语料的图排序算法TextRank 的思路源自 PageRank。它把文章里的每个词当成一个节点如果一个词和另一个词在一定窗口大小内共同出现就在两个节点之间连一条边。然后通过迭代计算每个节点的权重最终排序取 TopN。这样做的好处是它不需要 IDF 这类外部统计量单篇文档也能跑天然适合短文本或语料不足的场景。公式上简洁地写节点 (V_i) 的得分为 (S(V_i) (1-d) d \times \sum_{V_j \in In(V_i)} \frac{w_{ji}}{\sum_{V_k \in Out(V_j)} w_{jk}} S(V_j))其中 (d) 是阻尼系数通常取 0.85。这个公式不必死记但要知道几个会影响结果的参数窗口大小一般是 5窗口太小词共现关系稀疏太大引入噪音、是否有权共现次数是否算权重、以及是否过滤词性通常只保留名词、动词、形容词否则虚词会污染图结构。TextRank 的坑在于它选出的词常常偏向“在文章里被反复提到的内容”如果文章本身频繁重复同一个泛化概念比如“问题”这个词会被高估。所以实际落地时我一般会对 TextRank 结果再做一层词性过滤或位置加权标题、首段、段首句本身可以给更高初始权重。2.3 词向量与主题模型什么时候才值得升级TF-IDF 和 TextRank 属于“统计派”核心缺陷是忽略了语义。比如一篇讲“苹果发布新手机”的文章“苹果”和“手机”在语义上的关联没有被建模只按字面共现和频率打分。于是近些年出现基于词向量和主题模型的方案——用预训练向量表示词的语义再通过 TextRank 式图网络上的相似度矩阵来加权或者用 LDA、BERTopic 这类主题模型把文档归到若干主题再选每个主题下权重最高的词作为关键词。我的观点是这两种方案适合的是“关键词不是独立词而是领域概念”的场景。比如医疗病历里“心肌梗死”和“心梗”是同义词统计方案会分成两笔向量方案可以自动聚合。但代价也明显需要预训练模型、需要更多算力调参复杂度上升且结果可解释性变差。所以选型建议很朴素起步阶段、数据量小、场景要求快速上线直接 TF-IDF 或 TextRank 足够等积累了足够的标注数据或领域语料再引入词向量或主题模型做增强。多数项目在第一阶段就已经解决了 80% 的问题不必一上来就上预训练模型。3. 实战落地从零构建中文关键词提取模块3.1 环境准备与工具选型为什么先碰 jieba中文分词工具我实际用过几类jieba 属于轻量型速度快词典可控适合快速验证和中小规模应用HanLP 和 LTP 提供了更完整的分词、词性标注、依存分析能力适合需要更深语法信息的场景像 THULAC 也在多领域语料上表现稳定。如果你只是做关键词提取而不是完整的句法分析我建议先从 jieba 开始——它的词典体系、词性标注、内置 IDF 资源都和关键词提取直接配套安装之后基本零成本就能跑通。环境上只需要 Python 3.8安装 jieba 就够了。如果你后面要自己算 IDF需要 pandas 和 numpy要画效果对比图再加 matplotlib。我自己的经验是不要一上来就搭虚拟环境之外的额外依赖关键词提取模块的核心算法本身很轻方法级别跑通再考虑工程化。这里补充一个很多人不知道的细节jieba 自带 idf.txt 文件里面是通用领域语料统计得到的 IDF 值。它的覆盖范围偏向新闻和通用文本如果你的领域专业性很强强烈建议在领域语料里重新统计 IDF。做法在 3.2 节给出。3.2 TF-IDF 关键词提取的完整代码实现先给你可以直接落地的完整实现核心分两部分加载已有 IDF 词典以及对单篇文档提取 TopN 关键词。import jieba import jieba.posseg as pseg import math from collections import Counter import os # ------------------------------------------------------------ # 1. 读取 jieba 内置 IDF或自定义 IDF 词典 # IDF 词典格式词TAB数值每行一个词 # ------------------------------------------------------------ def load_idf_dict(idf_pathNone): idf_dict {} if idf_path and os.path.exists(idf_path): with open(idf_path, r, encodingutf-8) as f: for line in f: parts line.strip().split(\t) if len(parts) 2: w, score parts idf_dict[w] float(score) return idf_dict # 没有自定义词典时退回到 jieba 内置词典 idf_path os.path.join(os.path.dirname(jieba.__file__), analyse, idf.txt) with open(idf_path, r, encodingutf-8) as f: for line in f: parts line.strip().split( ) if len(parts) 2: w, score parts idf_dict[w] float(score) return idf_dict # ------------------------------------------------------------ # 2. 停用词加载 # ------------------------------------------------------------ def load_stopwords(stopwords_path): stopwords set() if stopwords_path and os.path.exists(stopwords_path): with open(stopwords_path, r, encodingutf-8) as f: for line in f: w line.strip() if w: stopwords.add(w) return stopwords # ------------------------------------------------------------ # 3. TF-IDF 关键词提取 # 自定义分词函数先按词性过滤再统计 TF 和 IDF # ------------------------------------------------------------ def tfidf_keywords(text, idf_dict, stopwords, top_k10): # 使用词性标注接口方便后续过滤 words pseg.cut(text) word_tf Counter() total_words 0 for word, flag in words: w word.strip().lower() if not w or len(w) 2: continue if w in stopwords: continue # 常见做法只保留名词、动词、形容词、动名词等实词 if flag.startswith(n) or flag.startswith(v) or flag.startswith(a) or flag in (nz, vn): word_tf[w] 1 total_words 1 scores {} for w, tf in word_tf.items(): idf_score idf_dict.get(w, 1.5) # 未登录词的 IDF 用一个默认值 tf_score tf / total_words scores[w] tf_score * idf_score sorted_words sorted(scores.items(), keylambda x: x[1], reverseTrue) return sorted_words[:top_k] # 使用示例 if __name__ __main__: text 华为在开发者大会上发布了最新的鸿蒙操作系统版本该系统优化了多设备协同能力 并推出了面向开发者的新工具链。与会专家表示操作系统的生态建设依赖于应用开发者的积极参与 而开发工具的完善程度直接影响开发效率和应用质量。 idf_dict load_idf_dict() # 使用 jieba 内置 IDF stopwords load_stopwords(stopwords.txt) # 自己准备一个停用词表 result tfidf_keywords(text, idf_dict, stopwords, top_k8) for w, score in result: print(f{w}\t{score:.4f})这段代码里有几个关键点需要解释第一词性过滤。我保留了n名词、v动词、a形容词和nz其他专名、vn动名词。关键词提取经典策略是“名词为主、动词辅助、形容词慎用”因为形容词主观色彩强容易引入噪音。你要是做新闻资讯类场景这组过滤规则基本够用。第二未登录词的 IDF 默认值。代码里写了idf_dict.get(w, 1.5)意思是词不在词典里时给一个默认 IDF。为什么是 1.5这是经验值按公式 (\log(\frac{N}{1})取对数)如果语料规模是几十万篇IDF 大约在 10 以上但那会让新词权重过高直接给 1.5 左右可以避免未登录词抢占排名。实际你可以根据自己的语料规模调。第三TF 归一化。我除以total_words而不是原始总词频是为了让长文档和短文档的 TF 可比。如果你处理的文档长度差异很大这一步很重要。3.3 自定义 IDF让垂直领域文本效果立住如果你只看通用新闻上一节代码就够了。但你要做的是垂直领域比如医疗、金融、法律jieba 内置 IDF 不一定准确。举一个我遇到的真实问题在医疗病历场景里“患者”这个词在几乎所有文档里都出现但 jieba 内置 IDF 给的权重很高导致每篇病历提取出的关键词都有“患者”这对后续聚类一点用没有。原因就是通用语料里“患者”没那么常见但垂直语料里它就是高频词。解决办法是在领域语料里重算 IDF。收集一批领域文档数量要尽量多建议至少几千篇统计每个词出现在多少篇文档里然后按 (IDF \log(\frac{N}{n_i1})1) 计算。import os import jieba from collections import Counter def build_idf_from_corpus(corpus_dir): doc_count 0 word_doc_freq Counter() for filename in os.listdir(corpus_dir): path os.path.join(corpus_dir, filename) if not filename.endswith(.txt): continue with open(path, r, encodingutf-8) as f: content f.read() if not content.strip(): continue doc_count 1 # 一句话去重同一个词在一篇文档里只算一次 words set([w for w in jieba.cut(content) if len(w.strip()) 1]) for w in words: word_doc_freq[w] 1 idf_dict {} for w, df in word_doc_freq.items(): idf_dict[w] math.log(doc_count / (df 1)) 1 return idf_dict细节强调一遍统计的是“文档频率”而不是“词频”。一个词在单篇文档里出现一百次在 IDF 计算里也只能贡献一次。如果按词频算长文档会污染统计结果。构建完自定义 IDF 后把它保存成文本再加载进 3.2 节的load_idf_dict即可。3.4 TextRank 关键词提取的代码实现TextRank 在 jieba 里也有现成实现jieba.analyse.textrank但为了让你理解机制我给出一个更容易改动的版本。核心逻辑建图、迭代、归一化。import jieba.posseg as pseg from collections import defaultdict import math def textrank_keywords(text, top_k10, window5, damping0.85, max_iter50): # 1. 分词 词性过滤 words pseg.cut(text) candidate [] for word, flag in words: w word.strip().lower() if len(w) 2: continue if flag.startswith(n) or flag.startswith(v) or flag.startswith(a) or flag in (nz, vn): candidate.append(w) # 2. 按窗口建共现图 graph defaultdict(Counter) for i, w in enumerate(candidate): for j in range(i 1, min(i 1 window, len(candidate))): # 注意这里 j 的窗口范围常见写法是 j in range(i1, iwindow1)我不建议在博文里直接把完整 TextRank 代码平铺出来因为 jieba 自带实现已经可用。这里演示的是核心建图逻辑以共现窗口构建稀疏矩阵然后迭代。实际项目里我更多会使用jieba.analyse.textrank(text, topK10, withWeightTrue)它内部已经实现了过滤、建图、迭代和归一化。如果你要改动窗口参数或阻尼系数再看源码调整。有一点值得提醒TextRank 对短文本效果比 TF-IDF 好因为不需要外部 IDF 统计量但这个优势在你只有单篇短文本时不明显。短文本词共现关系太少窗口内基本都是同一句话的词图结构严重退化。这时候它的输出和“词频统计”差不了太多不必神化。3.5 多词短语提取让关键词变成可读语义单元单个词提取出来往往不够用“操作系统”是一个词但“操作系统生态建设”在语义上是一个更完整的概念。要做多词短语提取我的方案分两步第一步分词并保留候选词第二步按词性规则合并相邻词。规则我总结了几组常用的模式名词 名词比如“系统 开发”形容词 名词“高效 系统”名词 动词“数据 处理”。这种分析在 jieba 的词性标注基础上实现并不复杂。你可以自己写一个相邻候选词的合并函数如果两词之间的词性组合命中规则并且连接后整体得分两词得分相加大于阈值就输出为短语。我在项目里实际体验是短语提取可以把关键词的“可读性”提升不少但代价是召回整体下降因为规则覆盖率有限。如果你的需求重点是召回比如用于搜索索引建议单字词和短语双通道输出不要互相替代。4. 效果优化与验收跑数据之前必须想清楚的事4.1 停用词表怎么建才实用不能直接抄网上的通用表停用词表是中文关键词提取里最容易被低估的模块。网上流传的通用停用词表覆盖了“的、了、是、在、我、你”这类高频虚词但真实项目里的“停用词”远不止这些。举一个真实例子在汽车论坛场景“车”“车子”“问题”“感觉”这类泛化词出现频率极高但它们对区分不同话题毫无贡献。这些词不在通用停用词表里需根据领域语料自己积累。我的建议是走双表制先加载通用停用词表再维护一个领域停用词表两个表取并集。领域停用词表怎么积累跑一遍原始提取结果把所有文档的关键词合并统计你会发现排名前几十的关键词里总有几个是明显的泛化词。把它们加入停用词表再迭代一次。重复两三轮效果会有肉眼可见的提升。这个过程不复杂但工作量比较碎值得耐心做。4.2 词性过滤与权重调节不同场景的关键词长相我前面提到词性过滤默认保留名词、动词、形容词这个规则在不同场景要调整。做舆情分析我希望关键词以名词为主“涨价”“监管”“投诉”因为名词更集中表达对象做问答系统意图识别动词的权重反而要上调“怎么办”“怎么修”做电商标题摘要形容词“高性价比”“便携”也是重要的卖点词。调整方式有两种改变词性白名单或者在算分时给不同词性乘一个系数。比如名词系数 1.0动词系数 0.8形容词系数 0.6。这个系数矩阵可以直接放进 3.2 节的代码里。另外位置权重也是一个常被忽略的点。标题、第一段、最后一段里的关键词对文档主旨的代表性远高于中间段落。实现上可以在统计 TF 时根据词所在位置给一个增量标题里的词频可以按双倍计入首段按 1.2 倍计入。这个技巧在长文档场景里效果很明显。4.3 效果评估与参数调优不知道好还是不好就等于白做关键词提取的评估比较特殊没有单一绝对正确的答案通常存在一组“可接受答案”。我做评估时用了三种手段第一抽样人工评价。取 50-100 篇文档给定参考答案或者没有参考答案时由 2-3 个人主观打分计算准确率提取结果中合理关键词的比例和召回率参考答案中被提取出来的比例。F1 取两者调和平均。第二任务效果间接评估。关键词不是终点把它放进一个下游任务比如搜索召回、文本分类比较不同参数下任务的准确率变化。这个评估更接近业务真实收益。第三稳定性测试。同一类文档重复提取看关键词是否稳定相同语义不同表达的两篇文档关键词是否出现合理的重合。这一步能暴露出参数敏感性问题生物都逃过吗——不是文本语义相近但字面不同的文档关键词差异不能过大。调参顺序我固定下来先调停用词表再调词性过滤规则然后调 TopN 和位置权重最后才考虑换算法或换分词器。前面的因素对结果的影响通常都大于算法本身。5. 常见问题与排查实录5.1 分词不准导致的关键词漂移现象提取出来的关键词和文章主题完全没关系或者明显是一个长词被拆碎。排查步骤先单独打印分词结果看该词是否被正确切分。多数情况是词典里没有该词或词典里词语权重被其他分词方案覆盖。确认不是切分问题后再看 IDF 值。如果该词被切分正确但权重很低说明词典里这个 IDF 可能不可靠。长词被拆碎时可以通过给 jieba 添加自定义词典来修正。自定义词典是 UTF-8 编码每行一个词格式为“词语 词频 词性”词频越高越倾向于切分后保留。一个典型的修复案例某政务项目里“放管服”老是被切成“放”“管”“服”加入用户词典后关键词提取结果立刻正常了。类似地专业术语、人名、机构名最好都进入自定义词典。5.2 短文本场景下 TF-IDF 直接失效短文本比如一两句话的活动评论分词后可能只有五六个候选词TF-IDF 的 TF 部分几乎没有区分度IDF 部分会主导结果导致提取出“平台”“内容”这类泛化词。我的处理经验是短文本直接用 TextRank 或规则方法不做 TF-IDF。如果一定要用 TF-IDF在短文本前先拼接同一用户的多个历史文本按用户维度聚合上下文。引入词典匹配手工维护一个小型领域词典短文本里一但命中词典直接视为强关键词。按我的经验“短文本 强规则 词典兜底”比“短文本 统计算法”效果稳定得多。5.3 高频无意义词拦截一个容易被所有人都忽略的大坑你们不要觉得停用词表能解决所有问题。常见停用词“的、了、是”能滤掉但像“内容”“方式”“相关”“问题”“方面”这类抽象名词很多项目会漏掉。它们不是虚词是实词在 TF-IDF 里得分往往不低对结果干扰极大。处理方法有二一是在领域停用词表里主动加入这类“泛化名词”二是设置一个逆文档频率上限比如某个词在语料里 80% 以上的文档中都出现直接视为无区分度词强制过滤。这个阈值变量在我的项目里通常会调通用文本设 0.7垂直领域设 0.85效果更精细。5.4 性能问题与工程化大规模文本的提取瓶颈单篇文档提取毫秒级很轻松但如果每天要处理百万级新闻性能就是瓶颈。我遇到过的瓶颈主要在分词组件的全局锁和内存占用上。优化手段从易到难先用jieba.enable_parallel(num_workers)做并行分词代价是内存翻倍再用 jieba 的cut_for_search模式替代全模式减少候选词数量速度提升约 40%最后如果还不够把分词结果缓存到磁盘或内存表同一文档不重复分词。这个缓存优化在很多同类项目中能带来成倍的访问效率提升。另外一个工程注意点是线程安全。jieba 在并发场景下容易出现共享词典加载的竞争建议在服务启动时一次性加载词典上线后不再动态增删词。动态增删词的 bug 排查起来很隐蔽明明代码没动结果却变了多半就是这里出了问题。写在最后的经验话说实话关键词提取这个模块我看过太多人一上来就搭预训练模型结果业务指标提升还不如老老实实调一遍停用词表和词典。个人经验是中文关键词提取的核心竞争力有三个分词质量、停用词表、迭代评估。三者都做到位TF-IDF 在大多数场景下已经能打 80 分剩下 20 分才轮到算法复杂度。上线前别忘了给模块做一个兜底设置一个超时和异常降级策略关键词提取失败时自动退回到简单的高频词拼接保证下游环节不会因为模块异常而整个挂掉。这套思路在我几个项目里都跑通了尤其在资源受限或数据质量参差的场景稳字比快字更重要。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑