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

# 1. はじめに
- Speech and Language Processingの第6章で説明されるtf-idf
- scikit-learnのTfidfVectorizer

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

# 2. 環境
- Python 3.8.7
- scikit-learn 0.24.1

# 3. 使用するデータ
[東京外国語大学言語モジュール](http://www.coelang.tufs.ac.jp/mt/)の[ベトナム語の例文](http://www.coelang.tufs.ac.jp/mt/vi/gmod/courses/c01/lesson02/step01/instances/008.html)から簡単な文を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 = 生徒" など、半角スペースを区切りにすると実際の単語がバラバラになってしまいますが、今回は無視します。）

```python
[['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](https://web.stanford.edu/~jurafsky/slp3/)の[第6章](https://web.stanford.edu/~jurafsky/slp3/6.pdf)のP.11-13の説明を簡単にまとめ、今回のデータを計算します。

## 4-1. tf (term frequency)
document(d)内での単語(t)の出現回数を数えたものです。
今回で言えば、documentは文に相当します。

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

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

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

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

```math
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番目の文...を表します。

```math
\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数を数えたものです。
今回のデータでは以下のようになります。

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

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

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

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

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

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

```math
\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)は以下の式で求められます。

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

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

```math
\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の計算方法の確認](#4-tf-idfの計算方法の確認)と同様に、scikit-learnのTfidfVectorizerを用いてtf-idfの値を計算します。

```python
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時の正規表現を指定する。<br>（デフォルトは2文字以上のアルファベット） |
| norm | {'l1', 'l2'} | 'l2' | tf-idfの結果をnormalizeする方法を指定する。<br>'l1'：documentごとにL1ノルムで割る<br>'l2'：documentごとにL2ノルムで割る<br>False/None/0：何もしない |
| use_idf | bool | True | tfにidfを掛けるかを指定する。<br>True：$tf_{t,d} \times idf_{t}$<br>False：$tf_{t,d}$ |
| smooth_idf | bool | True | idfを計算方法を指定する。<br>True：$\log_{e} (\frac{N_{document} + 1}{df_{t} + 1}) + 1$<br>False：$\log_{e} (\frac{N_{document}}{df_{t}}) + 1$ |
| sublinear_tf | bool | False | tfの計算方法を指定する。<br>True：$1 + \log_{e} count(t,d)$<br>False：$count(t,d)$

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

```math
\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}
```

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

```math
\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
    - [ドキュメント](https://scikit-learn.org/stable/modules/generated/sklearn.feature_extraction.text.TfidfVectorizer.html)
    - [ソースコード](https://github.com/scikit-learn/scikit-learn/blob/main/sklearn/feature_extraction/text.py#L1526)
    - [document frequencyの計算箇所](https://github.com/scikit-learn/scikit-learn/blob/a9ce392f3a58da5caf5ac9bd287205e220082fc5/sklearn/feature_extraction/text.py#L799)
    - [inverse document frequencyの計算箇所](https://github.com/scikit-learn/scikit-learn/blob/a9ce392f3a58da5caf5ac9bd287205e220082fc5/sklearn/feature_extraction/text.py#L1448)
- [Speech and Language Processingの第6章](https://web.stanford.edu/~jurafsky/slp3/6.pdf)