引言

正在做LintCode上的垃圾邮件分类,使用朴素贝叶斯方法解决,涉及到文本特征的提取。
TF-IDF(词频-逆文档频率)算法是一种统计方法,用以评估一字词对于一个文件集或一个语料库中的其中一份文件的重要程度。字词的重要性随着它在文件中出现的次数成正比增加,但同时会随着它在语料库中出现的频率成反比下降。

计算步骤

词频(TF)

Term Frequency,就是某个关键字出现的频率,具体来讲,就是词库中的某个词在当前文章中出现的频率。那么我们可以写出它的计算公式:

TFij=nijkni,kTF_{ij} = \frac{n_{ij}}{\sum_k n_{i, k}}

其中,nijn_{ij}表示关键词jj在文档ii中的出现次数。

单纯使用TF来评估关键词的重要性忽略了常用词的干扰。常用词就是指那些文章中大量用到的,但是不能反映文章性质的那种词,比如:因为、所以、因此等等的连词,在英文文章里就体现为and、the、of等等的词。这些词往往拥有较高的TF,所以仅仅使用TF来考察一个词的关键性,是不够的。

逆文档频率(IDF)

Inverse Document Frequency,文档频率就是一个词在整个文库词典中出现的频率,逆文档频率用下式计算

IDFj=logDDj+1IDF_j = \log \frac{|D|}{|D_j| + 1}

其中,D|D|表示总的文档数目,Dj|D_j|表示关键词jj出现过的文档数目

scikit-learn内为

IDFj=logD+1Dj+1+1IDF_j = \log \frac{|D| + 1}{|D_j| + 1} + 1

sklearn_tfidf

词频-逆文档频率(TF-IDF)

TFIDFi=TFi×IDFTF-IDF_{i} = TF_i × IDF

举例

例如有如下33个文本

1
2
3
文本1:My dog ate my homework.
文本2:My cat ate the sandwich.
文本3:A dolphin ate the homework.

提取字典,一般需要处理大小写、去除停用词a,处理结果为

1
ate, cat, dog, dolphin, homework, my, sandwich, the

故各个文本的词数向量为

1
2
3
文本1:[1, 0, 1, 0, 1, 2, 0, 0]
文本2:[1, 1, 0, 0, 0, 1, 1, 1]
文本3:[1, 0, 0, 1, 1, 0, 0, 1]

各个文本的词频向量(TF)

1
2
3
文本1:[0.2 , 0.  , 0.2 , 0.  , 0.2 , 0.4 , 0.  , 0.  ]
文本2:[0.2 , 0.2 , 0. , 0. , 0. , 0.2 , 0.2 , 0.2 ]
文本3:[0.25, 0. , 0. , 0.25, 0.25, 0. , 0. , 0.25]

各词出现过的文档次数

1
[3, 1, 1, 1, 2, 2, 1, 2]

总文档数为33,各词的逆文档频率(IDF)向量

这里使用scikit-learn内的方法求解

1
[1.        , 1.69314718, 1.69314718, 1.69314718, 1.28768207,  1.28768207, 1.69314718, 1.28768207]

故各文档的TF-IDF向量为

1
2
3
4
5
6
文本1:
[0.2 , 0. , 0.33862944, 0. , 0.25753641, 0.51507283, 0. , 0. ]
文本2:
[0.2 , 0.33862944, 0. , 0. , 0. , 0.25753641, 0.33862944, 0.25753641]
文本3:
[0.25 , 0. , 0. , 0.4232868 , 0.32192052, 0. , 0. , 0.32192052]

经单位化后,有

1
2
3
4
5
6
文本1:
[0.28680065, 0. , 0.48559571, 0. , 0.36930805, 0.73861611, 0. , 0. ]
文本2:
[0.31544415, 0.53409337, 0. , 0. , 0. , 0.40619178, 0.53409337, 0.40619178]
文本3:
[0.37311881, 0. , 0. , 0.63174505, 0.4804584 , 0. , 0. , 0.4804584 ]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
>>> import numpy as np
>>> vec_num = np.array([
[1, 0, 1, 0, 1, 2, 0, 0],
[1, 1, 0, 0, 0, 1, 1, 1],
[1, 0, 0, 1, 1, 0, 0, 1]
])
>>> vec_tf = vec_num / np.sum(vec_num, axis=1).reshape(-1, 1)
>>> vec_tf
array([[0.2 , 0. , 0.2 , 0. , 0.2 , 0.4 , 0. , 0. ],
[0.2 , 0.2 , 0. , 0. , 0. , 0.2 , 0.2 , 0.2 ],
[0.25, 0. , 0. , 0.25, 0.25, 0. , 0. , 0.25]])

>>> vec_num[vec_num>0] = 1
>>> n_showup = np.sum(vec_num, axis=0)
>>> n_showup
array([3, 1, 1, 1, 2, 2, 1, 2])

>>> d = 3
>>> vec_idf = np.log((d + 1) / (n_showup + 1)) + 1
>>> vec_idf
array([1. , 1.69314718, 1.69314718, 1.69314718, 1.28768207, 1.28768207, 1.69314718, 1.28768207])

>>> vec_tfidf = vec_tf * vec_idf
>>> vec_tfidf
array([[0.2 , 0. , 0.33862944, 0. , 0.25753641, 0.51507283, 0. , 0. ],
[0.2 , 0.33862944, 0. , 0. , 0. , 0.25753641, 0.33862944, 0.25753641],
[0.25 , 0. , 0. , 0.4232868 , 0.32192052, 0. , 0. , 0.32192052]])

>>> vec_tfidf = vec_tfidf / np.linalg.norm(vec_tfidf, axis=1).reshape((-1, 1))
>>> vec_tfidf
array([[0.28680065, 0. , 0.48559571, 0. , 0.36930805, 0.73861611, 0. , 0. ],
[0.31544415, 0.53409337, 0. , 0. , 0. , 0.40619178, 0.53409337, 0.40619178],
[0.37311881, 0. , 0. , 0.63174505, 0.4804584 , 0. , 0. , 0.4804584 ]])

验证

使用scikit-learn机器学习包计算结果

1
2
3
4
5
6
7
8
9
10
11
12
>>> from sklearn.feature_extraction.text import TfidfVectorizer
>>> vectorizer = TfidfVectorizer()
>>> text = [
"My dog ate my homework",
"My cat ate the sandwich",
"A dolphin ate the homework"]
>>> vectorizer.fit_transform(text).toarray()
array([[0.28680065, 0. , 0.48559571, 0. , 0.36930805, 0.73861611, 0. , 0. ],
[0.31544415, 0.53409337, 0. , 0. , 0. , 0.40619178, 0.53409337, 0.40619178],
[0.37311881, 0. , 0. , 0.63174505, 0.4804584 , 0. , 0. , 0.4804584 ]])
>>> vectorizer.get_feature_names()
['ate', 'cat', 'dog', 'dolphin', 'homework', 'my', 'sandwich', 'the']