LSIやLDAを手軽に試せるGensimを使った自然言語処理入門
Gensimはベクトル空間モデルを扱うためのPythonモジュールです。ベクトル空間モデルは画像や音声などのメディアにも用いることができますが、Gensimは特に文書を扱うのに便利な機能を多数用意しており、文書集合から簡単に単語文書行列(GensimではCorpusと呼ばれる)を作ることができます。さらに、LSIやLDAなどを用いた次元圧縮をコマンド一つで行うこともできます。Pythonで自然言語処理というとNLTKが有名ですが、こちらにはまだLSIやLDAのような言語モデルを扱う機能は実装されていないと思うので、LSIやLDAを単に使ってみたいだけなら、Gensimは便利なモジュールだと思います。
さて、公式チュートリアルに英語版のWikipediaの文書を用いた事例が紹介されていたので、これを応用して、今回は日本語版のWikipedia文書を使って遊んでみました。計算はとても時間がかかりますが、一回計算したらキャッシュできるので、次回からはすぐに利用することができます。
もくじ
インストール
Gensimは数値演算ライブラリとして有名な、NumPyとSciPyの上に構築されています。
依存するバージョン
Pythonはbz2が必要です。CentOSではbzip2-develを先にインストールしなければなりません。
$ yum install bzip2-devel
NumPyやSciPyのインストールの詳細は他の記事参照。
関連: yumを使わずにCentOS5.5にnumpyとscipyをインストールする - SELECT * FROM life;
gensimはeasy_installを使ってインストールすることができます。
$ easy_install gensim
今回インストールされたgensimのバージョンは0.7.8でした。gensimには複数のサーバ上で分散処理することで計算を高速化する機能があるのですが、今回はインストールしませんでした。これを使いたい場合は、本家のドキュメントを読んでください。
今回は日本語版Wikipediaの文書を単語に分割するのにMeCabを用いました。本家チュートリアルの英語版Wikipediaを使うだけなら不要です。
Wikipediaからコーパスを作成する
Wikipediaのデータはここからダウンロードして用いることが可能です。latestの中から jawiki-latest-pages-articles.xml.bz2 というファイルをダウンロードしましょう。
Gensimには英語版のWikipediaから単語を切りだして、Gensim用のコーパスと辞書ファイルを作成してくれるモジュールが付属しています。しかしながら、日本語版にはそのままでは適用できなかったので、このようなコードを作成しました。これを jawikicorpus.py という名前で保存し、次のように実行してください:
# 注意:このコマンドは終わるまでに6時間以上かかります $ python jawikicorpus.py jawiki-latest-pages-articles.xml.bz2 jawiki
これによりjawiki_bow.mm jawiki_tfidf.mm jawiki_wordids.txtというファイルが作られます。.mm という見慣れない拡張子のファイルはMatrix Market File Formatという形式のテキストファイルです。3つの中身はそれぞれ次ような意味を持ちます:
tfとtfidf
文書の特徴量を出現する単語を用いて決定する、というのはとても自然な考えです。
tfは文書の特徴量を各単語の出現回数を用いて表します。tfでは各文書の特徴量は、他の文書集合とは独立して決まることになります。しかしながら、多くの文書に共通して現れる単語が出てきても他の文書と区別する要因にはなりませんが、滅多に出てこない単語が出てきたら、その事実は他の文書には無い特徴であると考えられます。このように、他の文書に出てくる単語との兼ね合いをtfに加えたものがtfidfという指標です。
さて、ひとしきり説明したところで、上で作成されたtfidfに変換された文書単語行列を使ってみます。
まずは、作成されたCorpusとDictionaryを読み込みます
>>> import gensim >>> dictionary = gensim.corpora.Dictionary.loadFromText('jawiki_wordids.txt') >>> tfidf_corpus = gensim.corpora.MmCorpus('jawiki_tfidf.mm')
さて、あるクエリに対して文書の中からtfidfベクトル空間内で最も近い文書を見つけたいと思います。
>>> # このコマンドは数十分かかるし、メモリを結構使います >>> tfidf_index = gensim.similarities.SparseMatrixSimilarity(tfidf_corpus) >>> tfidf_index.save('jawiki_tfidf_similarity.index') # せっかく計算したので保存 >>> # 次回からは次のコマンドでロードできる >>> # tfidf_index = gensim.similarities.SparseMatrixSimilarity.load('jawiki_tfidf_wimilarity.index') >>> query = "大学 京都" # クエリ >>> query_vector = dictionary.doc2bow(query.split()) # クエリをベクトル空間にマッピング >>> print query_vector # queryの特徴ベクトル [(37159, 1), (78328, 1)] >>> sims = tfidf_index[query_vector] >>> # 類似度が高い順に並べ替えて上位10件を表示 >>> print sorted(enumerate(sims), key=lambda item: -item[1])[:10] [(157102, 0.67361993), (159079, 0.59990668), (331305, 0.58690864), (427226, 0.54940921), (376428, 0.54852891), (178744, 0.53930968), (206547, 0.53024179), (243683, 0.52715456), (32860, 0.50363398), (252710, 0.4989877)]
この結果、157102番の文書との類似度が0.67で最大ということが分かりました。157102番が何というWikipedia記事なのかが分からないので、どうしようもないんですけどねw
ちなみに内部ではコサイン類似度を使っているようです。
Latent Semantic Indexing(LSI)
ベクトル空間モデルを用いた検索では、"京都大学"と"京大"という索引語まったく別ものなので、"京大"とうクエリでは"京都大学"しかでてこない文書を見つけることができません。この時、人間が作ったシソーラス(類義語辞典)を利用することもできますが、あらゆる言葉を網羅するのは大変ですし、新語に対応するのも困難です。
LSI(自然言語処理の方面ではLatent Semantic Analysis, LSAと呼ばれます)は、前述の文書単語行列を、圧縮する技術の一つです。直感的には、似た様な使われ方をしている単語同士をひとまとまりにしてしまうことで、次元数を圧縮します。
[参考]:404 Not Found
それではLSIをやってみましょう。LSIでは圧縮後の次元(トピック)の数を指定する必要があります。ドキュメントによると、一般的に200から500くらいにするとよいらしいので、今回は300にしてみました。
>>> # このコマンドは8時間くらいかかります >>> lsi = gensim.models.LsiModel(corpus=tfidf_corpus, id2word=dictionary, numTopics=300) >>> lsi.save('jawiki_lsi_topics300.model') # せっかく計算したので保存 >>> # 次回からは次のコマンドでロードできる >>> # lsi = gensim.models.LsiModel.load('jawiki_lsi_topics300.model')
LSIでは複数の次元を1つにまとめることで次元を圧縮します。まとめられた次元の集合をトピックと呼びます。トピックをいくつか出力してみましょう。
>>> print lsi.printTopic(4) # 4番目のトピックを出力。見やすく改行をいれた -0.313*"作曲" + -0.294*"作詞" + -0.277*"アルバム" + -0.267*"編曲"+ 0.262*"選手" + -0.221*"曲" + -0.210*"シングル" + -0.180*"収録" + 0.139*"出場" + 0.136*"優勝"
音楽関連の次元が集まっています。作曲や作詞の係数がマイナスなのに対し、出場や選手や優勝は係数がプラスです。同じ次元に縮約されてはいますが、効果としては逆になっているようですね。このようにLSIで圧縮されたベクトル空間では、"作曲"と"作詞"が同じ次元で表されるので、"作曲"で検索した場合"作詞"もヒットすることになります。
>>> print lsi.printTopic(23) 0.365*"王座" + 0.200*"王者" + 0.170*"ボクシング" + -0.163*"話" + -0.158*"オリンピック" + -0.153*"種" + 0.150*"判定" + -0.142*"選手" + 0.127*"対戦" + 0.122*"戦" >>> print lsi.printTopic(27) -0.302*"秒" + -0.215*"オリンピック" + 0.159*"監督" + 0.158*"クラブ" + -0.154*"役" + 0.153*"サッカー選手" + 0.133*"車" + 0.132*"得点" + 0.130*"サッカー"
こちらはスポーツっぽいトピックです。
tfidfでやったのと同じように、LSIのコーパスを使ってもクエリによる検索を行うことが当然できるので、興味があったらやってみて下さい。こちらも、結果のページが何なんか分かったらもっと楽しいのだけど。。。
Latent Dirichlet Allocation(LDA)
LSIは単語文書行列をいい具合に次元圧縮してくれる手法ですが精度的な面で問題もあります。詳細な説明は省きますが*1、大雑把に言うと、特徴ベクトルに揺らぎがないことが原因の一つです。つまり、ある文書が与えられたら、まずtfidfなどの手法を用いて特徴ベクトルを作りますが、LSIではこの特徴ベクトルを完全に信用します。例えば、"作曲"は出現するが"作詞"は出ない文書があったとします。この文書の"作曲"次元は何かしらの値をとりますが、"作詞"は0です。LSIの結果、この二つの索引語が同じ次元に圧縮されたとしても、あくまでも"作詞"は0なのです。「作曲って出てくるから作詞でも検索してやるけど、お前の作詞は0だから」という感じです。
一方、「作詞出てこないけど、これは偶然出てきてないだけじゃね?ほんとは出てきてもおかしくなかったんじゃね?」と単語の特徴ベクトルにゆらぎを持たせることが考えられます。このように確率的な枠組みをLSIに導入したものをProbabilistic Latent Semantic Indexing (pLSI)といい、LSIに比べて精度が向上することが知られています。
ではLDAとは何なのか。LSIの説明で、圧縮した次元の集合をトピックと言うといいましたが、言い方を変えれば、LSIやLDAでは文書の特徴量を単語ではなく、トピックで表すわけです。実はpLSIでは単語による特徴は確率的でしたが、トピックは確定的なモデルなのですが、LDAはトピックも確率的にモデル化されている、というのが大雑把なLDAの説明です。
[参考]: LDA入門
では、LDAを使ってみましょう。
>>> # これも時間がかかる >>> lda = gensim.models.LdaModel(corpus=tfidf_corpus, id2word=dictionary, numTopics=100) >>> lda.save('jawiki_lda.model') # せっかく計算したので保存 >>> # lda = gensim.models.LdaModel.load('jawiki_lda.model')
こちらもトピックを表示してみると
>>> print lda.printTopic(6)
0.065*種 + 0.059*犬 + 0.029*分布 + 0.023*生息
+ 0.020*飼育 + 0.019*cm + 0.018*科
+ 0.015*動物 + 0.014*類 + 0.014*毛
動物系のトピックになっているようですね。
おわりに
今回はGensimを使った簡単な自然言語処理をしました。LSIやLDAは面白い技術ですが、数学的なバックグラウンドを理解するには大学レベルの線形代数の知識が要求されますし、LDAはもっと高度な知識が必要になりますから、なかなか勉強して自力で実装するのは大変だったりします。なので、「実装はさておき、とりあえずLDAで遊んでみたい」という場合はGensimを使ってみると良いかも知れません。
あと、GensimにはLSIとLDAの他にRandom Projectionというのも使うことができるので、ドキュメントを読んで使ってみると楽しそうです。
個人的にはアルゴリズムを理解するのには自分で実際に実装してみるのが一番だと思うので、以下のpLSIやLDAを実装してみた系の記事を参考に、自分でも実装してみるといいですね:
pLSIを試してみた - のんびり読書日記
satomacoto: PythonでLDAを実装してみる
*1:というか人に説明できるほど理解していない