sklearn中的tf-idf计算详解

教科书上的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+DiD)

但是在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+Di1+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=v2v=v12+v22++vn2 v

首先我们用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原理了。

你可能感兴趣的:(自然语言处理)