トピックモデルで単語の分散表現 – 実装編


scouty代表の島田です。 前回の記事 トピックモデルで単語の分散表現 – 理論編 では、トピックモデル・LDA(Latent Dirichlet Allocation)の基本構造とアイディアを、非エンジニアにも比較的わかりやすく説明しました。 今回はPythonで実際にトピックモデルを使って単語の分散(ベクトル)表現を作ってみます。エンジニア向けの記事ですが、最後の性能比較の部分はエンジニアでなくても、前回紹介した「分散表現」と「局所表現」の違いが理解できるかと思います。

自然言語処理ライブラリgensim

今回はフルスクラッチではなく、LDAやLSAやWord2vecが簡単に使えるPython用ライブラリgensimを利用します。gensimは、scoutyの実際のシステム内でも使われています。 gensimをインストール後(インストール方法は本家のWEBサイトを御覧ください)、

from gensim.models.ldamodel import LdaModel

でgensimのLDAModelをimportします。

以下の関数は、単語の共起表現ベクトルのリストvectorsを受け、gensimの関数LdaModelを用いてLDAモデルを学習(これにはデータサイズに依るが数十分の時間がかかります)し、指定したmodel_nameでモデルを保存する関数です。

def lda(vectors, word_mapping, model_name):

    # Convert to sparse vectors
    sparse_vectors = [convert_to_sparse(vector) for vector in vectors]

    trained_model = LdaModel(sparse_vectors, num_topics=NUM_OF_TOPICS, id2word=word_mapping, update_every=0)

    print("Saving Model to {0} ..".format(model_name))
    trained_model.save(model_name)

    return trained_model

第一引数のvectors、第二引数のword_mappingの形式は、以下のようになります。

word_mapping = {0: "have", 1: "get", 2: "house", 3: "home"}
vectors = [[0, 10, 12, 3], [10, 0 11, 5], [12, 11, 0, 21], [3, 5, 21, 0]

vectorsは共起表現を表す2次元ベクトルです。各単語が他のどの単語と一緒に生起しやすいかを、回数で表します。 vectorsの各行が各単語を表していて(これがトピックモデルでいう「文書」に対応します)、各列がその行の単語と各インデックスの単語との共起回数を表します。つまり、i 行 j 列目はインデックスi の単語とjの単語の共起回数ということです。たとえば、上の例では、ID0のhaveとID1のgetの共起回数は10回、という具合です。

このデータは文書集合から自分で作るも良し、どこかのソースから取ってくるもよし、とにかく自分で用意しなければいけません。(つまりここではLDAはベクトルの次元縮約として働く、というわけです) このベクトルは当然スパースになるので、gensimではスパースコーディングで渡す必要があります。つまり、0でない成分のインデックスとその成分の組のリストとして渡します。つまり、[1,0,0,4,0,0,8]は[(0,1), (3,4), (6,8)]と表されます。

word_mappingはオプションで、キーが単語ID、バリューが単語となっているディクショナリです。 トピックベクトルを表示するときなどに単語を表示させることが出来て便利 という程度のものなので面倒くさければ必要ありません。

普通のリストをスパースコーディングに変換する関数は、例えば以下のように書けます。

def convert_to_sparse(dense_vector):
    is_sparse = lambda vector: isinstance(vector[0], tuple)
    if not is_sparse(dense_vector):
        return [(i,v) for i,v in enumerate(dense_vector) if v > 0]
    else:
        return dense_vector

LdaModelのnum_topicsに指定するのはベクトルの次元数(=トピック数)で、大きければ表現力は上がる一方オーバーフィットしやすく計算量は増すので、100〜300くらいがちょうどよいでしょう。

gensimのLdaModelオブジェクト標準のメソッドsaveでモデルが保存できるので、一度学習して保存しておけば次回以降はすぐに読み込んでモデルを利用することができます。便利!

類似度比較

最も簡単な応用例として、類似度比較を試してみます。今回は得られたベクトルを使って単語 home, house, timeの類似度を測ってみました。類似度は次のコサイン類似度を用いています。(この類似度が大きいほど両者の単語が似ているということ)
$$cos(\boldsymbol{u}, \boldsymbol{v}) = \frac{\boldsymbol{u}\cdot\boldsymbol{v}}{|\boldsymbol{u}||\boldsymbol{v}|}$$
以下がLDAの結果(トピック数100)です。

Similarity between house.n and home.n is 0.432761038721
Similarity between house.n and time.n is 0.248780840955
Similarity between home.n and time.n is 0.21364984806

直観に違わず、homeとhouseの類似度がtimeとhouseの類似度を大きく上回っていることがわかります。

ちなみに、学習に使った共起表現の生ベクトル vectors(ボキャブラリーサイズ=ベクトルの次元数=2000)で同じく類似度を計った結果が以下です。

Similarity between house.n and home.n is 0.812743856464
Similarity between house.n and time.n is 0.82359219053
Similarity between home.n and time.n is 0.818144793373

ベクトルが長く、スパース過ぎて0がほとんどなため、コサイン相関だと差がほとんど表れません。 上2つを比較すれば、いかにLDAの次元圧縮が効いているかわかりますね。

前回の記事 トピックモデルで単語の分散表現 – 理論編 の「局所表現と分散表現」で説明したように、このように局所表現(スパースなベクトル、下の例)だとベクトルがほとんど0で埋まってしまって精度が出にくくなるので、トピックモデルやWord2Vecなどの分散表現が必要、ということですね。

自然言語処理に強いエンジニア募集!

scoutyでは、上記のようなトピックモデルやWord2vec, Doc2vecやRNNとアテンション、といった自然言語処理を使って退職率予測アルゴリズムや企業と人のマッチングアルゴリズムをつくる機械学習エンジニアを探しています! Pythonでバリバリ自然言語処理を使ってサービスに使っていきたいと考えている方がいたら、一度お話しましょう!