gensimの実装と設計|Pythonで学ぶシステム設計 #4

f:id:lib-arts:20190114184943p:plain

#2ではscikit-learn、#3ではDjangoについて取り扱いました。

#4は最近関わったプロジェクトでgensimの簡単なエラーで少々調べることになったので、gensimについて取り扱えればと思います。
以下目次になります。

 

1. 自然言語処理の基本とgensimの紹介
2. gensim.corpora.Dictionaryの実装
3. gensim.matutilsの実装
4. まとめ

 

1. 自然言語処理の基本とgensimの紹介

1節では自然言語処理(NLP; Natural Language Processing)の基本について軽く取り扱った後に、gensimの概要についてご紹介できればと思います。
NLPにあたって、まず理解すべきなのがBoW(Bag of Words)です。BoWは簡単に言うと文書群を行列で表したもののことです。大体のケースにおいては行方向が文書、列方向が単語の行列とすることが多いです。
このBoW形式を用いることで、非構造化データ(Unstructured Data)呼ばれる言語データをRDBMSなどでも取り扱うことができる表型の形式に直すことができます。

英語などのスペースで区切るタイプの言語についてはそのままBoW形式に直すことができるのですが、日本語の場合は単語単位に区切る必要があります。この処理のことを形態素解析と言うのですが、形態素解析の原理についてはこの記事の論旨から外れるため、別記事で詳細についてはまとめたいと思います。

単語を分かち書きしたリストを入力としてBoW形式をscipy.sparseの形式に変換したり、LDAやWord2vecなどの言語解析系のアルゴリズムを色々と実行したりと様々な処理をgensimで行うことができます。


今回の記事の背景としては、scikit-learnのインプットを作るにあたってgensim.corpora.Dictionaryやgensim.matutils.corpus2cscを使用して行列を作っていく際に、推論時に文書が少ないことでエラーが生じその解析のために30分程度かかったということがあります。それを受けて詳しく中身を読んでみたいと思ったので、実際に読んでみた内容について以下にまとめます。
2節ではgensim.corpora.Dictionary、3節ではgensim.matutils.corpus2cscについてまとめていければと思います。


2. gensim.corpora.Dictionaryの実装
gensimの実装を確認していくにあたって、Dictionaryクラスがベースになっていることが多いと思われるため、まずはDictionaryの実装についてまとめていければと思います。

これまでの読解と同様に考え、PJ_root/gensim/corpora/__init__.pyを確認したところ、

from .dictionary import Dictionary

上記のように同一ディクショナリ下のdictionary.pyからインポートを行なっていることがわかります。そのため、下記のファイルを見ればDictionaryの実装が確認していけそうです。

上記の読解にあたって、def __init__(...):の中のself.token2idなどは処理を行ったのちにキーワードとidの対応が辞書として保存されます。この初期値自体は空なのですが、同じ関数内を見ると下記の処理が見つかります。

if documents is not None:
    self.add_documents(documents, prune_at=prune_at)

ここでまずif文の意味ですが、引数としてdocumentsが与えられていたら処理を実行するとなっています。これを見た感じだと、add_documentsで諸々の処理を行っていそうだというあたりがつきます。また、add_documentの処理を確認すると『for docno, document in enumerate(documents):』が文書群の中の1文1文の処理を行っているようで、この繰り返し文内にself.doc2bowが出てきています。そのため、コアのBoWを作る処理はdoc2bowで行っているということがわかります。

token2id = self.token2id
(中略)
if allow_update or return_missing:
    (中略)
    if allow_update:
        for w, _ in missing:
            token2id[w] = len(token2id)

上記のような処理があり、これによってtoken2idに値が追加されているのではないかと思われます。self.token2idのアップデートについての処理が見受けられませんでしたが、多分辞書の複製への変更は元のオブジェクトも変更されるという処理なのではと思われます。(今回は概要を掴みたいと言うのが目的だったので細かい検証まではしていませんので推測です。)
ちなみに、このdoc2bowは返り値としてBoWを返すようになっているのですが、add_documentの処理では返り値を受け取らない実装になっています。(ここの実装はなんとなく引っかかるので時間があれば確認してみたいです。)

上記でgensim.corpora.Dictionaryの大体の概要が掴めたので、2節はこのくらいにしたいと思います。

 

3. gensim.matutilsの実装

f:id:lib-arts:20190116111723p:plain

from "https://radimrehurek.com/gensim/matutils.html#gensim.matutils.corpus2csc"

上記画像はgensim.matutils.corpus2cscのドキュメントです。このParametersの中のcorpusがメインで与える引数となっているようで、詳細を見ると"Input corpus in BoW format"となっています。そのため、ここではBoW形式の入力を入れる必要があります。返り値としては"sparse CSC matrix."となっており、このcscはCompressed Sparse Columnの略で、scipy.sparseの実装の一つのようです。(デフォルトの出力を確認した感じだとscipy.sparseの形式で出力がなされるようです。)
また、今回エラーになった箇所であるnum_termsについてもここで設定できます。推論時はここに辞書の長さの値を入れないと文書のサイズが小さい時などは列(BoWの単語)の数が小さくなってしまい分類器の入力にそのまま入れるとエラーが生じるようです。
今回はそのこともあるので、corpusとnum_termsがgensim.matutils.corpus2cscでどのように使用されているかについてフォーカスを当てて読解できればと思います。

gensim/matutils.py at develop · RaRe-Technologies/gensim · GitHub

上記を確認すると"def corpus2csc"があります。結構num_termsの値で条件分岐していてちょっと実装に重複が見られる印象でした。またreturnする返り値を作成するにあたってscipy.sparse.csc_matrixを用いており、これによってアウトプットはscipy.sparseの形式に変更されているようです。
こちらも細かい確認はしていませんが大体の概要は掴めたので、次回何かしらで使用する際はもう少し深く解析してみたいと思います。


4. まとめ

今回は大体の概要を掴む目的だったので詳しく読んではいないのですが、全体的な雰囲気がわかってよかったです。
ドキュメント読むだけだと解釈違いなどがちょっと怖いところではあるので、コードリーディング少しやっておくと安心できるところはあるなと思います。