Stanでトピックモデルを実装するメリット・デメリットについて簡単に触れたいと思います。
メリット
- 実装がラク。LDAでも30行ぐらい
- ややこしい推論部分は一切実装しなくてOK。全部Stanのサンプリングにお任せ
- モデルの拡張が簡単
デメリット
- 計算が遅い。文書x単語種類が1000x1500・総単語数12000のケースでは トピック数が20, iter=1000で9時間, iter=10000で35時間でした
- データが少ないと収束しない。特に単語種類が多いのに1文書あたりに含まれる単語数が少ない場合はダメ。僕の経験ではそのような場合はiteration増やしてもほとんどダメでした
これらのデメリットはStan2.9.0でリリースされた変分ベイズ(ADVI)を使って推定することでいくらか緩和されます。速度はモデルにもよりますがおおよそ50倍速ぐらいです。ただし、現状は推定が少し不安定のようです。このシリーズではNUTSというStanの標準のサンプラーを使用することにします。
ではさっそく、今回のトピックモデルシリーズで扱う問題設定から簡単に説明したいと思います。詳しくは各モデルの記事で触れます。
文書があります。Webのニュースのページを想像してください。読むと色々な単語が含まれています。「選挙」とか「勝った」とか。単語の種類と出現回数だけを見て、その文書がどんなトピックを扱っているか予測します。【政治】とか【エンターテインメント】とか。それが決まればさらに続きの次のページにはどんな単語が出現しそうかも予測できるでしょう。たくさんの文書を使って学習してモデルに含まれるパラメータを決めます。その後に予測の精度を見ます。以上の部分がざっくり言うとNB(Naive Bayes)とUM(Unigram Mixtures)です。NBでは誰かが文書ごとにつけた【政治】とか【エンターテインメント】とかのトピック情報が前もって学習セットについています。UMではついていないのでそのトピックを潜在変数として扱って推定します。
NBやUMでは「続きの次のページにはどんな単語が出現するか」の予測の精度がよくないことがしばしばあります。そこを改善するためにLDA(Latent Dirichlet Allocation)では改良点があります。それは文書ごとにつけた【政治】とか【エンターテインメント】とかのトピックを1つだけではなくて、1つの文書に複数のトピックを含めるようにした点です。その分布をトピック分布と呼びます。そして文書内の各単語はそのいずれかのトピックに属します。
LDAの拡張はたくさんあります。トピックに階層構造がある場合はPAM(Pachinko Allocation Model)というモデルが提唱されています。トピック分布が時間に従って変化するようなモデルもあります。DTM(Dynamic Topic Model)の一種です。また文書ごとに他のラベルデータ(ヒットしたか否かなど)がついている場合があります。そのラベルデータもモデルに組み込むようなものはSLDA(Supervised LDA)と呼ばれます。また文書間にリンクがあってそのリンクを予測したい場合もあります。RTM(Relation Topic Model)というモデルが提唱されています。またLDAではトピック分布は多項分布なので合計は1なのですが、その制限をはずしてgamma分布を使ったものがあります。GaP(Gamma-Poisson Model)です。今後はこれらの記事を1つずつ書いていきます。
次にこのシリーズの大半(NB, UM, LDA, PAM, GaP)で扱うサンプルデータの説明をします。リアルな文書から作ったデータの方が面白いし人気も出ると思うのですが評価がしにくいです。そこで例によって真のパラメータを自分で決めて自作しました。あとで推定結果と真の値を比べます。データのoverviewは以下になっています。
単語1 | 単語2 | 単語3 | … | 単語142 | 単語143 | 単語144 | |
---|---|---|---|---|---|---|---|
文書1 | 0 | 0 | 0 | … | 0 | 1 | 0 |
文書2 | 0 | 0 | 0 | … | 0 | 1 | 0 |
文書3 | 0 | 0 | 0 | … | 0 | 0 | 0 |
文書4 | 0 | 2 | 0 | … | 1 | 0 | 0 |
… | … | … | … | … | … | … | … |
文書97 | 0 | 0 | 0 | … | 0 | 0 | 0 |
文書98 | 0 | 1 | 0 | … | 0 | 1 | 0 |
文書99 | 0 | 1 | 0 | … | 0 | 0 | 0 |
文書100 | 0 | 0 | 0 | … | 0 | 0 | 0 |
縦に文書のIDが並び、横に単語のIDが並んでいます。bag-of-wordsと呼ばれます。数値はある文書である単語が何回出現したかを表します。RですとRMeCabなんかを使うことでお手軽にbag-of-wordsが作成できるみたいです(やり方はググってください。僕はRubyを使用しています)。
今回の例では文書数(M)が100, 単語の種類(V)を144, 総単語数(N)を少ない場合(2024; data1
)と多い場合(14955; data2
)で試しました。前者は1文書あたり20単語ぐらい, 後者は1文書あたり150単語ぐらいという目安になります。上記のoverviewはdata1のものです。
上記のデータを作ったRコードを載せます。各自の環境で実行すれば同じデータが出来上がると思います。
library(gtools) K <- 10 # num of topics M <- 100 # num of documents V <- 144 # num of words set.seed(123) alpha <- rep(0.8, K) beta <- rep(0.2, V) theta <- rdirichlet(M, alpha) phi <- rdirichlet(K, beta) z.for.doc <- apply(theta, 1, which.max) # for Naive Bayes num.word.v <- round(exp(rnorm(M, 3.0, 0.3))) # data1: low number of words per doc # num.word.v <- round(exp(rnorm(M, 5.0, 0.3))) # data2: high number of words per doc w.1 <- data.frame() # data type 1: stan manual's w w.2 <- data.frame() # data type 2: counted w for(m in 1:M){ z <- sample(K, num.word.v[m], prob=theta[m,], replace=T) v <- sapply(z, function(k) sample(V, 1, prob=phi[k,])) w.1 <- rbind(w.1, data.frame(Doc=m, Word=v)) w.2 <- rbind(w.2, data.frame(Doc=m, table(Word=v))) } w.2$Word <- as.integer(as.character(w.2$Word)) N.1 <- nrow(w.1) # total word instances N.2 <- nrow(w.2) # total word instances offset.1 <- t(sapply(1:M, function(m){ range(which(m==w.1$Doc)) })) offset.2 <- t(sapply(1:M, function(m){ range(which(m==w.2$Doc)) })) bow <- matrix(0, M, V) # data type 3: bag-of-words for(n in 1:N.2) bow[w.2$Doc[n], w.2$Word[n]] <- w.2$Freq[n] save.image("input/201402_data1.RData") # save.image("input/201402_data2.RData") ## cf. an example of conversion from bag-of-words (type3) to counted w (type2) # library(reshape2) # bow.df <- data.frame(1:M, bow) # colnames(bow.df) <- c('Doc', 1:V) # bow.melt <- melt(bow, id=c("Doc")) # bow.melt <- subset(bow.melt, value != 0) # colnames(bow.melt) <- c("Doc", "Word", "Freq") # w.2 <- bow.melt[order(bow.melt$Doc),] # w.2$Word <- as.integer(w.2$Word) # rownames(w.2) <- 1:nrow(w.2) # offset.2 <- t(sapply(1:M, function(x){ range(which(x==w.2$Doc)) }))
当分の間、data typeはStanのマニュアルと同様のw.1
を使っていきます。以下のようなdata.frame
です。
Doc | Word |
---|---|
1 | 6 |
1 | 29 |
1 | 49 |
1 | 143 |
1 | 31 |
1 | 127 |
1 | 112 |
1 | 79 |
1 | 6 |
… | … |
1 | 93 |
1 | 56 |
1 | 16 |
2 | 11 |
2 | 143 |
2 | 87 |
… | … |
100 | 118 |
100 | 133 |
100 | 86 |
100 | 119 |
上記では1列目が文書のID, 2列目がその文書に含まれている単語のIDです。実行してみると分かりますが、offset.1
には ある文書に含まれる単語がw.1
の何番目から何番目までかのindexが格納されています。このoffset.1
のおかげでモデルの記述が分かりやすくシンプルになります。
w.2
はw.1
ではある文書内に同じ単語が出てきたときも異なる行になっているので、それを1行にしてカウントデータ(Frequency)にしたものです。LDAを速度アップさせるときに使います。それまでは忘れてください。bag-of-wardsであるbow
はGaPモデルだけで使用します。
次回はNB(Naive Bayes)の説明をします。一転して図を豊富に使う予定です。