目录

  1. 什么是 TFIDF
    1. $\text{tf}(t, d)$
    2. $\text{idf}(t, D)$
  2. sklearn 中如何计算
  3. 例子
    1. 手算
    2. 使用 sklearn 计算
  4. Reference
  5. END

文中代码见 GitHub Gist 或者使用 nbviewer 查看。

什么是 TFIDF

简单来说,在一个文档集中,TFIDF 反映了一个词在一篇文档中的重要程度,或者说这个词在这篇文档中具有多大的「标志性」。我们可以用其作为每个词的权重进而通过计算余弦相似度来比较两篇文档的相似性。

TFIDF 是由 TF 和 IDF 的乘积得到的:

其中,$t$ 表示词项,$d \in D$ 表示文档,$D$ 表示所有 $d$ 组成的文档集。这其中 $\text{tf}(t, d)$ 和 $\text{idf}(t, D)$ 各自都有多种不同的计算方式,下面分别来说下。

$\text{tf}(t, d)$

tf 指的是 term frequency,即一个词在一篇文档中出现的次数,最原始的计算方式就是直接统计词项 $t$ 在文档 $d$ 中出现的次数,我们将其记为 $f_{t, d}$。除此之外,还有其他计算方式:

  • 二值:如果词项 $t$ 在文档 $d$ 中出现,则为 1,否则为 0。此时 $\text{tf}(t,d)$ 的取值范围为 ${0,1}$
  • 词项频率:即词项 $t$ 的频数(次数)除以文档 $d$ 的总词数,此时 $\text{tf}(t,d)$ 的取值范围为 $[0,1]$
  • 对数化(log normalization):此时 $\text{tf}(t,d)$ 的取值范围为 $[0,\infty]$
  • 双重标准化 0.5(double normalization 0.5):就是让原本的 $\text{tf}(t,d)$ 除以文档 $d$ 中词频最高的词项的频数,这样做是为了避免偏向长文档。此时 $\text{tf}(t,d)$ 的取值范围为 $[0,1]$
  • 双重标准化 K(double normalization K):就是将上面方法中的 $0.5$ 换成更为一般的 $K$。此时 $\text{tf}(t,d)$ 的取值范围为 $[0,1]$

$\text{idf}(t, D)$

idf 指的是 inverse document frequency,即逆文档频率,衡量一个词项能提供多少信息,如它在文档集 $D$ 中比较普遍还是比较少见。一般来说,是由文档集 $D$ 中的文档数 $N$,除以包含词项 $t$ 的文档数 $n_t$,然后再取对数得到:

其中 $n_t = |{d \in D:t \in d}|$。此时取值范围为 $[0, \infty)$

除此之外,还有其他计算方式:

  • 一元化(unary):即恒为 1,这也就意味着所有词项都不能提供有效信息
  • 平滑的逆文档频率(inverse document frequency smooth):这是为了避免由于词项 $t$ 没有出现在文档集中而发生的除零错误。此时 $\text{idf}(t,D)$ 的取值范围为 $[\log N,1]$
  • inverse document frequency max(这个中文不太好翻 😃):对于文档 $d$ 中的词项 $t’$,逐个计算他们的 $n_{t’}$,并选其中的最大值来替换 $N$
  • 概率逆文档频率(probabilistic inverse document frequency):还是对 $N$ 替换,这次是替换为 $N-n_t$

sklearn 中如何计算

sklearn 中计算 tfidf 的函数是 TfidfTransformerTfidfVectorizer,严格来说后者 = CountVectorizer + TfidfTransformerTfidfTransformerTfidfVectorizer 有一些共同的参数,这些参数的不同影响了 tfidf 的计算方式:

  • norm:归一化,l1l2(默认值)或者 Nonel1 是向量中每个值除以所有值的绝对值的和()1-范数,l2 是向量中每个值除以所有值的平方开根号(2-范数),即对于 l1对于 l2
  • use_idfbool,默认 True,是否使用 idf
  • smooth_idfbool,默认 True,是否平滑 idf,默认分子和分母 都+1,和上述任何一种都不一样,防止除零错误
  • sublinear_tfbool,默认 False,是否对 tf 使用 sublinear,即使用 1 + log(tf) 来替换原始的 tf

所以,默认参数下(norm='l2', use_idf=True, smooth_idf=True, sublinear_tf=False),sklearn 是这么计算 tfidf 的:

例子

手算

我们以如下文档集 $D$ 为例,列表中每个元素是一篇文档,共有 $N=4$ 篇文档,使用 jieba 分好词:

1
2
3
4
5
6
7
8
9
10
11
documents = [
"低头亲吻我的左手", # 文档 1
"换取被宽恕的承诺", # 文档 2
"老旧管风琴在角落", # 文档 3
"一直一直一直伴奏", # 文档 4
]
documents = [" ".join(jieba.cut(item)) for item in documents]
# ['低头 亲吻 我 的 左手',
# '换取 被 宽恕 的 承诺',
# '老旧 管风琴 在 角落',
# '一直 一直 一直 伴奏']

我们的词汇表如下,顺序无关:

1
一直 亲吻 伴奏 低头 在 宽恕 左手 我 承诺 换取 的 管风琴 老旧 被 角落

现在我们可以首先计算所有词的 idf,以第一个词 一直 为例:

这里的 $\log$ 为自然对数,$e$ 为底。

其实除了 ,其他所有词的 idf 都是 $1.916290731874155$,因为都只出现在一篇文档里。

以第一个词 一直 为例,来计算其 tfidf 值,按照上述 sklearn 的默认参数。其在前三篇文档中都未出现,即 $\text{tf}(一直, 文档1/2/3) = 0$,那么 $\text{tfidf}(一直, 文档1/2/3, D) = 0$。

最后一篇文档中,其出现了 3 次,则 $\text{tf}(一直, 文档4) = 3$,$\text{tfidf}(一直, 文档4, D) = 3 \times 1.916290731874155 = 5.748872195622465$。最后一篇剩下的词为 伴奏,同理可计算其 tfidf 值为 $1.916290731874155$,那么该文档的 tfidf 向量为

再经过2-范数归一化,得到

这就是文档 4 最终的 tfidf 向量了。

使用 sklearn 计算

代码如下:

默认情况下 sklearn 会莫名其妙地去除掉一些停用词,即使 stop_words=None,详细讨论参见 CountVectorizer can’t remain stop words in Chinese · Issue #10756 · scikit-learn/scikit-learn

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
39
import jieba
from sklearn.feature_extraction.text import TfidfTransformer, TfidfVectorizer, CountVectorizer

documents = [
"低头亲吻我的左手",
"换取被宽恕的承诺",
"老旧管风琴在角落",
"一直一直一直伴奏",
]
documents = [" ".join(jieba.cut(item)) for item in documents]
# 默认情况下 sklearn 会莫名其妙地去除掉一些停用词,即使 stop_words=None
# 详细讨论参见 https://github.com/scikit-learn/scikit-learn/issues/10756
vectorizer = TfidfVectorizer(token_pattern=r'(?u)\b\w+\b')
X = vectorizer.fit_transform(documents)

# 词汇表
print(' '.join(vectorizer.get_feature_names()))
# '一直 亲吻 伴奏 低头 在 宽恕 左手 我 承诺 换取 的 管风琴 老旧 被 角落'

# idf
print(vectorizer.idf_)
# array([1.91629073, 1.91629073, 1.91629073, 1.91629073, 1.91629073,
# 1.91629073, 1.91629073, 1.91629073, 1.91629073, 1.91629073,
# 1.51082562, 1.91629073, 1.91629073, 1.91629073, 1.91629073])

# tfidf
print(X.toarray())
# array([[0. , 0.46516193, 0. , 0.46516193, 0. ,
# 0. , 0.46516193, 0.46516193, 0. , 0. ,
# 0.36673901, 0. , 0. , 0. , 0. ],
# [0. , 0. , 0. , 0. , 0. ,
# 0.46516193, 0. , 0. , 0.46516193, 0.46516193,
# 0.36673901, 0. , 0. , 0.46516193, 0. ],
# [0. , 0. , 0. , 0. , 0.5 ,
# 0. , 0. , 0. , 0. , 0. ,
# 0. , 0.5 , 0.5 , 0. , 0.5 ],
# [0.9486833 , 0. , 0.31622777, 0. , 0. ,
# 0. , 0. , 0. , 0. , 0. ,
# 0. , 0. , 0. , 0. , 0. ]])

可以看到和我们手算的一样。

Reference

END