混合正規分布に対するEMアルゴリズムの解説記事を書いたので、pythonで実装してみます。色んな人がやってるのでコードは見る必要が無いかもしれません。最後に、EMアルゴリズムの一般化について触れます。EMアルゴリズムの解説記事はこちら。

EMアルゴリズムの復習
解こうとしている問題の整理と、EMアルゴリズムの復習をします。
データ
初めに、
その後、対数尤度
殆ど更新されなければ処理終了です。大きく更新されていれば、新しいパラメーターで
[E ステップ]
[M ステップ]
前に計算した量から、尤度と更新式を作ります。2回目以降は、以前の尤度と比較して、差が適当な数字
以上の事を、python で実装してみましょう。
EMアルゴリズムのpython による実装
まずは学習させたいデータを作りましょう。楽をするために、1次元正規分布が混合されているとします。
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
x1 = np.random.normal(loc=0.5, scale=1 , size =100).reshape(-1,1)
x2 = np.random.normal(loc=10, scale=2 , size =100).reshape(-1,1)
x3 = np.random.normal(loc=0, scale=3 , size =100).reshape(-1,1)
x = np.concatenate([x1 , x2 , x3])
sns.distplot( x )
plt.title("Gaussian Mixture Model")

次に、各種値を計算するための関数を定義します。1次元の正規分布なのでnumpy が良い感じに計算してくれます。
def Gaus(x,m, s): #正規分布の値を取得
g = np.exp( - pow((x-m), 2) /(2*s) )/np.sqrt(2*np.pi * s )
return g
def calc_gamma(x, pi, mu, sig):#事後分布の計算
gam = pi*Gaus(x,mu,sig)
gam/= np.sum(gam, axis=1).reshape(len(x),1)
return gam
def update_parmas(gamma, x, pi, mu, sig):#パラメーターの更新式
N_k = np.sum(gamma, axis=0)
N = np.sum(N_k)
mu_k = np.sum(x*gamma, axis=0 ) /N_k
sig_k = np.sum(gamma* pow(x-mu, 2), axis=0) /N_k
pi_k = N_k/N
return pi_k , mu_k, sig_k
def iteration(x,mu,sig,pi, I=100, e=0.01): #ε以下になるか、100回計算するまで尤度を更新する関数
LF=0
for i in range(I):
gamma = calc_gamma(x, pi, mu, sig)
LF_new =np.sum(np.log(np.sum(pi*Gaus(x,mu,sig),axis=1 )) )
ch = LF_new - LF
print("LF ={} . change = {}".format(LF_new, ch))
if np.abs(ch) < e:
print("Iteration is finished {} iter. ".format(i+1))
break
LF=LF_new
pi, mu, sig = update_parmas(gamma, x, pi, mu, sig)
return pi, mu, sig
これらの関数を使って、適当な初期値を与えて計算してみます。今回は計算の上限は100回で、
mu =np.array([0,10,3])
sig=np.array([1, 5, 10])
pi=np.array([0.1,0.4, 0.5])
pi, mu, sig = iteration(x,mu,sig, pi, I=100)
''''
LF =-892.8443809235707 . change = -892.8443809235707
LF =-840.4323250187838 . change = 52.41205590478694
LF =-832.8909016444818 . change = 7.541423374301985
LF =-830.0445451648682 . change = 2.8463564796136325
LF =-828.5494151559992 . change = 1.495130008868955
LF =-827.7057967180053 . change = 0.8436184379938823
LF =-827.2196118733798 . change = 0.4861848446255408
LF =-826.9346382849101 . change = 0.2849735884697111
LF =-826.761394736974 . change = 0.17324354793606744
LF =-826.650058986429 . change = 0.11133575054498124
LF =-826.5737961470172 . change = 0.076262839411811
LF =-826.5182732201362 . change = 0.055522926881053536
LF =-826.4757321464656 . change = 0.042541073670577134
LF =-826.4418517419456 . change = 0.03388040452000496
LF =-826.4141241860787 . change = 0.027727555866931652
LF =-826.391016011429 . change = 0.023108174649678404
LF =-826.371531020618 . change = 0.019484990810951786
LF =-826.3549798326762 . change = 0.01655118794178634
LF =-826.3408564862484 . change = 0.01412334642782298
LF =-826.3287709947581 . change = 0.012085491490324785
LF =-826.3184114191067 . change = 0.010359575651364139
LF =-826.3095216889953 . change = 0.008889730111377503
Iteration is finished 22 iter.
''''
print(pi, mu, np.sqrt(sig) )
#π [0.32736572 0.32643642 0.34619786]
#μ [0.45400037 9.86360541 -0.01145557]
#Σ [0.94493112 2.12668499 3.03373937]
初めに与えたデータは,
y0 = np.random.normal(loc=mu[0], scale=np.sqrt(sig)[0] , size =int(300*pi[0]) ).reshape(-1,1)
y1 = np.random.normal(loc=mu[1], scale=np.sqrt(sig)[1] , size =int(300*pi[1]) ).reshape(-1,1)
y2 = np.random.normal(loc=mu[2], scale=np.sqrt(sig)[2] , size =int(300*pi[2]) ).reshape(-1,1)
y=np.concatenate([y0, y1, y2])
sns.distplot(y)
plt.title("Predicted GMM")

大体同じ形のグラフが書けたので、めでたしめでたしです。
EMアルゴリズムの一般化
別の記事の予告になりますが、イエンセンの不等式で、一般的な状況で(周辺化)対数尤度の評価が与えられます。
最後の
もう少し計算が進められます。
対数尤度と、その下限の差は
確率分布の近似手法に平均場近似があります。その記事は以下からどうぞ。

まとめ
- EMアルゴリズムのまとめをした
- python 上で関数を定義し、EMアルゴリズムを実装した
- EMアルゴリズムを一般化した手法がある