文章目录
- 算法思想
- 算法原理
- TF-IDF与信息论
- 平滑处理
- 正则化处理
- 算法实现
算法思想
TF-IDF(term frequency–inverse document frequency,词频-逆向文件频率) 是用于信息检索与文本挖掘的重要算法,其中TF用于度量关键词在文档中的重要性,IDF用于度量关键词在全文档中的重要性, 即文档中某关键词的重要性,与它在当前文档中的频率成正比,而与包含它的文档数成反比。
TF-IDF的主要思想是,若一个关键词在一篇文档中出现的频率高,而在其他文档中很少出现,则该关键词可较好的反应当前文档的特征。
算法原理
度量某文档和查询的相关性,最简单的方法是利用各查询关键词在该文档中出现的总词频(Term Frequency,TF)。
具体地,对于包含M个关键词的w1, w2,…wn查询,各关键词在某文档中出现的频率分别为:TF(w1), TF(w2),…,TF(wM),则该文档与查询的相关性为:
某些关键词可能同时出现在多篇文档中,该类关键词的主题预测能力较弱,可见,仅使用TF不能很好的反应文档与查询的相关性。
关键词的主题预测能力越强,在度量与文档的相关性时,其权重应该越大。 也就是说,若某关键词在较少文档中出现,则该关键词的权重应该较高,如关键词原子能
的权重大于应用
的权重。因此,利用包含某关键词的文档数,修正仅用词频TF度量该关键词的权重。
在信息检索领域,使用逆文本频率(Inverse Document Frequency, IDF) 表示关键词的主题预测能力(权重),表示为
其中D为全部文档数,DF(w)为包含关键词w的文档数。
利用IDF的思想,文档与查询的相关性计算由简单的词频求和,变为以IDF为权重的加权求和,即
TF-IDF与信息论
一个查询中,每个关键词的权重应该反应其为查询提供的信息量,简单的方法就是,用关键词的信息量,作为它在查询中的权重,即
其中N为整个语料库中的总词数,是可忽略的常数,此时
若两个关键词在全文档中出现的频率相同,但第一个关键词集中分布在少数文章中,而第二个关键词分布在多篇文章中,显然,第一个关键词具有更好的主题预测能力,应赋予更高的查询权重。
为此,提出以下假设(总文档数D,总词数N,包含关键词w的文档数DF(w)):
- 每个文档含词数基本相同,即
- 每个关键词一旦在文档中出现,不论其出现多少次,权重都相同,即关键词w在文档中未出现,则权重为0;否则,则为
因此,关键词w的信息量
=>
易知,关键词w的TF-IDF值,与其信息量成正比;又由于M>c(w),知关键词w的TF-IDF值,与其在文档中出现的平均次数成反比,这些结论完全符合信息论。
平滑处理
经过平滑处理后, IDF的最终计算公式如下:
- log项中分子项和分母项均加1,表示虚拟增加一篇包含任意词的文档,避免分母项为0;
- IDF的最终值加1,避免某单词在所有文档中出现时,IDF的值为0,即不忽略出现在所有文档中的词;
正则化处理
sklearn
中类TfidfTransformer
默认对文档的TF-IDF特征向量做l2
正则化,即某文档的TF-IDF特征向量为v,则
若单词表为{w1, w2, w3},文档A=(w1, w2, w2),B=(w1, w2, w3),且w1, w2, w3的IDF值相同,则未正则化时
此时,文档A、B中单词w1的TF-IDF值相同。
若进行l2
正则化,则
可见文档B中w1的TF-IDF值(权重)更大,正则化后的意义为:考虑文档的TF-IDF特征分布,增加不同权重之间的差异。
不失一般性,文档A、B中正则化后w1的TF-IDF分别为
如TF(A_w1) = TF(B_w1),且TF之和为1,知
推导出
进而,推导出
当前仅当TF(B_w2) = 0或TF(B_w3) = 0,即B中w2或w3的频率为0时,等式成立。
算法实现
算法的实现参考了sklearn.feature_extraction.text
中的CountVectorizer
和TfidfVectorizer
类,如下:
import re
from collections import defaultdict
from sklearn.feature_extraction.text import CountVectorizer, TfidfVectorizer
import numpy as np
from scipy.sparse import csr_matrix, spdiags
from scipy.sparse.linalg import norm
PTN_SYMBOL = re.compile(r'[.!?\'",]')
def tokenize(doc):
"""
英文分词,小写输出
"""
for word in PTN_SYMBOL.sub(' ', doc).split(' '):
if word and word != ' ':
yield word.lower()
def count_vocab(raw_documents):
"""
返回文档词频的稀疏矩阵
参考sklearn.feature_extraction.text.CountVectorizer._count_vocab
矩阵大小:M*N, M个文档, 共计N个单词
:param raw_documents: ['Hello world.', 'Hello word', ...]
:return: csc_matrix, vocabulary
"""
vocab = {}
data, indices, indptr = [], [], [0]
for doc in raw_documents:
doc_feature = defaultdict(int)
for term in tokenize(doc):
# 词在词表中的位置
index = vocab.setdefault(term, len(vocab))
# 统计当前文档的词频
doc_feature[index] += 1
# 存储当前文档的词及词频
indices.extend(doc_feature.keys())
data.extend(doc_feature.values())
# 累加词数
indptr.append(len(indices))
# 构造稀疏矩阵
X = csr_matrix((data, indices, indptr), shape=(len(indptr) - 1, len(vocab)), dtype=np.int64)
# 将单词表排序,同时更新压缩矩阵数据的位置
map_index = np.empty(len(vocab), dtype=np.int32)
for new_num, (term, old_num) in enumerate(sorted(vocab.items())):
vocab[term] = new_num
map_index[old_num] = new_num
X.indices = map_index.take(X.indices, mode='clip')
X.sort_indices()
return X, vocab
def tfidf_transform(X, smooth_idf=True, normalize=True):
"""
将词袋矩阵转换为TF-IDF矩阵
:param X: 压缩的词袋矩阵 M*N, 文本数M, 词袋容量N
:param smooth_idf: 是否对DF平滑处理
:param normalize: 是否对TF-IDF执行l2标准化
:return: TF-IDF压缩矩阵(csc_matrix)
"""
n_samples, n_features = X.shape
df = np.bincount(X.indices, minlength=X.shape[1])
df += int(smooth_idf)
new_n_samples = n_samples + int(smooth_idf)
idf = np.log(float(new_n_samples) / df) + 1.0
# 对角稀疏矩阵N*N,元素值对应单词的IDF
idf_diag = spdiags(idf, diags=0, m=n_features, n=n_features, format='csr')
# 等价于 DF * IDF
X = X * idf_diag
# 执行l2正则化
if normalize:
norm_l2 = 1. / norm(X, axis=1)
tmp = spdiags(norm_l2, diags=0, m=n_samples, n=n_samples, format='csr')
X = tmp * X
return X
if __name__ == '__main__':
# 源文档
raw_documents = [
'This is the first document.',
'This is the second second document.',
'And the third one.',
'Is this the first document?',
]
# 转换为词袋模型
X, vocab = count_vocab(raw_documents)
# X = CountVectorizer().fit_transform(raw_documents)
"""
>> vocab
{'this': 8, 'is': 3, 'the': 6, 'first': 2, 'document': 1, 'second': 5, 'and': 0, 'third': 7,
'one': 4}
>> X.toarray()
[[0 1 1 1 0 0 1 0 1]
[0 1 0 1 0 2 1 0 1]
[1 0 0 0 1 0 1 1 0]
[0 1 1 1 0 0 1 0 1]]
"""
# 计算TF-IDF
tfidf_x = tfidf_transform(X)
# tfidf_x = TfidfVectorizer().fit_transform(raw_documents)
"""
>> tfidf_x.toarray()
[ [0. 0.439 0.542 0.439 0. 0. 0.359 0. 0.439]
[0. 0.272 0. 0.272 0. 0.853 0.223 0. 0.272]
[0.553 0. 0. 0. 0.553 0. 0.288 0.553 0. ]
[0. 0.439 0.542 0.439 0. 0. 0.359 0. 0.439] ]
"""