gensimでLDA(Latent Dirichlet Allocation)

Pocket

トピックモデルを試そうとgensimのLdaModelを使ってみたのですが、参考にした記事「LSIやLDAを手軽に試せるGensimを使った自然言語処理入門」は対象とするgensimのバージョンが古いようだったので、現状にあうようアレンジして試してみました。この記事で使ったgensimのバージョンは0.11.1です。

過去のgensimと今のものとでは、メソッド名等の命名規則が変わっています。旧来はcamel caseでしたが、今は小文字とアンダースコアの組み合わせになっています。たとえばnumTopicsという名前付き引数はnum_topicsに変わっています。

それ以外にも多少の違いはあるのですが、yuku_tさんのgistのコードを今のバージョンのgensimで動くように修正したのが以下になります。

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

import logging
import sys
import os.path
import bz2

from gensim.corpora import WikiCorpus
from gensim.corpora.wikicorpus import filter_wiki
import MeCab

logger = logging.getLogger('jawikicorpus')
logger.setLevel(logging.INFO)

tagger = MeCab.Tagger()

DEFAULT_DICT_SIZE = 100000
ARTICLE_MIN_CHARS = 500

def jatokenize(text):
    node = tagger.parseToNode(text.encode('utf-8')).next
    while node:
        if node.feature.split(',')[0] == '名詞':
            yield node.surface.lower()
        node = node.next

def tokenize(content):
    return [token for token in jatokenize(content) if not token.startswith('_')]

class JaWikiCorpus(WikiCorpus):
    def getArticles(self, return_raw=False):
        articles, articles_all = 0, 0
        intext, positions = False, 0
        for lineno, line in enumerate(bz2.BZ2File(self.fname)):
            if line.startswith('      <text'):
                intext = True
                line = line[line.find('>') + 1 : ]
                lines = [line]
            elif intext:
                lines.append(line)
            pos = line.find('</text>') # can be on the same line as <text>
            if pos >= 0:
                articles_all += 1
                intext = False
                if not lines:
                    continue
                lines[-1] = line[:pos]
                text = filter_wiki(''.join(lines))
                if len(text) > ARTICLE_MIN_CHARS: # article redirects are pruned here
                    articles += 1
                    if return_raw:
                        result = text
                    else:
                        result = tokenize(text) # text into tokens here
                        positions += len(result)
                    yield result

        logger.info("finished iterating over Wikipedia corpus of %i documents with %i positions"
                     " (total %i articles before pruning)" %
                     (articles, positions, articles_all))
        self.numDocs = articles # cache corpus length

if __name__ == '__main__':
    logging.basicConfig(format='%(asctime)s : %(levelname)s : %(message)s')
    logging.root.setLevel(level=logging.INFO)
    logging.info("running %s" % ' '.join(sys.argv))
    program = os.path.basename(sys.argv[0])
    if len(sys.argv) < 3:
        #print globals()['__doc__'] % locals()
        sys.exit(1)
    input, output = sys.argv[1:3]
    wiki = JaWikiCorpus(input)
    wiki.dictionary.save_as_text(output + "_wordids.txt")
    from gensim.corpora import MmCorpus
    MmCorpus.serialize(output + "_bow.mm", wiki)
    #del wiki
    from gensim.corpora import Dictionary
    id2token = Dictionary.load_from_text(output + '_wordids.txt')
    mm = MmCorpus(output + '_bow.mm')
    from gensim.models import TfidfModel
    tfidf = TfidfModel(mm, id2word=id2token, normalize=True)
    MmCorpus.save_corpus(output + '_tfidf.mm', tfidf[mm], progress_cnt=10000)
    logging.info("finished running %s" % program)

WikiCorpusクラスにはsaveAsTextと等価なメソッドが今は存在しないので、個別にDictionaryとCorpusをファイルに出力しています。

生成されたモデルをもとにLSI, LDAモデルを作成する流れは、メソッド名等を(たとえばPrintTopic→print_topic)書き換える点以外は元記事とほぼ変わりません。

また、LDAモデルの生成をマルチコアで実施するgensim.corpura.LdaMulticoreというクラスも用意されています。worker=2等とワーカースレッド数を指定できる点以外はLdaModelクラスと同じです。しかし、あまりよくスケールするわけではないようで、CPU論理コア数8の環境でworker=7としてみても対して速度が上がることはないようです(参考:Multicore LDA in Python: from over-night to over-lunch)。一応workerプロセスは指定しただけ起動はするのですけど。加えてマルチコアで処理した場合、出力されるログが少なくなって進捗状況がわかりづらくなるようです。

ところで以前書いたword2vecの記事でgensimのword2vecでのマルチスレッド動作について書いていなかったので、同様にworkerスレッド数を指定できることを追記しておきました。

また、Google C言語実装のword2vecのマルチスレッド処理について一つだけ気になるところがありました。それは、単純にファイルをバイト単位でスレッド数に分割し、処理している点です。単語の区切りを一切考慮しないので、その箇所がたまたま単語の途中であってもお構いなしに処理を進めてしまいます。

もっとも、出現頻度の少ない単語は捨てられるので、実用上問題になることはほとんどないはずではあります。運悪くUTF-8の途中で切られてしまい、なおかつ語彙として記録されてしまうと、gensimで読み込もうとするときにInvalidUnicodeErrorが発生してしまうので、その点だけは気を付ける必要があります。