版权声明:署名,允许他人基于本文进行创作,且必须基于与原先许可协议相同的许可协议分发本文 (Creative Commons)
简单来说,在一个文档集中,TFIDF 反映了一个词在一篇文档中的重要程度,或者说这个词在这篇文档中具有多大的「标志性」。我们可以用其作为每个词的权重进而通过计算余弦相似度来比较两篇文档的相似性。
TFIDF 是由 TF 和 IDF 的乘积得到的:
tfidf(t,d,D)=tf(t,d)⋅idf(t,D)\text{tfidf}(t, d, D) = \text{tf}(t, d) \cdot \text{idf}(t, D)tfidf(t,d,D)=tf(t,d)⋅idf(t,D)
其中,ttt 表示词项,d∈Dd \in Dd∈D 表示文档,DDD 表示所有 ddd 组成的文档集。这其中 tf(t,d)\text{tf}(t, d)tf(t,d) 和 idf(t,D)\text{idf}(t, D)idf(t,D) 各自都有多种不同的计算方式,下面分别来说下。
tf 指的是 term frequency,即一个词在一篇文档中出现的次数,最原始的计算方式就是直接统计词项 ttt 在文档 ddd 中出现的次数,我们将其记为 ft,df_{t, d}ft,d。除此之外,还有其他计算方式:
tf(t,d)=ft,dΣt′∈dft,d\text{tf}(t, d) = \dfrac{f_{t, d}}{\Sigma_{t' \in d}f_{t, d}}tf(t,d)=Σt′∈dft,dft,d
tf(t,d)=log(1+ft,d)\text{tf}(t, d) = \log(1+f_{t,d}) tf(t,d)=log(1+ft,d)
tf(t,d)=0.5+0.5⋅ft,dmax{t′∈d}ft′,d\text{tf}(t, d) = 0.5 + 0.5 \cdot \dfrac{f_{t,d}}{\max_{\{t'\in d\}}f_{t',d}}tf(t,d)=0.5+0.5⋅max{t′∈d}ft′,dft,d
tf(t,d)=K+(1−K)⋅ft,dmax{t′∈d}ft′,d\text{tf}(t, d) = K + (1-K) \cdot \dfrac{f_{t,d}}{\max_{\{t'\in d\}}f_{t',d}}tf(t,d)=K+(1−K)⋅max{t′∈d}ft′,dft,d
idf 指的是 inverse document frequency,即逆文档频率,衡量一个词项能提供多少信息,如它在文档集 DDD 中比较普遍还是比较少见。一般来说,是由文档集 DDD 中的文档数 NNN,除以包含词项 ttt 的文档数 ntn_tnt,然后再取对数得到:
idf(t,D)=logNnt\text{idf}(t, D) = \log\dfrac{N}{n_t}idf(t,D)=logntN
其中 nt=∣{d∈D:t∈d}∣n_t = |\{d \in D:t \in d\}|nt=∣{d∈D:t∈d}∣。此时取值范围为 [0,∞)[0, \infty)[0,∞)
除此之外,还有其他计算方式:
idf(t,D)=logN1+nt\text{idf}(t, D) = \log\dfrac{N}{1+n_t}idf(t,D)=log1+ntN
idf(t,D)=logmax{t′∈d}nt′1+nt\text{idf}(t, D) = \log\dfrac{\max_{\{t' \in d\}}n_{t'}}{1+n_t}idf(t,D)=log1+ntmax{t′∈d}nt′
idf(t,D)=logN−ntnt\text{idf}(t, D) = \log\dfrac{N-n_t}{n_t}idf(t,D)=logntN−nt
sklearn 中计算 tfidf 的函数是 TfidfTransformer
和 TfidfVectorizer
,严格来说后者 = CountVectorizer
+ TfidfTransformer
。TfidfTransformer
和 TfidfVectorizer
有一些共同的参数,这些参数的不同影响了 tfidf 的计算方式:
norm
:归一化,l1
、l2
(默认值)或者 None
。l1
是向量中每个值除以所有值的绝对值的和()1-范数,l2
是向量中每个值除以所有值的平方开根号(2-范数),即对于 l1
:
xi=xi∣∣x∣∣1=xi∑j∣xj∣ x_i = \dfrac{x_i}{||\pmb x||_1} = \dfrac{x_i}{\sum_j |x_j|} xi=∣∣xxx∣∣1xi=∑j∣xj∣xi
对于 l2
:
xi=xi∣∣x∣∣2=xi∑jxj2 x_i = \dfrac{x_i}{||\pmb x||_2} = \dfrac{x_i}{\sqrt{\sum_j x^2_j}} xi=∣∣xxx∣∣2xi=∑jxj2xiuse_idf
:bool
,默认 True
,是否使用 idfsmooth_idf
:bool
,默认 True
,是否平滑 idf,默认分子和分母 都+1,和上述任何一种都不一样,防止除零错误sublinear_tf
:bool
,默认 False
,是否对 tf 使用 sublinear,即使用 1 + log(tf)
来替换原始的 tf所以,默认参数下(norm='l2', use_idf=True, smooth_idf=True, sublinear_tf=False
),sklearn 是这么计算 tfidf 的:
tfidf(t,d,D)=tf(t,d)⋅idf(t,D)=tf(t,d)⋅(log1+N1+nt+1) \begin{aligned} \text{tfidf}(t, d, D) &= \text{tf}(t, d) \cdot \text{idf}(t, D) \\ &= \text{tf}(t, d) \cdot \left(\log{\dfrac{1 + N}{1+n_t}} + 1\right) \end{aligned} tfidf(t,d,D)=tf(t,d)⋅idf(t,D)=tf(t,d)⋅(log1+nt1+N+1)
我们以如下文档集 DDD 为例,列表中每个元素是一篇文档,共有 N=4N=4N=4 篇文档,使用 jieba 分好词:
documents = [
"低头亲吻我的左手", # 文档 1
"换取被宽恕的承诺", # 文档 2
"老旧管风琴在角落", # 文档 3
"一直一直一直伴奏", # 文档 4
]
documents = [" ".join(jieba.cut(item)) for item in documents]
# ['低头 亲吻 我 的 左手',
# '换取 被 宽恕 的 承诺',
# '老旧 管风琴 在 角落',
# '一直 一直 一直 伴奏']
我们的词汇表如下,顺序无关:
一直 亲吻 伴奏 低头 在 宽恕 左手 我 承诺 换取 的 管风琴 老旧 被 角落
现在我们可以首先计算所有词的 idf,以第一个词 一直
为例:
这里的 log\loglog 为自然对数,eee 为底。
idf(一直,D)=log1+N1+nt+1=log1+41+1+1=log52+1=1.916290731874155 \begin{aligned} idf(一直, D) &= \log{\dfrac{1 + N}{1+n_t}} + 1 \\ &= \log{\dfrac{1 + 4}{1+1}} + 1 \\ &= \log{\dfrac{5}{2}} + 1 \\ &= 1.916290731874155 \end{aligned} idf(一直,D)=log1+nt1+N+1=log1+11+4+1=log25+1=1.916290731874155
其实除了 的
,其他所有词的 idf 都是 1.9162907318741551.9162907318741551.916290731874155,因为都只出现在一篇文档里。
以第一个词 一直
为例,来计算其 tfidf 值,按照上述 sklearn 的默认参数。其在前三篇文档中都未出现,即 tf(一直,文档1/2/3)=0\text{tf}(一直, 文档1/2/3) = 0tf(一直,文档1/2/3)=0,那么 tfidf(一直,文档1/2/3,D)=0\text{tfidf}(一直, 文档1/2/3, D) = 0tfidf(一直,文档1/2/3,D)=0。
最后一篇文档中,其出现了 3 次,则 tf(一直,文档4)=3\text{tf}(一直, 文档4) = 3tf(一直,文档4)=3,tfidf(一直,文档4,D)=3×1.916290731874155=5.748872195622465\text{tfidf}(一直, 文档4, D) = 3 \times 1.916290731874155 = 5.748872195622465tfidf(一直,文档4,D)=3×1.916290731874155=5.748872195622465。最后一篇剩下的词为 伴奏
,同理可计算其 tfidf 值为 1.9162907318741551.9162907318741551.916290731874155,那么该文档的 tfidf 向量为
(5.748872195622465,0,1.916290731874155,0,0,0,0,0,0,0,0,0,0,0,0)(5.748872195622465, 0, 1.916290731874155, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)(5.748872195622465,0,1.916290731874155,0,0,0,0,0,0,0,0,0,0,0,0)
再经过2-范数归一化,得到
(0.9486833,0,0.31622777,0,0,0,0,0,0,0,0,0,0,0,0)(0.9486833, 0, 0.31622777, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)(0.9486833,0,0.31622777,0,0,0,0,0,0,0,0,0,0,0,0)
这就是文档 4 最终的 tfidf 向量了。
代码如下:
默认情况下 sklearn 会莫名其妙地去除掉一些停用词,即使
stop_words=None
,详细讨论参见 CountVectorizer can’t remain stop words in Chinese · Issue #10756 · scikit-learn/scikit-learn
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. ]])
可以看到和我们手算的一样。