LoginSignup
0
1

More than 1 year has passed since last update.

tf-idfの計算結果が合わない...scikit-learnはどのようにtf-idfを計算しているか

Last updated at Posted at 2021-05-04

1. はじめに

  • Speech and Language Processingの第6章で説明されるtf-idf
  • scikit-learnのTfidfVectorizer

の計算結果が合わず、その原因を探りました。

2. 環境

  • Python 3.8.7
  • scikit-learn 0.24.1

3. 使用するデータ

東京外国語大学言語モジュールベトナム語の例文から簡単な文を5つ使用します。

Tôi là sinh viên.
Tôi là người Nhật.
Chị có phải là người Việt không?
Vâng, tôi là người Việt.
Không, tôi không phải là người Việt.

今回は、記号(.,?)をなくし、すべて小文字にし、半角スペースで区切ったものを「単語」とします。
(実際は "sinh viên = 生徒" など、半角スペースを区切りにすると実際の単語がバラバラになってしまいますが、今回は無視します。)

[['tôi', 'là', 'sinh', 'viên'],
 ['tôi', 'là', 'người', 'nhật'],
 ['chị', 'có', 'phải', 'là', 'người', 'việt', 'không'],
 ['vâng', 'tôi', 'là', 'người', 'việt'],
 ['không', 'tôi', 'không', 'phải', 'là', 'người', 'việt']]

以降、以下の3単語に注目してtf-idfを計算していきます。

単語 説明
5文すべてに出てくる
chị 全体で1回しか出てこない
không 5番目の文に2度出てくる

4. tf-idfの計算方法の確認

Speech and Language Processing第6章のP.11-13の説明を簡単にまとめ、今回のデータを計算します。

4-1. tf (term frequency)

document(d)内での単語(t)の出現回数を数えたものです。
今回で言えば、documentは文に相当します。

tf_{t,d} = count(t,d)

多くの場合、対数を取ってから使用します。
その際、$\log_{10} 0$とならないように1を足します。

tf_{t,d} = \log_{10} (count(t,d) + 1)

もしくは、$count(t,d)$が0かどうかで場合分けします。

tf_{t,d} = 
\begin{cases}
1 + \log_{10} count(t,d) & if\ count(t,d) > 0 \\
0 & otherwise
\end{cases}

今回のデータで2つ目の式を計算すると、以下のようになります。
ここで、$d1$は1番目の文、$d3$は3番目の文...を表します。

\begin{align}
tf_{là,d1} & = \log_{10} (count(là,d1) + 1) = \log_{10} (1 + 1) \approx 0.301 \\
tf_{chị,d3} & = \log_{10} (count(chị,d3) + 1) = \log_{10} (1 + 1) \approx 0.301 \\
tf_{không,d5} & = \log_{10} (count(không,d5) + 1) = \log_{10} (2 + 1) \approx 0.477
\end{align}

4-2. idf (inverse document frequency)

まず、df (document frequency)は、単語(t)が出現するdocument数を数えたものです。
今回のデータでは以下のようになります。

\begin{align}
df_{là} & = 5 \\
df_{chị} & = 1 \\
df_{không} & = 2
\end{align}

idf (inverse document frequency)は、全体のducument数をdfで割ったものです。
よって、document固有の単語に対して値がより大きくなります。

idf_{t} = \frac{N_{document}}{df_{t}}

こちらも、多くの場合$N_{document}$が大きくなるため、対数を取って使用します。

idf_{t} = \log_{10} \frac{N_{document}}{df_{t}}

今回のデータでは以下のようになります。

\begin{align}
idf_{là} & = \log_{10} \frac{5}{5} = \log_{10} 1 = 0 \\
idf_{chị} & = \log_{10} \frac{5}{1} = \log_{10} 5 \approx 0.700 \\
idf_{không} & = \log_{10} \frac{5}{2} = \log_{10} 2.5 \approx 0.398
\end{align}

4-3. tf-idf

tf-idfで重みづけされた値(w)は以下の式で求められます。

w_{t,d} = tf_{t,d} \times idf_{t}

今回のデータでは以下のようになります。

\begin{align}
w_{là,d1} & = tf_{là,d1} \times idf_{là} = 0.301 \times 0 = 0 \\
w_{chị,d3} & = tf_{chị,d3} \times idf_{chị} = 0.301 \times 0.700 \approx 0.211 \\
w_{không,d5} & = tf_{không,d5} \times idf_{không} = 0.477 \times 0.398 \approx 0.190
\end{align}

ちなみに計算結果から、以下のことが分かります。
- すべての文に出現するlàは0になった
- 1つの文にしか出現しないchịは、idfが大きくなるため値が最大になった
- khôngは5番目の文に2度出現するが、3番目の文にも現れるため、結局値が抑えられた

5. scikit-learnでの実装

5-1. まず計算してみる

4. tf-idfの計算方法の確認と同様に、scikit-learnのTfidfVectorizerを用いてtf-idfの値を計算します。

from sklearn.feature_extraction.text import TfidfVectorizer

corpus = [
    'Tôi là sinh viên.',
    'Tôi là người Nhật.',
    'Chị có phải là người Việt không?',
    'Vâng, tôi là người Việt.',
    'Không, tôi không phải là người Việt.',
]

tfidf = TfidfVectorizer()
result = tfidf.fit_transform(corpus).toarray()
vocab = tfidf.get_feature_names()

print(f'là: {result[1-1][vocab.index("là")]:.3f}')
print(f'chị: {result[3-1][vocab.index("chị")]:.3f}')
print(f'không: {result[5-1][vocab.index("không")]:.3f}')
là: 0.299
chị: 0.483
không: 0.755

あれ、結果が全然違う...?

5-2. 実装の確認

scikit-learnのTfidfVectorizerにおいて、計算結果に関わりそうな引数を調べた結果が以下の通りです。

引数 型・値 デフォルト 説明
lowercase bool True tokenize前にlowercaseにするかを指定する。
token_pattern str r'(?u)\b\w\w+\b' tokenize時の正規表現を指定する。
(デフォルトは2文字以上のアルファベット)
norm {'l1', 'l2'} 'l2' tf-idfの結果をnormalizeする方法を指定する。
'l1':documentごとにL1ノルムで割る
'l2':documentごとにL2ノルムで割る
False/None/0:何もしない
use_idf bool True tfにidfを掛けるかを指定する。
True:$tf_{t,d} \times idf_{t}$
False:$tf_{t,d}$
smooth_idf bool True idfを計算方法を指定する。
True:$\log_{e} (\frac{N_{document} + 1}{df_{t} + 1}) + 1$
False:$\log_{e} (\frac{N_{document}}{df_{t}}) + 1$
sublinear_tf bool False tfの計算方法を指定する。
True:$1 + \log_{e} count(t,d)$
False:$count(t,d)$

よって、TfidfVectorizerのデフォルトでの計算式は以下の通りになることが分かりました。
ここで、vocabは入力として与えた文に含まれる全単語のユニークな集合を表します。

\begin{align}
w_{t,d} & = l2\_normalize_{d}(tf_{t,d} \times idf_{t}) \\
& = \frac{tf_{t,d} \times idf_{t}}{\sqrt{\sum_{v \in vocab} (tf_{v,d} \times idf_{v})^{2}}} \\
tf_{t,d} \times idf_{t} & = count(t,d) \times (\log_{e} (\frac{N_{document} + 1}{df_{t} + 1}) + 1)
\end{align}

これをもとに、もう一度値の計算を行います。

\begin{align}
w_{là,d1} & = l2\_normalize_{d1} (1 \times (\log_{e} (\frac{5 + 1}{5 + 1}) + 1)) \\
& = \frac{1 \times 1}{3.347...} \approx 0.299 \\
w_{chị,d3} & = l2\_normalize_{d3} (1 \times (\log_{e} (\frac{5 + 1}{1 + 1}) + 1))\\
& = \frac{1 \times 2.098...}{4.349...} \approx 0.483 \\
w_{không,d5} & = l2\_normalize_{d5} (2 \times (\log_{e} (\frac{5 + 1}{2 + 1}) + 1)) \\
& = \frac{2 \times 1.693...}{4.483...} \approx 0.755
\end{align}

参考

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1