做英文文本的关键词提取,TF-IDF 是我最常用也最愿意教别人上手的方法。别急着去背那些复杂的公式,你先把它当成一个“打分系统”:每个词都按它在一篇文档里的重要程度打分,分高就是关键词。配合 Python 的 Sklearn,整个过程 20 行代码就能跑通,没 GPU 也能跑。这篇文章我不会只扔一份代码给你,而是把 TF-IDF 的原理、Sklearn 的坑、参数怎么调、结果怎么解读都串起来。适合刚学 NLP 的同学,也适合已经在做文本分析但被结果“不对味”困扰的同行。所有代码我都验证过,直接复制就能跑,前提是 Python 3.8 以上、装了 scikit-learn。
1. TF-IDF 原理速通:别被公式吓到
1.1 TF:词频,只看出现次数还不够
TF 是词频(Term Frequency),字面意思就是一个词在文档里出现了多少次。这个指标非常直觉,因为一个词如果反复出现,通常说明它和文档主题相关。比如一篇讲机器学习的文章里,“model”“training”“data”大概率会出现很多次,那它们就应该是关键词候选。
但问题也恰恰出在这里:只看词频,你没法区分“重要的词”和“到处都是的词”。英文里the、a、is、of这类停用词出现频率往往比实义词还高,但它们对文章主题没有任何指示作用。长文档也吃亏,一篇 5000 词的报告和一篇 300 词的摘要,同样一个词出现的绝对次数不可比。所以 TF 很少单独使用,它通常会做平滑或归一化。举个例子,Sklearn 里的sublinear_tf会把 TF 变成1 + log(tf),目的就是让“出现 10 次”和“出现 100 次”的差距不要被放大得太离谱。你想想,如果一篇文档里某个词被塞了 100 次,它是不是一定就比出现 10 次的词重要 10 倍?显然不一定。
1.2 IDF:逆文档频率,让“噪音词”现原形
IDF 的全称是 Inverse Document Frequency,逆文档频率。它衡量的是“这个词在整个语料里有多常见”。如果某个词在几乎所有文档里都出现,那它对区分文档没有贡献,IDF 值就低;反过来,如果某个词只在极少数文档里出现,说明它很有辨识度,IDF 值就高。
Sklearn 的TfidfVectorizer默认开启smooth_idf=True,采用的公式是:
idf = ln((1 + N) / (1 + df)) + 1其中N是语料中的文档总数,df是包含这个词的文档数。加 1 是为了防止分母为零,也是让 IDF 值平滑一些。这个公式里隐藏了一个关键点:IDF 的“稀有度”是相对整个语料而言的。如果你拿一篇孤零零的文章来跑 TF-IDF,IDF 就基本失效了,因为语料里只有一篇文档,哪个词都只出现在这一篇里,分母都是 1,IDF 全一样。后面我会专门讲这种场景怎么处理。
1.3 直觉理解:一个人在一群人里说话
把 TF-IDF 想成一个社交场景:你在一个上千人的行业交流群里,如果某个人嘴里老是蹦出只有他自己用的专业黑话,那这词就能帮你判断他是哪个细分方向的;如果所有人都在说“会议”“项目”“好的”,那这些词完全无法帮你判断任何人的身份。TF 是“他说的次数”,IDF 是“别人是不是也这么说话”。两者相乘,就是这个词对这个人身份的“辨识度”。
这个类比放到文档里也是一样。machine learning如果在一篇文档里反复出现,同时整个语料里别的文档很少提到,那它的 TF-IDF 分数自然会很高。反过来,the虽然 TF 高,但 IDF 接近最低值,相乘之后基本被压下去了。理解了这一点,你再看 Sklearn 输出的分数就不会觉得是玄学。
2. 动手前的准备:环境、语料与工具选型
2.1 Python 环境与 sklearn 安装
这一步没什么好炫技的,但版本问题值得说两句。我推荐直接用 pip 安装官方包名scikit-learn,不是sklearn。虽然sklearn曾经是安装包名,但现在已经不建议这么写了,直接装 scikit-learn 才是正路。
pip install scikit-learn装完以后你可以快速验证一下:
import sklearn print(sklearn.__version__)只要不报错,版本号看起来是 1.0 或以上,基本就够用。如果你用的是 Anaconda,或者公司内网环境,也可以:
conda install scikit-learn我个人的经验是:千万不要在还没装 scikit-learn 的时候直接from sklearn.feature_extraction.text import TfidfVectorizer,然后被 ModuleNotFoundError 卡住。很多人卡在环境上,不是卡在算法上。另外,如果你的 Python 版本过老,比如 3.6,建议换到 3.8 以上,因为新版 scikit-learn 已经逐渐放弃对旧版本 Python 的支持,旧环境里装新包经常会出现依赖冲突。
2.2 英文文本预处理为什么比中文省心
英文文本做关键词提取,最大的优势是天然有空格分词。你不需要像中文那样引入 jieba、分词模型或者词表,只要简单切分就能得到词序列。但“省心”不等于“不处理”。实际项目里我至少会做这几步:
- 转小写:让
Machine和machine变成同一个词。 - 去标点:把逗号、句号、引号等去掉,否则
learning,和learning会被当成两个词。 - 合并多余空格:避免空字符串进入特征矩阵。
- 按需去停用词:虽然 TF-IDF 本身能压低常见词,但提前去掉
the、and、of这些词能让特征维度更小,结果更干净。
你还要注意,正则清洗会连带把数字去掉。如果文本是新闻、技术文档,数字通常不是关键词,删掉没问题。但如果是金融日志、版本发布说明,数字可能很重要,比如“v2.0”“2024”“99%”,这时候就不能一刀切。我会在清洗函数里留一个参数,根据场景决定是否保留数字。
2.3 工具选型:Sklearn 还是 Gensim、TextRank
文本关键词提取的工具不少,我自己主流场景还是用 Sklearn,原因很简单:它和后续的机器学习流程衔接最顺。你可以把 TF-IDF 矩阵直接丢给分类器、聚类算法,而 Gensim 的 TfidfModel 更偏向主题建模和向量化,接口风格有点不一样。Jieba 在中文场景很好用,但对英文意义不大。TextRank 是一种基于图排序的方法,效果也不错,但它不是向量化方法,不能直接生成特征矩阵。
我列个对比表,方便你按场景选:
| 工具 | 核心思路 | 适合场景 | 主要短板 |
|---|---|---|---|
| Sklearn TfidfVectorizer | TF-IDF 统计加权 | 关键词提取、文本向量化、特征工程 | 对单篇文档效果差 |
| Gensim TfidfModel | TF-IDF + 稀疏矩阵 | 大规模语料、主题建模 | 与 sklearn pipeline 集成略麻烦 |
| TextRank | 图排序 | 单篇文档关键词/摘要 | 速度较慢,参数敏感 |
| RAKE | 基于标点和停用词切分 | 短文本、无监督关键词 | 对长文档效果一般 |
如果你的目标是“快速提取一批英文文档的关键词,顺便要做特征工程”,Sklearn 是性价比最高的选择。
3. 完整实战:用 Sklearn 提取英文文本关键词
3.1 数据准备与基础清洗
我先给出一份很典型的清洗函数,你可以直接抄。这里的逻辑是保留英文字母和空格,把其他字符统一替换成空格,最后压缩空格。
import re def clean_text(text: str) -> str: text = text.lower() text = re.sub(r'[^a-z\s]', ' ', text) text = re.sub(r'\s+', ' ', text).strip() return text如果你的文本里有 5G、iPhone、C++ 这种带数字或符号的词,这个清洗会把它们拆得不成样子。这是为了演示方便,真实项目里你要么改用更宽容的正则,比如保留[a-z0-9+#.],要么干脆不做正则,只靠 Sklearn 的token_pattern参数来控制词元格式。记住,清洗不是越狠越好,而是要和你的语料匹配。
我准备了一个 5 条短文档的小语料,用来演示完整流程:
docs = [ "The quick brown fox jumps over the lazy dog near the riverbank.", "Machine learning models learn patterns from data to make predictions.", "Natural language processing helps computers understand human language.", "The lazy fox slept under the big tree while the dog chased birds.", "Search engines use keyword extraction to rank relevant documents." ]这种短文档在小演示里足够说明问题。如果是真实项目,你一般会有几百到几十万条文档,操作步骤完全一样,只是需要适当调整min_df和max_df。
3.2 TfidfVectorizer 核心参数详解
Sklearn 的TfidfVectorizer参数很多,但真正影响关键词提取效果的,我梳理下来主要是这几个:
stop_words='english':使用内置英文停用词表,能去掉the、a、is等常见词。你也可以传一个自定义列表,后面进阶部分会讲。ngram_range=(1, 2):同时考虑单个词和相邻两个词组成的短语。只取单个词会丢掉“machine learning”这种组合关键词;选到 3-gram 以上噪声会明显增加,所以要谨慎。max_df=0.9:在超过 90% 文档里出现的词会被忽略。这个值是用来清除语料级高频噪音的,比如新闻语料里到处都是“said”“report”。min_df=2:只在 1 篇文档里出现的词会被忽略。这个值对大规模语料很有效,但如果是像我们这样只有 5 条文档的小演示,设置成 1 更合适,否则很多有区分度的词会被过滤掉。sublinear_tf=True:使用1 + log(tf)做子线性缩放,削弱高频词对分数的过度影响。我几乎总是开启这个参数。smooth_idf=True:IDF 平滑,防止分母为零,Sklearn 默认开启,通常不需要动。norm='l2':每个文档的 TF-IDF 向量做 L2 归一化,默认就是它,用来平衡不同长度文档的向量差异。
这些参数不是越多越好。项目里我习惯先小步调:先跑默认参数看结果,再根据特征数量、关键词合理性去调ngram_range、min_df、max_df,其他保持默认。
3.3 提取关键词的完整代码
下面这段代码可以直接运行,里面包含了清洗、向量化、提取每篇文档 TopN 关键词的完整逻辑:
from sklearn.feature_extraction.text import TfidfVectorizer import re def clean_text(text: str) -> str: text = text.lower() text = re.sub(r'[^a-z\s]', ' ', text) text = re.sub(r'\s+', ' ', text).strip() return text docs = [ "The quick brown fox jumps over the lazy dog near the riverbank.", "Machine learning models learn patterns from data to make predictions.", "Natural language processing helps computers understand human language.", "The lazy fox slept under the big tree while the dog chased birds.", "Search engines use keyword extraction to rank relevant documents." ] clean_docs = [clean_text(doc) for doc in docs] vectorizer = TfidfVectorizer( stop_words='english', ngram_range=(1, 2), max_df=0.9, min_df=1, sublinear_tf=True ) tfidf_matrix = vectorizer.fit_transform(clean_docs) feature_names = vectorizer.get_feature_names_out() def extract_top_keywords(row_index: int, top_n: int = 5): scores = tfidf_matrix[row_index].toarray().ravel() top_indices = scores.argsort()[-top_n:][::-1] return [(feature_names[i], round(scores[i], 4)) for i in top_indices] for i in range(len(docs)): print(f"文档 {i+1}: {extract_top_keywords(i, 5)}")这个代码的核心逻辑是:先fit_transform把文档列表转成一个稀疏矩阵,然后对每一行取出分数数组,用argsort排序后取最大的几个下标,再映射回特征名。argsort()[-top_n:][::-1]这种写法看起来有点绕,其实就是在做“取后 n 个最大值的下标,再倒序让它从大到小排列”。
3.4 输出结果与解读
跑完上面的代码,你会得到类似这样的输出,不同 sklearn 版本可能有细微差异:
文档 1: [('riverbank', 0.6204), ('quick', 0.4930), ('jumps', 0.4930), ('brown', 0.4930), ('fox', 0.2365)] 文档 2: [('machine learning', 0.4973), ('predictions', 0.4725), ('patterns', 0.4725), ('models', 0.4725), ('learn', 0.4725)] 文档 3: [('processing', 0.5000), ('computers', 0.5000), ('understand', 0.5000), ('human language', 0.5000), ('language', 0.4482)] ...你可能会有两个疑问。
第一,为什么riverbank这类只出现一次的词得分这么高?因为 IDF 的设定就是“越稀有越重要”。在只有 5 篇文档的小语料里,某个词只出现在 1 篇里,IDF 会被顶到很高。这是 TF-IDF 的正常行为,不算 bug。但这也提醒你:小语料里提取的关键词会偏向生僻词,这时你可以用min_df=2来过滤,代价是会丢掉一部分真实的关键词。
第二,为什么分数加起来不等于 1?因为 L2 归一化让每个文档向量的模长为 1,每个词的分数是这个向量在某个维度上的分量,它们之间没有简单的求和关系。你只需要比较同一篇文档内部不同词的相对大小,不用跨文档对比绝对分数。
4. 进阶优化:让关键词更准的 4 个技巧
4.1 自定义停用词与词形还原
Sklearn 自带的stop_words='english'覆盖的是高频虚词,但很多领域词也会成为噪音。比如做招聘文本分析时,position、requirement、responsibility每篇都会出现;做科研论文分析时,paper、study、result也到处都是。这时候我会维护一个自定义停用词列表,把它和自带停用词合并:
custom_stop_words = [ "position", "requirement", "responsibility", "paper", "study", "result", "according" ] vectorizer = TfidfVectorizer( stop_words=custom_stop_words, ngram_range=(1, 2) )但要注意,Sklearn 的stop_words参数如果你传列表,它就不会自动叠加内置英文停用词,相当于完全用你自己的词表。所以如果你想“内置 + 自定义”一起用,最简单的做法是把两者拼起来:
from sklearn.feature_extraction.text import ENGLISH_STOP_WORDS stop_words = list(ENGLISH_STOP_WORDS) + custom_stop_words另外,词形还原(Lemmatization)能让learn、learns、learning变成同一个词,避免特征维度爆炸。实际测试中,对英文文档做词形还原之后,关键词往往会更稳定。我常用 NLTK 的WordNetLemmatizer,示例代码如下:
from nltk.stem import WordNetLemmatizer from nltk.tokenize import word_tokenize lemmatizer = WordNetLemmatizer() def lemmatize_text(text: str) -> str: tokens = word_tokenize(text) return ' '.join([lemmatizer.lemmatize(tok) for tok in tokens])然后把lemmatize_text放在clean_text之后、TfidfVectorizer之前。一个小提醒:词形还原比较慢,如果语料很大,可以考虑用 spaCy,或者干脆不做这一步。对关键词提取来说,词形还原是“锦上添花”,不是“雪中送炭”。
4.2 n-gram 与短语级别关键词
很多情况下,单独一个词信息量不够。比如“machine learning”作为一个整体,要比单独的“machine”和“learning”更有主题指向性。把ngram_range设置成(1, 2),就可以同时保留单词和相邻双词短语。
不过 n-gram 也有坑。越是长的短语,在语料里出现的文档数通常越少,IDF 会偏高,所以你会看到一些很别扭的组合被排到前面。我建议在结果里观察一下,如果 TopN 里出现了大量语义不完整的短语,比如“dog near”“near the”,就说明你的清洗或停用词表没做到位。这种短语在去停用词之后通常会消失,因为 Sklearn 的 n-gram 构造会跳过包含停用词的组合。stop_words='english'开启后,the dog、near the这类组合就不会出现在特征里。
如果你想进一步控制特征数量,可以用max_features。比如特征超过 10000 时,只保留 IDF 最高的前 10000 个词,能显著减少内存和训练时间。
4.3 与 TextRank 结合做交叉验证
TF-IDF 是统计加权方法,TextRank 是图排序方法,两者的错误模式并不完全一样。TF-IDF 容易被高频生僻词带偏,TextRank 容易因为窗口内共现关系不稳定而产生奇怪结果。所以在重要项目里,我会把两者跑出来的 Top 关键词做交集,或者加权融合。
TextRank 在 Python 里的实现有很多,简单用pytextrank或者jieba.analyse.textrank都可以,但英文场景我一般用textrank库或者自己实现一套非常简化的版本。这里不展开完整的图算法,核心思路是:把每个词当成节点,词在滑动窗口内共现则连边,用 PageRank 思路迭代计算词权重。
实际操作中,我一般这样融合:
- 分别取 TF-IDF Top 15 和 TextRank Top 15。
- 取两者交集,优先作为核心关键词。
- 如果交集少于 3 个,说明语料噪声大,回到清洗环节检查。
这种方法的好处是,你能得到一个“两种算法都认为重要”的关键词集合,可解释性强很多。
4.4 多文档场景下的 TF-IDF 取舍
TF-IDF 本质上是“在一组文档里找每篇文档的差异化关键词”。如果你的任务是从整个大文档集里抽取全局热点词,就不能对每篇文档单独跑 TF-IDF 再取均值,那样会把每篇文档的独有词放大。更合理的做法是,把整个文档集合并成一个“大文档”,或者按时间段聚合后再提取关键词。
反过来,如果只有一篇文档,TF-IDF 会退化得很厉害。我的经验是:要么把这篇文档拆成段落,把段落当成“伪文档”来计算,要么引入一个背景语料。背景语料可以是维基百科摘要、新闻库或者你自己积累的同领域文档。有了背景语料之后,目标文档里的领域黑话才能获得应有的高 IDF。
所以遇到“单篇文档提关键词效果差”的问题,不要急着调参,先想想你的语料边界在哪里。
5. 常见问题与排查技巧实录
5.1 老遇到 FutureWarning 或版本不兼容怎么办
Sklearn 更新很快,很多旧代码用的是get_feature_names(),新版本已经用get_feature_names_out()替代。如果你看到类似get_feature_names is deprecated的警告,直接把方法名改掉就行。
还有一种常见情况是from sklearn.feature_extraction.text import TfidfVectorizer直接报错。原因大概率是包没装,或者装成了旧的sklearn包。我建议先检查:
import sklearn print(sklearn.__version__)如果 import 没问题,但TfidfVectorizer报错,请把 scikit-learn 升级到最新稳定版:
pip install --upgrade scikit-learn升级后代码一般都能跑,因为 TfidfVectorizer 的接口已经非常稳定。
5.2 提取结果全是停用词或数字怎么办
如果你没有设置stop_words='english',那么 TopN 结果里出现the、and、for是非常正常的。TF-IDF 会降低常见词的分数,但挡不住语料太少时 IDF 区分度不够。解决方案很简单,加上stop_words='english'。
如果结果里全是数字,比如2024、100、99,说明你的清洗规则没有处理数字。对大部分文本分析场景,我会在清洗函数里把数字替换掉或者整体去掉。如果数字本身有业务含义,那就保留,同时把token_pattern改一下,让它可以匹配数字和字母的组合。比如:
vectorizer = TfidfVectorizer(token_pattern=r'(?u)\b\w+\b')这个正则允许单词中带下划线或数字,但也会引入更多噪声,需要自己权衡。
5.3 为什么我的 IDF 值都一样
如果你发现所有词的分数看起来都差不多,或者每一篇文档的 Top 关键词都是那些只出现一次的生僻词,大概率是你的语料太小了。在 5 篇文档的小语料里,很多词的df都是 1,IDF 自然相同,最后区分度全靠 TF。这时候有两个方向:
- 扩大语料,让高频词和低频词的差距拉出来。
- 改用
min_df=2,过滤只出现一次的词,让剩下的词有区分度。
如果是在生产环境里跑大批量数据,IDF 值还一样,那就要检查是不是fit_transform的时候不小心只传了一篇文档,或者数据加载逻辑出了问题。我遇到过有人循环读取文件,结果每篇文章都单独fit_transform了一遍,导致每个文档的 IDF 都是相对自己算的,效果一团糟。正确做法是:所有文档一起fit_transform,或者先用fit拟合语料,再用transform转换新文档。
5.4 结果排序和 TopN 选取的心得
TopN 不是越大越好。对短文本,我通常取 5 到 10 个关键词;对长文档,取 10 到 20 个。还有一个比较实用的技巧是看“分数断崖”,也就是把每个文档的 TF-IDF 分数从大到小排出来,看看第几名之后分数突然掉下去。那个断点往往就是自然的关键词数量。
举个例子,某篇文档的分数是0.62, 0.51, 0.49, 0.31, 0.28, 0.12, 0.11,那我会优先取前 5 个,因为从第 4 名开始分数已经明显下降,第 6 名之后基本就是长尾词。这个规则不是固定的,但比无脑取前 10 要靠谱得多。
最后分享一个小技巧:我在实际项目里很少只跑一次默认参数的 TF-IDF。我会先跑一遍默认,把特征数、Top 10 关键词、分数分布打出来看一眼,再根据语料大小调max_df/min_df。尤其是min_df,小语料直接用 1,上了万篇再提到 2 或 5,否则你会看到一个特别稀疏的矩阵,时间全花在调参上。TF-IDF 本身不复杂,复杂的是语料和业务目标。你把这一步想明白了,后面接分类、聚类都会顺手很多。