Qiitaにもupしています。
wikipediaから引用すると、
tf-idfは文章中に含まれる単語の重要度を評価する手法の1つであり、主に情報検索やトピック分析などの分野で用いられている。
と記載されています。文書中にどの単語が重要かを測定するために
- TF (Term Frequency )
- IDF (Inverse Document Frequenc)
を掛けることでその指標としています。デジタルライブラリの文章レコメンドシステムの83%がtf-idfを使用しているそうです。
ここでは、tf-idfの考え方と実際にコーディングをすることで理解を深めたいと思います。
目次
Term frequency (TF)
Term fequency (単語の出現頻度)は、文字通り文書中に単語がでてくる頻度です。tfだけでも重み付けにより複数の方法が提案されています。最も単純な選択はドキュメント中のタームのカウントです。ドキュメント、タームをそれぞれとします。
- バイナリ
ブール型の頻度「frequencies」です。文章中に単語が存在するなら1にそれ以外なら0です。これは単純なfingerprintに対応します。
\begin{align} \text{tf}(t, d) = 1 \quad \text{if t occurs in d else 0 } \end{align}
- カウント (raw count)
文章中に単語が存在したとき単純にカウントしていく方法です。sklearnのCountVectorizer()
を使えば簡単に計算できます。
\begin{align} \text{tf}(t, d) = f_{t, d} \end{align}
- 単語の出現頻度 (term frequency)
これがいわゆるtfです。上記のカウントを文章中のカウントの総和で割り出現頻度を求める方法です。
\begin{align} \text{tf}(t, d) = \frac{f_{t, d}}{\text{number of words in d}} = \frac{f_{t, d}}{\sum_{t' \in d} f_{t', d}} \end{align}
他の方法では以下のようなものが提案されています。
- 対数正規化 (log normalization)
\begin{align} \text{tf}(t, d) = \log{(1 + f_{t, d})} \end{align}
- 二重K正規化 (double normalization K)
K = 0.5とした場合が使用されるようです。
\begin{align} \text{tf}(t, d) = K + (1 - K) \cdot \frac{f_{t,d}}{\max\{f_{t', d} :t' \in d\} } \end{align}
Inverse document frequency (IDF)
inverse document frequency (逆文書頻度)は、単語が与える情報がどれくらいを測る指標です。よく出てくるもの値は下がり、レアなものに値が大きくなるように重みづけされます。一般語フィルタとして働きます。
\begin{align} \text{idf}(t, D) = \log \frac{N}{|\{ d \in D:t \in d\}|} \end{align}
ここで、Nはコーパス中の文書数です。分母はdf (document frequency)であり、文書全体の単語の出現頻度です(tfは文章中の出現頻度)。
- IDF
良く紹介されるものは次の式
\begin{align} \text{idf}(t, D) = \log \frac{N}{df_{t}} \end{align}
です。
- smooth IDF
sklearnのTfidfVectorizer
ではデフォルトでsmooth_idf = True
と平滑化しており、
\begin{align} \text{idf}(t, D) = \log (\frac{1 + N}{1+ df_t}) + 1 \end{align}
となっています。他の方法として
- idf-max
\begin{align} \text{idf}(t, D) = \log (\frac{\max_{t' \in d} df_t'}{1+ df_t}) \end{align}
- probabilistic idf
\begin{align} \text{idf}(t, D) = \log \frac{N - df_t}{df_t} \end{align}
があります。ドキュメントの頻度が小さいときはほとんどIDFの値は変わりませんが、頻度が大きくなるとIDFの値はsmooth IDF > IDF > proba. IDF の順になります。proba. IDFだとdf = 50からIDFの値は負になります。
TF-IDF
上記の二つの指標を掛け合わせたものです。
\begin{align}
\text{tfidf} (t, d, D) = \text{tf} (t,d) \cdot \text{idf}(t, D)
\end{align}
複数の組み合わせがあります。document term weight
とquery term weight
があります。
手法 | dtw | qtw |
---|---|---|
1 | ||
2 | ||
3 |
コード
例文としてsklearnにある文章を用います。
corpus = [ 'This is the first document.', 'This document is the second document.', 'And this is the third one.', 'Is this the first document?']
sklearnを使う場合
こちらは非常に簡単で3行でtfidfの計算ができます。
from sklearn.feature_extraction.text import TfidfVectorizer tfidf = TfidfVectorizer() x = tfidf.fit_transform(corpus)
tfidfの結果をみるために、データフレームに変換します。
import pandas as pd df_tfidf = pd.DataFrame(x.toarray(), columns=tfidf.get_feature_names()) print(df_tfidf)
自作した場合
sklearnのものと同じになるか試してみます。corpus内の単語を数えるためにCountVectorizer
を用います。
from sklearn.feature_extraction.text import CountVectorizer from sklearn.preprocessing import normalize import numpy as np smooth_idf = True norm_idf = True wc = CountVectorizer() x = wc.fit_transform(corpus) wcX = np.array(x.toarray()) # term frequency: N = wcX.shape[0] tf = np.array([wcX[i, :] / np.sum(wcX, axis=1)[i] for i in range(N)]) # inverse documents frequency df = np.count_nonzero(wcX, axis=0) idf = np.log((1 + N) / (1 + df)) + 1 if smooth_idf else np.log( N / df ) # normalize tfidf = normalize(tf*idf) if norm_idf else tf*idf tfidf = pd.DataFrame(tfidf, columns=wc.get_feature_names())
※ 正規化しないと同じ結果になりません。
print(tfidf) and document first ... the third this 0 0.000000 0.469791 0.580286 ... 0.384085 0.000000 0.384085 1 0.000000 0.687624 0.000000 ... 0.281089 0.000000 0.281089 2 0.511849 0.000000 0.000000 ... 0.267104 0.511849 0.267104 3 0.000000 0.469791 0.580286 ... 0.384085 0.000000 0.384085
CountVectorizerも使わない場合
本来はscipy.sparse.csr_matrix
を使って処理すべきです。
import re from collections import defaultdict documents = [re.sub('[.|?]', '', i.lower()) for i in corpus] documents = [doc.split(' ') for doc in documents] vocab = defaultdict() vocab.default_factory = vocab.__len__ for doc in documents: feature_counter = {} for feature in doc: feature_idx = vocab[feature] if feature_idx not in feature_counter: feature_counter[feature_idx] = 1 else: feature_counter[feature_idx] += 1 sorted_feature = sorted(vocab.items()) for new_val, term in enumerate(sorted_feature): vocab[term] = new_val X = np.zeros(shape=(len(corpus), len(sorted_feature)), dtype=int) for idx, doc in enumerate(documents): for word in doc: if word in vocab.keys(): X[idx, vocab[word]] += 1
これでCountVectorizer()
と同じ結果になります。
最後に
tf-idfの計算だけならsklearnを用いれば非常に簡単に行えることがわかります。テキストの前処理はnltk
のWordNetLemmatizer
がありこちらも必須です。また特徴量を作るのにナイーブベイズも良く用いられますね。
前処理としては、tf-idfは使っていきたいとも思います。目標は化学言語のSMILESを生成させることですので、tf-idfは用途が違うかもしれません。SMILESでは文章のつながりが重要ですので、文章中の単語を予測するために用いられるword2vec
やBERT
などもこちらの方が良さそうな気がしています。こちらと生成モデルのVAE (Seq2Seq)も試していきたいところです。