作为自然语言处理(NLP)领域的“老将”,TF-IDF以“词频-逆文档频率”的巧妙逻辑,成为连接原始文本与智能分析的桥梁。今天我们来盘一盘TF-IDF。
TF-IDF
简介
TF-IDF(Term Frequency-Inverse Document Frequency,词语频率-逆文档频率)是一种强大的技术,用于分析和理解单词在文档语料库中的重要性。
TF-IDF 最初是为文档搜索和信息检索设计的,其中运行查询,系统必须找到最相关的文档。
TF-IDF 是一种数字统计量,它反映了文档中某个单词相对于文档集合(称为语料库)的重要性。TF-IDF 背后的理念是量化词语在文档中的重要性,包括它在文档中的频率及其在多个文档中的稀有性。
字词的重要性随着它在文档中出现的次数成正比增加,但同时会随着它在语料库中出现的频率成反比下降。简单的解释为,一个单词在一个文档中出现次数很多,同时在其他文档中出现此时较少,那么我们认为这个单词对该文档是非常重要的。
TF-IDF 在文本挖掘、信息检索和文档分类等任务中发挥着至关重要的作用。
- 信息检索 :搜索引擎通过TF-IDF计算查询词与文档的相关性,排序结果。查询词的 TF-IDF 分数较高的文档被认为更相关,并在搜索结果中排名更高。
- 文本分类 :将文档转化为TF-IDF特征向量后(TF-IDF 用于将文档表示为数值向量),输入分类模型(如SVM、朴素贝叶斯)
- 关键词提取 :选取TF-IDF值最高的若干词项作为文档关键词
- LDA主题模型 :作为预处理步骤,过滤低权重词以提升主题建模效果。识别文档中最重要的句子或短语,帮助生成简洁的摘要
组件
TF(Term Frequency,词频) ,某个词条在文中出现的次数。
IDF(Inverse Document Frequency,逆向文档频率) ,一个词在文档集合中的逆文档频率。罕见词得分高,常见词得分低。衡量词语在文档集合中的稀有性。
如果只用词频来表达一个词对一篇文档的重要程度显然是不合适的,因为在一篇文档里面,可能出现频率很高的词是一些没有意义的词语,真正能代表这篇文档的关键词可能只出很少的次数。
如果有太多文档都涵盖了某个单词,这个单词也就越不重要,或者说是这个单词就越没有信息量
因此,我们需要对 TF 的值进行修正,而 IDF 的想法是用 DF (文档频率)的倒数来进行修正。倒数的应用正好表达了这样的思想,DF 值越大越不重要。
结合 TF-IDF
文档中某个术语的 TF-IDF 分数是通过将 TF 和 IDF 分数相乘来获得。
步骤
- 预处理数据:清洗数据、规范化数据、词形还原、去除停用词等
- 词频分词
- 计算TF
- 计算IDF
- 向量化词汇
优缺点
-
优点:易于计算
-
缺点:
- 不能捕捉词在文档中的位置,无法获取语义。
- TF-IDF 算法主要适用于英文,中文首先要分词,分词后要解决多词一义,以及一词多义问题,这两个问题通过简单的TF-IDF方法不能很好的解决。
- 对文档中出现次数较少的重要人名、地名的提取效果不佳
案例
提取关键词【纯python】
预处理
import jieba
import math
import numpy as np
from collections import defaultdict
import nltk
from nltk.corpus import stopwords
# 下载 NLTK 中文停用词(需确认资源是否存在)
try:
nltk.download('stopwords')
stopwords_cn = stopwords.words('chinese')
print("已下载 NLTK 中文停用词")
except:
# 若 nltk 中文停用词缺失,手动补充常用中文停用词
stopwords_cn = [
'的', '了', '和', '是', '在', '中', '也', '都', '而', '就',
'那', '这', '之', '为', '对', '与', '于', '上', '下', '前'
]
# 中文文本预处理(分词 + 去停用词)
def preprocess(text):
tokens = jieba.cut(text)
return [token for token in tokens if token not in stopwords_cn and token.strip()]
TF-DF基类
class TFIDFBase:
def __init__(self, corpus):
self.corpus = corpus
self.tokenized_corpus = [preprocess(doc) for doc in corpus]
self.N = len(corpus) # 文档总数
self.df = self._compute_df()
self.idf = self._compute_idf()
def _compute_df(self):
"""计算文档频率 (Document Frequency)"""
df = defaultdict(int)
for tokens in self.tokenized_corpus:
unique_tokens = set(tokens)
for token in unique_tokens:
df[token] += 1
return df
def _compute_idf(self):
"""计算逆文档频率 (IDF)"""
idf = defaultdict(float)
for token, count in self.df.items():
idf[token] = math.log(self.N / (count + 1)) + 1 # 平滑处理
return idf
def compute_tf(self, tokens):
"""计算词频 (TF)"""
tf = defaultdict(int)
total = len(tokens)
for token in tokens:
tf[token] += 1 / total # 归一化
return tf
def compute_tfidf(self, tf):
"""计算 TF-IDF 向量"""
return {token: tf[token] * self.idf[token] for token in tf}
def get_tfidf_vector(self):
"""生成整个语料库的 TF-IDF 向量"""
tfidf_vectors = []
for tokens in self.tokenized_corpus:
tf = self.compute_tf(tokens)
tfidf = self.compute_tfidf(tf)
tfidf_vectors.append(tfidf)
return tfidf_vectors
提取关键词
class KeywordExtractor(TFIDFBase):
def extract_keywords(self, top_k=5):
"""提取每个文档的 top-k 关键词"""
tfidf_vectors = self.get_tfidf_vector()
results = []
for idx, tfidf in enumerate(tfidf_vectors):
sorted_keywords = sorted(tfidf.items(), key=lambda x: -x[1])[:top_k]
results.append([kw[0] for kw in sorted_keywords])
return results
结果
Document 1 Keywords: ['人工智能', '领域', '一个']
Document 2 Keywords: ['技术', '文档', '找到']
Document 3 Keywords: ['算法', '常用', '经典']
Document 4 Keywords: ['中文', '分词', '基础']
Document 5 Keywords: ['搜索引擎', '搜索', '相关性']
Document 6 Keywords: ['TF', '-', 'IDF']
Document 7 Keywords: ['深度', '学习', '进展']
Document 8 Keywords: ['倒排', '索引', '核心技术']
Document 9 Keywords: ['查询', '扩展', '提高']
Document 10 Keywords: ['准确率', '评价', '指标']
提取关键词【调用依赖】
安装相关依赖
pip install sklearn
实现TF-IDF向量化
from sklearn.feature_extraction.text import TfidfVectorizer
# 使用TF-IDF向量化器
vectorizer = TfidfVectorizer(
tokenizer=preprocess, # 指定自定义分词函数
token_pattern=r"(?u)\b\w+\b", # 匹配中文分词结果
max_features=1000 # 控制词汇表大小
)
# 拟合语料库并生成TF-IDF矩阵
tfidf_matrix = vectorizer.fit_transform(corpus)
提取关键词
for i, text in enumerate(corpus):
feature_index = tfidf_matrix[i, :].nonzero()[1] # 非零元素的索引
tfidf_scores = zip(feature_index, [tfidf_matrix[i, x] for x in feature_index]) # (词索引, 权重)
sorted_tfidf = sorted(tfidf_scores, key=lambda x: x[1], reverse=True) # 按权重排序
top_n = 3 # 提取每个文档的前3个关键词
keywords = [vectorizer.get_feature_names_out()[idx] for idx, score in sorted_tfidf[:top_n]]
print(f"文档 {i + 1} 的关键词: {keywords}")
结果
文档 1 的关键词: ['人工智能', '领域', '一个']
文档 2 的关键词: ['技术', '文档', '找到']
文档 3 的关键词: ['算法', '常用', '经典']
文档 4 的关键词: ['中文', '分词', '基础']
文档 5 的关键词: ['搜索引擎', '搜索', '相关性']
文档 6 的关键词: ['tf', '-', 'idf']
文档 7 的关键词: ['深度', '学习', '进展']
文档 8 的关键词: ['倒排', '索引', '核心技术']
文档 9 的关键词: ['查询', '扩展', '提高']
文档 10 的关键词: ['准确率', '评价', '指标']
我们写的纯python实现与调用依赖实现的结果一致
面试高频问题
1. 为什么TF要进行标准化操作?
解决长文档、短文档问题,仅用频次来表示,长文本中出现频次高的概率会更大,这一点会影响到不同文档词权值的比较。(文档有长短之分,为了便于不同文档的比较)
2. 为什么要取对数?
- 使用log可以防止权重爆炸。如果某些词只出现一篇或者少数几篇文档中(比如错别字),在没有log的情况下,IDF就会非常小(分母过小),从而影响其权重,而使用log能减轻这种影响。(平滑)
- 利用对数来达到非线性增长的目的,压缩了数据之间的距离,使数据更加平稳,削弱他们之间的差异性。缩小数据的绝对数值,方便计算(缩放)
3. 为什么IDF分母中要进行+1(IDF如何进行平滑处理的)?
- 采用了拉普拉斯平滑(分母+ 1)是为了避免分母为 0(即所有文档都不包含该词),增强了算法的健壮性。如果一个词越常见那么分母就越大,逆文档频率就越小越接近 0。
4. 为什么要词频 * 逆文档频率(TF-IDF要用乘法)?
-
的乘积可以提高重要性较高的词的权重,同时降低重要性较低的词的权重,从而更好地区分不同的词语。
文档中的停用词(如“的”,“地”,“了”)词频很高,文档集中含停用词的数量约等于总的文档集数量,即。在只使用TF的情况下,停用词所占的权重很大,但是它并没有区分能力,与我们的期望不符。使用IDF之后,由于log(1)=0,则停用词通过TF-IDF计算出的权重就为0(根号达不到这样的效果),就可以消除很多停用词的影响。。
-
涉及概率,用乘法,简单有效。
结语
TF-IDF作为信息检索与文本挖掘领域的经典算法,凭借其简单直观的逻辑和强大的实用性,成为从海量文本中提取价值的“黄金法则”。 然而,它并非万能钥匙——面对复杂的语义关系和动态变化的文本数据,TF-IDF的局限性也逐渐显现(例如无法捕捉词序、上下文关联等)。