はじめに
『ベイズ推論による機械学習入門』の学習時のノートです。基本的な内容は「数式の行間を読んでみた」とそれを「RとPythonで組んでみた」になります。「数式」と「プログラム」から理解するのが目標です。
この記事は、3.3.1項の内容です。「尤度関数を平均が未知の1次元ガウス分布(正規分布)」、「事前分布を1次元ガウス分布」とした場合の「平均パラメータの事後分布」と「未観測値の予測分布」の計算をPythonで実装します。
省略してある内容等ありますので、本とあわせて読んでください。初学者な自分が理解できるレベルまで落として書き下していますので、分かる人にはかなりくどくなっています。同じような立場の人のお役に立てれば幸いです。
【数式読解編】
【他の節の内容】
【この節の内容】
・Pythonでやってみよう
人工的に生成したデータを用いて、ベイズ推論を行ってみましょう。
利用するライブラリを読み込みます。
# 3.3.1項で利用するライブラリ import numpy as np from scipy.stats import norm # 1次元ガウス分布 import matplotlib.pyplot as plt
この例では、1次元ガウス分布の確率密度を求めるのにSciPy
ライブラリのnorm.pdf()
を利用します。
・モデルの構築
まずは、モデルを設定します。
尤度(ガウス分布)のパラメータを設定します。
# 真のパラメータを指定 mu_truth = 25 lmd = 0.01
平均パラメータ$\mu$をmu_truth
として値を指定します。これが真のパラメータであり、この値を求めるのがここでの目的です。
精度パラメータ$\lambda$をlmd
として値を指定します。こちらは与えられている値として使います。精度は分散の逆数なので、値が大きいほど散らばり具合が小さくなり、逆数の平方根が標準偏差$\sigma = \sqrt{\lambda^{-1}}$になります。
尤度の確率密度を計算します。
# 作図用のxの値を設定 x_line = np.linspace( mu_truth - 4 * np.sqrt(1 / lmd), mu_truth + 4 * np.sqrt(1 / lmd), num=1000 ) # 尤度を計算:式(2.64) ln_C_N = - 0.5 * (np.log(2 * np.pi) - np.log(lmd)) # 正規化項(対数) true_model = np.exp(ln_C_N - 0.5 * lmd * (x_line - mu_truth)**2) # 確率密度 # 尤度を計算:SciPy ver #true_model = norm.pdf(x=x_line, loc=mu_truth, scale=np.sqrt(1 / lmd)) # 確率密度
作図用に、ガウス分布に従う変数$x_n$がとり得る値をnp.linspace()
で作成してx_line
とします。この例では、平均値を中心に標準偏差の4倍を範囲とします。np.linspace()
を使うと指定した要素数で等間隔に切り分けます。np.arange()
を使うと切り分ける間隔を指定できます。処理が重い場合は、この値を調整してください。
x_line
の値ごとに確率密度を計算します。1次元ガウス分布の確率密度は、対数をとった定義式
で計算して、最後にnp.exp()
をします。scipy.stats
のガウス分布の確率密度関数norm.pdf()
でも計算できます。
計算結果を確認しましょう。
# 確認 print(x_line[:10]) print(true_model[:10])
[-15. -14.91991992 -14.83983984 -14.75975976 -14.67967968
-14.5995996 -14.51951952 -14.43943944 -14.35935936 -14.27927928]
[1.33830226e-05 1.38182046e-05 1.42666228e-05 1.47286482e-05
1.52046611e-05 1.56950518e-05 1.62002199e-05 1.67205753e-05
1.72565380e-05 1.78085384e-05]
これを使って作図します。
尤度を作図します。
# 尤度を作図 plt.figure(figsize=(12, 9)) plt.plot(x_line, true_model, color='purple') # 尤度 plt.xlabel('x') plt.ylabel('density') plt.suptitle('Gaussian Distribution', fontsize=20) plt.title('$\mu=' + str(mu_truth) + ', \lambda=' + str(lmd) + '$', loc='left') plt.show()
真のパラメータを求めることは、この真の分布を求めることを意味します。
・データの生成
続いて、構築したモデルに従って観測データ$\mathbf{X} = \{x_1, x_2, \cdots, x_N\}$を生成します。
ガウス分布に従う$N$個のデータをランダムに生成します。
# (観測)データ数を指定 N = 50 # ガウス分布に従うデータを生成 x_n = np.random.normal(loc=mu_truth, scale=np.sqrt(1 / lmd), size=N)
生成するデータ数$N$をN
として値を指定します。
ガウス分布に従う乱数は、np.random.normal()
で生成できます。平均の引数loc
にmu_truth
、標準偏差の引数scale
にnp.sqrt(1 / lmd)
、試行回数の引数size
にN
を指定します。生成したN
個のデータをx_n
とします。
観測したデータを確認しましょう。
# 観測データを確認 print(x_n[:5])
[14.18201367 30.80838773 16.07128915 14.48499972 10.89531313]
観測データをヒストグラムでも確認します。
# 観測データのヒストグラムを作図 plt.figure(figsize=(12, 9)) plt.hist(x=x_n, bins=50) # 観測データ plt.xlabel('x') plt.ylabel('count') plt.suptitle('Gaussian Distribution', fontsize=20) plt.title('$N=' + str(N) + ', \mu=' + str(mu_truth) + ', \sigma=' + str(np.sqrt(1 / lmd)) + '$', loc='left') plt.show()
データ数が十分に大きいと、分布の形状が真の分布に近づきます。
・事前分布の設定
次に、尤度に対する共役事前分布を設定します。
$\mu$の事前分布(ガウス分布)のパラメータ(超パラメータ)を設定します。
# muの事前分布のパラメータを指定 m = 0 lambda_mu = 0.001
ガウス分布のパラメータ$m,\ \lambda_{\mu}$をそれぞれm, lambda_mu
として値を指定します。
$\mu$の事前分布の確率密度を計算します。
# 作図用のmuの値を設定 mu_line = np.linspace(mu_truth - 50, mu_truth + 50, num=1000) # muの事前分布を計算:式(2.64) ln_C_N = - 0.5 * (np.log(2 * np.pi) - np.log(lambda_mu)) # 正規化項(対数) prior = np.exp(ln_C_N - 0.5 * lambda_mu * (mu_line - m)**2) # 確率密度 # muの事前分布を計算:SciPy ver #prior = norm.pdf(x=mu_line, loc=m, scale=np.sqrt(1 / lambda_mu)) # 確率密度
作図用に、ガウス分布に従う変数$\mu$がとり得る値をnp.linspace()
で作成してmu_line
とします。この例では、真の値を中心に指定した範囲とします(本当は自動で良い感じの範囲になるようにしたかった)。
尤度のときと同様にして、確率密度を計算します。
計算結果は次のようになります。
# 確認 print(mu_line[:10]) print(prior[:10])
[-25. -24.8998999 -24.7997998 -24.6996997 -24.5995996 -24.4994995
-24.3993994 -24.2992993 -24.1991992 -24.0990991]
[0.00922982 0.0092529 0.00927594 0.00929895 0.00932192 0.00934486
0.00936776 0.00939062 0.00941344 0.00943622]
$\mu$の事前分布を作図します。
# muの事前分布を作図 plt.figure(figsize=(12, 9)) plt.plot(mu_line, prior, label='prior', color='purple') # muの事前分布 plt.xlabel('$\mu$') plt.ylabel('density') plt.suptitle('Gaussian Distribution', fontsize=20) plt.title('$m=' + str(m) + ', \lambda_{\mu}=' + str(lambda_mu) + '$', loc='left') plt.show()
・事後分布の計算
観測データ$\mathbf{X}$からパラメータ$\mu$の事後分布を求めます(パラメータ$\mu$を分布推定します)。
観測データx_n
を用いて、$\mu$の事後分布(ガウス分布)のパラメータを計算します。
# muの事後分布のパラメータを計算:式(3.53),(3.54)
lambda_mu_hat = N * lmd + lambda_mu
m_hat = (lmd * np.sum(x_n) + lambda_mu * m) / lambda_mu_hat
$\mu$の事後分布のパラメータは
で計算して、結果をlambda_mu_hat, m_hat
とします。
# 確認 print(lambda_mu_hat) print(m_hat)
0.501
24.06262890299042
求めたパラメータを使って、$\mu$の事後分布の確率密度を計算します。
# muの事後分布を計算:式(2.64) ln_C_N = - 0.5 * (np.log(2 * np.pi) - np.log(lambda_mu_hat)) # 正規化項(対数) posterior = np.exp(ln_C_N - 0.5 * lambda_mu_hat * (mu_line - m_hat)**2) # 確率密度 # muの事前分布を計算:SciPy ver #prior = norm.pdf(x=mu_line, loc=m_hat, scale=np.sqrt(1 / lambda_mu_hat)) # 確率密度
更新した超パラメータm_hat, lambda_mu_hat
を用いて、事前分布のときと同様にして計算します。
計算結果は次のようになります。
# 確認 print(posterior[:10])
[3.76748187e-263 4.40090383e-262 5.11507978e-261 5.91538167e-260
6.80664276e-259 7.79296940e-258 8.87754352e-257 1.00624208e-255
1.13483303e-254 1.27344821e-253]
$\mu$の事後分布を作図します。
# muの事後分布を作図 plt.figure(figsize=(12, 9)) plt.plot(mu_line, posterior, label='posterior', color='purple') # muの事後分布 plt.vlines(x=mu_truth, ymin=0, ymax=max(posterior), label='$\mu_{truth}$', color='red', linestyle='--') # 真のmu plt.xlabel('$\mu$') plt.ylabel('density') plt.suptitle('Gaussian Distribution', fontsize=20) plt.title('$\hat{m}=' + str(np.round(m_hat, 1)) + ', \hat{\lambda}_{\mu}=' + str(np.round(lambda_mu_hat, 3)) + '$', loc='left') plt.legend() plt.show()
パラメータ$\mu$の真の値付近をピークとする分布を推定できています。
・予測分布の計算
最後に、観測データ$\mathbf{X}$から未観測のデータ$x_{*}$の予測分布を求めます。
$\mu$の事後分布のパラメータm_hat, lambda_mu_hat
、または観測データx_n
と$\mu$の事前分布のパラメータm, lambda_mu
を用いて、予測分布(ガウス分布)のパラメータを計算します。
# 予測分布のパラメータを計算:式(3.62') lambda_star_hat = lmd * lambda_mu_hat / (lmd + lambda_mu_hat) mu_star_hat = m_hat # 予測分布のパラメータを計算:式(3.62') #lambda_star_hat = (N * lmd + lambda_mu) * lmd / ((N + 1) * lmd + lambda_mu) #mu_star_hat = (lmd * np.sum(x_n) + lambda_mu * m) / (N * lmd + lambda_mu)
予測分布のパラメータは
で計算して、結果をmu_star_hat, lambda_star_hat
とします。
それぞれ上の式だと、事後分布のパラメータlambda_mu_hat, m_hat
を使って計算できます。下の式だと、観測データx_n
と事前分布のパラメータm, lambda_mu
を使って計算できます。
# 確認 print(lambda_star_hat) print(mu_star_hat)
0.009804305283757338
24.06262890299042
$\mathbf{X}$から$\hat{\mu}_{*},\ \hat{\lambda}_{*}$を学習しているのが式からも分かります。
求めたパラメータを使って、予測分布を計算します。
# 予測分布を計算:式(2.64) ln_C_N = - 0.5 * (np.log(2 * np.pi) - np.log(lambda_star_hat)) # 正規化項(対数) predict = np.exp(ln_C_N - 0.5 * lambda_star_hat * (x_line - mu_star_hat)**2) # 確率密度 # 予測分布を計算:SciPy ver #predict = norm.pdf(x=x_line, loc=mu_star_hat, scale=np.sqrt(1 / lambda_star_hat)) # 確率密度
尤度のときと同様に、x_line
の値ごとに確率密度を計算します。
計算結果は次のようになります。
# 確認 print(predict[:10])
[2.22861299e-05 2.29794950e-05 2.36929423e-05 2.44270042e-05
2.51822258e-05 2.59591647e-05 2.67583918e-05 2.75804913e-05
2.84260608e-05 2.92957121e-05]
予測分布を尤度と重ねて作図します。
# 予測分布を作図 plt.figure(figsize=(12, 9)) plt.plot(x_line, predict, label='predict', color='purple') # 予測分布 plt.plot(x_line, true_model, label='true', color='red', linestyle='--') # 真の分布 plt.xlabel('x') plt.ylabel('density') plt.suptitle('Gaussian Distribution', fontsize=20) plt.title('$\hat{\mu}_{*}=' + str(np.round(mu_star_hat, 1)) + ', \hat{\lambda}_{*}=' + str(np.round(lambda_star_hat, 3)) + '$', loc='left') plt.legend() plt.show()
観測データが増えると、予測分布が真の分布に近づきます。
・おまけ:アニメーションで推移の確認
animation
モジュールを利用して、事後分布と予測分布の推移をアニメーション(gif画像)で確認するためのコードです。
・コード(クリックで展開)
異なる点のみを簡単に解説します。
# 利用するライブラリ import numpy as np from scipy.stats import norm # 1次元ガウス分布 import matplotlib.pyplot as plt import matplotlib.animation as animation
・モデルの設定
# 真のパラメータを指定 mu_truth = 25 lmd = 0.01 # muの事前分布のパラメータを指定 m = 0 lambda_mu = 0.001 # 初期値による予測分布のパラメータを計算:式(3.62) mu_star = m lambda_star = lmd * lambda_mu / (lmd + lambda_mu) # 作図用のxの値を設定 x_line = np.linspace( mu_truth - 4 * np.sqrt(1 / lmd), mu_truth + 4 * np.sqrt(1 / lmd), num=1000 ) # 作図用のmuの値を設定 mu_line = np.linspace(mu_truth - 50, mu_truth + 50, num=1000)
・推論処理
各試行の結果をリストに格納していく必要があります。事後分布をtrace_posterior
、予測分布をtrace_predict
、パラメータについてもそれぞれtrace_***
として、初期値の結果を持つように作成しておきます。
# データ数(試行回数)を指定 N = 100 # 推移の記録用の受け皿を初期化 x_n = np.empty(N) trace_m = [m] trace_lambda_mu = [lambda_mu] trace_posterior = [norm.pdf(x=mu_line, loc=m, scale=np.sqrt(1 / lambda_mu))] trace_mu_star = [mu_star] trace_lambda_star = [lambda_star] trace_predict = [norm.pdf(x=x_line, loc=mu_star, scale=np.sqrt(1 / lambda_star))] # ベイズ推論 for n in range(N): # ガウス分布に従うデータを生成 x_n[n] = np.random.normal(loc=mu_truth, scale=np.sqrt(1 / lmd), size=1) # muの事後分布のパラメータを計算:式(3.53),(3.54) lambda_mu_old = lambda_mu lambda_mu += lmd m = (lmd * x_n[n] + lambda_mu_old * m) / lambda_mu # muの事後分布(ガウス分布)を計算:式(2.64) trace_posterior.append( norm.pdf(x=mu_line, loc=m, scale=np.sqrt(1 / lambda_mu)) ) # 予測分布のパラメータを計算:式(3.62) mu_star = m lambda_star = lmd * lambda_mu / (lmd + lambda_mu) # 予測分布(ガウス分布)を計算:siki(2.64) trace_predict.append( norm.pdf(x=x_line, loc=mu_star, scale=np.sqrt(1 / lambda_star)) ) # 超パラメータを記録 trace_m.append(m) trace_lambda_mu.append(lambda_mu) trace_mu_star.append(mu_star) trace_lambda_star.append(lambda_star)
観測された各データによってどのように学習する(分布が変化する)のかを確認するため、for
文で1データずつ処理します。よって、データ数N
がイタレーション数になります。
超パラメータに関して、$\hat{m},\ \hat{\lambda}_{\mu}$に対応するm_hat, lambda_mu_hat
を新たに作るのではなく、m, lambda_mu
をイタレーションごとに更新していきます。
それに伴い、事後分布のパラメータの計算式(3.53-54)の$\sum_{n=1}^N x_n$と$N$の計算は、ループ処理によってN回繰り返しx_n[n]
と1
を加えることで行います。n回目のループ処理のときには、n-1回分のx_n[n]
と1
が既にm
とlambda_mu
に加えられているわけです。
・事後分布の推移
# 画像サイズを指定 fig = plt.figure(figsize=(12, 9)) # 作図処理を関数として定義 def update_posterior(n): # 前フレームのグラフを初期化 plt.cla() # nフレーム目のmuの事後分布を作図 plt.plot(mu_line, trace_posterior[n], label='posterior', color='purple') # muの事後分布 plt.vlines(x=mu_truth, ymin=0, ymax=np.nanmax(trace_posterior), label='$\mu_{trurh}$', color='red', linestyle='--') # 真のmu plt.xlabel('$\mu$') plt.ylabel('density') plt.suptitle('Gaussian Distribution', fontsize=20) plt.title('$N=' + str(n) + ', \hat{m}=' + str(np.round(trace_m[n], 1)) + ', \hat{\lambda}_{\mu}=' + str(np.round(trace_lambda_mu[n], 2)) + '$', loc='left') plt.legend() # gif画像を作成 posterior_anime = animation.FuncAnimation(fig, update_posterior, frames=N + 1, interval=100) posterior_anime.save("ch3_3_1_Posterior.gif")
初期値(事前分布)を含むため、フレーム数の引数frames
はN + 1
です。
・予測分布の推移
# 尤度を計算:式(2.64) true_model = norm.pdf(x=x_line, loc=mu_truth, scale=np.sqrt(1 / lmd)) # 確率密度 # 画像サイズを指定 fig = plt.figure(figsize=(12, 9)) # 作図処理を関数として定義 def update_predict(n): # 前フレームのグラフを初期化 plt.cla() # nフレーム目の予測分布を作図 plt.plot(x_line, trace_predict[n], label='predict', color='purple') # 予測分布 plt.plot(x_line, true_model, label='true', color='red', linestyle='--') # 真の分布 plt.xlabel('x') plt.ylabel('density') plt.suptitle('Gaussian Distribution', fontsize=20) plt.title('$N=' + str(n) + ', \hat{\mu}_{*}=' + str(np.round(trace_mu_star[n], 1)) + ', \hat{\lambda}_{*}=' + str(np.round(trace_lambda_star[n], 3)) + '$', loc='left') plt.legend() # gif画像を作成 predict_anime = animation.FuncAnimation(fig, update_predict, frames=N + 1, interval=100) predict_anime.save("ch3_3_1_Predict.gif")
(よく理解していないので、animation
の解説は省略…とりあえずこれで動きます……)
参考文献
- 須山敦志『ベイズ推論による機械学習入門』(機械学習スタートアップシリーズ)杉山将監修,講談社,2017年.
おわりに
年度内に何とか終わらせたい。コード自体は8割方できてるのですが、そこからブログ用に解説を書くのもわりと大変。
2021年3月16日は、モーニング娘。'21の北川莉央さん17歳のお誕生日!
もう1人で歌ってて凄いなぁ。「オリビアを聴きながら」って初めて知りましたけど良い曲ですね。
【次節の内容】