资讯详情

中文关键词抽取实战:TF-IDF、TextRank与Word2Vec融合方案

📅 2026/10/11 12:33:46 | 华诺云谱 👁 阅读
中文关键词抽取实战:TF-IDF、TextRank与Word2Vec融合方案
简介这份资源面向具备一定Python基础、希望系统掌握中文文本关键词抽取的开发者与学习者围绕TF-IDF、TextRank、Word2Vec词聚类三种主流方法展开实践。相比网上零散示例它重点补齐了Word2Vec词聚类抽取流程不够清晰的短板并以专利文本为样例同样适用于新闻、论文等中文语料。压缩包共20个文件约879KB包含14个csv结果与样例数据、4个py实现脚本、2个txt停用词与词性标注参考脚本分别对应三种算法csv则保存各方法的抽取结果便于横向对比效果。目前已有443人学习下载。通过数据、脚本与结果文件的完整配套读者可快速跑通三种关键词抽取方案理解词向量聚类在关键词发现中的作用并借助结果文件对照分析不同算法的差异为后续文本挖掘与聚类项目提供可复用的代码基础。1. 中文关键词抽取的三条路线为什么我把 TF-IDF、TextRank、Word2Vec 放在同一个工程里跑做中文文本处理的人迟早会撞上同一个需求给一段没有标注的文本把里面真正重要的词捞出来。搜索、推荐、舆情监控、论文辅助阅读底层都绕不开这一步。但真正上手就会发现关键词抽取不是调一个库就完事——TF-IDF 快但吃不住语义TextRank 靠词共现但容易把高频虚词带进来Word2Vec 能抓语义但单独用又抽不出词。我见过太多人只跑通一个方法就交差结果换一批语料就翻车。这份 Python 项目实践的价值在于它把三条路线放进同一个工程里对照着跑TF-IDF 做统计基线TextRank 做图排序Word2Vec 做词向量聚类最后用聚类把语义相近的词归并成关键词簇。它适合已经会写 Python、但没系统比较过这几种方法的中文 NLP 从业者也适合拿它当课程设计或大作业的骨架——因为三种方法的输入输出格式统一替换语料就能复现。下面我按「先讲清每条路线怎么落地再讲怎么把它们串起来」的顺序拆。2. TF-IDF 与 TextRank从分词到词权重的两条实现路径2.1 中文分词与停用词处理一切权重计算的前置条件TF-IDF 和 TextRank 都建立在「词」这个单位上而中文没有天然空格分隔所以分词质量直接决定后面所有结果。常见做法是用 jieba它自带三种模式精确模式、全模式、搜索引擎模式。关键词抽取一般用精确模式因为全模式会把「人工智能」切成「人工」「智能」「人工智能」三个词权重被稀释。import jieba import jieba.analyse # 加载自定义词典把领域专有词固定下来 jieba.load_userdict(user_dict.txt) text 人工智能正在从尝鲜工具变成日常帮手Python 是其中最常用的实现语言。 # 精确模式分词 words jieba.lcut(text) print(words) # [人工智能, 正在, 从, 尝鲜, 工具, 变成, 日常, 帮手, , Python, 是, 其中, 最, 常用, 的, 实现, 语言, 。] # 过滤停用词和单字 stopwords set(open(stopwords.txt, encodingutf-8).read().split()) filtered [w for w in words if w not in stopwords and len(w) 1] print(filtered) # [人工智能, 尝鲜, 工具, 变成, 日常, 帮手, Python, 其中, 常用, 实现, 语言]这段代码的逻辑是先加载用户词典把「人工智能」「词向量」这类会被 jieba 切碎的词固定成一个整体然后精确模式切分最后用停用词表和长度阈值过滤。参数上len(w) 1这个阈值不是绝对的——如果你的语料里「云」「链」这类单字是核心概念就得放宽到 1但代价是噪声词会明显增多。停用词表建议用哈工大停用词表打底再手动补上你语料里的高频无意义词比如「进行」「方面」「问题」。提示自定义词典的格式是「词语 词频 词性」词频可以省略但词性如果写了后续做词性过滤时能直接用。2.2 TF-IDF 抽取统计基线怎么调才不虚TF-IDF 的核心思想很直白一个词在当前文档里出现越多、在整个语料里出现越少它就越可能是关键词。jieba 的analyse.extract_tags已经封装好了这套逻辑但默认参数在中文短文本上经常抽出一堆无意义的高频词。import jieba.analyse text open(sample.txt, encodingutf-8).read() # 提取 TopK 关键词带权重 keywords jieba.analyse.extract_tags( text, topK20, # 返回前 20 个 withWeightTrue, # 返回权重 allowPOS(n, nr, ns, nt, nz, v, vn) # 只保留名词和动词 ) for word, weight in keywords: print(f{word}\t{weight:.4f})这里最关键的两个参数是topK和allowPOS。topK按经验取文档长度的平方根左右比较稳比如 500 字的文本取 15 到 20 个。allowPOS是过滤词性的只保留名词、人名、地名、机构名、其他专名、动词和名动词能把「的」「了」「是」这类虚词和大部分形容词挡在外面。如果你不设这个参数TF-IDF 很容易把「可以」「进行」排到前面。TF-IDF 的短板在于它完全依赖词频统计不理解语义。比如「电脑」和「计算机」在它眼里是两个无关的词权重各算各的。这就是为什么需要 TextRank 和 Word2Vec 来补。2.3 TextRank 抽取把词共现变成图排序TextRank 的思路来自 PageRank把每个词当成图里的节点如果两个词在同一个窗口内共现就在它们之间连一条边边的权重是共现次数。然后在这个图上迭代计算每个节点的重要性最后按分数排序取 TopK。import jieba.analyse text open(sample.txt, encodingutf-8).read() # TextRank 抽取窗口默认 5 keywords jieba.analyse.textrank( text, topK20, withWeightTrue, allowPOS(n, vn, v) ) for word, weight in keywords: print(f{word}\t{weight:.4f})TextRank 和 TF-IDF 最大的区别是TF-IDF 看的是「这个词在多少文档里出现过」TextRank 看的是「这个词和多少重要词一起出现过」。所以在单篇长文本上TextRank 往往比 TF-IDF 更稳因为它不依赖外部语料库的 IDF 值。但 TextRank 有个坑窗口大小span默认是 5如果你的文本句子很短窗口会跨句共现把不相关的词连起来。常见做法是先按句号、问号、感叹号分句再在每句内部做 TextRank或者把span调到 3 到 4。另一个坑是 TextRank 对高频虚词敏感。虽然allowPOS能挡掉一部分但像「使用」「进行」这种动词如果没进停用词表还是会被排上来。我的习惯是TextRank 跑完之后再拿停用词表过一遍把明显无意义的词手动剔掉。2.4 两种方法的对照与选型把 TF-IDF 和 TextRank 放在一起跑同一段文本你会发现结果重叠度大概在 40% 到 60% 之间。重叠的部分通常是真正的核心词不重叠的部分各有偏向TF-IDF 偏向全局高频词TextRank 偏向局部共现密集的词。维度TF-IDFTextRank依赖外部语料需要 IDF 语料库不需要单篇可跑适合文本长度中长文本、多文档单篇长文本对分词质量敏感度高高语义理解无无典型翻车场景短文本抽出通用词跨句共现引入噪声选型上如果你手头是一批文档要做关键词提取TF-IDF 更合适因为 IDF 值能跨文档计算如果是一篇几千字的报告要抽核心概念TextRank 通常更准。但两者都解决不了同义词问题这就得靠 Word2Vec。3. Word2Vec 词向量与聚类让语义相近的词自动归堆3.1 训练词向量语料、参数与常见翻车点Word2Vec 把词映射成稠密向量语义相近的词在向量空间里距离更近。用它做关键词抽取的思路是先训练词向量再对词向量做聚类把语义相近的词归成一簇每簇选一个代表词作为关键词。from gensim.models import Word2Vec import jieba import logging logging.basicConfig(format%(asctime)s : %(levelname)s : %(message)s, levellogging.INFO) # 读取语料每行一篇文档 corpus [] with open(corpus.txt, encodingutf-8) as f: for line in f: words jieba.lcut(line.strip()) words [w for w in words if len(w) 1 and w not in stopwords] if words: corpus.append(words) # 训练 Word2Vec model Word2Vec( sentencescorpus, vector_size100, # 向量维度 window5, # 上下文窗口 min_count5, # 低于此频率的词丢弃 workers4, # 并行线程数 sg1, # 1 表示 Skip-gram0 表示 CBOW epochs10 # 迭代轮数 ) model.save(word2vec.model)参数上vector_size取 100 到 300 之间比较常见语料小就取 100语料大可以到 300。window控制上下文范围中文一般取 5。min_count是过滤低频词的语料小于 10 万词时取 3 到 5语料大可以取 10 以上。sg1用 Skip-gram对低频词更友好但训练慢sg0用 CBOW训练快但低频词效果差。epochs一般 5 到 15 轮再多容易过拟合。注意Word2Vec 训练语料如果太小比如只有几万字词向量质量会很差聚类结果基本不可用。常见做法是拿领域内的大规模无标注文本先训一版或者直接用预训练词向量。3.2 K-Means 聚类把词向量归成关键词簇有了词向量之后对词向量做 K-Means 聚类每个簇里的词语义相近。然后从每个簇里选一个离质心最近的词作为代表就是关键词。from sklearn.cluster import KMeans import numpy as np # 取出词表中所有词的向量 words list(model.wv.index_to_key) vectors np.array([model.wv[w] for w in words]) # 聚类K 取 10 到 20 之间 k 15 kmeans KMeans(n_clustersk, random_state42, n_init10) labels kmeans.fit_predict(vectors) # 每个簇选离质心最近的词作为代表 for i in range(k): cluster_words [words[j] for j in range(len(words)) if labels[j] i] if not cluster_words: continue cluster_vectors np.array([model.wv[w] for w in cluster_words]) centroid kmeans.cluster_centers_[i] distances np.linalg.norm(cluster_vectors - centroid, axis1) representative cluster_words[np.argmin(distances)] print(f簇 {i}: 代表词{representative}, 成员{cluster_words[:10]})n_clusters的选择没有固定公式一般按语料规模和关键词数量预期来定。语料几万字取 10 到 15几十万字取 20 到 30。n_init10表示用不同初始质心跑 10 次取最优避免局部最优。random_state固定住是为了结果可复现。聚类结果里经常会出现一些簇全是噪声词或者一个簇里混了多个主题。这时候要么调 K要么在聚类前先用 TF-IDF 或 TextRank 筛一遍候选词只对候选词做聚类能明显提升质量。3.3 三种方法的融合候选词筛选加语义归并单独用 Word2Vec 聚类的问题是它会把所有词都拿来聚包括大量无意义词。所以实际工程里常见做法是先用 TF-IDF 或 TextRank 抽出一批候选词再对候选词的词向量做聚类把语义相近的候选词归并成簇每簇选一个代表。# 先用 TextRank 抽候选词 candidates jieba.analyse.textrank(text, topK50, withWeightFalse, allowPOS(n, vn, v)) # 只保留在词向量词表里的候选词 candidates [w for w in candidates if w in model.wv] # 对候选词向量聚类 candidate_vectors np.array([model.wv[w] for w in candidates]) kmeans KMeans(n_clusters8, random_state42, n_init10) labels kmeans.fit_predict(candidate_vectors) # 输出每个簇的代表词 for i in range(8): cluster_words [candidates[j] for j in range(len(candidates)) if labels[j] i] if cluster_words: print(f关键词簇 {i}: {cluster_words})这样做的逻辑是TextRank 负责从原文里筛出「值得考虑」的词Word2Vec 聚类负责把「电脑」「计算机」「PC」这类同义词归到一起最后每个簇输出一个代表词。参数上候选词数量topK取 30 到 50 比较合适太少会漏掉重要词太多会引入噪声。聚类数n_clusters取候选词数量的 1/5 到 1/3。3.4 效果验证怎么判断抽出来的关键词靠不靠谱关键词抽取没有绝对的标准答案但可以用几个方法交叉验证。一是人工抽查随机抽 20 篇文档看抽出的关键词是否覆盖了文档核心内容。二是对比不同方法的重叠度如果 TF-IDF、TextRank、Word2Vec 三种方法都抽出了同一个词那这个词大概率是真正重要的。三是用下游任务验证把关键词喂给分类器或检索系统看效果有没有提升。我一般会先跑一版三种方法的结果人工看一遍把明显不对的词记下来反推是分词问题、停用词问题还是参数问题。这个过程比调参本身更重要。4. 避坑与排查中文关键词抽取里最容易翻车的五个地方4.1 分词把关键词切碎导致权重全错现象TF-IDF 和 TextRank 抽出来的词都是「人工」「智能」这种碎片而不是「人工智能」。原因jieba 默认词典里没有「人工智能」这个整体词或者你的领域专有词没进自定义词典。解决在分词前加载自定义词典把领域词固定下来。格式是每行一个词可以带词频和词性。如果词很多可以从领域语料里用新词发现算法先跑一遍把高频共现的字组合提取出来。4.2 停用词表不匹配高频虚词霸榜现象抽出来的关键词里有「进行」「方面」「问题」「使用」这类词。原因停用词表用的是通用表没有覆盖你语料里的高频无意义词。解决先跑一遍词频统计把 Top 100 高频词里明显无意义的词手动加进停用词表。这个步骤每个新语料都要做一遍没有一劳永逸的停用词表。4.3 TextRank 窗口跨句共现引入无关词现象TextRank 抽出的词里有一些和文档主题无关的词。原因默认窗口span5在短句文本上会跨句共现把不同句子的词连起来。解决先按标点分句再在每句内部做 TextRank或者把span调到 3。如果文本句子普遍很短分句处理是必须的。4.4 Word2Vec 语料太小词向量质量差现象聚类结果里同一个簇的词语义不相关或者所有词都聚成一簇。原因训练语料太小词向量没有学到有效的语义关系。解决要么扩大语料要么直接用预训练词向量。如果必须用小语料训练把vector_size降到 50 到 100min_count降到 2 到 3epochs增加到 15 到 20但效果提升有限。4.5 聚类数 K 拍脑袋定结果不可用现象聚类结果要么一个簇里几百个词要么每个簇只有一两个词。原因n_clusters没有根据候选词数量调整。解决先用肘部法或轮廓系数粗估一个范围再人工看几组结果。经验值是候选词数量的 1/5 到 1/3。如果候选词只有 30 个K 取 6 到 10如果候选词有 200 个K 取 20 到 40。5. 把三种方法串成一条流水线一个可复用的抽取脚本前面几章分别讲了 TF-IDF、TextRank、Word2Vec 聚类的实现但实际工程里不会单独用某一种而是串成一条流水线分词 → 候选词筛选 → 语义聚类 → 输出关键词。下面这个脚本把三种方法整合在一起输入一段中文文本输出最终的关键词列表。import jieba import jieba.analyse import numpy as np from gensim.models import Word2Vec from sklearn.cluster import KMeans class KeywordExtractor: def __init__(self, stopwords_path, userdict_pathNone, w2v_modelNone): self.stopwords set(open(stopwords_path, encodingutf-8).read().split()) if userdict_path: jieba.load_userdict(userdict_path) self.w2v w2v_model def preprocess(self, text): words jieba.lcut(text) return [w for w in words if w not in self.stopwords and len(w) 1] def extract_tfidf(self, text, topK20): return jieba.analyse.extract_tags( text, topKtopK, withWeightTrue, allowPOS(n, nr, ns, nt, nz, v, vn) ) def extract_textrank(self, text, topK20): return jieba.analyse.textrank( text, topKtopK, withWeightTrue, allowPOS(n, vn, v) ) def extract_w2v_cluster(self, text, topK50, n_clusters8): if self.w2v is None: raise ValueError(需要传入训练好的 Word2Vec 模型) candidates jieba.analyse.textrank( text, topKtopK, withWeightFalse, allowPOS(n, vn, v) ) candidates [w for w in candidates if w in self.w2v.wv] if len(candidates) n_clusters: return [(w, 1.0) for w in candidates] vectors np.array([self.w2v.wv[w] for w in candidates]) kmeans KMeans(n_clustersn_clusters, random_state42, n_init10) labels kmeans.fit_predict(vectors) result [] for i in range(n_clusters): cluster_words [candidates[j] for j in range(len(candidates)) if labels[j] i] if cluster_words: cluster_vectors np.array([self.w2v.wv[w] for w in cluster_words]) centroid kmeans.cluster_centers_[i] distances np.linalg.norm(cluster_vectors - centroid, axis1) representative cluster_words[np.argmin(distances)] result.append((representative, len(cluster_words))) return result def extract_all(self, text): tfidf self.extract_tfidf(text) textrank self.extract_textrank(text) w2v self.extract_w2v_cluster(text) return { tfidf: tfidf, textrank: textrank, w2v_cluster: w2v } # 使用示例 extractor KeywordExtractor( stopwords_pathstopwords.txt, userdict_pathuser_dict.txt, w2v_modelWord2Vec.load(word2vec.model) ) text open(sample.txt, encodingutf-8).read() results extractor.extract_all(text) print(TF-IDF:, [w for w, _ in results[tfidf][:10]]) print(TextRank:, [w for w, _ in results[textrank][:10]]) print(Word2Vec 聚类:, [w for w, _ in results[w2v_cluster]])这个脚本的关键设计是extract_all同时返回三种方法的结果方便对照。extract_w2v_cluster里先做候选词筛选再聚类避免噪声词干扰。n_clusters默认取 8实际用时按候选词数量调整。如果候选词少于聚类数直接返回候选词本身不强行聚类。参数调整上topK控制每种方法返回的关键词数量一般 15 到 30 之间。n_clusters控制 Word2Vec 聚类的簇数候选词 50 个左右时取 8 到 12 比较合适。停用词表和自定义词典需要根据语料手动维护没有通用版本。验证方法上我习惯先跑一遍三种方法把结果并排看。如果三种方法都抽出了同一个词这个词基本可以确定是核心关键词。如果只有一种方法抽出某个词就要人工判断是不是噪声。这个对照过程跑几轮之后你对参数的直觉会准很多。从那以后我每次换新语料都强制先跑一遍词频统计和分词检查确认停用词表和自定义词典没问题再开始调抽取参数。这个习惯帮我省掉了大量返工时间。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑