Shingoの数学ノート

プログラミングと機械学習のメモ

Pythonで文章の近さを計算しよう2(bag-of-words/TF-IDF)

Pythonで文章の近さを計算しよう1の続き。今回は文章のベクトル化を扱う。

文章のベクトル化方法 bag-of-words

文章のベクトル化方法として一番簡単なのは、単語の頻度をそのままベクトル化することだ。次のように書くことができる。

文章リンゴ食べるバナナ買う
私はリンゴを食べる。11100
私はバナナを食べる。10110
私はリンゴを買う。11001

このようにすることで、文章を数値化することができる。この方法を「bag-of-words」という。

文章のベクトル化方法 TF-IDF

bag-of-wordsはシンプルでわかりやすいが、欠点も存在する。 そのひとつに、文章の特徴をうまく捉えられない場合があることが挙げられる。上記の例では「私」と「リンゴ」の重みは同じであるが、「私」は全ての文章で出てきているので「リンゴ」より文章の特徴としては弱い。そこで、単語の頻度に全文章中の単語の出現頻度(つまりレア度)を考慮したTF-IDFを紹介する。TF-IDF値の算出式は以下の通りである。(様々なサイトを見たが、TF-IDFの式は細かいところで表記揺れがよくある。大まかなところは共通しているので、参考程度に見ていただきたい。)

TFIDF(t,d)=TF(t,d)×IDF(t) TFIDF(t,d)=TF(t,d) \times IDF(t)

ただし、

TF(t,d)=文章dの中の単語tの出現数文書dの単語総数IDF(t)=log2(全文書数単語tが出現した文書数)+1 TF(t,d)=\frac{文章dの中の単語tの出現数}{文書dの単語総数}\\ IDF(t)=\log_2\left(\frac{全文書数}{単語tが出現した文書数}\right)+1

簡単に言えば、TFは1文章中の単語の頻度、IDFは全文章中、単語が含まれる文書の頻度の逆数(にlogをとって1を足したもの)である。

TF-IDFを計算した結果を以下の表にまとめた。

文章リンゴ食べるバナナ買う
私はリンゴを食べる。0.330.530.5300
私はバナナを食べる。0.3300.530.860
私はリンゴを買う。0.330.53000.86

文章全てに出てきている「私」より、「リンゴ」、「バナナ」のほうが値が高くなっている。

TF-IDFを使用すれば、他の文章では出てこない特徴のある単語の値を大きくすることができる。

bag-of-wordsの実装

実際にpythonでbag-of-wordsとTF-IDFの実装を試みる。使うモジュールはMeCabとgensim(とpandas)である。

まずは、形態素解析を実施して、使用する単語を抽出する。最初に必要なモジュールと関数を定義する。

python
import MeCab
import pandas as pd
import gensim

def parse(tweet):
    t_list=[]
    t=MeCab.Tagger()
    temp1=t.parse(tweet)
    temp2=temp1.split("\n")
    for word in temp2:
        if word not in ["EOS",""]: 
            word_sp=word.split("\t")
            word_sp=word_sp[:1]+word_sp[1].split(",")[:7]
            t_list.append(word_sp)
    return t_list

parseの実行結果はこんな感じ。

python
parse_doc=parse("私はリンゴを食べる。") parse_doc

結果

text
[['私', '名詞', '代名詞', '一般', '*', '*', '*', '私'], ['は', '助詞', '係助詞', '*', '*', '*', '*', 'は'], ['リンゴ', '名詞', '一般', '*', '*', '*', '*', 'リンゴ'], ['を', '助詞', '格助詞', '一般', '*', '*', '*', 'を'], ['食べる', '動詞', '自立', '*', '*', '一段', '基本形', '食べる'], ['。', '記号', '句点', '*', '*', '*', '*', '。']]

品詞情報までリストに格納している。これを使用して、テキストから名詞、動詞、形容詞を抽出する。

python
texts=["私はリンゴを食べる。", "私はバナナを食べる。", "私はリンゴを買う。"]
docs=[[w[7]
for w in parse(text)
if w[1] in ["名詞","動詞","形容詞"]]
for text in
texts] docs

結果

text
[['私', 'リンゴ', '食べる'], ['私', 'バナナ', '食べる'], ['私', 'リンゴ', '買う']]

次に、gensimを使用してbowを計算する。gensimを使用する際の基本的な流れは、以下の通りである。

  1. 辞書を作成し、bowを計算し、コーパスを作成
python
dictionary = gensim.corpora.Dictionary(docs)
corpus = [dictionary.doc2bow(doc) for doc in docs]
  1. コーパスを使用してベクトル化を行う。
python
vector_bow=gensim.matutils.corpus2dense(corpus, num_terms=len(dictionary)).T # これを入れないとdictionary.id2tokenが生成されない。 dictionary[0]
word=[dictionary.id2token[i]


for i in range(len(dictionary))] df_bow=
pd.DataFrame(vector_bow,columns=word)[["私","リンゴ","食べる","バナナ","買う"]] df_bow

結果

bow1

gensimを使用すれば、簡単にbowを使用することができる。

TF-IDFの実装

gensimを使用すると、TF-IDFも簡単に実装できる。しかし、上記で示したTF-IDFの式とは少し違うので、合わせるには少しカスタマイズする必要がある。

  1. TF-IDFを計算し、コーパスを作成
python
# gensimではTF=単語頻度より、文章中の全単語数を割る必要がある。
def wlocal(tf):
    return tf/tf.sum() # gensimではIDFに1を足していないので、「add=1」を加える。
    def new_df2idf(docfreq, totaldocs):
        return gensim.models.tfidfmodel.df2idf(docfreq, totaldocs,add=1) tfidf = gensim.models.TfidfModel(corpus,normalize = False,wlocal=wlocal,wglobal=new_df2idf)
        corpus_tfidf = tfidf[corpus]
  1. コーパスを使用してベクトル化を行う。
python
word=[dictionary.id2token[i]


for i in range(len(dictionary))] vector_tfidf=gensim.matutils.corpus2dense(corpus_tfidf, num_terms=len(dictionary)).T df_tfidf=
pd.DataFrame(vector_tfidf,columns=word)[["私","リンゴ","食べる","バナナ","買う"]] df_tfidf

結果

bow2

ちなみに、gensim.models.TfidfModelの引数のwlocalはTFに、wglobalはIDFに関数を適応させるための引数である。

次回はこのベクトルを使用して文章の類似度を測ってみる。

Comments

Loading comments...