Collapsed Variational Bayesian での LDA 推論も実装してみたのだが、そのときに「パープレキシティが下がりきるのは非常に早いのに、その時点ではトピック-単語分布がストップワードだらけ」「イテレーションの最初のうちはパープレキシティがほとんど動かない」という現象にぶちあたってしまった。
で、その解決方法を考えているうちに、一つひらめいたことがあったので、また Collapsed Gibbs LDA に戻ってちょいと試してみた。
といっても大層なことではなく、推論の初期値に各 term のトピックをランダムに割り振るのだが、それを完全にランダム( K 個のトピックが一様分布)にするのではなく、Gibbs サンプリングに用いる事後分布を逐次更新しつつ、その分布からトピックをサンプリングするようにしてみたのだ。
つまり p( z_mn | z_mn より一つ前までの z , x_mn までの x ) から z_mn をサンプリングして、それを次の z_m(n+1) のサンプリングに使って……ということ。
ソース的にはこんな感じ。
self.z_m_n = [] # topics of words of documents self.n_m_z = numpy.zeros((len(self.docs), K)) + alpha # word count of each document and topic self.n_z_t = numpy.zeros((K, V)) + beta # word count of each topic and vocabulary self.n_z = numpy.zeros(K) + V * beta # word count of each topic self.N = 0 for m, doc in enumerate(docs): self.N += len(doc) z_n = [] for t in doc: p_z = self.n_z_t[:, t] * self.n_m_z[m] / self.n_z # 逐次更新された(事後)分布 z = numpy.random.multinomial(1, p_z / p_z.sum()).argmax() # z_mn を p_z からサンプリング z_n.append(z) self.n_m_z[m, z] += 1 self.n_z_t[z, t] += 1 self.n_z[z] += 1 self.z_m_n.append(numpy.array(z_n))
普通の一様ランダムなら、コメントを付けた2行が z = numpy.random.randint(0, K) とかになってたイメージだから、変更量は少なめ。*1
けど、これがなかなかいい感じ。
- perplexity が最初からある程度小さく、収束も早い。イテレーションの最初からがんがん下がる。
- ストップワードが自然に1つのトピックに固まりやすい。これならストップワードの辞書を持たなくても良さそう。
一様ランダム初期化では 100回ほどイテレーションしないといけないところが、20〜30回でそれを上回る良い結果が出るようになった。
実装はいつものこちらに転がっている。
- https://github.com/shuyo/iir/blob/master/lda/lda.py
- https://github.com/shuyo/iir/blob/master/lda/vocabulary.py
実装の説明はこの辺。
この工夫は Collapsed Variational Bayesian や HDP-LDA ではもっと有効だったので、そっちの話はまた今度。
*1:実際には1ドキュメント分を一気にサンプリングしてたので、その2行以外にもちょっぴりいじっているが、それらを含めてもたいしたことない。