Gensimを使ったトピック単語抽出 LSAを使ったトピック語抽出まで

以前,ちょろっとだけGensimを使ったことがあったんだけど,久しぶりに本格的に使うことになりそうなので,メモに残しておく
Gensimでのコーパスの作り方 - kensuke-miの日記
gensim 文章をベクトル空間にする方法 - kensuke-miの日記

インストールができてないよぉふぇぇ,って人は解説してくださっている方がいるので,そっちを見ればいいと思う
Python GensimでLDAを使うための前準備・パッケージのインストール - Hive Color

LSAって何だよぉふぇぇ,って人は「文章を与えると,重要っぽい語を返してくれるアルゴリズム」って解釈しておけばいいと思う.
ちなみに,日本語での訳語は「潜在的意味インデキシング」という,機械学習一般ではLSIと呼び,LSAって呼ぶのは自然言語処理分野だけらしい.


と,いうわけで前提がしっかり整理できたところで,実際にさくさく使っていく.

まず,用意するコーパスはトークン化されている状態とする.
今回,ぼくはローマ字化ペルシア語のコーパスを使うので,普通の人は意味不明だと思う.なので,適時,英語の文章を持って来てトークン化しておくといいと思う.

まず,コーパスの状態を確認.corpusは配列で,リストは一文ごとにsentenceというマップを持っている.で,sentenceの中で['after_conv_tokens']がtokenに対応するキー.

    for sentence in corpus:
        tokend_sentence.append(sentence['after_conv_tokens']);
        print sentence['after_conv_tokens']

一応,token状態の文を確認すると,

[u'yky', u'bwd', u'yky', u'nbwd', u',', u'qyr', u'az', u'xda', u'ey\u010d', u'ks', u'nbwd', u'.']
[u'xale', u'pyr', u'zny', u'bwd', u'yk', u'\u0127ya\u0163', u'da\u0161t', u'(w)', u'\u0155d', u'yk', u'qrbyl', u'(1)', u'.']
[u'yk', u'drxt', u'snjd', u'tw', u'ayn', u'\u0127ya\u0163\u0161', u'da\u0161t', u',', u'\u0155d', u'yk', u'\u010dwb', u'kbryt', u'.']

tokend_sentenceは二次元配列になっていて,一次元目に文ごとの配列,二次元目にtokenが格納されている.


ここまでが前処理.
ここから実際にGensimの機能を使っていく.

tokend_sentenceから単語IDへと変換する辞書を構築する.(Gensimのマニュアルでもdictionaryと表現されている)キーが表層単語で,要素がidになっている状態.

    #表層単語をidに変換するdictionaryが構築された状態
    gensim_dictionary=corpora.Dictionary(tokend_sentence);
    gensim_dictionary.save('./tmp/test.dict');

さらに,この辞書を使って,コーパスのid化を行う

    #corpusをid表示に変換する.二次元リストで構築しているので内包表記で記述
    id_corpus=[gensim_dictionary.doc2bow(sentence) for sentence in tokend_sentence];
    #保存しておく 
    corpora.MmCorpus.serialize('./tmp/test.mm', id_corpus);

Gensimのマニュアルでも保存が推奨されていたので,保存しておいた.

ここまできたら,ようやくTF-IDFを適用できる.
流れは,まずTF-IDFモデルを構築し,構築したモデルにid化したコーパスを与えることで,TF-IDFモデルで計算済みのコーパスが返される.

    #tfidfモデルの構築とcorpusの変換
    tfidf_instance=models.TfidfModel(id_corpus);
    tfidf_corpus=tfidf_instance[id_corpus];

なお,tfidf_instanceという名前の通り,これはインスタンスになっている.コーパスを変換するだけでなく,類似度の計算を行うメソッドもあるので,タスクによって使い分けてみるとよいだろう.

さらに,TF-IDFモデルで計算したコーパスからLSAモデルを構築する.

    #lsiモデルの構築とcorpusの変換
    lsi_instance=models.LsiModel(corpus=tfidf_corpus, num_topics=10 id2word=gensim_dictionary);
    lsi_corpus=lsi_instance[tfidf_corpus];

モデルを作成するときに与えている引数のnum_topicsは抽出するトピック数を示している.文章の大きさに見合ったトピック数を指定するとよいだろう.ここでは適当に10とした.(つまり,単語のベクトルの次元を10次元まで落とす.という意味になる)他にもパラメータをいくつか指定できるが,ここでは割愛した.

で,最初の目的あった,トピック語の抽出だが,

    print lsi_instance.print_topic(5)

で確認することができる.ちなみに与えている引数は「何番目のトピックか?」を示している.

実行結果は以下の通り

0.296*"?" + 0.273*"bzn" + 0.273*"ndydy" + 0.249*"zn" + -0.234*"mn" + -0.232*"xwb" + 0.214*"kdwql" + -0.203*"bxwrm" + 0.187*"pyr" + 0.160*"pyre"

見方は,+だとそのトピックに強く関係している語という意味になる.で,値が大きい程,関連度が強い.なので,"?", "bzn", "ndydy", "zn", "kdwql", "pyr", "pyre"が5番目のトピックに大きく関係している語である.ペルシア語がわからない人には「何のこっちゃだろうが」そんなものだと思ってもらいたい.
同じ結果をきちんと日本語で実行されている方がいらっしゃるので,その記事を見ると,理解できると思う.
LSIやLDAを手軽に試せるGensimを使った自然言語処理入門 - SELECT * FROM life;

逆にマイナスはほとんど関係がない.と解釈して良い.

きちんと説明すると,この結果は5番目のベクトルを表示していて,示されている語は,5番目のベクトルを構成している(寄与している)語.数字は5番目のベクトルへの寄与率である.マイナス表示は負の相関を示している.

さらに,「0番目からi番目のトピック」をまとめて得る場合には

    for i in  lsi_instance.print_topics(2):
        print i

で良い.

-0.392*"gft" + -0.364*"ndydm" + -0.341*"āmd" + -0.307*"w" + -0.291*"alle" + -0.215*"ql" + -0.169*"be" + -0.150*"," + -0.134*"walle" + -0.122*"mn"
-0.497*"ndydm" + -0.395*"alle" + 0.386*"āmd" + -0.329*"gft" + 0.264*"ql" + 0.214*"w" + -0.187*"walle" + 0.112*"bde" + 0.112*"rsyd" + 0.105*"āŕa"

語を抽出するときに一番大切なのはstop wordの管理.
というのも,TF-IDFは頻度がすべてのモデルなので,頻度が高い語はそれだけ重要とみなされてしまう(いや,それは正しいんだけど)けど,英語の冠詞のtheは頻度が高いけど,重要語だろうか?いや,絶対そんなことない.

と,いうことを避けるために,事前に頻度が高くて意味がない語は省いておく必要がある.



最後に,一応コードを最初から最後までさらしてく

#! /usr/bin/python
# -*- coding:utf-8 -*-

__author__='Kensuke Mitsuzawa';
__date__='2013/10/22';

import codecs, sys, json;
from gensim import corpora, models, similarities;
#import logging
#logging.basicConfig(format='%(asctime)s : %(levelname)s : %(message)s', level=logging.INFO)

def corpus2id_format(corpus_path):
    tokend_sentence=[];
    #本来はjsonファイルで保管されているので,ここでload
    with codecs.open(corpus_path, 'r', 'utf-8') as f: 
        corpus=json.load(f);

    #corpusからtokenizeされた文(型:リスト)を読み込んで,tokend_sentenceに追加
    #tokend_sentenceを二次元リストにする
    for sentence in corpus:
        tokend_sentence.append(sentence['after_conv_tokens']);

    #表層単語をidに変換するdictionaryが構築された状態
    gensim_dictionary=corpora.Dictionary(tokend_sentence);
    gensim_dictionary.save('./tmp/test.dict');
    #corpusをid表示に変換する.二次元リストで構築しているので内包表記で記述
    id_corpus=[gensim_dictionary.doc2bow(sentence) for sentence in tokend_sentence];
    #保存しておく 
    corpora.MmCorpus.serialize('./tmp/test.mm', id_corpus);

    return id_corpus, gensim_dictionary;

def main():
    corpora_path='./corpus_dir/json_after_conv_table/e1998t0001.xml';
    id_corpus, gensim_dictionary=corpus2id_format(corpora_path);

    #tfidfモデルの構築とcorpusの変換
    tfidf_instance=models.TfidfModel(id_corpus);
    tfidf_corpus=tfidf_instance[id_corpus];

    #lsiモデルの構築とcorpusの変換
    lsi_instance=models.LsiModel(corpus=tfidf_corpus, num_topics=2, id2word=gensim_dictionary);
    lsi_corpus=lsi_instance[tfidf_corpus];

    print lsi_instance.print_topic(0)

if __name__=='__main__':
    main();