sklearnを使ってtf-idfの勉強した

sklearnを使ってtf-idfの勉強をしたのでその内容をまとめておく。

tf-idf

tf-idfを説明するために、まずtf(term frequency)とidf(inverse document frequency)について説明します。

tfは文書 d_jに単語 t_iが出現する頻度のことです。次の式で表されます。


\displaystyle tf(t_i,d_j)=\frac{文書d_j内での単語t_iの出現回数}{文書d_j内での全単語の出現回数}=\frac{f(t_i,d_j)}{\sum_{k} f(t_k,d_j)}

tfはある単語の出現回数が多いほど大きい値となり、その単語の重要度は大きくなります。

一方で、idfは単語 t_iが全文書 N中いくつの文書に含まれるかを示します。次の式で表されます。


\displaystyle idf(t_i)=\log{}\left(\frac{全文書数N}{単語t_iが出現する文書数}\right)=\log{}\left(\frac{N}{df(t_i)}\right)

しかし、文書中に存在しない単語のidfを計算しようとしたときに、分母が0になり、0除算が発生してしまうことから、分母に1を加えて計算することがあります。 また、idfが0になるのを避けるために、全体に1を加えることもあります。 idfはある単語が他の文書に出現しているほど値は小さくなり、その単語の重要度は小さくなります。

tf-idfは文書集合中に含まれる単語の重要度をスコアリングするために利用され、 tf \times idfで計算されます。 そのため、tf-idfは、ある文書中で頻繁に出現する(tfが示す)が他の文書ではあまり出現しない(idfが示す)単語が重要度が高い単語としてスコアリングされます。

sklearn

sklearnpython機械学習ライブラリでオープンソースとして公開されています。sklearnには、サポートベクターマシンやランダムフォレストなどの様々な機械学習の手法が実装されており、その中にtf-idfも実装されています。 今回はこのsklearnを使ってtf-idfの計算を行いました。また、日本語の文章にtf-idfを適用する場合、予め形態素解析を行っておく必要があり、形態素解析にはMeCabを利用しました。

tfを計算する

次のような文章のtfを求める。この文章では、1文を1文書とみなしています。

今日はとんこつラーメンが食べたい気分だな。
でも味噌ラーメンもいいなあ。
味噌最高。

この文章をMeCabを使って形態素解析を行い、sklearnでtfを計算します。 次にサンプルコード示します。

import MeCab
import codecs
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.feature_extraction.text import TfidfTransformer

tagger = MeCab.Tagger('-Owakati')

with codecs.open("tfidf-doc0.txt", "r", "utf-8") as f:
    lines = f.read().splitlines()

corpus = [tagger.parse(line).strip() for line in lines]

vectorizer = CountVectorizer(token_pattern=u'(?u)\\b\\w+\\b')

tf = vectorizer.fit_transform(corpus)

# 分かち書きした単語を表示
print(vectorizer.get_feature_names())
# 単語の出現箇所
print(tf.toarray())
$ python tf.py
['いい', 'が', 'たい', 'だ', 'でも', 'とんこつ', 'な', 'なあ', 'は', 'も', 'ラーメン', '今日', '味噌', '最高', '気分', '食べ']
[[0 1 1 1 0 1 1 0 1 0 1 1 0 0 1 1]
 [1 0 0 0 1 0 0 1 0 1 1 0 1 0 0 0]
 [0 0 0 0 0 0 0 0 0 0 0 0 1 1 0 0]]

出力された行列は、行が何文書目(sample.txtにおける何行目)かを表していて、列がget_feature_names()で表示した単語に対応している。 つまり、どの文書に何の単語が何回現れたか、を行列が表している。

コードの補足

  • CountVectorizer()
    • tfを求めるクラス
    • token_pattern: マッチするトークンを定義。デフォルトでは2文字以上のトークンしか扱わない
    • token_pattern=u'(?u)\\b\\w+\\b'
      • これは2文字のトークンを除外しないような設定
      • 詳細:(?u):unicodeマッチング、\b:単語の先頭末尾の空白文字、\w:unicode一文字、+:直前の文字の繰り返し

tf-idfを求める

次の2つの分かち書きされた文書のtf-idfを計算する。

文書A: 無料 ランチ ラーメン ランチ
文書B: ランチ ラーメン クーポン

この文書は、wakatiというディレクトリ配下にそれぞれファイルを作っているものとする。 wakati配下のファイルを読み込み、文書集合におけるtfidfを単語ごとに計算する。

import os
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.feature_extraction.text import TfidfTransformer

vectorizer = CountVectorizer(input='filename', token_pattern=u'(?u)\\b\\w+\\b')

files = ['wakati/' + path for path in os.listdir('wakati')]

tf_vec = vectorizer.fit_transform(files).toarray()

features = vectorizer.get_feature_names()
print("features:\n{}".format(features))
print("tf:\n{}".format(tf_vec))

tfidf_transformer = TfidfTransformer(norm=None, sublinear_tf=False)

idf = tfidf_transformer.fit(tf_vec)

print("idf:\n{}".format(idf.idf_))

tfidf = tfidf_transformer.fit_transform(tf_vec)

print("tfidf:\n{}".format(tfidf.toarray()))
$ python tfidf.py
features:
['クーポン', 'ランチ', 'ラーメン', '無料']
tf:
[[1 1 1 0]
 [0 2 1 1]]
idf:
[1.40546511  1.         1.         1.40546511]
tfidf:
[[1.40546511  1.         1.         0.        ]
 [0.         2.         1.         1.40546511]]

出力結果から、まずidfが単語ごとに計算されていることが分かります。次にtfとidfのベクトルの積をとった値がtfidfとして計算されていることが分かります。 ここで、tfidfを計算するクラスTfidfTransformerは設定のパラメータによって、tfidfの計算式が変化することに注意したい。今回の場合だとnorm=Noneを渡しているが、これはtfidfを正規化するかどうかの設定です。norm=l2とやると最後に結果を正規化します(デフォルト)。 また、sublinear_tf=Falseはtfの値を対数で計算するかどうかです。Trueの場合はtfは対数で計算されます。 計算式などの詳細はこちらをご覧ください。

コードの補足

  • TfidfTransformer(norm='l2', sublinear_tf=True)
    • tf-idfを計算するクラス
    • sublinear_tf: tfをlogを使って計算するかどうか
    • norm: l2でl2正規化を行う。noneで何もしない。
      • l2正規化とはベクトルを単位ベクトルに変換すること
    • smooth_idf: idfの計算方法が変化する。詳しくはsklearnのソースコードを参照
      • Trueだと文書中で発生しない単語を計算しようとしたときに分母が0になることによる0除算が発生しない

まとめ

今回はtfidfについてsklearnやMeCabを使って勉強しました。 今回用意した文章は短い文章だったので、形態素解析が上手くいくものばかりでしたが、実際の文章に適用すると形態素解析が上手く行かない場合がありそうです。 例えば、Web上の文書の場合、文章中にURLやメールアドレスが含まれていることが考えられます。 そのため、実際の文書に適用するには、形態素解析の前処理について考える必要が出てきそうです。

参考