グラフ機械学習と強化学習について

主にグラフ機械学習や強化学習手法を記載します。

tf-idfを実装する

Qiitaにもupしています。

qiita.com

wikipediaから引用すると、

tf-idfは文章中に含まれる単語の重要度を評価する手法の1つであり、主に情報検索やトピック分析などの分野で用いられている。

と記載されています。文書中にどの単語が重要かを測定するために

  • TF (Term Frequency )
  • IDF (Inverse Document Frequenc)

を掛けることでその指標としています。デジタルライブラリの文章レコメンドシステムの83%がtf-idfを使用しているそうです。

ここでは、tf-idfの考え方と実際にコーディングをすることで理解を深めたいと思います。

目次

Term frequency (TF)

Term fequency (単語の出現頻度)は、文字通り文書中に単語がでてくる頻度です。tfだけでも重み付けにより複数の方法が提案されています。最も単純な選択はドキュメント中のタームのカウントです。ドキュメント、タームをそれぞれ d, tとします。

  • バイナリ

ブール型の頻度「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 weightquery term weightがあります。

手法 dtw qtw
1  f_{t, d} \cdot \log \frac{N}{df_t}  \left(0.5 + 0.5 \cdot \frac{f_{t,d}}{\max\{f_{t', d} :t' \in d\} } \right) \cdot  \log \frac{N}{df_t}
2  1 + \log f_{t, d}  \log (1 + \frac{N}{df_t})
3  (1 + \log f_{t, d}) \cdot \log \frac{N}{df_t}  (1 + \log f_{t, d}) \cdot \log \frac{N}{df_t}

コード

例文として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を用いれば非常に簡単に行えることがわかります。テキストの前処理はnltkWordNetLemmatizerがありこちらも必須です。また特徴量を作るのにナイーブベイズも良く用いられますね。

前処理としては、tf-idfは使っていきたいとも思います。目標は化学言語のSMILESを生成させることですので、tf-idfは用途が違うかもしれません。SMILESでは文章のつながりが重要ですので、文章中の単語を予測するために用いられるword2vecBERTなどもこちらの方が良さそうな気がしています。こちらと生成モデルのVAE (Seq2Seq)も試していきたいところです。

参考文献