
简介资源为基于Python实现中文文本关键词抽取的完整课程设计包适合自然语言处理初学者、高校学生及需要完成文本挖掘相关实验的开发者。内含课程论文、项目源码、实验数据与运行截图体系完整便于对照学习。资源共31个文件涵盖docx课程论文、py源码脚本、csv结果数据、png图表与txt词表等类型压缩包仅1.78MB轻量易用。已有2114人学习下载。内容围绕TF-IDF、TextRank和Word2Vec词向量聚类三种主流方法展开从原理、流程到代码实现均作详细讲解并给出原始语料扩展、标题加权、n_clusters聚类数调整等可优化方向。读者可借助源码直接复现关键词抽取实验理解不同算法的适用场景与调参思路也为课程设计或毕业设计提供可参考的完整方案。1. 中文关键词抽取先跑通 TF-IDF再治 TextRank 和词向量的水土不服做中文文本关键词抽取最容易踩的坑是套英文 NLP 的思路直接用 TF-IDF结果中文分词先埋了雷停用词列表不干净、词性没过滤、语料只有几十篇文档IDF 统计出来的权重失真抽出来的词往往是一堆名词堆砌找不到能概括全文的核心实体。这个工程真正有价值的地方在于它不只给一套 TF-IDF 脚本而是把 TF-IDF、TextRank、Word2Vec 词向量聚类三条路线打包到一起配上sample_data.csv、stopWord.txt、词性标注参考和课程论文让你在同一个数据集上对比三种方法的结果差异。适合做课程设计、信息检索入门和 NLP 特征工程选型的人项目里每个脚本都可以直接改路径复用不必从头写。2. 三种抽取算法的原理边界TF-IDF、TextRank 与词向量聚类差在哪2.1 TF-IDF统计权重里的「反文档频率」才是关键TF-IDF 的核心假设很简单一个词在当前文档里出现得多同时在别的文档里出现得少那它对当前文档的区分度就高。TF 是词频IDF 是逆文档频率工程实现里 IDF 的平滑写法通常是log((总文档数1)/(含该词的文档数1)) 1加 1 是为了防止分母为零时出现无穷大或负值。# 简化版 TF-IDF 打分逻辑用于理解评分走向实际项目直接调 sklearn import math def tfidf_score(tf, doc_freq, n_docs): idf math.log((n_docs 1) / (doc_freq 1)) 1 return tf * idf这段代码里tf是词在当前文档的归一化词频doc_freq是包含该词的文档数n_docs是语料总文档数。工程上不会手写这个公式而是用TfidfVectorizer但理解 IDF 的平滑逻辑对调参有用smooth_idfTrue时内部就是这套1策略sublinear_tfTrue会把 TF 变成1 log(TF)削弱高频词的线性增长。这个细节在短文本里尤其重要因为短文本 TF 全是 0 或 1不做 sublinear 变换TF 项基本没有区分度。TF-IDF 的优势是快、可解释性强缺点也一样明显它只认词面不认语义。「电脑」和「计算机」在 TF-IDF 看来是两个独立的词各自频率被摊薄这在短文本或小语料上会导致关键词分散。2.2 TextRank把文档织成一张共现图再排序TextRank 的思路是把词当作图的节点词与词在滑动窗口内共现就当连一条边节点的重要性用类似 PageRank 的迭代公式计算。与 TF-IDF 不同它不需要外部语料单篇文档就能跑这对数量不够的课程设计数据集是天然优势。# TextRank 权重迭代核心公式的简化示意 def textrank_score(vertex, graph, d0.85, max_iter100): score 1.0 for _ in range(max_iter): new_score (1 - d) d * sum( graph[neighbor] / out_degree(neighbor) * score_probe for neighbor in graph[vertex] ) # 实际工程用上一轮的全局分值做累加 return new_scored是阻尼系数默认 0.85继承自 PageRank含义是用户随机跳转到任意节点的概率补偿max_iter控制迭代收敛次数一般 100 次内就能收敛。用这个公式前需要先做分词、词性过滤和去停用词否则「的」「了」「是」这类功能词会连成高权重的团把实词压下去。TextRank 的边界问题在于窗口大小window直接影响共现边的密度窗口太小词与词连不上窗口太大又会出现全连接图导致每个词分数趋同。一般取 5 比较稳专业文档可以试 3 或 7。2.3 Word2Vec 聚类用语义密度替代词频先验Word2Vec 把每个词映射成稠密向量词的语义关系体现在向量距离上。关键词抽取任务里常结合聚类先对所有候选词向量化再用 KMeans 聚成若干类最后在每一类里挑离簇中心最近的词作为该类的代表关键词。这样抽取出来的词不一定在原文里高频但能覆盖不同的语义子系统避免 TF-IDF 那种「高频但重复」的输出。工程实现上有两种路线一个用预训练好的词向量直接映射候选词另一个是先用语料从头训练 word2vec 模型再提取词向量。这个项目里keyextract_word2vec_1.py和keyextract_word2vec_2.py就是这两条路线的对照实现。前者的前提是语料量够至少要几十万句才能训练出稳定的向量后者在小数据集上更现实但会引入预训练词表和文本分词结果的词汇覆盖问题OOV 词直接落空。2.4 三种方案的选型对照方法依赖资源语义感知单文档可用主要瓶颈TF-IDF文档集合弱否小语料 IDF 失真同义词无法聚合TextRank单篇文档弱是窗口大小敏感功能词需清洗Word2Vec 聚类大规模语料或预训练向量强否未登录词聚类簇数难定选型时我会这么判断句子数量少的任务直接上 TextRank图里连得起来的词本身就说明结构紧密文档集合能给到几百篇以上让 TF-IDF 输出更稳定可以考虑 TF-IDF 加词性过滤的组合如果明确要跨文档归纳同义表达再上 Word2Vec 路线。三种方法看的是不同维度的信号实际项目中先用 TF-IDF 或 TextRank 粗筛一遍再用词向量对粗筛结果做语义去重比单独跑任何一种效果都稳。3. 从数据到特征分词、停用词与词性过滤的实现3.1 输入数据与目录结构这个项目的数据组织方式很小而完整sample_data.csv作为测试集stopWord.txt提供停用词表词性标注参考.txt用来辅助过滤词性三个抽取脚本分别输出keys_TFIDF.csv、keys_TextRank.csv、keys_word2vec.csv。先看数据格式再动手避免脚本读取时报编码错误。# 查看数据文件的前两行确认列名和分隔符 head -2 sample_data.csvhead -2输出第一行是表头第二行是正文样例。文本文件统一用 UTF-8 编码读取Windows 下用记事本存过的话文件头可能带 BOMpandas.read_csv()默认utf-8读会报UnicodeDecodeError这时加encodingutf-8-sig就能解掉。CSDN 上很多中文项目踩的都是这一个坑不值得花时间排查。3.2 分词与停用词过滤jieba 的精确模式与词性标注关键词抽取的前提是拿到干净的中文词序列。jieba 提供三种分词模式这里用默认的精确模式因为它最贴合「保留完整词汇」的诉求搜索引擎模式会把词再切碎反而破坏关键词的完整性。分词后还需要去掉停用词和纯标点并控制词长和词性。# keyextract_tfidf.py 中分词部分的常见写法 import jieba import jieba.posseg as pseg stopwords set() with open(stopWord.txt, r, encodingutf-8) as f: for line in f: stopwords.add(line.strip()) def cut_and_filter(text): words [] for word, flag in pseg.cut(text): # flag 以 n 开头通常是名词v 开头是动词a 开头是形容词 if word.strip() and word not in stopwords and len(word) 1: if flag.startswith((n, v, a)): words.append(word) return words代码里用了jieba.posseg.cut而不是普通的jieba.cut原因是在一个函数里同时拿到词和词性避免再跑一遍标注流程。flag.startswith((n, v, a))取出名词、动词、形容词这些词性是关键词的高频来源len(word) 1过滤单字词因为中文单字词通常是「的」「是」「在」这类功能词或不成词的语素留它们进图模型会拉低 TextRank 的信噪比。注意flag的取值不是固定的比如「研究」在 jieba 里可能是n也可能是vn因此只判断前缀比判断全等更可靠。3.3 TF-IDF 向量化用 sklearn 一步到位了解了 IDF 的平滑逻辑后实际实现直接用TfidfVectorizer最省事但参数得按中文场景调好。# 中文 TF-IDF 关键词抽取脚本的核心段落 from sklearn.feature_extraction.text import TfidfVectorizer vectorizer TfidfVectorizer( tokenizercut_and_filter, # 直接用上面定义的分词函数做 tokenizer max_features5000, # 限制特征数量压缩噪声 sublinear_tfTrue, # 对 TF 做对数变换降低高频词主导 min_df1, max_df0.8 # 过滤极端低频和 80% 以上文档都出现的高频词 ) tfidf_matrix vectorizer.fit_transform(docs)tokenizercut_and_filter把之前写的分词过滤函数直接塞给 sklearn向量化不再走默认的英文 token 切分这是中文 TfidfVectorizer 最关键的配置。min_df1表示词至少要出现在 1 个文档里max_df0.8表示词若出现在 80% 以上的文档中则排除这两个参数等价于全局高频噪声过滤。sublinear_tfTrue在上述原理里解释过是为了缓解长文档里高频词对分数的垄断。抽取时不要直接取tfidf_matrix的全局最大值而是对每个文档单独排序后取 Top N避免长文档在全局比较中占优。3.4 阈值思考什么时候需要去高频词三份源码都涉及一个共同问题有些词在每个文档里都出现虽然 IDF 不高但 TF 乘出来依然能排进 Top N。TextRank 对这类词更敏感共现图上它们几乎和所有节点相连迭代后分数虚高。项目摘要里专门提到「去除所有文档中都包含某一出现频次超过指定阈值的词语」就是针对这个现象的优化。我一般会在分词函数里直接收集全局词频对出现文档占比超过 0.6 且不是业务核心术语的词做剔除。典型的例子是「问题」「方法」「研究」这类课程论文高频词它们本身有意义但无法区分文档主题。判断是否剔除要看业务语义如果是财经领域「风险」这个词在每篇研报里都出现剔除反而会让输出的关键词缺乏领域色彩。批量筛选测试时可以先保留观测输出后再决定。4. 三份源码的实战走查从 TextRank 构图到 Word2Vec 聚类4.1 keyextract_textrank.py共现窗口与基于 PageRank 迭代打分TextRank 脚本的核心是先构建窗口共现图再用自实现的 PageRank 迭代计算词权重。构建图时每个词是一个节点一个窗口内的任意两个词之间连一条边边的权重用两个词的共现次数累积。# keyextract_textrank.py 简化后的图构建与打分主流程 import jieba.posseg as pseg from collections import defaultdict def build_word_graph(words, window5): graph defaultdict(lambda: defaultdict(int)) for i in range(len(words)): for j in range(i 1, min(i window, len(words))): w1, w2 words[i], words[j] if w1 ! w2: graph[w1][w2] 1 graph[w2][w1] 1 return graph def rank_words(graph, d0.85, max_iter100): scores {node: 1.0 for node in graph} nodes list(graph.keys()) for _ in range(max_iter): new_scores {} for node in nodes: s 1 - d for neighbor in graph[node]: total sum(graph[neighbor].values()) s d * (graph[node][neighbor] / total) * scores[neighbor] new_scores[node] s scores new_scores return scores第一段滑窗代码里window5的含义是当前词只看往后 4 个词避免窗口太大导致图过度稠密graph[w1][w2] 1是共现边的无向累积权重越大代表两词联系越紧密。第二段的迭代公式里graph[neighbor]的values()求和得到 neighbor 的出度总权重graph[node][neighbor] / total相当于把节点的重要性按比例分配出去d控制保留多少原始传递权重。迭代收敛后取scores里最高的 N 个词即可输出到keys_TextRank.csv。一个容易出错的点words列表必须提前去掉停用词和单字词否则「的」会成为全连接枢纽节点。另一个点是窗口滑动的边界min(i window, len(words))保证了最后一小段不越界但这也让末尾词的邻居数变少长文档里末尾关键词往往被低估。要修正可以在结尾补文档结束符或者对每个词的得分除以它的度做归一化。4.2 keyextract_word2vec_1.py / _2.py词向量训练与 KMeans 聚类Word2Vec 路线分两段先用 gensim 训练或加载词向量再用 KMeans 做聚类抽代表词。脚本_1.py是训练词向量版本_2.py是加载预训练版本结构上后者少了训练步骤但多了词表对齐逻辑。# keyextract_word2vec_1.py 的核心训练与聚类段 from gensim.models import Word2Vec from sklearn.cluster import KMeans import numpy as np # sentences 是分好词的文档列表每一篇是 list[str] model Word2Vec( sentences, vector_size100, window5, min_count2, workers4, epochs10 ) def cluster_keywords(word_list, model, n_clusters3): # 只保留在词表中的候选词OOV 词直接丢弃 valid [w for w in word_list if w in model.wv] vecs np.array([model.wv[w] for w in valid]) km KMeans(n_clustersn_clusters, random_state42, n_init10) labels km.fit_predict(vecs) result [] for i in range(n_clusters): cluster_vecs vecs[labels i] center km.cluster_centers_[i] dist np.linalg.norm(cluster_vecs - center, axis1) result.append(valid[np.argmin(dist)]) return resultvector_size100对课程设计的数据量是合适的数据量更大时可以升到 200 或 300min_count2过滤掉只出现一次的词这些词训练不充分向量噪声大。n_clusters3对应期望抽出的主题组数如果文本实际覆盖 5 个主题就应该设成 5这一点笔记摘要里专门提醒过。聚类后取每类离簇中心最近的那个词作为代表词代表的是该类语义的中心而不是最高频的词这正是 Word2Vec 与 TF-IDF 的显著差异。n_init10是 KMeans 为避免局部最优而做的多次初始化次数结果不稳定时可以调大到 20。要注意_2.py加载预训练模型后候选词在词表里找不到的情况非常多尤其专业领域词。常见的兜底策略是把 OOV 词直接剔除如果你不希望丢词可以用 Word2Vec 里compute_lossTrue继续微调模型或者退回到 TextRank 结果做融合。4.3 跑通实验三份脚本一键产出三份 CSV项目根目录下直接按顺序运行三个脚本输出文件各归各的互不覆盖。# 运行三个关键词抽取脚本产出三份 CSV python keyextract_tfidf.py python keyextract_textrank.py python keyextract_word2vec_2.py如果keyextract_tfidf.py跑的时候报TypeError: NoneType object is not callable大概率是TfidfVectorizer的tokenizer参数和token_pattern冲突token_pattern在自定义 tokenizer 时会失效需要显式设token_patternNone。word2vec脚本报ModuleNotFoundError: No module named gensim就执行pip install gensim装完如果 import 时因 numpy 版本编译报错优先把 numpy 降到 gensim 兼容的版本区间再不行换pip install gensim3.8.3。Windows 环境下注意文件路径分隔符脚本里写死了/就统一用正斜杠不要改成\否则读取sample_data.csv时会定位不到文件。4.4 结果对比同一文本三份关键词为什么不同跑完后再看三份 CSV差异会比想象中大。用sample_data.csv中的一条文本举例TF-IDF 的结果偏向「有辨识度的实体词」TextRank 的结果偏向「结构上的枢纽词」Word2Vec 聚类的输出则是「语义中心词」方法输出关键词示例特性TF-IDF人工智能、医疗、应用、算法实体名词为主和语料稀有度强相关TextRank领域、模型、研究、算法与词共现结构强相关偏概括性词汇Word2Vec 聚类技术、场景、数据、系统语义中心词可能与原文措辞不完全一致这个对比结果不是某个脚本出了 bug而是三种方法对「重要」的定义不同。TF-IDF 认为稀有度更重要TextRank 认为连接度更重要词向量认为语义代表性更重要。文本中「算法」既高频又连接度高所以三种方法都会选中「医疗」只在少数几篇出现TF-IDF 才会给它高分。用哪个方法取决于你到底想要「这篇文档的辨识词」还是「这篇文档的主题概括」。4.5 批量验证用精确率和召回率检验抽取质量只看单条结果容易自我感觉良好更客观的做法是人工标注后再批量评估。给每条文本标好标准关键词然后跑一遍 Top N 命中率。# 用标准答案评估 Top N 抽取质量 def evaluate_topn(pred_keywords, gold_keywords, k): pred_set set(pred_keywords[:k]) gold_set set(gold_keywords) hit len(pred_set gold_set) precision hit / k recall hit / len(gold_set) if gold_set else 0 return precision, recallk取 5 到 10 之间比较合理课程设计里一般取 5。注意precision的分母是k而不是len(pred_set)因为抽取结果去重后数量可能小于k分母用k可以体现「输出质量」而非「命中密度」。复盘时如果 precision 和 recall 双双偏低先检查分词和停用词再考虑调特征维度不要一上来就换算法。5. 从课程设计到生产落盘、编码、稳定性验证5.1 输出编码与 CSV 一致性三份keys_*.csv在 Windows 上打开出现乱码不要急着怀疑脚本逻辑先看写文件的编码。常见做法是写 CSV 时用utf-8-sig编码让 Excel 正确识别 BOMimport csv with open(keys_TFIDF.csv, w, newline, encodingutf-8-sig) as f: writer csv.writer(f) writer.writerow([doc_id, keywords])encodingutf-8-sig会在文件开头写入 BOMExcel 自动识别为 UTF-8如果用纯utf-8写Excel 默认按 GBK 解码中文全部乱码。Linux 服务器上处理这份 CSV 时反而要去掉 BOM否则pd.read_csv(keys_TFIDF.csv)会把第一列列名读成\ufeffdoc_id。两边场景不同处理方式正好相反这算是 Python 中文文本项目最典型的落盘问题了。5.2 三处值得改的细节标题加权、n_clusters、领域语料课程论文里提到的三个改进点都值得落到代码里。标题加权可以在预处理阶段增加词的初始分数标题词在 doc 权重里乘个 1.5 的系数但深层的分词阶段就要把「标题」和「正文」分开处理。word2vec 的n_clusters按文本分类数设置如果数据本身是单主题长文本就不要用聚类输出全部中心词改成每个簇取 Top 3 才能补足关键词数量。模型训练时加入领域语料是对效果提升最明显的一步课程设计用的sample_data.csv太小Word2Vec 在几万词级别的语料上训练不出稳定的语义关系可以把同领域的额外文本拼进去再重新训练词向量。5.3 一个自检脚本确认输出是否可复现实践中我习惯给三份输出脚本各补一个自检逻辑固定随机种子并输出文本哈希用于确认同一输入多次运行结果一致避免在答辩时前后两次演示结果不一致。# 稳定性检查固定 TextRank 的随机行为输出摘要 import random random.seed(42) # 对每条文本计算 hash加入输出文件头 import hashlib digest hashlib.md5(text.encode(utf-8)).hexdigest() print(digest, ,.join(top_keywords))random.seed(42)只对依赖随机初始化的算法有效TF-IDF 和 TextRank 本身不依赖随机性Word2Vec 则要留意seed参数。换一台机器、换一个 Python 小版本gensim 的训练结果也可能发生细微偏移这是正常的。真正需要关注的是排序结果是否整体稳定而不是具体某个词的分数浮动了零点零几。落在 CSV 里的关键词顺序就是最终交付结果保证稳定输出的最直接办法是固定随机种子、固定数据读取顺序、关闭多线程不确定性。本文还有配套的精品资源点击获取