scikit-learnでTF-IDFを計算する

# はじめに
最近自然言語処理に手をつけ始めました。文書内の特徴的な単語を見つけるTF-IDFについてまとめました。

## 参考
- [6.2. Feature extraction; scikit-learn 0.22.2 documentation](https://scikit-learn.org/stable/modules/feature_extraction.html#text-feature-extraction)

## 言葉の定義
この記事内の定義を示します。

「文書」は「単語」の集合とします。例えば「バナナにはビタミンミネラル食物繊維などが豊富です。」という文書があったとき、これを抽象的なレベルで$\mathfrak{d}$とラベル付することにします。文書$\mathfrak{d}$を単語の集合に変換したものを$d$と書くことにします。

```math
d = \{\mathrm{バナナ, に, は, ビタミン, ミネラル, 食物繊維, など, が, 豊富, です, 。 }\}
```

ここで、文書が先にあって、どのように単語に分解するか、という問題があると思いますが、文書は方法$M$を指定すれば、一意に分解できるとします。

```math
M:\mathfrak{d}\rightarrow d
```
混同のおそれが無い限り、文書$\mathfrak{d}$のことを文書$d$と略記することにします。

例えば「バナナ」は文書$d$の要素です。

```math
\mathrm{バナナ} \in d
```

また、文書の集合を$D$と書くことにします。

## Term frequency
Term frequencyはその名の通り、文書中の各単語の出現頻度を表しています。文書$d$の中における単語$t$の出現回数を$f_{t,d}$とします。Term frequencyを次のように定義します。

```math
\mathrm{tf}(t, d) = \frac{f_{t,d}}{\sum_{t'\in d}f_{t',d}}
```

[Wiki](https://en.wikipedia.org/wiki/Tf%E2%80%93idf)を見ると、他の定義方法もあるようで、単純にカウントを使用するもの$\mathrm{tf}(t, d) = f_{t,d}$や、対数をとる定義の仕方など他にもあるようです。

```math
\mathrm{tf}(t, d) = \log(1+f_{t,d})
```

$d$を固定して、$\mathrm{tf}(t_1, d) > \mathrm{tf}(t_2, d)$のとき、単語$t_1$のほうが文書中に多く出現していることを意味します。$t$を固定して$\mathrm{tf}(t, d_1) > \mathrm{tf}(t, d_2)$のとき、文書$d_1$のほうが単語$t$が出現する頻度が高いことを意味します。したがって$\mathrm{tf}(t, d)$が大きい値をとる場合、文書$d$において単語$t$は重要な単語であると推測されます。


## Inverse document frequency
文書中に頻繁に現れる単語は重要な単語に思えます。しかし、例えば英文の中の「The」は文書中に頻繁に出現するものの、文書の特徴を分析する際に役に立つものではありません。したがって、このような単語の重要度を下げる必要があります。「The」のような単語は、文書のジャンルなどに関係なく様々な文書に出現するので、様々な文書に出現する単語の重要度を下げればよいと考えられます。

文書の集合を$D$とし、Inverse document frequencyを次のように定義します。

```math
\mathrm{idf}(t, D) = \log\frac{\left| D \right|}{\left| \{ d \in D ; t \in d \} \right|}
```
$\log$の中の分母は、単語$t$が出現する文書$d$の数です。「The」などのような単語は$\log$の中の分母が大きくなり、重要度が小さくなります。全ての文書に登場する単語は$\log$の中の分母が$|D|$になるので、$\log(|D|/|D|)=0$となります。したがって、$\mathrm{idf}(t, D) \geq 0$です。


## Term frequency–Inverse document frequency
Term frequencyとInverse document frequencyを組み合わせて、文書$d$に出現する単語$t$を特徴づけます。

```math
\mathrm{tfidf}(t, d, D) = \mathrm{tf}(t, d) \cdot \mathrm{idf}(t, D)
```

文書$d$で単語$t$の出現頻度が高く、文書の集合全体での単語$t$の出現頻度が低いほど$\mathrm{tfidf}(t, d)$は大きくなります。


## sklearn.feature_extraction.text.TfidfTransformer
scikit-learnの`sklearn.feature_extraction.text.TfidfTransformer`でtfidfを計算することができます。`TfidfTransformer`では`smooth_idf=True`で滑らかにしたidfを使用します。

```math
\mathrm{idf}(t, D) = \log\frac{1 + \left| D \right|}{1 + \left| \{ d \in D : t \in d \} \right|} + 1
```

`smooth_idf=False`では元の定義のtfidfに1を加えたものを使用します。

```math
\mathrm{idf}(t, D) = \log\frac{\left| D \right|}{ \left| \{ d \in D : t \in d \} \right|} + 1
```

tfは単語の出現回数を使用します。

```math
\mathrm{tf}(t, d) = f_{t,d}
```

`TfidfTransformer`の使い方を見てみます。文書は6つあるとします。3つの単語に着目して、それらの単語の各文書での出現回数を行列`counts`にします。例えば1つ目の文書では単語1は3回、単語2は0回、単語3は1回という具合です。

```python
import numpy as np
from sklearn.feature_extraction.text import TfidfTransformer

transformer = TfidfTransformer(smooth_idf=False)
counts = np.array(
    [[3, 0, 1],
     [2, 0, 0],
     [3, 0, 0],
     [4, 0, 0],
     [3, 2, 0],
     [3, 0, 2]])

tfidf = transformer.fit_transform(counts)
tfidf.toarray()

# output ####################################
# array([[0.81940995, 0.        , 0.57320793],
#        [1.        , 0.        , 0.        ],
#        [1.        , 0.        , 0.        ],
#        [1.        , 0.        , 0.        ],
#        [0.47330339, 0.88089948, 0.        ],
#        [0.58149261, 0.        , 0.81355169]])
```
文書1内での単語1のtfidfは約$0.82$、単語2は$0$、単語3は約$0.57$となりました。なお、この値は行ごとにL2規格化されていることに注意してください。

単語のカウントは、英文であれば次のようにできます。

```python
from sklearn.feature_extraction.text import CountVectorizer

corpus = [
    'This is the first document.',
    'This is the second second document.',
    'And the third one.',
    'Is this the first document?',
]

vectorizer = CountVectorizer()
vectorizer.fit(corpus)
X = vectorizer.transform(corpus)
print(vectorizer.get_feature_names())
print(X.toarray())

# output ####################################
# ['and', 'document', 'first', 'is', 'one', 'second', 'the', 'third', 'this']
# [[0 1 1 1 0 0 1 0 1]
#  [0 1 0 1 0 2 1 0 1]
#  [1 0 0 0 1 0 1 1 0]
#  [0 1 1 1 0 0 1 0 1]]
```

一気にtfidfを計算する場合は`TfidfVectorizer`を使用します。

```python
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?',
]

vectorizer = TfidfVectorizer()
X = vectorizer.fit_transform(corpus)
print(vectorizer.get_feature_names())
print(X.toarray())

# output ####################################
# ['and', 'document', 'first', 'is', 'one', 'second', 'the', 'third', 'this']
# [[0.         0.46979139 0.58028582 0.38408524 0.         0.          0.38408524 0.         0.38408524]
#  [0.         0.6876236   0.         0.28108867 0.         0.53864762         0.28108867 0.         0.28108867]
#  [0.51184851 0.         0.         0.26710379 0.51184851 0.         0.26710379 0.51184851 0.26710379]
#  [0.         0.46979139 0.58028582 0.38408524 0.         0.         0.38408524 0.         0.38408524]]
```

# おわりに
scikit-learnでは日本語対応が必要ですが、TF-IDFは簡単なので自分で計算することもできます。[すぐに使える! 業務で実践できる! Pythonによる AI・機械学習・深層学習アプリのつくり方](https://www.amazon.co.jp/%E3%81%99%E3%81%90%E3%81%AB%E4%BD%BF%E3%81%88%E3%82%8B-%E6%A5%AD%E5%8B%99%E3%81%A7%E5%AE%9F%E8%B7%B5%E3%81%A7%E3%81%8D%E3%82%8B-Python%E3%81%AB%E3%82%88%E3%82%8B-AI%E3%83%BB%E6%A9%9F%E6%A2%B0%E5%AD%A6%E7%BF%92%E3%83%BB%E6%B7%B1%E5%B1%A4%E5%AD%A6%E7%BF%92%E3%82%A2%E3%83%97%E3%83%AA%E3%81%AE%E3%81%A4%E3%81%8F%E3%82%8A%E6%96%B9-%E3%82%AF%E3%82%B8%E3%83%A9%E9%A3%9B%E8%A1%8C%E6%9C%BA/dp/4802611641)ではMeCabを使用してTF-IDFを計算するモジュールを作成しており、こちらも参考になります。