TfidfVectorizerのidfについて

この記事では、scikit-learnのTfidfVectorizerの、idf部分について掘り下げてみていきます。
以前の記事でも書いた通り、デフォルトでのidfの挙動は一般的な定義とは異なります。
参考: tf-idfの一般的な定義とscikit-learnにおけるtf-idfの定義

単語$t$についてみていくと、通常の定義は
$$
\text{idf}_{t} = \log{\frac{\text{総文書数}}{\text{単語tを含む文書数}}}
$$
であり、
TfidfVectorizer のデフォルトのオプションでは、
$$
\text{idf}_{t} = \log{\frac{1+\text{総文書数}}{1+\text{単語tを含む文書数}}}+1
$$
となっています。

まず、実際にこの数式通り動いていることを見ておきましょう。


import numpy as np
from sklearn.feature_extraction.text import TfidfVectorizer

# データを準備しておく
corpus = [
    'This is the first document.',
    'This document is the second document.',
    'And this is the third one.',
    'Is this the first document?',
]

tfidf_model = TfidfVectorizer(
    token_pattern="(?u)\\b\\w+\\b",
)
tfidf_model.fit(corpus)

# モデルから抽出した idfの値
for t, idf in zip(tfidf_model.get_feature_names(), tfidf_model.idf_):
    print(t, idf)

"""
and 1.916290731874155
document 1.2231435513142097
first 1.5108256237659907
is 1.0
one 1.916290731874155
second 1.916290731874155
the 1.0
third 1.916290731874155
this 1.0
"""

# 出現回数から計算したidfの値
for i in range(1, 5):
    print(f"出現回数{i}回, idf: ", np.log((1+4)/(1+i))+1)
"""
出現回数1回, idf:  1.916290731874155
出現回数2回, idf:  1.5108256237659907
出現回数3回, idf:  1.2231435513142097
出現回数4回, idf:  1.0
"""

document は3文書に登場しているから、idfは 1.2231435513142097、firtstは2文書に登場しているから、idfは1.5108256237659907と言うふうに、
scikit-learnのドキュメント通りに計算されていることがわかりますね。

さて、scikit-learnの定義がデフォルトと異なっている理由は、次の2つがあります。
(1) コーパス中に登場しない単語で0除算が発生しないように、log中の分数の分子と分母に1を足す。
(2) コーパス中の全ての文書に登場した単語のidfが0にならないように全ての単語のidfに1を足す。

(1)の方は平滑化と呼ばれる操作です。
実はTfidfVectorizerは、学習する単語をコーパスから自動的にピックアップするのではなく、モデルの引数として渡すことができます。
次のコード例では、コーパスに登場しない、oovという単語を明示的に加えてみました。
結果、$\log((1+4)/(1+0))+1 = 2.6094379124341005$ がoovのidf値になっていることがわかります。


tfidf_model = TfidfVectorizer(
    token_pattern="(?u)\\b\\w+\\b",
    vocabulary=['and', 'document', 'first', 'is',
                'one', 'second', 'the', 'third', 'this', "oov"],
)
tfidf_model.fit(corpus)

# モデルから抽出した idfの値
for t, idf in zip(tfidf_model.get_feature_names(), tfidf_model.idf_):
    print(t, idf)
"""
and 1.916290731874155
document 1.2231435513142097
first 1.5108256237659907
is 1.0
one 1.916290731874155
second 1.916290731874155
the 1.0
third 1.916290731874155
this 1.0
oov 2.6094379124341005
"""

この分子と分母の+1については、smooth_idfと言う引数にFalseを渡すことで使わないようにもできます。
参考: sklearn.feature_extraction.text.TfidfVectorizer

この場合、vocabulary引数で、コーパスに登場しない単語が渡されていると、0除算のワーニングが発生し、該当単語のidf値はinfになります。


tfidf_model = TfidfVectorizer(
    token_pattern="(?u)\\b\\w+\\b",
    vocabulary=['and', 'document', 'first', 'is',
                'one', 'second', 'the', 'third', 'this', "oov"],
    smooth_idf=False,
)
tfidf_model.fit(corpus)
"""
RuntimeWarning: divide by zero encountered in true_divide
  idf = np.log(n_samples / df) + 1
"""

# モデルから抽出した idfの値
for t, idf in zip(tfidf_model.get_feature_names(), tfidf_model.idf_):
    print(t, idf)
"""
and 2.386294361119891
document 1.2876820724517808
first 1.6931471805599454
is 1.0
one 2.386294361119891
second 2.386294361119891
the 1.0
third 2.386294361119891
this 1.0
oov inf
"""

tfidfの本来の定義に近い値で使う場合は、smooth_idf=Falseを指定するべきですが、この場合は安全のため、vocabularyは指定せず、
コーパスから自動的に学習するのに任せるべきでしょう。
(僕の場合は、vocabulary引数を使うことはほとんどありません。)

次に、(2)の全てのコーパスに倒叙する単語のidfが0にならないように、全部単語のidfに1足されている部分についてです。
こちらについては、これをオフにする引数のようなものは実装されていなさそうです。

どうしてもtfidfの本来の定義で使いたいんだ、と言う場合は、かなり無理矢理な操作になりますが、
モデルが学習したidfの値(idf_プロパティに格納されている)から1を引いてしまう手があります。
(当然、サポートされている操作ではないので実行は自己責任でお願いします。)

transform する際には、idf_ の値が使われるので次のようになります。


import pandas as pd
# 表示桁数を小数点以下3位までにする
pd.options.display.precision = 3

tfidf_model = TfidfVectorizer(
    token_pattern="(?u)\\b\\w+\\b",
    norm=False,
    smooth_idf=False,
)
tfidf_model.fit(corpus)
tfidf = tfidf_model.transform(corpus)

print(pd.DataFrame(tfidf.toarray(), columns=tfidf_model.get_feature_names()))
"""
     and  document  first   is    one  second  the  third  this
0  0.000     1.288  1.693  1.0  0.000   0.000  1.0  0.000   1.0
1  0.000     2.575  0.000  1.0  0.000   2.386  1.0  0.000   1.0
2  2.386     0.000  0.000  1.0  2.386   0.000  1.0  2.386   1.0
3  0.000     1.288  1.693  1.0  0.000   0.000  1.0  0.000   1.0
"""

# idf_ に足されている1を引く 
tfidf_model.idf_ -= 1
tfidf = tfidf_model.transform(corpus)

print(pd.DataFrame(tfidf.toarray(), columns=tfidf_model.get_feature_names()))
"""
     and  document  first   is    one  second  the  third  this
0  0.000     0.288  0.693  0.0  0.000   0.000  0.0  0.000   0.0
1  0.000     0.575  0.000  0.0  0.000   1.386  0.0  0.000   0.0
2  1.386     0.000  0.000  0.0  1.386   0.000  0.0  1.386   0.0
3  0.000     0.288  0.693  0.0  0.000   0.000  0.0  0.000   0.0
"""

コーパスの全ての文書に含まれていた、 is, the, this の idf値とtfidf値が0になっていることが確認できます。
また、他の単語についても、出現回数分値が落ちているのがみて取れます。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です