いっきのblog

技術とか色々

TF-IDFの理論をざっくり理解する①

最近、自社のプロダクトで自然言語処理がよく使われるようになってきたので、勉強も兼ねてまとめてみる。

TF-IDFとは?

tf-idfは、文書中に含まれる単語の重要度を評価する手法の1つであり、主に情報検索やトピック分析などの分野で用いられている。 tf-idfは、tf(英: Term Frequency、単語の出現頻度)とidf(英: Inverse Document Frequency、逆文書頻度)の二つの指標に基づいて計算される。

tf-idf - Wikipedia

Wikipediaだと少し難しい言葉でかいてあるので噛み砕いていくと、ある人のブログから重要度の高い言葉(特徴量)を抽出したいとする。
tfはある人のブログ内の言葉を分解し「頻度」を計算し、
idfはある人のブログ内の言葉がWikipediaの全ての言葉と比べた中での「希少性」を計算する。

どこの言葉と比較してidf(希少性)を計算するかは重要で、本来は他人を含めた全てのブログの言葉を使ったほうがいいと考えられるけど、今回はテストするので取れるデータということでWikipediaにしてある。
図にすると、

f:id:kzkohashi:20180721143506p:plain

こんな感じ・・・なのかな。今はこれくらいの理解にしておこう。
論よりなんたらっていうので、早速Pythonで試してみる。

下準備

自分のブログを元にやってみる。下準備が多めなので別の記事に書いた。

kzkohashi.hatenablog.com

kzkohashi.hatenablog.com

今回は、tf値を測るのを過去に書いた自分のブログをまずは分かち書きにする。

kzkohashi.hatenablog.com

mecab -d /usr/local/mecab/lib/mecab/dic/mecab-ipadic-neologd/ -Owakati blog.txt -o blog_wakati_neo.txt -b 16384

別記事にある wiki_wakati_neo.txtを使おうとしたが2000万行あって処理が遅すぎるのでとりあえず適当に100万行くらいにしておく。

head -n 1000000 wiki_wakati_neo.txt > wiki_wakati_neo_head.txt

wiki_wakati_neo_headと先ほど作った blog_wakati_neo.txtを使って色々計算してみる。

ブログのTF値を計算

Pythonもよくわからないのとコードは汚いのでそこはお気にせずに。。
TF値を計算するには、 sklearnにある CountVectorizerを使えばできるぽい

from sklearn.feature_extraction.text import CountVectorizer
from sklearn.decomposition import TruncatedSVD
from sklearn.preprocessing import Normalizer
import numpy as np
import logging

logging.basicConfig(format='%(asctime)s : %(levelname)s : %(message)s', level=logging.INFO)


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

CountVectorizerはデフォルトで一文字の物を消してしまうため、その対策でtoken_patten設定すると良いみたい。

bag_of_words = count_vectorizer.fit_transform(['../data/blog_wakati_neo.txt'])

fit_transformはBag of Words(単語の袋)と言われる、ある文章における単語の出現回数を表したものに変換する。
以下のように、すすも1回、も2回、もも2回...のように数えてくれる。

[~]mecab
すもももももももものうち
すもも  名詞,一般,*,*,*,*,すもも,スモモ,スモモ
も      助詞,係助詞,*,*,*,*,も,モ,モ
もも    名詞,一般,*,*,*,*,もも,モモ,モモ
も      助詞,係助詞,*,*,*,*,も,モ,モ
もも    名詞,一般,*,*,*,*,もも,モモ,モモ
の      助詞,連体化,*,*,*,*,の,ノ,ノ
うち    名詞,非自立,副詞可能,*,*,*,うち,ウチ,ウチ
EOS

あとはソートして頻度順にだしたいなーと思って調べてみたらぴったりやりたいことをやってくれてる方がいたのでそちらのを真似させていただく。

medium.com

sum_words = tf.sum(axis=0)
words_freq = [(word, sum_words[0, idx]) for word, idx in count_vectorizer.vocabulary_.items()]
words_freq =sorted(words_freq, key = lambda x: x[1], reverse=True)
words_freq

結果以下になるが、いろんな記事に書かれてたけど単語分割する前にノイズを除去しなきゃいけないと思わせるいい例になってしまった。
助詞などが多くなるのは当たり前か・・。名詞だけにしたいが、idfにおける希少性を利用するとこう行った一般用語は消えると思うので、このまま続けて見る。

[('た', 32),
 ('の', 27),
 ('に', 26),
 ('を', 19),
 ('と', 15),
 ('し', 13),
 ('て', 12),
 ('ない', 10),
 ('から', 10),
 ('な', 10),
 ('も', 8),
 ('こと', 8),
 ('サービス', 8),
 ('が', 6),
 ('は', 6),
省略

------追記------
よくよく計算式を確認したら、単語数数えるだけなのはTF値ではないので、単語ごとにカウントした値を文章全体の単語数で割る必要がある。
たとえば、文章全体の単語数が560で、「た」の場合、
32/560 = 0.05714285714
となる。
-------追記終わり----

ただ、以下のようにCountVectorizerを呼び出す際に低すぎる・高すぎる頻度の単語は除去できるっぽい。
全文章における出現割合なので、小数点になるのかな?

count_vectorizer = CountVectorizer(min_df=0.24, max_df=0.85, input='filename', token_pattern=u'(?u)\\b\\w+\\b')

TF-IDFの計算

TF-IDFの計算はTfidfVectorizerを使えば良い。

vectorizer = TfidfVectorizer(input='filename', token_pattern=u'(?u)\\b\\w+\\b', use_idf=True)
features = vectorizer.fit_transform(['../data/blog_wakati_neo.txt','../data/wiki_wakati_neo_head.txt'])
terms = vectorizer.get_feature_names()
tfidfs = features.toarray()
tfidfs

結果は以下のようにそれっぽさそうな数値になった。

array([[0.00000000e+00, 0.00000000e+00, 0.00000000e+00, ...,
        0.00000000e+00, 0.00000000e+00, 0.00000000e+00],
       [7.10477245e-03, 1.31955447e-03, 2.44020240e-03, ...,
        5.92487576e-06, 1.69282165e-06, 1.69282165e-06]])

実際に上位の10単語を抽出して見る。

def extract_feature_words(terms, tfidfs, i, n):
    tfidf_array = tfidfs[i]
    top_n_idx = tfidf_array.argsort()[-n:][::-1]
    words = [terms[idx] for idx in top_n_idx]
    return words

for x in  extract_feature_words(terms, tfidfs, 0, 10):
        print(x,)

結果はTF値と一緒になってしまった。。。fit_transformあたりでミスってしまったのだろうか・・。長くなりそうなので次回に持ち越し。

た
の
に
を
と
し
て
から
な
ない

終わりに

歯切れは若干わるかったが、理論を理解するという意味だと理解はできたかなと思う。
このままだと悔しいので、次回はTF-IDFの正確な値にチャレンジして見る。