Tf-idf 总结笔记

简单的概念,强大的效用。附 sklearn 和 nltk 实现代码。

部分截图、概念描述来自 CMU 95 - 865 Text Analytics,上课的时候没有好好做笔记,到实习的时候发现,有些概念虽然很简单,但确实很实用,理解透彻才能发挥无穷效力。是在评估聚类算法的时候偶然想到的方法,确!实!很!有!用!

TF-IDF(term frequency–inverse document frequency),一种常用的加权技术,用以评估一字词对于一个文件集或一个语料库中的其中一份文件的重要程度。主要逻辑是:字词的重要性随著它在文件中出现的次数成正比增加,但同时会随著它在语料库中出现的频率成反比下降。 TF-IDF加权的各种形式常被搜寻引擎应用,作为文件与用户查询之间相关程度的度量或评级。除了TF-IDF以外,因特网上的搜寻引擎还会使用基于连结分析的评级方法,以确定文件在搜寻结果中出现的顺序。

预备知识

Heap’s law 通常用于预测 vocabulary size,Zipf’s law 则用于描述 term frequency 的分布。

Heaps’ Law

一张图解释。

Zipf’s Law

齐普夫定律,本科信息计量学就学过,大致是说,在一个自然语言的语料库中,一个词的出现频数和这个词在这个语料中的排名(这个排名是基于出现次数的)成反比,频数和排名的乘积是常数。即 Rank Frequency = Constant (Constant = 0.1 N),或者 P(tR) = 0.1/N。


Zipf’s law 告诉我们以下几点

  • ‘A few terms are very common’,这些大部分是 stopwords。
  • ‘Most terms are very rare’,多数 term 只出现两三次,完全可以忽略。
  • very common 的,very rare 的在文本分析中都可以忽略,真正重要的是在中间的一部分 terms。

两幅图概括。

从 Zipf’s law 得到的一些数字是:

  • 排名第一的词占全部词的 10%
  • 排名前5的词占全部词的 23%
  • 前100 的词占全部词的 52%
  • 50% 的 term 只出现了一次
  • 91% 的 term 出现的次数小于 10

应用

合理使用 Zipf’s law,我们可以大大减少字典存储内存。
另外, Zipf’s law 还可以为特定的任务制作特定的 stopwords list。

Example: “trading” and “prices” are frequent Wall Street Journal terms
• They are candidate stopwords
• They also are important terms for financial analysis
• If your task is financial analysis, leave them in
• If your task is analysis of technology products, maybe discard them

Tf-idf 笔记

逻辑

Zipf’s law 给出了自然语言的统计性质,term frequence is highly skewed.

由此发展出了更能代表 term weight 的方法,tf - idf,用中文来表述没那么直观,且看英文解释。简单来讲就是说,如果某个词或短语在一篇文章中出现的频率 TF 高,并且在其他文章中很少出现,则认为此词或者短语具有很好的类别区分能力,适合用来分类。

公式

tf 通常会被正规化,以防止它偏向长的文件。(同一个词语在长文件里可能会比短文件有更高的词频,而不管该词语重要与否。)

意义

  • Reward words that better represent each document
  • Reward words that discriminate among different documents
  • Scale for document length

当然也有些情境下 tf 已经足够,并不需要 tfidf,如

  • 文件都是同一长度
  • 并不需要区分文件(也就不需要idf)
  • 机器学习算法可以学习特征等

代码实现

sklearn 方法

首先看下要用到的两个类,CountVectorizer 和 TfidfTransformer。参数一目了然,之后对比与 nltk 结果差异时再做解释。
CountVectorizer

1
2
3
4
5
6
7
8
>>> vectorizer = CountVectorizer()
>>> vectorizer
CountVectorizer(analyzer=u'word', binary=False, decode_error=u'strict',
dtype=<type 'numpy.int64'>, encoding=u'utf-8', input=u'content',
lowercase=True, max_df=1.0, max_features=None, min_df=1,
ngram_range=(1, 1), preprocessor=None, stop_words=None,
strip_accents=None, token_pattern=u'(?u)\\b\\w\\w+\\b',
tokenizer=None, vocabulary=None)

TfidfTransformer

1
2
3
4
>>> tfidfTransformer = TfidfTransformer()
>>> tfidfTransformer
TfidfTransformer(norm=u'l2', smooth_idf=True, sublinear_tf=False,
use_idf=True)

计算 tfidf 代码

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
from sklearn.feature_extraction.text import CountVectorizer, TfidfTransformer
corpus = []
doc1 = "I love Chinese food"
doc2 = "I love American spirits"
corpus.append(doc1)
corpus.append(doc2)
def calTfidf(corpus):
# 该转换为词频矩阵 矩阵元素a[i][j] 表示j词在i类文本下的词频
vectorizer = CountVectorizer()
# vectorizer = CountVectorizer(token_pattern='(?u)\\b\\w+\\b')
# 统计每个词语的tf-idf权值
transformer = TfidfTransformer()
# transformer = TfidfTransformer(norm=None)
tfidf = transformer.fit_transform(vectorizer.fit_transform(corpus))
tfidf_weight = tfidf.toarray()
idf_weight = transformer.idf_ # idf np.log(float(n_samples) / df) + 1.0
word = vectorizer.get_feature_names() # 获取词袋模型中的所有词语
termWeights = dict()
for i in range(len(tfidf_weight)):
lwords = [(word[j], float(tfidf_weight[i][j]), float(idf_weight[j]))
for j in range(len(word))]
lwords = sorted(lwords, key=lambda m: -m[1])
termWeights[i] = lwords
for key, items in termWeights.items():
print u"-------第", key, u"类文本的词语tf-idf权重------"
for item in items:
print 'Word: {}\tTfidf: {}\tIdf: {}'.format(item[0], item[1], item[2])

输出

-------第 0 类文本的词语tf-idf权重------
Word: chinese    Tfidf: 0.631667201738    Idf: 1.40546510811
Word: food    Tfidf: 0.631667201738    Idf: 1.40546510811
Word: love    Tfidf: 0.449436416524    Idf: 1.0
Word: american    Tfidf: 0.0    Idf: 1.40546510811
Word: spirits    Tfidf: 0.0    Idf: 1.40546510811
-------第 1 类文本的词语tf-idf权重------
Word: american    Tfidf: 0.631667201738    Idf: 1.40546510811
Word: spirits    Tfidf: 0.631667201738    Idf: 1.40546510811
Word: love    Tfidf: 0.449436416524    Idf: 1.0
Word: chinese    Tfidf: 0.0    Idf: 1.40546510811
Word: food    Tfidf: 0.0    Idf: 1.40546510811

为什么少了个 I ?看 CountVectorizer 构造器有一个参数是 token_pattern,忽略了单个字母的 word。

token_pattern (default u'(?u)\b\w\w+\b'), regular expression identifying tokens–by default words that consist of a single character (e.g., ‘a’, ‘2’) are ignored, setting token_pattern to '(?u)\b\w+\b' will include these tokens

改成 vectorizer = CountVectorizer(token_pattern=’(?u)\b\w+\b’) 就会包括 I

-------第 0 类文本的词语tf-idf权重------
Word: chinese    Tfidf: 0.576152355165    Idf: 1.40546510811
Word: food    Tfidf: 0.576152355165    Idf: 1.40546510811
Word: i    Tfidf: 0.40993714596    Idf: 1.0
Word: love    Tfidf: 0.40993714596    Idf: 1.0
Word: american    Tfidf: 0.0    Idf: 1.40546510811
Word: spirits    Tfidf: 0.0    Idf: 1.40546510811
-------第 1 类文本的词语tf-idf权重------
Word: american    Tfidf: 0.576152355165    Idf: 1.40546510811
Word: spirits    Tfidf: 0.576152355165    Idf: 1.40546510811
Word: i    Tfidf: 0.40993714596    Idf: 1.0
Word: love    Tfidf: 0.40993714596    Idf: 1.0
Word: chinese    Tfidf: 0.0    Idf: 1.40546510811
Word: food    Tfidf: 0.0    Idf: 1.40546510811

nltk 方法

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
34
35
36
37
38
from __future__ import division, unicode_literals
from textblob import TextBlob as tb
import math
corpus = []
doc1 = "I love Chinese food"
doc2 = "I love American spirits"
corpus.append(doc1)
corpus.append(doc2)
def tf(word, blob):
return blob.words.count(word) / len(blob.words)
def n_containing(word, bloblist):
return sum(1 for blob in bloblist if word in blob.words)
def idf(word, bloblist):
return math.log(len(bloblist) / n_containing(word, bloblist)) + 1
def tfidf(word, blob, bloblist): return tf(word, blob) * idf(word, bloblist)
def calTfidf(corpus):
bloblist = [tb(doc.strip('\n')) for doc in corpus]
for i, blob in enumerate(bloblist):
tf_scores = {word: tf(word, blob) for word in blob.words}
idf_scores = {word: idf(word, bloblist) for word in blob.words}
tfidf_scores = {word: tfidf(word, blob, bloblist)
for word in blob.words}
sorted_words = sorted(tfidf_scores.items(),
key=lambda x: x[1], reverse=True)
print 'u"-------第", key, u"类文本的词语tf-idf权重------"'
for word, score in sorted_words[:50]:
print ("Word: {}\t TF-IDF: {}\t TF: {}\t IDF: {}".format(word, round(score, 5), tf_scores[word], round(idf_scores[word], 5)))

输出

u"-------第", key, u"类文本的词语tf-idf权重------"
Word: food     TF-IDF: 0.42329     TF: 0.25     IDF: 1.69315
Word: Chinese     TF-IDF: 0.42329     TF: 0.25     IDF: 1.69315
Word: I     TF-IDF: 0.25     TF: 0.25     IDF: 1.0
Word: love     TF-IDF: 0.25     TF: 0.25     IDF: 1.0
u"-------第", key, u"类文本的词语tf-idf权重------"
Word: American     TF-IDF: 0.42329     TF: 0.25     IDF: 1.69315
Word: spirits     TF-IDF: 0.42329     TF: 0.25     IDF: 1.69315
Word: I     TF-IDF: 0.25     TF: 0.25     IDF: 1.0
Word: love     TF-IDF: 0.25     TF: 0.25     IDF: 1.0

比较

发现 sklearn 和 nltk 两种方法计算的结果不同,研究一下发现是对 tf 的处理不同。

  • 看 sklearn 的 TfidfTransformer,发现进行了正则化(L2),norm=u’l2’。
  • 而 nltk,我们用的是 Linear scaling 的方法。

为什么要进行这样的比较,实际是因为当时需要看 tf-idf, tf, idf 各自的情况,却发现 sklearn 得不到正确的 tf,于是才折腾的用了 nltk,实际上通过 nltk 自己写函数会更灵活,具体问题具体分析吧。

缺陷与不足

TF-IDF 并不是万能的,它单纯地认为文本频数小的单词就越重要,文本频数大的单词就越无用,显然这并不是完全正确的。可能会出现的结果:

  • 被忽略的高频词。高频词 != 无意义的词。引入 idf,初衷是抑制无意义的高频词(通常是 stopwords)的影响,如上文提到的 Wall Street Journal 例子,如果在金融分析的场景下,“trading” 和 “prices” 这类高频词本不该被忽略。
  • 被强调的低频词。

这也是为什么我在计算了 tf-idf 的同时,还要观察 tf、idf 的值的原因。根据不同的场景,可能需要引入阈值,限制IDF值过大的词语导入。另外 tfidf 这种基于词袋的算法还有个与生俱来的硬伤 – 位置信息被忽略。

徐阿衡 wechat
欢迎关注:徐阿衡的微信公众号
客官,打个赏呗~