混合多項分布のEMアルゴリズム・その2 | ぽんのブログ

ぽんのブログ

自分用の備忘録ブログです。書いてある内容、とくにソースは、後で自分で要点が分かるよう、かなり簡略化してます(というか、いい加減)。あまり信用しないように(汗

前回の混合多項分布のEMアルゴリズムに則りテストするためのプログラムを作ってみました。

まずはユーティリティ群を。
#include <gsl gsl_math.h>
#include <gsl gsl_rng.h>
#include <gsl gsl_randist.h>

const gsl_rng    *_rng_;

/*** ユーティリティー ***/
/* ベクトルの和をとる */
double
_sum (int n, double *vector)
{
    int       i;
    double    sum = 0.;
    for (i = 0; i < n; i++) sum += vector[i];
    return sum;
}

/* ベクトルをその和で規格化する */
void
_normalize (int n, double *vector)
{
    int       i;
    double    sum = _sum (n, vector);
    for (i = 0; i < n; i++) vector[i] /= sum;
    return;
}

/* ベクトルを出力 */
void
fprintf_vector (FILE *stream, int n, double *vector, char *format)
{
    int       i;
    for (i = 0; i < n; i++) {
        fprintf (stream, format, vector[i]);
        if (i < n - 1) fprintf (stream, "\t");
    }
    fprintf (stream, "\n");
   return;
}

/* 行列を出力 */
void
fprintf_matrix (FILE *stream, int nrow, int ncol, double **matrix, char *format)
{
    int       i;
    for (i = 0; i < nrow; i++) fprintf_vector (stream, ncol, matrix[i], format);
    return;
}

これらでベクトル(一次元配列)の和の計算、ベクトルをその和で規格化、ベクトルの出力及び行列(2次元配列)の出力を行います。
次にEMアルゴリズムの部分。
/*** EMアルゴリズム ***/
/* 対数尤度の条件付き期待値 */
double
log_likelihood (int ncls, int nitems, double *xi, double **z, int **x, double **theta)
{
    int       i, k;
    double    ll = 0.;
    // ll = sum_i sum_k { z[i][k] * log (xi[k] * f (x[i] | theta[k]) ) }
    for (i = 0; i < nitems; i++) {
        for (k = 0; k < ncls; k++) {
            ll += z[i][k] * (gsl_ran_multinomial_lnpdf (nitems, theta[k], x[i]) + log (xi[k]));
        }
    }
    return ll;
}

/* Eステップ */
void
E_step (int n, int ncls, int nitems, double *xi, double **z, int **x, double **theta)
{
    int        i, k;
    for (i = 0; i < n; i++) {
        for (k = 0; k < ncls; k++) {
            // z[i][k] = xi[k] * f(x[i] | theta[k])
            z[i][k] = xi[k] * gsl_ran_multinomial_pdf (nitems, theta[k], x[i]);       
        }
        // z[i][k] = z[i][k] / sum_k z[i][k]
        _normalize (ncls, z[i]);
    }
    return;
}

/* Mステップ */
void
M_step (int n, int ncls, int nitems, double *xi, double **z, int **x, double **theta)
{
    int       i, k, l;
    for (k = 0; k < ncls; k++) {
        /* theta の更新 */
        // theta[k][l] = sum_i (z[i][k] * x[i][l])
        for (l = 0; l < nitems; l++) {
            theta[k][l] = 0.;
            for (i = 0; i < n; i++) theta[k][l] += z[i][k] * ((double) x[i][l]);       
        }
        // theta[k] = theta[k] / sum_l theta[k][l]
        _normalize (nitems, theta[k]);

        /* xi の更新 */
        // xi[k] = sum_i z[i][k] / n
        xi[k] = 0.;
        for (i = 0; i < n; i++) xi[k] += z[i][k];
        xi[k] /= (double) n;
    }
    return;
}

一番目の関数で対数尤度の条件付期待値を計算します。この時、gsl の関数

double    gsl_ran_multinomial_lnpdf (size_t K, const double p[], const unsigned int x[]);

という関数を使っています。
これはパラメータ const double p[] (次元は size_t K)の多項分布でデータ const unsigned int x[] が得られる密度関数の log を計算します。
ちなみに

double    gsl_ran_multinomial_pdf (size_t K, const double p[], const unsigned int x[]);

が密度関数、

void       gsl_ran_multinomial_lnpdf (const gsl_rng *r, size_t K, unsigned int  n, const double p[], unsigned int x[]);

が多項分布の乱数発生器になります。
つづいて入力とするデータを作成する部分

/*** テストデータ作成用 ***/
/* パラメータを作成 (nitems 次元ベクトルを ncls 個分) */
double **
create_parameter (int ncls, int nitems)
{
    int           k, l;
    double        **theta = (double **) malloc (ncls * sizeof (double *));
    for (k = 0; k < ncls; k++) {
        theta[k] = (double *) malloc (nitems * sizeof (double));
        for (l = 0; l < nitems; l++) theta[k][l] = gsl_ran_flat (_rng_, 0., 1.);
        _normalize (nitems, theta[k]);
    }
    return theta;
}

/* データ作成 (nitems 次元ベクトルを n 個分) */
int **
create_data (int n, int ncls, int nitems, int *m, double *xi, double **theta)
{
    int        i, k;
    int        **x;

    x = (int **) malloc (n * sizeof (int *));
    for (i = 0; i < n; i++) {
        // 混合率 xi に応じて多項分布(パラメータを指定する k の値)を選ぶ
        double    r = gsl_ran_flat (_rng_, 0., 1.);
        for (k = 0; k < ncls; k++) {
            if (r < xi[k]) break;
            r -= xi[k];
        }
        x[i] = (int *) malloc (nitems * sizeof (int));
        gsl_ran_multinomial (_rng_, nitems, m[i], theta[k], x[i]);
    }
    return x;
}
1番目の関数で、コンポーネント数 ncls 個分の多項分布のパラメータを作成し、2次元配列に格納します。
その際、各々の多項分布のパラメータの和が 1 になるよう規格化しています。

2番目の関数で混合多項分布に従うデータを作成しています。
与えられた混合比に応じてパラメータを選び、乱数を生成して2次元配列に入れて返します。

最後にメイン関数。
int
main (void)
{
    int       ncls = 3;      // コンポーネント数 (K)
    int       nitems = 4;     // アイテム数 (L)
    int       n = 1000;      // データセット数 (N)

    int       **x;           // データ

    double    **theta;        // パラメータ
    double    *xi;            // 混合比
    double    **z;            // 事後確率

    _rng_ = gsl_rng_alloc (gsl_rng_default); // gslの乱数初期化

    // 混合多項分布に従うデータの作成
    {
        int       i;
        double    **theta0 = create_parameter (ncls, nitems); // 各多項分布のパラメータ
        double    xi0[3] = {0.2, 0.5, 0.3};                   // 混合率
        int       *m0;                                        // 試行回数 ( m0[i] = sum_l x[i][l] )

        m0 = (int *) malloc (n * sizeof (int));
        for (i = 0; i < n; i++) m0[i] = 500 + gsl_rng_uniform_int (_rng_, 100); // 500~600回
        x = create_data (n, ncls, nitems, m0, xi0, theta0);

        fprintf (stdout, "xi0 :\n");
        fprintf_vector (stdout, ncls, xi0, "%.3f");
        fprintf (stdout, "\ntheta0 :\n");
        fprintf_matrix (stdout, ncls, nitems, theta0, "%.3f");
    }

    // 領域確保と初期値設定
    {
        int       i, k, l;

        theta = (double **) malloc (ncls * sizeof (double *));
        for (k = 0; k < ncls; k++) {
            theta[k] = (double *) malloc (nitems * sizeof (double));
            for (l = 0; l < nitems; l++) theta[k][l] = 1. / (double) nitems;
        }

        z = (double **) malloc (n * sizeof (double *));
        for (i = 0; i < n; i++) {
            z[i] = (double *) malloc (ncls * sizeof (double));
            for (k = 0; k < ncls; k++) z[i][k] = 0.;
        }

        xi = (double *) malloc (ncls * sizeof (double));
        for (k = 0; k < ncls; k++) xi[k] = gsl_ran_flat (_rng_, 0., 1.);
        _normalize (ncls, xi);
    }

    // EMアルゴリズムを実行
    {
        int       iter = 0;
        double    l_prev;
        double    l = -GSL_DBL_MAX;

        while (1) {
            l_prev = l;
            E_step (n, ncls, nitems, xi, z, x, theta);
            l = log_likelihood (ncls, nitems, xi, z, x, theta);
            if (fabs (l - l_prev) < 1.e-16 || ++iter >= 1000) break;
            M_step (n, ncls, nitems, xi, z, x, theta);
        }
        fprintf (stdout, "\niter = %d, l = %f\n\n", iter, l);
        fprintf (stdout, "xi :\n");
        fprintf_vector (stdout, ncls, xi, "%.3f");
        fprintf (stdout, "\ntheta :\n");
        fprintf_matrix (stdout, ncls, nitems, theta, "%.3f");
    }
    return 0;
}
これを動かした結果は

<真のパラメータ>
xi0 :
0.200    0.500    0.300

theta0 :
0.418    0.068    0.118    0.396
0.096    0.201    0.396    0.308
0.200    0.274    0.282    0.244

iter = 15, l = -41.475837

<計算結果>
xi :
0.498    0.317    0.185

theta :
0.096    0.200    0.396    0.309
0.201    0.275    0.281    0.243
0.419    0.066    0.119    0.396

となりました。
でも少し条件を変えると収束が遅くなったり、更新されなくなったりします。
そういうものなのか、或いは何処か間違ってるのか・・・多分後者だと思う(汗