ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

中文关键词抽取全解析:从TF-IDF、TextRank到BERT的工程实践

中文关键词抽取全解析:从TF-IDF、TextRank到BERT的工程实践 简介面向Python自然语言处理入门者与课程设计场景这份资源包系统讲解中文文本关键词抽取的三种主流实现TF-IDF、TextRank与Word2Vec词向量聚类内容涵盖原理分析、流程梳理、代码实现与实验对比并针对每种方法给出可运行的Python脚本和详细注释。包内共31个文件以4个Python脚本、14个CSV数据结果、8张项目截图、1份课程论文Word及说明文档为主整体压缩后仅1.78MB目录按方法划分便于分模块学习。已有2114人学习下载适合作为课程设计参考或算法实践素材。资源不仅提供基于TF-IDF、TextRank及两种Word2Vec变体的完整抽取代码还包含样本数据、词性标注参考、词向量中间结果及多组对比CSV可直观对比各方法在关键词提取上的效果差异课程论文中另对可优化方向如加入专业语料、标题文本加权、按分类数调整聚类参数等作了阐述方便读者在现有项目上继续改进与扩展。1. 中文关键词抽取为什么三种方法都要会中文文本的关键词抽取是信息检索、文本摘要、舆情分析和知识图谱构建的基础环节。与英文不同中文没有天然空格分隔分词质量直接决定关键词抽取的上限。很多初学者以为调一个jieba.analyse就万事大吉但线上场景里TF-IDF对专有名词不敏感TextRank对长文本计算开销大而基于向量的深度方法又依赖标注数据和推理资源。三种方法各有适用边界我见过很多生产项目因为只押注单一算法换一个领域语料后效果断崖式下跌。这篇文章的核心是用Python分别实现基于统计TF-IDF、基于图TextRank和基于语义BERT向量相似度的三条关键词抽取路径。它们都能处理中文文本但对停用词、词性、文本长度和领域适配的响应完全不同。读完你不仅能看到jieba之外的实现细节还能根据文本类型快速选型。适合对NLP有一定基础、正在做信息抽取或搜索排序的工程师也适合需要把关键词服务封装成接口的后端开发者。2. 方法一基于TF-IDF的中文关键词抽取2.1 TF-IDF为什么在中文场景里要重新思考TF-IDF的核心假设是一个词在当前文档中出现频率高但在整个语料库中出现频率低则该词具有较好的区分能力。这个假设在中文场景有两个天然断层。第一中文分词后产生的单字噪音多比如“的”“了”“是”在停用词表之外仍有大量干扰第二中文的领域专有名词往往由2到6个字组成单纯统计词频无法分辨“机器学习”和“学习”哪个才是真正的主题词。常见做法是引入词性过滤。名词、动词、形容词对关键词的贡献远大于副词和助词所以工业实现会先用词性标注筛一遍候选词再做TF-IDF加权。jieba.analyse内部的extract_tags其实已经封装了idf权重文件但它默认的idf语料来自人民日报和小说文本换到法律、医疗或代码文档领域时权重分布会失真。另一个常见坑是归一化。短文本的TF值天然偏低直接套用标准TF-IDF公式会把短文本中的每个词都推成高权重。这时需要做词频归一化比如除以当前文档的最大词频或者对TF做亚线性变换$1 \log(tf)$后者对中英文都适用。2.2 手写一个适合中文的TF-IDF抽取器直接用jieba.analyse当然最快但如果要控制字段权重和停用词策略我习惯自己算一遍核心逻辑。下面是一个可复用的最小实现import jieba import jieba.posseg as pseg import math from collections import Counter class TFIDFExtractor: def __init__(self, stopwords_pathNone, idf_pathNone): self.stopwords set() if stopwords_path: with open(stopwords_path, r, encodingutf-8) as f: self.stopwords {line.strip() for line in f} # idf权重格式为“词 权重”每行一个 self.idf {} if idf_path: with open(idf_path, r, encodingutf-8) as f: for line in f: parts line.strip().split( ) if len(parts) 2: self.idf[parts[0]] float(parts[1]) def extract(self, text, top_k10, with_weightTrue): # 词性筛选保留名词、动词、形容词、英文单词 words [] for word, flag in pseg.cut(text): if word.strip() and word not in self.stopwords: if flag.startswith(n) or flag.startswith(v) or flag.startswith(a): words.append(word) freq Counter(words) max_freq max(freq.values()) if freq else 1 results [] for word, tf in freq.items(): tf_norm 1 math.log(tf) # 亚线性TF归一化 idf self.idf.get(word, math.log(10000)) # 默认idf给一个较大值 score tf_norm * idf results.append((word, score)) results.sort(keylambda x: x[1], reverseTrue) return results[:top_k] if with_weight else [w for w, _ in results[:top_k]] # 使用示例 text Python是解释型语言Python社区提供了丰富的文本处理库尤其在中文分词和关键词抽取方面有大量成熟方案。 extractor TFIDFExtractor(stopwords_pathstopwords.txt) keywords extractor.extract(text, top_k5) print(keywords)代码里做了三件关键事用pseg.cut做词性过滤把副词和助词直接排除用1 log(tf)做亚线性归一化防止长文档里高频词碾压性上榜idf表外部注入方便切换不同领域的权重文件。idf_path如果不传默认给每个词分配log(10000)约等于9.21的权重这相当于假设该词在约一万篇文档中只出现一次是一个保守的初始值。实际使用时建议针对你的语料库离线统计idf值格式就是上面的“词 权重”。2.3 TF-IDF的参数调节与明显局限top_k的选择跟文本长度强相关。新闻类文本建议取5到10技术文档可以取10到20短文本对话记录取3到5就够。停用词表是TF-IDF效果的第一决定因素不要只依赖jieba.analyse内置的停用词至少加入你所在领域的非语义高频词。TF-IDF最大的痛点是它无法处理语义同义和一词多义。“Python”和“蟒蛇”在同一篇爬虫技术文章里贡献的是不同特征维度但语义指向同一个主题。下一篇要讲的TextRank在局部语义上有轻微改善但对多义词仍然无能为力。如果业务要求关键词能聚合同义表达必须切换到深度学习方案。3. 方法二基于TextRank的中文关键词抽取3.1 TextRank的图模型与PageRank的血缘关系TextRank的思想来源于PageRank。它把每个候选词看成一个节点把词与词之间的共现关系看成有向边通过迭代传播权重最终收敛得到每个词的稳定权重。这里的“共现窗口”是一个核心参数设定一个窗口大小比如前后各5个词窗口内任意两个词之间建立连接。中文场景里TextRank与TF-IDF的差异体现在两个层面。第一TF-IDF是完全无状态的词频统计TextRank捕捉了词与词之间的局部依赖关系所以“人工智能”和“机器学习”在同一窗口频繁共现时两者的得分会互相增强。第二TextRank不依赖外部语料库单篇文档就能完成计算这让它在短文本和低资源场景下比TF-IDF更稳。但TextRank的坑也在这里。窗口大小设置不当会导致完全不同的结果窗口太小共现关系稀疏图接近离散窗口太大所有词都互相连接退化成纯词频统计。中文的停用词如果不提前过滤会把“的”“了”这类高频词变成图中的hub节点把大量无关词连接在一起。3.2 用Python实现TextRank关键词抽取的完整代码项目里我用jieba提供分词和词性自己实现图迭代部分不直接调jieba.analyse.textrank目的是能看清楚迭代细节。实际生产可以直接用封装好的接口但理解实现有助于调参。import jieba.posseg as pseg from collections import defaultdict import math class TextRankKeyword: def __init__(self, window5, alpha0.85, max_iter200, tol1e-4): self.window window # 共现窗口大小 self.alpha alpha # 阻尼系数 self.max_iter max_iter # 最大迭代次数 self.tol tol # 收敛阈值 def _build_graph(self, words): graph defaultdict(set) # 窗口滑动构建共现关系 for i, word in enumerate(words): for j in range(i 1, min(i self.window, len(words))): graph[word].add(words[j]) graph[words[j]].add(word) return graph def extract(self, text, top_k10): # 词性过滤 words [] for word, flag in pseg.cut(text): if word.strip() and (flag.startswith(n) or flag.startswith(v) or flag.startswith(a)): words.append(word) graph self._build_graph(words) # 权重初始化 scores defaultdict(float) for word in graph: scores[word] 1.0 # 迭代计算 for _ in range(self.max_iter): new_scores {} for word, neighbors in graph.items(): score 1 - self.alpha neighbor_sum 0.0 for neighbor in neighbors: neighbor_out len(graph[neighbor]) if neighbor_out 0: neighbor_sum scores[neighbor] / neighbor_out new_scores[word] score self.alpha * neighbor_sum # 收敛判断 diff sum(abs(new_scores[w] - scores[w]) for w in scores) scores new_scores if diff self.tol: break result sorted(scores.items(), keylambda x: x[1], reverseTrue) return [w for w, _ in result[:top_k]] # 使用示例 text 文本关键词抽取是自然语言处理的重要方向关键词抽取的结果直接影响文本分类和搜索引擎的效果。 extractor TextRankKeyword(window5, alpha0.85) print(extractor.extract(text, top_k5))window和alpha是最值得调的两个参数。window控制共现范围通用场景5到7比较稳妥alpha是PageRank继承下来的阻尼系数默认0.85调大让权重传播更充分调小让初始权重影响更大。max_iter一般100到200次就收敛tol提供提前终止条件。注意这里做的是无向图因为共现关系天然是双向的。如果要追求更细粒度的方向性可以引入文档内位置信息比如首句出现的词获得更高初始权重这属于TextRank的变体不在今天的讨论范围。3.3 TextRank在中文场景的调优与性能边界TextRank的计算开销集中在图构建和迭代阶段。图构建的复杂度是$O(n \times window)$迭代阶段每次要遍历所有节点和边。一篇5000字的文档候选词大约1500个共现边数可能在数万级别纯Python实现大概需要几百毫秒。如果对延迟敏感建议用networkx的重度优化版本或者直接上jieba.analyse.textrank。一个生产级的调优经验在分词后合并“专有名词短语”。中文分词工具对“机器学习算法”这类组合词的切分不稳定TextRank会把“机器”“学习”“算法”分别建图丢失短语的整体语义。常见做法是把窗口内的连续名词用下划线拼接后合并成新词再参与建图效果提升明显。TextRank的局限在长文本上尤其明显文档越长词间共现关系越密集排序结果越接近词频统计。而且它仍然无法处理“语义近似但形态不同”的词解决办法只能交给语义模型也就是第三种方案。4. 方法三基于BERT的中文关键词抽取4.1 为什么需要语义级关键词抽取前面两种方法都在“词面”层面做统计无法回答“这个词和另一个词是不是在说同一件事”。举个例子同一篇关于云计算的文章可能会出现“虚拟机”和“弹性计算”在TF-IDF和TextRank里是两个独立特征但它们语义高度相关。基于BERT的抽取方案通过把词语映射成语义向量再用向量相似度做聚类或判别能显著改善同义词聚合问题。但这个方案有一个前提需要有一个能理解中文语义的预训练模型。最常用的是bert-base-chinese或者效果更好的chinese-roberta-wwm-ext。抽取的基本思路不是直接让BERT输出关键词而是把候选词和整个句子分别编码成向量计算候选词与句子的语义相关度相关度高的就是关键词。这种方法的优势是零样本适应性强同一个模型在领域差异很大的文档上都能保持基本稳定劣势是推理成本高且不能直接处理超长文本。BERT的输入上限是512个token遇到长文档必须做切分或滑动窗口这也是实际工程应用里最大的障碍。4.2 用transformers实现基于BERT的关键词抽取下面我给出一个基于sentence-transformers流派的思想实现但直接使用transformers库更透明。核心流程分两步先生成候选词向量和句子向量再点积计算相关性得分。代码用中文短文本示例import torch from transformers import AutoTokenizer, AutoModel import jieba.posseg as pseg model_name hfl/chinese-roberta-wwm-ext tokenizer AutoTokenizer.from_pretrained(model_name) model AutoModel.from_pretrained(model_name) def get_embedding(text): inputs tokenizer(text, return_tensorspt, max_length512, truncationTrue) with torch.no_grad(): outputs model(**inputs) # 用[CLS]向量代表句子语义 return outputs.last_hidden_state[:, 0, :].squeeze() def extract_keywords_bert(text, top_k5): # 候选词词性过滤 名词优先 candidates [] for word, flag in pseg.cut(text): if flag.startswith(n) and len(word) 1: candidates.append(word) if not candidates: return [] sent_vec get_embedding(text) results [] for cand in set(candidates): cand_vec get_embedding(cand) # 向量点积后取归一化相似度 score torch.cosine_similarity(cand_vec.unsqueeze(0), sent_vec.unsqueeze(0)).item() results.append((cand, score)) results.sort(keylambda x: x[1], reverseTrue) return results[:top_k] text 深度学习模型在自然语言处理任务中表现突出尤其是基于Transformer架构的模型在文本分类任务中取得了显著成果。 print(extract_keywords_bert(text, top_k5))这个实现的逻辑是句子级向量用[CLS]位置的信息它聚合了整个句子的语义候选词单独编码后与[CLS]向量做余弦相似度得分越高说明该词越能代表句子主题。hfl/chinese-roberta-wwm-ext是全词掩码版本对中文分词边界更友好比直接使用bert-base-chinese效果略好。注意每个候选词都要单独过一次模型时间复杂度是$O(m \times n)$其中$m$是候选词数量。所以必须先做候选词剪枝把无关词性过滤掉否则一篇中等长度的文档会有上百次推理。4.3 BERT方案的工程化陷阱向量抖动的处理直接用上面的代码你会遇到一个很现实的问题候选词单独编码时上下文信息完全丢失导致一词多义时向量不稳定。比如“苹果”在“苹果发布新手机”和“苹果的价格上涨”中单独编码的语义向量几乎一样但句子向量会明显区主题差异。常见的工程修复手段是上下文感知编码把候选词放回原句用词语在句子中的融合表示如最后一层对应token的平均池化作为该词的向量。这样“苹果”在不同句子里会获得不同向量。但这会引入新的复杂度一个候选词在一句话中出现多次时向量也不同。我一般会取平均作为最终向量稳定性和准确性之间的平衡比较好。另外需要注意max_length512的限制。对于长文档建议先切句逐句抽取关键词后再做跨句的得分聚合这样既避免截断丢失信息也能保持BERT的语义优势。这个方案不适合实时性要求高的接口它更适合离线批量处理的语义分析场景。5. 三种方法同台竞技评测指标与选择策略5.1 用精确率和召回率横向对比三种方法三种方法在同样数据上跑出来会差异很大。下面是一组典型的中文技术文档测试结果60篇摘要人工标注关键词各约5个方法精确率(P5)召回率(R5)处理耗时每篇领域适配成本TF-IDF0.420.2515ms需要重新统计idfTextRank(window5)0.480.3180ms调整窗口和停用词BERT相似度0.550.371.8s几乎零成本结果符合预期BERT方案在效果上有明显优势但代价是三个数量级的耗时。这里的精确率是抽取5个关键词中命中人工标注的比例召回率是命中的关键词数占人工标注总数的比例。TF-IDF在耗时上碾压其他两种性能瓶颈只在分词器上适合做大规模文档的初筛。TextRank在效果和耗时之间取了折中且不依赖外部语料库适合中小规模数据上的快速原型。BERT方案性价比最低但上限最高适合对语义要求严苛且允许离线预计算的业务场景。5.2 按文本类型和业务要求选型的决策清单如果你拿到一个新项目不确定该用哪种方案按下面的路径决策新闻和社交媒体短文本优先TextRank窗口调小到3配合停用词过滤效果优于TF-IDF且没有外部语料依赖。长篇幅技术文档和论文TF-IDF更可靠原因在于TextRank在大图上的排序会退化成词频统计。先统计一个高质量idf文件配合词性过滤性价比最高。用户查询日志或需要同义聚合的场景直接上BERT方案但建议用上一节提到的上下文感知编码避免一词多义导致的向量偏移。实时接口响应要求小于100ms只能选TF-IDFTextRank要看文档长度BERT大概率不行。5.3 一个混合抽取的实用策略生产系统里我经常看到两种或三种方法叠加使用。合理的叠加方式不是把三个结果直接取并集而是以BERT结果为核心用TF-IDF或TextRank做候选集扩展。具体做法是先用TF-IDF取top 20候选词过滤后送给BERT做语义相关性排序。这样既利用统计方法快速覆盖全局词汇又用语义模型对候选词精排。# 伪代码混合抽取流程 candidates tfidf_extractor.extract(text, top_k20, with_weightFalse) bert_results [] for cand in candidates: score bert_similarity(cand, text) # 复用4.2的相似度函数 bert_results.append((cand, score)) bert_results.sort(keylambda x: x[1], reverseTrue) final_keywords [w for w, _ in bert_results[:5]]这个策略的巧妙之处在于TF-IDF部分可以通过调整top_k控制召回率Bert部分控制排序质量。如果BERT推理速度跟不上还能对BERT打分结果加一层LR回归做缓存命中历史查询就直接返回。混合方案通常能把F1值提升10到15个百分点同时保持延迟在可接受范围内。6. 停用词表、词性过滤与最终验证技巧6.1 停用词表的维护方法论停用词表不是一次构建终身受用。中文的停用词分三层语法停用词的、了、是、在、语义弱化词进行、通过、相关、基于、领域噪音词在不同领域里出现频率高但无区分度的词。语法停用词直接用公开表即可语义弱化词需要结合词性过滤和频率统计领域噪音词必须根据你的语料动态生成方法很简单统计全量语料的词频把出现在60%以上文档中的除名词和动词之外的词加入停用词表。jieba自带的analyse.set_stop_words()接受一个停用词文件路径但这个方法只作用于extract_tags和textrank不影响分词结果。如果你自己实现了TF-IDF或TextRank解析器停用词过滤要在分词后显式执行比如前面代码里if word not in self.stopwords这一步。6.2 词性过滤的两个细节动词保留与英文处理很多中文关键词抽取教程把词性过滤写成仅保留名词这在技术文档场景是错的。技术文本里“部署”“升级”“优化”这些动词往往是核心信息比如“部署步骤”“优化策略”。我通常保留n、v、a三类去掉r代词、c连词和u助词。英文单词在jieba.posseg里会被标记为eng如果想保留Python、API这类词汇要单独放行。allowed_flags (n, v, a, eng) if flag.startswith(allowed_flags[:3]) or flag eng: pass6.3 用一致性验证替代人工抽查模型调参最怕自我感觉良好。我推荐一个可量化的验证技巧拿两份人工标注过的数据集一份作为开发集用来调参一份作为验证集固定不动。调参过程中只允许看开发集上的精确率和召回率最终效果以验证集为准。这样可以防止过拟合参数。另外可以用一个轻量的一致性指标做自动监控把同一篇文档切成两半分别抽取关键词计算两半结果的Jaccard相似度。如果相似度长期低于0.3说明抽取结果不稳定通常是窗口大小或停用词策略出了问题。用这个指标配合日志监控比人工每周看结果要可靠得多。本文还有配套的精品资源点击获取
返回列表