名前はまだない

データ分析とかの備忘録か, 趣味の話か, はたまた

ゼロ過剰ポアソン非負値行列因子分解をRstanで

はじめに

こちらの本を読みました。

こちらには非負値行列因子分解(NMF)の話も出てきます。

前から、各要素がガウス分布ポアソン分布に従うとする場合が多いですが、ゼロ過剰ポアソン分布に従うとする方が適切だろうと思う場合が多かったです。

そのため、各要素がゼロ過剰ポアソン分布に従う非負値行列因子分解を考えて、Rstanで実装してみました。

ZIP NMF

NMF

非負値行列因子分解(NMF : Nonnegative Matrix Factorization)とは、各要素が非負値から構成されている行列\rm{X}\in \mathbb{R}^{D\times N}を、\rm{W}\in \mathbb{R}^{D\times M}\rm{H}\in \mathbb{R}^{M\times N}の二つの行列に分解する解析手法です。

 \displaystyle{
X_{d,n}\approx\sum_{m=1}^M W_{d,m}H_{m,n}
}

データを低次元空間に写像する方法であるとも呼ばれています。

ZIP

ゼロ過剰ポアソン分布(Zero-Inflated Poisson model)とは、0が多いポアソン分布のことです。

具体的には、ベルヌーイ分布により0のみの分布?とポアソン分布が切り替わる混合分布になっています。

 \displaystyle{
\begin{eqnarray}
ZIP(x | q, \lambda) \sim  
  \left\{
    \begin{array}{l}
Bern(0|q) +  Bern(1|q)\times Poi(y=0|\lambda) & \:  if \: x=0 \\ 
Bern(1|q) \times Poi(y|\lambda) & \: if \: x>0 

    \end{array}
  \right.

\end{eqnarray}
}

ここで特筆すべきなのは、0の中にはベルヌーイ分布により発生したされた0と、ポアソン分布から発生した0が混ざっていることです。

ZIP NMF

\rm{X}の各要素がZIPに従うと仮定したNMFを考えます。

確率\thetaで0、1-\thetaポアソン分布のNMFにあるようなNMFであり、次のような確率分布の式で表現することができます。

 \displaystyle{
\begin{eqnarray}
p(X_{d,n}) \sim  
  \left\{
    \begin{array}{l}
Bern(0|\theta_{d,n}) +  Bern(1|\theta_{d,n})\times Poi(y=0|\sum_{m=1}^M W_{d,m}H_{m,n}) & \:  if \: X_{d,n}=0 \\ 
Bern(1|\theta_{d,n}) \times Poi(y|\sum_{m=1}^M W_{d,m}H_{m,n}) & \: if \: X_{d,n}>0 

    \end{array}
  \right.

\end{eqnarray}
}

[tex: W{d,m} ]と[tex: H{m,n} ]は2つの行列に分解された際の各要素であり、ガンマ分布に従っているとします。

 \displaystyle{
\begin{eqnarray}
p(W_{d,m})\sim{\rm Gam}(W_{d,m}|a,b) \\
p(H_{m,n})\sim{\rm Gam}(H_{d,m}|a,b)
\end{eqnarray}
}

Rstanでやってみる

使うデータはこんな感じのものです。

library(tidyverse)
library(rstan)
library(Rcpp)
library(bayesplot)

# 並列化のおまじない
rstan_options(auto_write=TRUE)
options(mc.cores=parallel::detectCores())

mat = c(5,0,2,2,2,3,0,0,2,0,
        3,2,0,1,0,3,0,1,0,3,
        4,3,0,0,0,5,0,0,2,0,
        1,0,2,3,1,4,1,0,2,0,
        2,0,4,2,3,0,0,2,0,0,
        1,0,1,3,2,4,0,0,0,1)
dat = matrix(mat, 10,6) %>% t

分布を見ると、いい感じに0過剰ポアソン分布に従うようになっています。

data.frame(X = mat) %>% 
  ggplot(aes(x=X))+
  geom_histogram(binwidth = 1, alpha=0.5) + 
  ggtitle("Data histogram for NMF")

f:id:saltcooky:20200617234217p:plain

これに対して、ポアソン分布を仮定したNMFをこちらの記事のStanコードを用いて実行してみました。

潜在因子数は3としました。

復元されたデータの分布は次にようになりました。

0過剰の部分が表現されていません。

f:id:saltcooky:20200618014820p:plain

復元誤差(RMSE)は次にようになりました。 (stanオブジェクトからパラメータをtidyに扱う方法が見つからなかったので、良い方法があれば教えていただきたいです。)

> (summary(stan.fit)$summary[K:(K+length(mat)-1),"50%"] -mat)^2 %>% mean() %>% sqrt
[1] 0.9831921

ここからが、ZIP NMFです。

上記のZIP NMFをstanでモデル化したのは次のような感じです。

data{
  int<lower=1> D;
  int<lower=1> N;
  int<lower=1> M;
  
  int X[D,N]; 
}

parameters{
  matrix<lower=0>[D, M] W;
  matrix<lower=0>[M, N] H;
  matrix<lower=-10, upper=10>[D, N] theta;
  
  real<lower=0> alpha;
  real<lower=0> beta; 
}

model{  
  alpha ~ gamma(1, 2); 
  beta ~ gamma(1,2);

  for(d in 1:D){
    for(n in 1:N){
      theta[d,n] ~ uniform(-10,10);
    }
  }
  
  for(d in 1:D){
    for(m in 1:M){
      W[d,m] ~ gamma(alpha, beta);
    }
  }

  for(m in 1:M){
    for(n in 1:N){
      H[m,n] ~ gamma(alpha, beta);
    }
  }
  
  for(d in 1:D){
      for(n in 1:N){
        if(X[d,n] == 0){
          target += log_sum_exp(
              bernoulli_lpmf(0|inv_logit(theta[d, n])),
              bernoulli_lpmf(1|inv_logit(theta[d, n])) + poisson_lpmf(0|W[d,:]*H[:,n])
          );
        }else {
          target += bernoulli_lpmf(1|inv_logit(theta[d, n])) + poisson_lpmf(X[d,n]|W[d,:]*H[:,n]);
        }
      }
  }
}

generated quantities {
   int Xpred[D,N];
   real Xlogit[D,N];
   for(d in 1:D){
       for(n in 1:N){
          Xlogit[d,n] = inv_logit(theta[d, n]);          
          if(Xlogit[d,n] < 0.5){
             Xpred[d,n] =  0;
          }else{
             Xpred[d,n] = poisson_rng(W[d,:]*H[:,n]);
          }          
       }
   }
}

キックするコードは次にような感じです。 潜在因子数は3としました。

run_data <- list(D=nrow(dat),N=ncol(dat),M=3,X=dat)

stan.fit <- stan("ZIP_NMF.stan", 
                 data = run_data,
                 seed = 42,
                 iter = 4000,
                 warmup = 2000,
                 chains = 4)

収束したか見てみます。

mcmc_rhat(rhat(stan.fit))

ちゃんと収束しているようにですね f:id:saltcooky:20200614184052p:plain

サンプリングに自己相関がないことも確認しました。

復元された値の分布を確認してみました。

0過剰の部分が表現できているようです。

f:id:saltcooky:20200620000856p:plain

復元誤差も確認してみました。

> (summary(stan.fit)$summary[K:(K+length(mat)-1),"50%"] -mat)^2 %>% mean() %>% sqrt
[1] 0.8164966

単にポアソン分布を仮定した場合よりも当てはまりが良い状態にすることができました。

0の発生の潜在構造を捉えていないため、ベルヌーイ分布のパラメータの表列を行列分解により表現するするように拡張する必要があるかなと感じました。

参考