TF-IDFを自力実装してみる
前回ライブラリーを使って実装しようとして断念した「TF-IDF」について自力で実装したいと思う。
ちなみに上記の記事は、TF値の出し方が間違ってて理解不足だった・・。 一度実装するとしないでは理解度が変わるという教訓。。いや計算式の理解の問題か・・。(修正済み)
TF値を求める
まずは数式の確認からすると、以下のようになる。
:文書における、単語のTF値
:単語の文書における出現回数
:文書におけるすべての単語の出現回数の和
まずは、前回同様に単語ごとの出現回数()をもとめると以下のようになる。
from sklearn.feature_extraction.text import CountVectorizer import numpy as np import logging logging.basicConfig(format='%(asctime)s : %(levelname)s : %(message)s', level=logging.INFO) count_vectorizer = CountVectorizer(min_df=1, max_df=100, input='filename', token_pattern=u'(?u)\\b\\w+\\b') bag_of_words = count_vectorizer.fit_transform(['../data/blog_wakati_neo.txt']) sum_words = bag_of_words.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)
[('た', 32), ('の', 27), ('に', 26), ('を', 19), ('と', 15), ('し', 13), ('て', 12), ('ない', 10), ('から', 10), ('な', 10), ('も', 8), ('こと', 8), ('サービス', 8), ('が', 6), ('は', 6), 省略
続いて全ての単語数()を求めると
# 扱いやすいディクショナリーに変換 dict_words_freq = dict(words_freq) all_words_count = sum(dict_words_freq.values())
あとは「単語ごとの出現回数 / 全ての単語数」を行えば良いので以下のようになる
# TF値の計算 dict_words_freq = dict(words_freq) all_words_count = sum(dict_words_freq.values()) for key in dict_words_freq: print(key, dict_words_freq[key], dict_words_freq[key]/all_words_count)
無事できた。
た 32 0.05714285714285714 の 27 0.048214285714285716 に 26 0.04642857142857143 を 19 0.033928571428571426 と 15 0.026785714285714284 し 13 0.023214285714285715 て 12 0.02142857142857143 ない 10 0.017857142857142856 から 10 0.017857142857142856 な 10 0.017857142857142856 も 8 0.014285714285714285 こと 8 0.014285714285714285 サービス 8 0.014285714285714285 が 6 0.010714285714285714 は 6 0.010714285714285714 省略
IDF値を求める
まずは数式の確認からすると、以下のようになる。
:単語におけるIDF値
:すべての文章数
:において、単語が含まれる文章数
元となる文章は以前使った、Wikipediaのデータを元にする。
ただし、量が多くて重いため(2400万行)、100万行ほどのデータのみを使うこととする。
まずは、100万行のみのデータにする。
head -n 1000000 wiki_wakati_neo.txt > wiki_wakati_neo_head.txt
このままだと全ての文章が同じファイルにあるので、文章ごとに分割する。
「<\/ doc >」が区切りっぽいのでそれで分割する。
f = open('../data/wiki_wakati_neo_head.txt') wiki_texts = f.read().split('</ doc >') print(wiki_texts[0]) f.close()
< doc id =" 3216890 " url =" https :// ja . wikipedia . org / wiki ? curid = 3216890 " title =" 2015年アムトラック脱線事故 "> 2015年アムトラック脱線事故 ...省略
次の処理で使うので、リストの中の文字をユニークのリストに返してつかう関数を用意。
def unique(list1): unique_list = [] for x in list1: if x not in unique_list: unique_list.append(x) return unique_list
IDF値を求める。
from math import log # 初期化する # ブログの文章を含めると最低出現数は1となるため idf = {} for key in dict_words_freq: idf[key] = 1 # wikipediaが文章単位でわかれてるので # 文章ごとにブログ内の単語が存在するかカウントする for wiki_text in wiki_texts: # 同一文章内で複数のカウントをさせないため word_list = unique(wiki_text.split(' ')) for key in dict_words_freq: if key in word_list: # 文章内にブログの単語があった場合 if key in idf: idf[key] += 1 # idf[key]はただのカウントのため # 上記の計算式通りに計算し直す for key in idf: idf[key] = log(len(wiki_texts)/idf[key]) + 1 print(idf[key])
た 1.3175143932942954 の 1.0777938305785737 に 1.2131304792181579 を 1.3313370482515308 と 1.4096924908938504 し 1.4009755108032362 て 1.450652175643083 ない 2.5456519664401 から 1.7110617213423613 な 2.1554299751676784 も 1.8192044116505661 こと 2.1342823075479105 サービス 5.17021502327521 が 1.4339912023116144 は 1.0493246452432414 だ 2.732840227719004 けど 6.566265559340465 自分 4.343764024527529
自分のブログ内の文書をみると、「た」などの助動詞より、「サービス」や「自分」などよく使われてなさそうな値が高いことがわかる。(希少性が高い)
TF-IDF値を求める
まずは、先ほど求めたTF値を格納しておく
tf = {} for key in dict_words_freq: tf[key] = float(dict_words_freq[key]/all_words_count)
あとは掛け算するだけなので
tfidf = {} for key in dict_words_freq: tfidf[key] = tf[key] * idf[key]
た: 0.07528653675967402 サービス: 0.07386021461821728 色々: 0.06481618517768223 けど: 0.05862737106553986 に: 0.056323915106557335 もっと: 0.05489118978308473 の: 0.05196505968860981 なぁ: 0.050354351268196505 エンジニア: 0.0496422258575285 ない: 0.045458070829287496 を: 0.045170364137105505 視点: 0.04516931372977967 すごく: 0.043155772268012066 思う: 0.043005388411332744 振り返り: 0.04292777433362566 セールス: 0.040755639825903356 自分: 0.038783607361852934 やら: 0.03853413038011895 ....
あれ、、想定では、助動詞とかはよく出るからもっと低いはずが・・・100万行じゃちょっと少なかったのかもしれない。
名詞だけに絞ると
import MeCab m = MeCab.Tagger('-d /usr/local/mecab/lib/mecab/dic/mecab-ipadic-neologd/') feature_vector = {} for k, v in sorted(tfidf.items(), key=lambda x: -x[1]): node= m.parseToNode(k) while node: meta = node.feature.split(",") if meta[0] == ("名詞"): print(str(k) + ": " + str(v)) node = node.next
サービス: 0.07386021461821728 エンジニア: 0.0496422258575285 視点: 0.04516931372977967 セールス: 0.040755639825903356 自分: 0.038783607361852934 チーム作り: 0.037493182828802825 やめ: 0.03584693947657752 コミュニティ: 0.03359602203641041 たい: 0.031145458128418263 プロダクト: 0.03067338816850126 こと: 0.030489747250684436 業種: 0.029452854976636772 作り: 0.029273521205558244 ジム: 0.025032815107084036 顧客: 0.02418713487438621
綺麗になった。
今回調べたブログ内の言葉では、「サービス」がぶっちぎりで重要な言葉として抽出されていることがわかる。
また「エンジニア」や「視点」、「セールス」など自分が考えてそうな言葉が出てくるので納得感はある。
IDF値は「+1」の部分でチューニングができるっぽいので、もう少しやってみたい感はあるが次回の理解した時にする。
終わりに
ライブラリに頼らず(一部頼ったけど)、自前で実装するのは勉強になったし面白かった。
文章内の言葉から重要なキーワードを抽出するTF-IDF値は色々なところで使えそうだし、うちのサービスでも次使う予定なので次は実践しながら深掘りしていきたい。