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を計算していきます。
単語 | 説明 |
---|---|
là | 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}
参考
- sklearn.feature_extraction.text.TfidfVectorizer
- Speech and Language Processingの第6章