Shingoの数学ノート

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

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

日付:    カテゴリ: 自然言語処理


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


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

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

文章 リンゴ 食べる バナナ 買う
私はリンゴを食べる。 1 1 1 0 0
私はバナナを食べる。 1 0 1 1 0
私はリンゴを買う。 1 1 0 0 1

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


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

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


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

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

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

文章 リンゴ 食べる バナナ 買う
私はリンゴを食べる。 0.33 0.53 0.53 0 0
私はバナナを食べる。 0.33 0 0.53 0.86 0
私はリンゴを買う。 0.33 0.53 0 0 0.86

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

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

bag-of-wordsの実装

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

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

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の実行結果はこんな感じ。
parse_doc=parse("私はリンゴを食べる。")
parse_doc
結果
[['私', '名詞', '代名詞', '一般', '*', '*', '*', '私'],
 ['は', '助詞', '係助詞', '*', '*', '*', '*', 'は'],
 ['リンゴ', '名詞', '一般', '*', '*', '*', '*', 'リンゴ'],
 ['を', '助詞', '格助詞', '一般', '*', '*', '*', 'を'],
 ['食べる', '動詞', '自立', '*', '*', '一段', '基本形', '食べる'],
 ['。', '記号', '句点', '*', '*', '*', '*', '。']]

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

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

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

  1. 辞書を作成し、bowを計算し、コーパスを作成
  2. dictionary = gensim.corpora.Dictionary(docs)
    corpus = [dictionary.doc2bow(doc) for doc in docs]
  3. コーパスを使用してベクトル化を行う。
  4. 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を計算し、コーパスを作成
  2. # 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]
  3. コーパスを使用してベクトル化を行う。
  4. 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に関数を適応させるための引数である。

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


Comment Box is loading comments...