教科书上的tf-idf公式是长这样的:
t f × i d f ( i , j ) = t f i j × i d f i = n i j ∑ k n k j × l o g ( ∣ D ∣ 1 + ∣ D i ∣ ) tf \times idf(i,j) = tf_{ij} \times idf_i = \frac {n_{ij}}{\sum_{k}{n_{kj}}} \times log\left(\frac{|D|}{1+|D_i|}\right) tf×idf(i,j)=tfij×idfi=∑knkjnij×log(1+∣Di∣∣D∣)
但是在sklearn中却完全不是这样的。
首先,sklearn中的tf只有分子上的 n i j n_{ij} nij,而没有上面公式的分母。也就是说sklearn中的tf只是数了一个词在一篇文档中出现的个数,并没有除以该文档的总词数。
其次,sklearn中的idf是 l o g ( 1 + ∣ D ∣ 1 + ∣ D i ∣ ) + 1 log\left(\frac{1+|D|}{1+|D_i|}\right)+1 log(1+∣Di∣1+∣D∣)+1,其中分子是总文档数量,分母是含有该词的文档数量,默认分子分母都加1平滑。注意最后面的1,是先计算对数再加1,而不是先加1再计算对数。这里的对数是自然对数。
这一步结束后会得到一个原始tf-idf值,然后sklearn会进行一个欧几里得范数归一化:
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 + \dots + v{_n}^2}} vnorm=∣∣v∣∣2v=v12+v22+⋯+vn2v
首先我们用sklearn来实现一个tf-idf的计算:
from sklearn.feature_extraction.text import TfidfTransformer
from sklearn.feature_extraction.text import CountVectorizer
vectorizer = CountVectorizer() # 实例化
transformer = TfidfTransformer()
corpus = ["我 来到 中国 旅游", "中国 欢迎 你","我 喜欢 来到 中国 天安门"]
result_list2 = transformer.fit_transform(vectorizer.fit_transform(corpus)).toarray().tolist()
word = vectorizer.get_feature_names()
print('词典为:')
print(word)
print('归一化后的tf-idf值为:')
for weight in result_list2:
print(weight)
上面代码输出:
词典为:
[‘中国’, ‘喜欢’, ‘天安门’, ‘旅游’, ‘来到’, ‘欢迎’]
归一化后的tf-idf值为:
[0.4254405389711991, 0.0, 0.0, 0.7203334490549893, 0.5478321549274363, 0.0]
[0.5085423203783267, 0.0, 0.0, 0.0, 0.0, 0.8610369959439764]
[0.34520501686496574, 0.5844829010200651, 0.5844829010200651, 0.0, 0.444514311537431, 0.0]
然后我们用纯python来实现相同的计算:
import math
def TF_IDF_weight(corpus):
weight_long = [eve.split() for eve in corpus]
word_all = []
for eve in weight_long:
for x in eve:
if len(x)>1:
word_all.append(x)
word_all = list(set(word_all)) # 集合去重词库
# 开始计算tf-idf
weight = [[] for i in corpus]
weight_idf = [[] for i in corpus]
for word in word_all:
for i in range(len(corpus)):
temp_list = corpus[i].split()
n1 = temp_list.count(word)
tf = n1
n2 = len(corpus)
n3 = 0
for eve in corpus:
temp_list_ = eve.split()
if word in temp_list_:
n3 += 1
idf = math.log(((n2+1)/(n3+1)))+1
weight_idf[i].append(idf)
weight[i].append(tf*idf)
print('词典为:')
print(word_all)
print('原始tf-idf值为:')
for w in weight:
print(w)
# L2范式归一化过程
l2_weight = [[] for i in range(len(corpus))]
for e in range(len(weight)):
all2plus = 0
for ev in weight[e]:
all2plus += ev**2
for ev in weight[e]:
l2_weight[e].append(ev/(all2plus**0.5))
return l2_weight # 返回最终结果
corpus = ["我 来到 中国 旅游", "中国 欢迎 你","我 喜欢 来到 中国 天安门"]
result_list = TF_IDF_weight(corpus)
print('归一化后的tf-idf值为:')
for weight in result_list:
print(weight)
以上代码输出:
词典为:
[‘来到’, ‘欢迎’, ‘喜欢’, ‘旅游’, ‘中国’, ‘天安门’]
原始tf-idf值为:
[1.2876820724517808, 0.0, 0.0, 1.6931471805599454, 1.0, 0.0]
[0.0, 1.6931471805599454, 0.0, 0.0, 1.0, 0.0]
[1.2876820724517808, 0.0, 1.6931471805599454, 0.0, 1.0, 1.6931471805599454]
归一化后的tf-idf值为:
[0.5478321549274363, 0.0, 0.0, 0.7203334490549893, 0.4254405389711991, 0.0]
[0.0, 0.8610369959439764, 0.0, 0.0, 0.5085423203783267, 0.0]
[0.444514311537431, 0.0, 0.5844829010200651, 0.0, 0.34520501686496574, 0.5844829010200651]
可以看到,除了词典的顺序有些不一样,每个词的tf-idf值完全一样。
我们接下来用词袋模型来实现一下:
transformer = TfidfTransformer(smooth_idf=True)
counts = [[1, 0, 0, 1, 1, 0],
[1, 0, 0, 0, 0, 1],
[1, 1, 1, 0, 1, 0]]
tfidf = transformer.fit_transform(counts)
print(tfidf.toarray())
上面代码输出:
[[0.42544054 0. 0. 0.72033345 0.54783215 0. ]
[0.50854232 0. 0. 0. 0. 0.861037 ]
[0.34520502 0.5844829 0.5844829 0. 0.44451431 0. ]]
可以看到跟上面两种方式得到的每个词的tf-idf值完全一样。
我们最后来手动计算一下第一句,"我 来到 中国 旅游"这句用词袋模型的词向量是这样表示的[1, 0, 0, 1, 1, 0],它的意思是,在词典[‘来到’, ‘欢迎’, ‘喜欢’, ‘旅游’, ‘中国’, ‘天安门’]中,来到在第一句中出现了1次,欢迎在第一句中出现了0次,喜欢在第一句中出现了0次,旅游在第一句中出现了1次,中国在第一句中出现了1次,天安门在第一句中出现了0次。实际上,这个词向量的每个元素就是词典中每个词在第一句的tf值。
现在来计算idf值,我们只考虑tf不为零的词。文档的总数是3,“来到”在2个文档出现过,“中国”在3个文档中出现过,“旅游”在1个文档中出现过。由于这三个词的tf值都为1,因此idf值就等于tf-idf值。
代入idf公式中可以得到:
来到 = math.log(((3+1)/(2+1)))+1
中国 = math.log(((3+1)/(3+1)))+1
旅游 = math.log(((3+1)/(1+1)))+1
print(来到)
print(中国)
print(旅游)
上面代码输出:
1.2876820724517808
1.0
1.6931471805599454
再来一步欧几里得范数归一化:
print(来到/math.sqrt(来到**2 + 中国**2 + 旅游**2))
print(中国/math.sqrt(来到**2 + 中国**2 + 旅游**2))
print(旅游/math.sqrt(来到**2 + 中国**2 + 旅游**2))
上面代码输出:
0.5478321549274363
0.4254405389711991
0.7203334490549893
可以看到手动计算的跟sklearn给出的tf-idf值完全一样。经过上面四种计算tf-idf值的方法,应该可以彻底搞懂tf-idf原理了。