算法思想
TF-IDF(term frequency–inverse document frequency,词频-逆向文件频率) 是用于信息检索与文本挖掘的重要算法,其中TF用于度量关键词在文档中的重要性,IDF用于度量关键词在全文档中的重要性, 即文档中某关键词的重要性,与它在当前文档中的频率成正比,而与包含它的文档数成反比。
TF-IDF的主要思想是,若一个关键词在一篇文档中出现的频率高,而在其他文档中很少出现,则该关键词可较好的反应当前文档的特征。
算法原理
度量某文档和查询的相关性,最简单的方法是利用各查询关键词在该文档中出现的总词频(Term Frequency,TF)。
具体地,对于包含M个关键词的w1, w2,…wn查询,各关键词在某文档中出现的频率分别为:TF(w1), TF(w2),…,TF(wM),则该文档与查询的相关性为:
T
F
(
w
1
)
+
T
F
(
w
2
)
+
⋯
+
T
F
(
w
M
)
TF(w_1)+TF(w_2)+\cdots+TF(w_M)
TF(w1)+TF(w2)+⋯+TF(wM)
某些关键词可能同时出现在多篇文档中,该类关键词的主题预测能力较弱,可见,仅使用TF不能很好的反应文档与查询的相关性。
关键词的主题预测能力越强,在度量与文档的相关性时,其权重应该越大。 也就是说,若某关键词在较少文档中出现,则该关键词的权重应该较高,如关键词原子能
的权重大于应用
的权重。因此,利用包含某关键词的文档数,修正仅用词频TF度量该关键词的权重。
在信息检索领域,使用逆文本频率(Inverse Document Frequency, IDF) 表示关键词的主题预测能力(权重),表示为
I
D
F
(
w
)
=
log
D
D
F
(
w
)
IDF(w)=\log\frac{D}{DF(w)}
IDF(w)=logDF(w)D
其中D为全部文档数,DF(w)为包含关键词w的文档数。
利用IDF的思想,文档与查询的相关性计算由简单的词频求和,变为以IDF为权重的加权求和,即
T
F
(
w
1
)
⋅
I
D
F
(
w
1
)
+
T
F
(
w
2
)
⋅
I
D
F
(
w
2
)
+
⋯
+
T
F
(
w
M
)
⋅
I
D
F
(
w
M
)
TF(w_1)\cdot IDF(w_1)+ TF(w_2)\cdot IDF(w_2)+\cdots+ TF(w_M)\cdot IDF(w_M)
TF(w1)⋅IDF(w1)+TF(w2)⋅IDF(w2)+⋯+TF(wM)⋅IDF(wM)
TF-IDF与信息论
一个查询中,每个关键词的权重应该反应其为查询提供的信息量,简单的方法就是,用关键词的信息量,作为它在查询中的权重,即
I
(
w
)
=
−
P
(
w
)
log
P
(
w
)
 
=
−
T
F
(
w
)
N
log
T
F
(
w
)
N
=
T
F
(
w
)
N
log
N
T
F
(
w
)
\begin{aligned} I(w) & =-P(w)\log P(w) \\\,\\ & = -\frac{{TF}(w)}{N}\log\frac{{TF}(w)}{N}=\frac{{TF}(w)}{N}\log\frac{N}{{TF}(w)} \end{aligned}
I(w)=−P(w)logP(w)=−NTF(w)logNTF(w)=NTF(w)logTF(w)N
其中N为整个语料库中的总词数,是可忽略的常数,此时
I
(
w
)
=
T
F
(
w
)
log
N
T
F
(
w
)
I(w)={TF}(w)\log\frac{N}{{TF}(w)}
I(w)=TF(w)logTF(w)N
若两个关键词在全文档中出现的频率相同,但第一个关键词集中分布在少数文章中,而第二个关键词分布在多篇文章中,显然,第一个关键词具有更好的主题预测能力,应赋予更高的查询权重。
为此,提出以下假设(总文档数D,总词数N,包含关键词w的文档数DF(w)):
-
每个文档含词数基本相同,即
M = N D = ∑ w T F ( w ) D M=\dfrac{N}{D}=\dfrac{\sum_w{TF}(w)}{D} M=DN=D∑wTF(w) -
每个关键词一旦在文档中出现,不论其出现多少次,权重都相同,即关键词w在文档中未出现,则权重为0;否则,则为
c ( w ) = T F ( w ) D F ( w ) c(w)=\dfrac{TF(w)}{DF(w)} c(w)=DF(w)TF(w)
因此,关键词w的信息量
I
(
w
)
=
T
F
(
w
)
log
N
T
F
(
w
)
=
T
F
(
w
)
log
M
D
c
(
w
)
⋅
D
F
(
w
)
=
T
F
(
w
)
[
log
D
D
F
(
w
)
+
log
M
c
(
w
)
]
\begin{aligned} I(w) & = {TF}(w)\log\frac{N}{{TF}(w)}= {TF}(w)\log\frac{MD}{c(w)\cdot DF(w)} \\ & = {TF}(w)\left[\log\frac{D}{DF(w)}+\log\frac{M}{c(w)}\right] \end{aligned}
I(w)=TF(w)logTF(w)N=TF(w)logc(w)⋅DF(w)MD=TF(w)[logDF(w)D+logc(w)M]
=>
T
F
−
I
D
F
(
w
)
=
I
(
w
)
−
T
F
(
w
)
log
M
c
(
w
)
{TF-IDF}(w)=I(w)-{TF}(w)\log\frac{M}{c(w)}
TF−IDF(w)=I(w)−TF(w)logc(w)M
易知,关键词w的TF-IDF值,与其信息量成正比;又由于M>c(w),知关键词w的TF-IDF值,与其在文档中出现的平均次数成反比,这些结论完全符合信息论。
平滑处理
经过平滑处理后, IDF的最终计算公式如下:
I
D
F
(
w
)
=
log
N
+
1
D
F
(
w
)
+
1
+
1
IDF(w) = \log\frac{N+1}{DF(w)+1} + 1
IDF(w)=logDF(w)+1N+1+1
- log项中分子项和分母项均加1,表示虚拟增加一篇包含任意词的文档,避免分母项为0;
- IDF的最终值加1,避免某单词在所有文档中出现时,IDF的值为0,即不忽略出现在所有文档中的词;
正则化处理
sklearn
中类TfidfTransformer
默认对文档的TF-IDF特征向量做l2
正则化,即某文档的TF-IDF特征向量为v,则
V
n
o
r
m
=
v
∣
∣
v
∣
∣
2
=
v
v
1
2
+
v
2
2
+
⋯
+
v
n
2
V_{norm}=\frac{v}{||v||_2}=\frac{v}{\sqrt{v_1^2+v_2^2+\cdots + v_n^2}}
Vnorm=∣∣v∣∣2v=v12+v22+⋯+vn2v
若单词表为{w1, w2, w3},文档A=(w1, w2, w2),B=(w1, w2, w3),且w1, w2, w3的IDF值相同,则未正则化时
T
F
−
I
D
F
(
A
)
=
(
0.333
,
0.666
,
0
)
⋅
I
D
F
(
w
)
T
F
−
I
D
F
(
B
)
=
(
0.333
,
0.333
,
0.333
)
⋅
I
D
F
(
w
)
\begin{aligned} & TF-IDF(A) = (0.333, 0.666, 0)\cdot IDF(w) \\ & TF-IDF(B) = (0.333, 0.333, 0.333)\cdot IDF(w) \end{aligned}
TF−IDF(A)=(0.333,0.666,0)⋅IDF(w)TF−IDF(B)=(0.333,0.333,0.333)⋅IDF(w)
此时,文档A、B中单词w1的TF-IDF值相同。
若进行l2
正则化,则
T
F
−
I
D
F
(
A
)
l
2
=
(
0.447
,
0.894
,
0
)
T
F
−
I
D
F
(
B
)
l
2
=
(
0.577
,
0.577
,
0.577
)
\begin{aligned} & TF-IDF(A)_{l2} = (0.447, 0.894, 0) \\ & TF-IDF(B)_{l2} = (0.577, 0.577, 0.577) \end{aligned}
TF−IDF(A)l2=(0.447,0.894,0)TF−IDF(B)l2=(0.577,0.577,0.577)
可见文档B中w1的TF-IDF值(权重)更大,正则化后的意义为:考虑文档的TF-IDF特征分布,增加不同权重之间的差异。
不失一般性,文档A、B中正则化后w1的TF-IDF分别为
T
F
−
I
D
F
(
A
w
1
)
l
2
=
T
F
(
A
w
1
)
T
F
(
A
w
1
)
2
+
T
F
(
A
w
2
)
2
 
T
F
−
I
D
F
(
B
w
1
)
l
2
=
T
F
(
B
w
1
)
T
F
(
B
w
1
)
2
+
T
F
(
B
w
2
)
2
+
+
T
F
(
B
w
3
)
2
TF-IDF(A_{w_1})_{l2}=\frac{TF(A_{w_1})}{\sqrt{TF(A_{w_1})^2+TF(A_{w_2})^2}}\\\,\\ TF-IDF(B_{w_1})_{l2}=\frac{TF(B_{w_1})}{\sqrt{TF(B_{w_1})^2+TF(B_{w_2})^2++TF(B_{w_3})^2}}
TF−IDF(Aw1)l2=TF(Aw1)2+TF(Aw2)2TF(Aw1)TF−IDF(Bw1)l2=TF(Bw1)2+TF(Bw2)2++TF(Bw3)2TF(Bw1)
如TF(A_w1) = TF(B_w1),且TF之和为1,知
T
F
(
A
w
2
)
=
T
F
(
B
w
2
)
+
T
F
(
B
w
3
)
TF(A_{w_2})=TF(B_{w_2})+TF(B_{w_3})
TF(Aw2)=TF(Bw2)+TF(Bw3)
推导出
T
F
(
A
w
2
)
2
=
T
F
(
B
w
2
)
2
+
T
F
(
B
w
3
)
2
+
2
T
F
(
B
w
2
)
⋅
T
F
(
B
w
3
)
≥
T
F
(
B
w
2
)
2
+
T
F
(
B
w
3
)
2
\begin{aligned} TF(A_{w_2})^2 & =TF(B_{w_2})^2+TF(B_{w_3})^2+2TF(B_{w_2}) \cdot TF(B_{w_3})\\ & \geq TF(B_{w_2})^2+TF(B_{w_3})^2 \end{aligned}
TF(Aw2)2=TF(Bw2)2+TF(Bw3)2+2TF(Bw2)⋅TF(Bw3)≥TF(Bw2)2+TF(Bw3)2
进而,推导出
T
F
−
I
D
F
(
A
w
1
)
l
2
≤
T
F
−
I
D
F
(
B
w
1
)
l
2
TF-IDF(A_{w_1})_{l2} \leq TF-IDF(B_{w_1})_{l2}
TF−IDF(Aw1)l2≤TF−IDF(Bw1)l2
当前仅当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] ]
"""