前回の記事が、TfidfVectorizerのidfにフォーカスしたので、今回はtfの方を取り上げます。
以前の記事で書いた通り、一般的なtdの定義(Wikipedia日本語版の定義)では、tfはその単語の文書中の出現頻度です。
$$
\text{tf}_{t,d} = \frac{\text{文書d中の単語tの出現回数}}{\text{文書dの全ての単語の出現回数の和}}
$$
しかし、TfidfVectorizerにおいては、単純に出現回数が採用されています。
$$
\text{tf}_{t,d} = \text{文書d中の単語tの出現回数}
$$
これは、TfidfVectorizerでは通常の設定(norm=’l2′)では文書ベクトルは最後に長さ1になるように正規化するので、
tfを定数倍しても結果が変わらず、無駄な操作になるからだと考えられます。
とりあえず、norm=Falseを指定して、正規化せずにtfがただの出現回数になっていることを見ていきましょう。
サンプルのコーパスで学習して、idfとtfidfを出してみました。
出現回数が1回の単語はidf=tfidfとなり、2回の単語は、idf*2=tdidfとなっているのがみて取れます。
# %%pycodestyle
import pandas as pd
from sklearn.feature_extraction.text import TfidfVectorizer
# 表示桁数を小数点以下3位までにする
pd.options.display.precision = 3
# データを準備しておく
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",
norm=False,
)
tfidf_model.fit(corpus)
tfidf = tfidf_model.transform(corpus)
# モデルから抽出した idfの値
for t, idf in zip(tfidf_model.get_feature_names(), tfidf_model.idf_):
print(t, idf.round(3))
"""
and 1.916
document 1.223
first 1.511
is 1.0
one 1.916
second 1.916
the 1.0
third 1.916
this 1.0
"""
# tfidfの値
print(pd.DataFrame(tfidf.toarray(), columns=tfidf_model.get_feature_names()))
"""
and document first is one second the third this
0 0.000 1.223 1.511 1.0 0.000 0.000 1.0 0.000 1.0
1 0.000 2.446 0.000 1.0 0.000 1.916 1.0 0.000 1.0
2 1.916 0.000 0.000 1.0 1.916 0.000 1.0 1.916 1.0
3 0.000 1.223 1.511 1.0 0.000 0.000 1.0 0.000 1.0
"""
もし本来の定義で使いたいのであれば、それ専用のオプションは用意されていないので、
各文のベクトルをそれぞれの文の単語数で割ってあげる必要があります。
あまり綺麗な書き方が思いつかなかったのですが、実装するとしたら次のようなコードになるでしょうかでしょうか。
途中のif分は元の文が空だったときに0除算を発生させないためのものです。
for i in range(len(corpus)):
word_count = len(corpus[i].split(" "))
if word_count > 0:
tfidf[i] = tfidf[i] / word_count
# tfidfの値
print(pd.DataFrame(tfidf.toarray(), columns=tfidf_model.get_feature_names()))
"""
and document first is one second the third this
0 0.000 0.245 0.302 0.200 0.000 0.000 0.200 0.000 0.200
1 0.000 0.408 0.000 0.167 0.000 0.319 0.167 0.000 0.167
2 0.319 0.000 0.000 0.167 0.319 0.000 0.167 0.319 0.167
3 0.000 0.245 0.302 0.200 0.000 0.000 0.200 0.000 0.200
"""
TfidfVectorizerのインスタンスを作るときに、use_id = False と指定すると、idfが1で統一されるので、tfの結果だけ確認できます。
要はCountVectorizerと似た挙動になりますね。(CountVectorizerは正規化しませんが)
tf_model = TfidfVectorizer(
token_pattern="(?u)\\b\\w+\\b",
norm=False,
use_idf=False,
)
tf_model.fit(corpus)
tf = tf_model.transform(corpus)
print(pd.DataFrame(tf.toarray(), columns=tf_model.get_feature_names()))
"""
and document first is one second the third this
0 0.0 1.0 1.0 1.0 0.0 0.0 1.0 0.0 1.0
1 0.0 2.0 0.0 1.0 0.0 1.0 1.0 0.0 1.0
2 1.0 0.0 0.0 1.0 1.0 0.0 1.0 1.0 1.0
3 0.0 1.0 1.0 1.0 0.0 0.0 1.0 0.0 1.0
"""
さて、tfの値ですが、全ての単語数で割る一般的な定義を実現するオプションはなくてもそれ以外の亜種のオブションはあります。
参考: sklearn.feature_extraction.text.TfidfVectorizer
わかりやすくみるために、norm=False, usr_idf=False で試していきましょう。
まず、binaryと言う引数をTrueにすると、出現回数ではなく、出現する(1)か出現しない(0)かの2値になります。
tf_model = TfidfVectorizer(
token_pattern="(?u)\\b\\w+\\b",
norm=False,
use_idf=False,
binary=True,
)
tf_model.fit(corpus)
tf = tf_model.transform(corpus)
print(pd.DataFrame(tf.toarray(), columns=tf_model.get_feature_names()))
"""
and document first is one second the third this
0 0.0 1.0 1.0 1.0 0.0 0.0 1.0 0.0 1.0
1 0.0 1.0 0.0 1.0 0.0 1.0 1.0 0.0 1.0
2 1.0 0.0 0.0 1.0 1.0 0.0 1.0 1.0 1.0
3 0.0 1.0 1.0 1.0 0.0 0.0 1.0 0.0 1.0
"""
次に、 sublinear_tfという引数にTrueを渡すと、tfが、$1+\log(\text{tf})$に置き換えられます。
$1+\log(2)=1.693\dots$に注意して結果を見てください。
tf_model = TfidfVectorizer(
token_pattern="(?u)\\b\\w+\\b",
norm=False,
use_idf=False,
sublinear_tf=True
)
tf_model.fit(corpus)
tf = tf_model.transform(corpus)
print(pd.DataFrame(tf.toarray(), columns=tf_model.get_feature_names()))
"""
and document first is one second the third this
0 0.0 1.000 1.0 1.0 0.0 0.0 1.0 0.0 1.0
1 0.0 1.693 0.0 1.0 0.0 1.0 1.0 0.0 1.0
2 1.0 0.000 0.0 1.0 1.0 0.0 1.0 1.0 1.0
3 0.0 1.000 1.0 1.0 0.0 0.0 1.0 0.0 1.0
"""
英語版のWikipediaにtfの亜種がいろいろ紹介されていますが、その中にもない珍しいタイプの定義です。
$\log(1+\text{tf})$ならあるのですが。
参考: tf–idf – Wikipedia (English)
一つの文章の中に同じ単語が極端に繰り返し出現した場合などに、その影響を抑えられる良い形式だと思います。