機械学習・自然言語処理の勉強メモ

学んだことのメモやまとめ

Stan:LDA

はじめに



自然言語処理の領域では広く知られいるLDA(Latent Dirichlet Allocation)について復習する。

LDAはトピックモデルの1種であり、文書がどのようなトピックから構成されているかを推論するモデル。
推論するパラメータは以下の2つ。

  • トピック分布:文書ごとのトピック構成比率
  • 単語分布:トピックごとの単語比率

トピックモデルに関する理解はこの1枚に尽きると思う。
f:id:kento1109:20180627110029p:plain
Fast and Scalable Algorithms for Topic Modeling | Center for Big Data Analyticsより引用

後、日本語でのLDAの説明としては視覚的にも以下が分かりやすかった。
LDA for Pokemon analysis | haripo.com

モデリング


数式によるLDAはググれば色々出てくるのでここでは割愛する。今回は「尤度計算」の具体例のみを数式で紹介する。これが分かれば、Stanコードも理解できると思う。

\begin{eqnarray}
L &=& \prod_{d=1}^D\prod_{w=1}^W \sum_{k=1}^K p(\theta_{d,k})\times p(\phi_{k,w}) \\
\log L  &=& \sum_{d=1}^D\sum_{w=1}^W \log\left(\sum_{k=1}^K  \{p(\theta_{d,k})\times  p(\phi_{k,w})\}\right)
\end{eqnarray}
各記号の説明は以下の通り。

  • トピック分布(\Theta\in\mathbb{R}^{d\times k}
  • 単語分布(\Phi\in\mathbb{R}^{k\times w}

dは文書数、kはトピック数、wは単語数を表す。
これは「各文書内に出現する各単語の尤度(出現確率)」を計算している。

ちなみに単語の生成確率は文書dn番目の単語のトピック割り当てを意味する潜在変数z_{d,n}\in\{0,1\}^Kを用いると以下のように表せた。
\begin{eqnarray}p(w_{d,n})=\prod_{k=1}^K p(\phi_k)^{z_{d,n,k}} \end{eqnarray}
しかし、Stanでは、整数型の潜在変数が使えないので、上式のようにしてsumming outしている。

対数尤度の計算

まず、以下の文書のトピックを考える。

play soccer

全ての文書が「音楽」か「スポーツ」の何れかに関するものであるとき、この文書はどちらに分類されるか。
常識で考えると「スポーツ」トピックから生成された(「スポーツ」トピックの方が混合比が大きい)と考えるのが自然だろう。

ただ、そんな常識を機械は知らない。
では、どう考えればよいか。

それを考えるため、この文書のトピック分布(\theta_{d,k})及び単語分布(\phi_{k,w})が事前にディレクレ分布から以下のように生成されたとする。

  • トピック分布(\theta_{d,k}
music 0.5
sports 0.5
  • 単語分布(\phi_{k,w}
play soccer
music 0.01 0.01
sports 0.01 0.01
guitar 0.01 0.01
baseball 0.01 0.01
the 0.01 0.01

※guitar,baseball,theに関しては後で言及する。

この文書(d)における尤度は以下のように計算できる。
\begin{eqnarray}
L_1 &=& \sum_{w=1}^W \log\left(\sum_{k=1}^K  \{p(\theta_{d,k})\times  p(\phi_{k,w})\}\right)\\
&=&
  \log\left(
    \begin{array}{ccc}
      \theta_{d,music}\times \phi_{music,play} \\
      + \\
      \theta_{d,sports}\times \phi_{sports,play}\\
    \end{array}
  \right)+  \log\left(
    \begin{array}{ccc}
      \theta_{d,music}\times \phi_{music,soccer} \\
      + \\
      \theta_{d,sports}\times \phi_{sports,soccer}\\
    \end{array}
  \right)\\
&=&
  \log\left(
    \begin{array}{ccc}
      0.5\times 0.01 \\
      + \\
      0.5\times 0.01\\
    \end{array}
  \right)+  \log\left(
    \begin{array}{ccc}
      0.5\times 0.01 \\
      + \\
      0.5\times 0.01\\
    \end{array}
  \right)\\
&=& \log(0.01)+\log(0.01)=-4.0
\end{eqnarray}

次に尤度が大きくなるよう以下のように単語分布をサンプリングされたとする。(トピック分布は固定)

  • 単語分布(\phi_{k,w}
play soccer
music 0.01 0.01
sports 0.01 0.1
guitar 0.01 0.01
baseball 0.01 0.01
the 0.01 0.01

この場合、尤度は以下のように更新される。
\begin{eqnarray}
L_1 &=& \sum_{w=1}^W \log\left(\sum_{k=1}^K  \{p(\theta_{d,k})\times  p(\phi_{k,w})\}\right)\\
&=&
  \log\left(
    \begin{array}{ccc}
      \theta_{d,music}\times \phi_{music,play} \\
      + \\
      \theta_{d,sports}\times \phi_{sports,play}\\
    \end{array}
  \right)+  \log\left(
    \begin{array}{ccc}
      \theta_{d,music}\times \phi_{music,soccer} \\
      + \\
      \theta_{d,sports}\times \phi_{sports,soccer}\\
    \end{array}
  \right)\\
&=&
  \log\left(
    \begin{array}{ccc}
      0.5\times 0.01 \\
      + \\
      0.5\times 0.01\\
    \end{array}
  \right)+  \log\left(
    \begin{array}{ccc}
      0.5\times 0.01 \\
      + \\
      0.5\times 0.1\\
    \end{array}
  \right)\\
&=& \log(0.01)+\log(0.55)=-1.3
\end{eqnarray}
単語分布を更新したことで尤度が大きくなったことが確認できた。
※当然だが、機械は「スポーツ」トピックかは知らないので、逆になることもある。(計算結果、人間がこれは「スポーツ」と判断するに過ぎない。)

次に、単語分布を固定してトピック分布を以下のようにサンプリングし直す。

  • トピック分布(\theta_{d,k}
music 0.3
sports 0.7

尤度は以下のように更新される。
\begin{eqnarray}
L_1 &=& \sum_{w=1}^W \log\left(\sum_{k=1}^K  \{p(\theta_{d,k})\times  p(\phi_{k,w})\}\right)\\
&=&
  \log\left(
    \begin{array}{ccc}
      \theta_{d,music}\times \phi_{music,play} \\
      + \\
      \theta_{d,sports}\times \phi_{sports,play}\\
    \end{array}
  \right)+  \log\left(
    \begin{array}{ccc}
      \theta_{d,music}\times \phi_{music,soccer} \\
      + \\
      \theta_{d,sports}\times \phi_{sports,soccer}\\
    \end{array}
  \right)\\
&=&
  \log\left(
    \begin{array}{ccc}
      0.3\times 0.01 \\
      + \\
      0.7\times 0.01\\
    \end{array}
  \right)+  \log\left(
    \begin{array}{ccc}
      0.3\times 0.01 \\
      + \\
      0.7\times 0.1\\
    \end{array}
  \right)\\
&=& \log(0.01)+\log(0.73)=-1.1
\end{eqnarray}
さらに尤度が大きくなったことが確認できた。
トピック分布・単語分布は合計1の制約があるので、ある単語の出現確率を大きくすると別の単語の確率を小さくする必要がある。
では、どのように単語の割合を調整するのか。

例えば、最初の文書が

play soccer and baseball

だったとする。
この場合、sportsトピックにおけるsoccerの出現確率の増加分をどのように調整するのが良いか。簡単のため、baseball,guitar,theの何れかの出現確率を小さくして調整するとする。
baseballの確率を小さくした場合、この文書の尤度が小さくなるので、baseballは調整しない。一方、guitar,theの確率を小さくしてもこの文書の尤度には何ら影響はない。では、guitar,theであればどちらでもよいのか。結論から言うと、一般的にはguitarの確率を小さくようにサンプリングされる(勿論、これはコーパスによる)。この文書だけを見ると、どちらも尤度に影響を与えない。しかし、コーパス全体で見た時に、guitar,theはどちらの単語の出現確率が高いか。一般的にはtheの方が高い。なので、theの出現確率を下げると、尤度が小さくなる文書の数が多くなる。一方、guitarはそれほど多くの文書に出現するとは考えられないので尤度が下がる文書の数が限定される。文章全体の尤度を大きくすることを考えた場合、guitarの確率を小さくするのが良いと考えられる。
baseballは調整しないと書いたが、これは一般的は共起性での話であり、他の文書でbaseball以上にguitarがsoccerと共起する場合、baseballの確率が小さくなるよう調整するかもしれない。

サンプリングを文書全体で繰り返しすることで、同じトピックに高確率で出現する単語同士(soccerとbaseballなど)は「共起性」があると考えることが可能となる。
また、同じようなトピック分布をもつ文書同士は類似した文書である可能性が高いと考えることが可能となる。

さいごに



さいごに、Stanコードを載せておく。
Stan モデリング言語: ユーザーガイド・リファレンスマニュアルとほとんど同じであるが・・)

data {
  int<lower=2> K;               // num topics
  int<lower=2> V;               // num words
  int<lower=1> M;               // num docs
  int<lower=1> N;               // total word instances
  int<lower=1,upper=V> w[N];    // word n
  int<lower=1,upper=M> doc[N];  // doc ID for word n
  vector<lower=0>[K] alpha;     // topic prior
  vector<lower=0>[V] beta;      // word prior
}
parameters {
  simplex[K] theta[M];   // topic dist for doc m
  simplex[V] phi[K];     // word dist for topic k
}
model {
  for (m in 1:M)  
    theta[m] ~ dirichlet(alpha);  // prior
  for (k in 1:K)  
    phi[k] ~ dirichlet(beta);     // prior
  for (n in 1:N) {
    real gamma[K];
    for (k in 1:K) 
      gamma[k] = log(theta[doc[n], k]) + log(phi[k, w[n]]);
    target += log_sum_exp(gamma); // likelihood;
  }
}


以下のようにしてキックできる。

data <- read.csv("lda.csv")
stan.data = list(K=10, V=max(data$w), M=max(data$d), N=nrow(data),
                 w=data$w, doc=data$d, alpha=rep(0.1,10), beta=(0.1,max(data$w))
stan.fit <- stan(file="lda.stan", data=dat)

あまりよい推定結果は得られないかもしれないが、とりあえず動作検証にはなると思う。
テストデータは以下に置いた。
github.com