资讯动态

中文关键词抽取算法对比:词频、TF-IDF与TextRank原理及Python实现

发布时间:2026/9/23 1:21:06 来源:尧图企业网站定制
简介面向需要完成 NLP 课程设计或入门中文关键词抽取的读者这套基于 Python 的实战资源围绕 TF-IDF、TextRank 与 Word2Vec 词向量聚类三种方法展开包含课程论文、项目源码、实验数据与截图。压缩包共 31 个文件其中 4 个 Python 脚本对应三种抽取算法的完整实现14 个 CSV 为各方法输出的关键词结果配套的 docx 论文、txt 停用词表与 PNG 截图可辅助理解原理和核对流程。课程论文从思路、原理到实现细节逐层讲解并提出了类似增加专业语料、标题权重、按分类数调整聚类簇数等改进方向能帮助读者快速掌握方法对比与实验设计思路。资源包仅 1.78MB轻量易用已有 2114 人学习下载适合作为课程设计参考或 NLP 入门实践素材。1. 中文关键词抽取不只靠TF-IDF选错方法等于白做有一次给客户做舆情系统的关键词功能第一版用简单的词频统计结果“记者”“报道”这种高频词直接霸榜客户看了一眼就打了回来。后来换成TF-IDF效果好了不少但换到另一批语料又出现波动。最后把三种方法都接进去让用户按场景自己切换问题才算解决。这个经历说明关键词抽取没有万能解。基于词频统计、TF-IDF、TextRank是Python生态里最常用的三套方案分别适合不同的数据环境。这篇博客就把三者的原理、实现和调优讲透附带可以直接改用的代码。无论是做课程设计还是实际项目都能快速复现并判断该用哪一种。在动手之前建议先搞清楚一个概念中文关键词抽取不是简单的“数词频”它需要分词、去停用词、甚至词性过滤。理解了这一点后面调参才不会慌。2. 三种关键词抽取算法词频、TF-IDF、TextRank的原理与对比2.1 词频统计最直观但最容易翻车词频统计是关键词抽取的起点。先对文本分词去掉标点和停用词然后统计每个词的出现次数取前N个。看起来简单但有一个致命问题高频词往往不是关键词尤其是新闻语料里“记者”“报道”“今天”这类词出现的频率远超真正有信息量的词。下面是一个用Python实现词频统计的最小示例from collections import Counter import jieba def tf_topk(text, stopwords, topk10): words [w for w in jieba.cut(text) if w.strip() and w not in stopwords and len(w) 1] counter Counter(words) return counter.most_common(topk)这段代码先用jieba把文本切成词再用列表推导式过滤掉停用词和单字词最后用Counter统计并返回频率最高的topk个词。参数stopwords是一个set对象这里用len(w) 1过滤单字是因为很多无关的虚词都是单字。但即使做了这些词频统计只反映“在文档里出现的绝对次数”并不考虑这个词在所有文档里的分布。如果拿它去做全网站的关键词排行结果会被通用词霸占。所以它只适合对单文档做临时观察不适合跨文档的严肃分析。2.2 TF-IDF用逆向文档频率压住通用词TF-IDF的思路是一个词在文档里出现得多TF高但在其他文档里出现得少IDF高它才具有区分度。公式是TF-IDF TF × log((N 1) / (DF 1)) 1其中N是语料库文档总数DF是包含该词的文档数。关键点在于IDF它把“的”“了”“记者”这类几乎每篇文档都会出现的词的权重压到很低。也就是说TF-IDF需要一份语料才能算IDF——这是它和纯词频最大的区别也是它的适用前提必须有一个能代表目标场景的文档集合。在Python生态里主要用jieba.analyse.extract_tags来算TF-IDF。它内置了一套IDF语料但如果你的文本领域性很强比如医学、法律最好用自己的语料重新训练IDF。这个我们后面在实现章节会讲。2.3 TextRank用图模型和PageRank排序TextRank本质是把文档看作一个词图。先对文本分词、去停用词然后假定一个窗口大小比如5窗口内任意两个词之间都有一条边。边的权重可以是共现次数。然后像PageRank一样迭代计算每个词的重要性。TextRank最大的优势是不依赖外部语料库单篇文档就能跑。它适合长文本但对很短的一句话效果不稳定因为共现关系太稀疏。另外TextRank默认会把所有词都放进去如果不限制词性容易把介词、副词也排进来所以一般要配合词性过滤。2.4 三种方法的选型对比方法核心思想是否需要语料库虚词过滤单文档效果跨文档效果词频统计统计绝对次数不需要依赖人工停用词差差TF-IDF词频×逆文档频率需要自动人工一般好TextRank图模型迭代不需要需额外词性过滤好一般从选型角度看课程设计最稳的是TF-IDFTextRank做对比实验再加一个词频统计作为baseline正好构成三种方法。如果你的项目只有一个文档且没有背景语料选TextRank如果有一批文档且领域明确选TF-IDF只有想快速出个草稿才用纯词频。3. 基于Python和jieba快速实现TF-IDF与TextRank关键词抽取3.1 环境准备Python、jieba与停用词表开始之前先确认环境。关键词抽取不涉及GPU普通Python 3.6即可但建议用虚拟环境。安装依赖只需要jieba命令如下pip install jieba如果你的网络环境装不上可以换国内镜像源这里不展开。另外需要一份停用词表。常见做法是准备一个stopwords.txt每行一个词包含“的、了、是、我、你、他”这种高频虚词。读取代码如下def load_stopwords(pathstopwords.txt): with open(path, encodingutf-8) as f: return set(line.strip() for line in f)注意文件编码必须utf-8否则在Windows下会报编码错误。读取后转成set后续判断词是否在停用词表里的时间复杂度为O(1)。3.2 调jieba.analyse的TF-IDF接口jieba自带了TF-IDF实现入口是analyse.extract_tags。一个最小调用示例import jieba.analyse def tfidf_extract(text, topK10): jieba.analyse.set_stop_words(stopwords.txt) return jieba.analyse.extract_tags( text, topKtopK, withWeightTrue, allowPOS(ns, n, vn, v) )参数说明topK最终保留多少个关键词课程设计一般取10到20。withWeight设为True时返回(词, 权重)元组列表方便后续画图。allowPOS允许出现的词性元组形式。ns是地名n是名词vn是动名词v是动词。这能过滤掉形容词、副词。这里调用了set_stop_words它的作用是告诉jieba在内部计算时忽略停用词表中的词。如果不设置extract_tags也会使用内置的默认停用词表但内置表偏小建议显式传入自己的领域停用词。输出形式类似[(深度学习, 0.45), (神经网络, 0.32), ...]权重越大代表越重要。3.3 调jieba.analyse的TextRank接口TextRank同样有现成接口就是analyse.textrank。但它默认的窗口是5而且支持直接传窗口大小实际上要改窗口大小需要构造TextRank类from jieba.analyse import TextRank def textrank_extract(text, topK10): tr TextRank(span5) tr.set_stop_words(stopwords.txt) return tr.textrank(text, topKtopK, withWeightTrue)TextRank的构造参数span共现窗口大小默认5。窗口越大词与词之间的关联越泛化窗口越小越容易找到紧邻的词对。对新闻文本5表现较好对代码片段可以尝试3。allowPOS默认(ns, n, vn, v)如果不想分词性可以置为None。这里用tr.set_stop_words()设置停用词表作用方式和TF-IDF的一致。注意textrank返回的关键词权重之和并不等于1只用于内部排序不要拿不同文档的权重跨文档比较。3.4 三种方法封装到一个函数里课程设计通常要求对比三种方法。把它们封装成一个统一入口比较合适from collections import Counter import jieba.analyse from jieba.analyse import TextRank def extract_keywords(text, methodtfidf, topK10, stopwords_filestopwords.txt): jieba.analyse.set_stop_words(stopwords_file) if method tfidf: return jieba.analyse.extract_tags(text, topKtopK, withWeightTrue) elif method textrank: tr TextRank(span5) tr.set_stop_words(stopwords_file) return tr.textrank(text, topKtopK, withWeightTrue) elif method tf: stopwords load_stopwords(stopwords_file) words [w for w in jieba.cut(text) if w.strip() and w not in stopwords and len(w) 1] return Counter(words).most_common(topK) else: raise ValueError(method must be tfidf, textrank or tf)这里几个细节要注意set_stop_words相当于全局配置多次调用会互相覆盖如果同时用TF-IDF和TextRank可以在每次调用前都设置一次。纯词频方法没有用analyse所以必须自己加载停用词。这样封装后调用者只需一行extract_keywords(text, tfidf, 10)。为了直观对比三个接口的差异可以看下面这个表方法核心接口关键参数返回值TF-IDFjieba.analyse.extract_tagstopK, withWeight, allowPOS词权重列表TextRankjieba.analyse.textrankspan, withWeight, allowPOS词权重列表词频统计自写Counter统计topK, 停用词词频次列表3.5 常见坑停用词失效与词性过滤过度第一个坑是set_stop_words对textrank生效吗生效但只对后创建的TextRank实例生效。所以上面代码中先set_stop_words再构造TextRank顺序不能反。第二个坑是allowPOS过滤太狠。比如你想保留“平静”这种形容词但allowPOS里没有a它会直接被丢掉。课程设计报告里如果出现“结果不合理”大部分都是这个原因。第三个坑是jieba内置的IDF语料和你的文本领域不匹配。这个会在无法用内置接口时出现我们下一章手写TF-IDF就是为了解决这个问题。4. 不依赖高级API从零手写TF-IDF和TextRank排序4.1 分词与去停用词是成败关键调用封装接口很快但课程设计要想拿高分最好能展示你理解内部逻辑。手写版本的预处理和前面一样但需要注意三点一是自定义词性过滤规则时优先保留名词和动名词二是文本过长时建议分句再合并共现关系三是对英文单词和数字要单独处理通常直接剔除。下面是一个完整的预处理函数import jieba.posseg as pseg def preprocess(text, stopwords, keep_pos(n, v, vn, ns)): result [] for word, pos in pseg.cut(text): if word in stopwords or len(word.strip()) 2: continue if pos and pos not in keep_pos: continue result.append(word.lower()) return result这里用了jieba的词性标注接口把pos作为过滤条件。keep_pos控制了保留哪些词性stopwords是set。返回的是一串小写词列表方便后续共现统计。注意这个预处理同时服务TF-IDF和TextRank差异只在排序阶段。4.2 手写TF-IDF自己控制IDF语料手写TF-IDF的核心在于怎么组织语料。假设我们有一个documents列表里面是若干篇文档的原始文本。先对每篇文档预处理建立word-文档出现次数DF的映射import math from collections import defaultdict def build_idf(documents, stopwords): df defaultdict(int) for doc in documents: words set(preprocess(doc, stopwords)) for w in words: df[w] 1 n len(documents) idf {w: math.log((n 1) / (c 1)) 1 for w, c in df.items()} return idf代码逻辑对每篇文档的预处理结果去重后统计每个词出现在多少篇文档中df。然后按平滑公式计算IDF分母加1防止除零整体加1防止IDF为0。这里用(n 1) / (c 1)是常见平滑写法你也可以换成math.log(n / (c 1))只是当c0时不会报错但结果会偏小。有了IDF抽取某篇文档的关键词就简单了def extract_tfidf_manual(text, idf, stopwords, topK10): words preprocess(text, stopwords) tf Counter(words) total len(words) scores {w: (tf[w] / total) * idf.get(w, 0.0) for w in tf} return sorted(scores.items(), keylambda x: x[1], reverseTrue)[:topK]TF部分归一化到词总数这样长文档与短文档之间的分数才有可比性。idf.get(w, 0.0)处理了未见过的词——如果某词不在IDF表里它的IDF记为0相当于认为它对区分文档没有贡献。4.3 手写TextRank窗口共现与迭代排序TextRank的手写实现比TF-IDF复杂一些。主要步骤是先对文档预处理得到词序列然后用一个滑动窗口收集词对构建无向图最后迭代计算权重。代码如下def textrank_manual(words, span5, max_iter100, d0.85): graph defaultdict(set) for i in range(len(words)): for j in range(i 1, min(i span, len(words))): graph[words[i]].add(words[j]) graph[words[j]].add(words[i]) scores {w: 1.0 for w in graph} for _ in range(max_iter): new_scores {} for node in graph: s 1 - d for neighbor in graph[node]: s d * scores[neighbor] / len(graph[neighbor]) new_scores[node] s scores new_scores return sorted(scores.items(), keylambda x: x[1], reverseTrue)这段代码使用集合来存储邻居避免重复边。初始化所有节点权重为1.0逐轮按PageRank公式更新。d是阻尼系数默认0.85经验值。循环中的关键公式1 - d是基础概率邻居节点把它的权重均分给其所有邻居当前节点累加。需要注意这个实现没有把词频权重加入初值也没有限制窗口内的距离权重。工程上更精细的做法是让词对的距离越近权重越高比如权重设为1 / 距离。简单版本已足够课程设计展示。4.4 手写实现与jieba封装的结果差异与调参手写和jieba封装的结果通常会有差别原因主要有四个差异点原因如何调整分词词性不同手写只保留n,v,vn,nsjieba可能保留英文调整keep_pos或allowPOSIDF语料不同jieba内置语料来自网络文本手写来自小语料增大语料规模TextRank细节不同jieba对窗口距离有权重衰减手写是纯布尔共现引入距离或词频做边权重停用词表不同内置表和自定义表不完全一致统一停用词表当实验数据出现“jieba抽取效果不如手写”时不要急着说算法不好先检查这四项。调参建议TF-IDF的TopK取10-20IDF平滑常数从1改成0.5可以放大低频词权重TextRank的span取5是标准值文本较短时改成3max_iter默认100已经收敛不用改d在0.8到0.9之间微调预处理不要贪多词性保留名词和动词就足够停用词表宁缺毋滥否则误删专有名词。5. 课程设计验收技巧用三种方法做对比实验并展示结果5.1 用Jaccard系数量化三种方法的结果差异课程设计验收时除了跑通代码还要能回答“三种方法有什么不同”这个问题。我的做法是准备两份语料一份新闻类、一份小说类各取50篇作为IDF来源另外选3篇作为测试文档。对同一篇文档分别调用封装函数和手写函数抽取top10然后计算两两之间的重叠度Jaccard系数。def jaccard(a, b): sa, sb set(a), set(b) if not sa and not sb: return 1.0 if a b else 0.0 return len(sa sb) / len(sa | sb) for text_id, test_text in enumerate(test_texts): k_tfidf [w for w, _ in extract_keywords(test_text, tfidf, 10)] k_textrank [w for w, _ in extract_keywords(test_text, textrank, 10)] print(text_id, overlap:, jaccard(k_tfidf, k_textrank))Jaccard值一般在0.2到0.5之间都属于正常因为两者的排序逻辑完全不同。如果重叠度超过0.7说明分词和停用词已经主导了结果算法的差异没有体现出来这时要调整IDF语料或TextRank窗口。5.2 用权重条形图提升答辩效果展示结果时我习惯用matplotlib把权重条形图画出来横排关键词纵排权重import matplotlib.pyplot as plt def show_weights(keywords): words, weights zip(*keywords) plt.barh(range(len(words)), weights, tick_labelwords) plt.gca().invert_yaxis() plt.show()注意中文字体问题Windows下要设置plt.rcParams[font.sans-serif] [SimHei]否则汉字会变方块macOS下可以改成[Arial Unicode MS]。另外报告里不要只贴结果建议设计一个人工评估表让同组同学对每一篇文档按“是否属于关键信息”给每个候选词打分最后算出三类方法的平均准确率。这个指标不复杂但能说明你有评估意识。把三种方法封装成命令行或简单GUI用同一个输入框切换radioButton也能显著提升答辩效果不需要复杂框架tkinter就够用。本文还有配套的精品资源点击获取

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

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

免费获取报价