EMアルゴリズム入門
テックタヌキです。
皆さんは「EMアルゴリズム」という言葉を聞いたことがありますか?本記事では、私が日々のデータ分析業務を通じて学んだ、この強力な統計的手法についてご紹介します。
本記事では、EMアルゴリズムの基本概念から実際の応用例まで、できるだけわかりやすく解説していきます。もし記事の中で不明点や疑問があれば、ぜひコメントでお知らせください。
それでは、次から本編です!
この記事に書いてあること
- EMアルゴリズムの概要
- 混合正規モデルのパラメータ推定
EMアルゴリズムのモチベーション
EMアルゴリズム(expectation-maximization algorithm)は、確率モデルのパラメータに対する最尤推定を行う方法の一つです。
EMアルゴリズムの前に、指数分布のパラメータ推定の例を考えてみましょう。
このとき、未知なパラメータ
実際に、
これを
です。これを0と置いて、
となります。つまり、最尤推定によって得られた、未知のパラメータ
ここまでの手順を簡単に整理すると、
- 尤度関数を計算する。
- (対数)尤度関数を微分して、最大となる点をもとめる。
となります。しかし尤度関数が複雑の場合、微分しても陽に解けないこともありそうです。このようなときに、威力を発揮するのが、EMアルゴリズムです。
混合正規分布モデル
EMアルゴリズムについて考えるために、本記事では混合正規分布モデルについて考えてみましょう。
構成されるクラス数をK、構成比率を
ここで、
対数尤度を考えてみると、
となります。[4][5]
先ほどの手順に従うと、
一般的なEMアルゴリズムの導入
一般的なEMアルゴリズムについて考えていきましょう。[6]元々の目的は、
ここで、この
つまり、
で、
潜在変数
ここで、潜在変数というものを考えてみましょう。
潜在変数とは、直接は観察されませんが、観測データを制御する何らかの確率変数
これはXとの同時分布を考えることでモデル化します。[8]
この式を、上の式に代入すると、
になります。
差の評価
この差を評価したいです。zについての確率の和は1で、log関数が凸関数であることに着目すると、イェンセンの不等式が使えそうです。
と評価できました。つまり以下のように書けます。
つまり、
また、
となるので、
そのため、尤度関数の最大化の目的のためにこのような補助関数
一応、私が思うこの補助関数の良い点を整理しておきます。
-
は尤度関数の値l(\theta_n|\theta_n) と一致するので、良いL(\theta_n) を見つけることさえできれば尤度の実現値となる\theta_n -
は上に有界で、l(\theta|\theta_n) で抑えられているので、L(\theta) の更新に都合がいい(後述)\theta_n
\theta_n の更新
次に、
実際に、計算してみましょう。
よって、潜在変数ZとXの同時分布の対数尤度に対するzの条件付き期待値が最大となるような
これらの手順を以下のように分けて、EMアルゴリズムと呼びます。
- Eステップ: 条件付き期待値、
を決定する。E_{z|X, \theta_n}[\log p(X,z|\theta)]=:Q(\theta|\theta_n) - Mステップ:
をQ(\theta|theta_n) に関して、最大化して\theta を求める。\theta_{n+1}
これらのステップを
混合正規分布モデルに対するEMアルゴリズム
一般論から混合正規分布モデルに戻って考えましょう。
ここで、
zはXがどのクラスに属するかを決めるので、n個の確率変数
また、どのクラスに入るかが決まればどの確率密度関数に従うかがきまるので、以下のように条件付き確率密度関数を考えることができます。
ここで、
すると、対数尤度は、
となります。潜在変数を導入することで、
Eステップ
ここで、ベイズの定理を使うと
を負担率と表現するそうです。
Mステップ
Eステップで求めた、
あまり本質的ではないので、求めた結果を以下に載せます。[11]
シミュレーション
シミュレーション条件
K=3 N=1000 \hat{p}=(0.5, 0.2, 0.3) \hat{\mu}=(-1, 0, 1) \hat{\sigma}^2=(0.2, 1, 0.3) - イテレーション回数:1000[12]
の設定でやってみました。データをサンプルした結果とサンプルを作るスクリプトは以下のようになります。
import numpy as np
class GetMixtureGaussSample:
def __init__(self, K=3, N=1000, p=(0.1, 0.7, 0.2), mu=(-1, 0, 1), v = (0.2, 1, 0.5)):
# 混合正規分布のデータを取得するクラス
# チェック
assert K == len(p), "Kとpの長さが一致しません"
assert K == len(mu), "Kとmuの長さが一致しません"
assert K == len(v), "Kとvの長さが一致しません"
assert np.isclose(sum(p), 1), "確率の合計が1になっていません"
# p_i * N が整数になるように設定すること
for i in range(K):
assert np.isclose((p[i]*N) % 1, 0), f"p[{i}] * N が整数になるように設定されていません"
self.K = K
self.N = N
self.p = np.array(p)
self.mu = np.array(mu)
self.v = np.array(v)
def sample(self) -> np.ndarray:
# 混合正規分布モデルからのサンプル
data = np.concatenate([
np.random.normal(loc=self.mu[i], scale=np.sqrt(self.v[i]), size=int(self.p[i] * self.N))
for i in range(self.K)
])
np.random.shuffle(data) # データをシャッフル
return data
EMアルゴリズムのソースコードは以下のようになります。
各パラメータの初期値は、一般的かわかりませんが、Kmeansによって決めました。[13]
class EMAlgorithm:
def __init__(self, N=1000, K=3, iteration=2000, history=True):
self.K = K
self.N = N
self.iteration = iteration
if history:
# 履歴を保存するなら、履歴用のリストを作っておく
self.mu_history = np.zeros((self.iteration+1, self.K))
self.v_history = np.zeros((self.iteration+1, self.K))
self.p_history = np.zeros((self.iteration+1, self.K))
def init_em(self, data):
# 初期化する
self.data = data
# K-means法でクラスタリング
kmeans = KMeans(n_clusters=self.K, n_init=10)
kmeans.fit(data.reshape(-1, 1))
# 初期パラメータの設定
self.mu = kmeans.cluster_centers_.flatten()
self.v = np.array([np.var(data[kmeans.labels_ == k]) for k in range(self.K)])
self.p = np.bincount(kmeans.labels_) / self.N
def e_step(self):
# 尤度は計算する必要はなく、負担率eta_nkさえわかればOK
self.eta_nk = np.zeros((self.N, self.K))
for k in range(self.K):
self.eta_nk[:,k] = self.p[k] * norm.pdf(self.data, self.mu[k], np.sqrt(self.v[k]))
self.eta_nk /= self.eta_nk.sum(axis=1, keepdims=True)
def m_step(self):
# 負担率をもとに、パラメータ更新
sum_eta_n = self.eta_nk.sum(axis=0)
self.mu = np.dot(self.eta_nk.T, self.data) / sum_eta_n
self.p = sum_eta_n / self.N
for k in range(self.K):
self.v[k] = np.sum(self.eta_nk[:, k] * (self.data - self.mu[k])**2) / sum_eta_n[k]
def simulation(self, data):
# simulation
self.init_em(data)
self.save_history(0)
for i in tqdm(range(self.iteration)):
self.e_step()
self.m_step()
self.save_history(i+1)
print('p:', self.p)
print('m:', self.mu)
print('v:', self.v)
def save_history(self, i):
# 履歴を保存する。
self.mu_history[i, :] = self.mu
self.v_history[i, :] = self.v
self.p_history[i, :] = self.p
それぞれのパラメータの推移のグラフは以下です。
パラメータは収束していますが、iterationの1200回あたりの妙な動きが気になりました。およその収束値は以下のようになります。構成比率、平均、分散の順に記載します。
- 分布1: (0.04, 0.12, 0.03)
- 分布2: (0.584, -0.98, 0.20)
- 分布3: (0.38, 0.99, 0.29)
真の分布についても再掲します。
- 分布1: (0.2, 0, 1)
- 分布2: (0.5, -1, 0.2)
- 分布3: (0.3, 1, 0.3)
両端の分布の平均と分散はおおむね合致していますが、構成比率や真ん中の分布は正確に当てるのが難しいようです。真ん中の分布は両隣から大きな影響を受けるので、なかなか難しいというだと思いますが、数理的に何か言えるかはわかりません。
ヒストグラムに重ねがきしてみると以下のようになります。
分布推定としてみると、割と合っているような気もします。
まとめ
今回は、EMアルゴリズムを使って、混合正規分布のパラメータ推定をしてみました。本質的に最も重要なポイントは複雑な尤度関数が潜在変数を導入することによって、推定しやすい形に変形できることです。
最後になりますが、何かおかしいところがあればコメントもらえると助かります。
-
とかくことも多いですが、\partial は定数と思っているのでx_i でいいかなと思います。 ↩︎d -
対数尤度関数の
に関する2階微分は-\lambda なので常に負です(1/\lambda^2 )。そのため、対数尤度関数は凹関数です。よって、微分が0になる点が対数尤度関数が最大となる\lambda>0 です。 ↩︎\lambda -
指数分布に従う確率変数の期待値が
になっていることから、納得しやすい結果ですね。 ↩︎\lambda -
はすべてのパラメータ\theta をひとまとめにしたものです。 ↩︎p_1, \cdots, p_K, \sigma^2_1, \cdots, \sigma_K^2, \mu_1, \cdots, \mu_K -
ベイズで混合分布の推定をしたことがある方はこの積と和の形が出てこないので、(ベイズでは尤度関数の最大化はしませんが)戸惑ったかもしれません。 ↩︎
-
混合指数分布については一般の場合を導出してから考えます。 ↩︎
-
ここから、Lは対数尤度関数とします。 ↩︎
-
連続の場合は、積分ですが測度論によってどちらでも正当化されるので、区別しません。 ↩︎
-
収束性の議論はさぼります。 ↩︎
-
今後、
を\theta_n 、\theta_{old} を\theta_{n+1} に読み替えます。 ↩︎\theta_{new} -
線形制約のため、凸制約になります。また凸制約のもとでの、凹関数の最大化問題は少なくとも1つの最適解をもちます。また凹関数の局所最大化は同時に大域的最大値でもあるので、最大値をもちます。
は対数関数の重ね合わせなので凹関数といっていいと思います。間違ってたら教えてください。 ↩︎Q(\theta) -
賢い終了条件がありそうですが、今回はさぼります。 ↩︎
-
初期値の決め方は、平均を各クラスタの平均値、分散を各クラスタ内分散、構成割合をクラス内に属するデータの比率としました。詳しくはコードを見てください。 ↩︎
Discussion