からっぽのしょこ

読んだら書く!書いたら読む!同じ事は二度調べ(たく)ない

【Python】3.3.1:1次元ガウス分布の学習と予測:平均が未知の場合【緑ベイズ入門のノート】

はじめに

 『ベイズ推論による機械学習入門』の学習時のノートです。基本的な内容は「数式の行間を読んでみた」とそれを「RとPythonで組んでみた」になります。「数式」と「プログラム」から理解するのが目標です。

 この記事は、3.3.1項の内容です。「尤度関数を平均が未知の1次元ガウス分布(正規分布)」、「事前分布を1次元ガウス分布」とした場合の「平均パラメータの事後分布」と「未観測値の予測分布」の計算をPythonで実装します。

 省略してある内容等ありますので、本とあわせて読んでください。初学者な自分が理解できるレベルまで落として書き下していますので、分かる人にはかなりくどくなっています。同じような立場の人のお役に立てれば幸いです。

【数式読解編】

www.anarchive-beta.com

【他の節の内容】

www.anarchive-beta.com

【この節の内容】

・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次元ガウス分布の確率密度は、対数をとった定義式

$$ \ln \mathcal{N}(x_n | \mu, \lambda^{-1}) = - \frac{1}{2} \{ (x_n - \mu)^2 \lambda - \ln \lambda + \ln 2 \pi \} \tag{2.65'} $$

で計算して、最後に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()

f:id:anemptyarchive:20210316180742p:plain
尤度:ガウス分布

 真のパラメータを求めることは、この真の分布を求めることを意味します。

・データの生成

 続いて、構築したモデルに従って観測データ$\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()で生成できます。平均の引数locmu_truth、標準偏差の引数scalenp.sqrt(1 / lmd)、試行回数の引数sizeNを指定します。生成した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()

f:id:anemptyarchive:20210316180805p:plain
観測データのヒストグラム:ガウス分布

 データ数が十分に大きいと、分布の形状が真の分布に近づきます。

・事前分布の設定

 次に、尤度に対する共役事前分布を設定します。

 $\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()

f:id:anemptyarchive:20210316180842p:plain
平均パラメータの事前分布:ガウス分布


・事後分布の計算

 観測データ$\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$の事後分布のパラメータは

$$ \begin{align} \hat{\lambda}_{\mu} &= N \lambda + \lambda_{\mu} \tag{3.53}\\ \hat{m} &= \frac{ \lambda \sum_{n=1}^N x_n + \lambda_{\mu} m }{ \hat{\lambda}_{\mu} } \tag{3.54} \end{align} $$

で計算して、結果を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()

f:id:anemptyarchive:20210316180931p:plain
平均パラメータの事後分布:ガウス分布

 パラメータ$\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)

 予測分布のパラメータは

$$ \begin{aligned} \hat{\lambda}_{*} &= \frac{ \lambda \hat{\lambda}_{\mu} }{ \lambda + \hat{\lambda}_{\mu} } \\ &= \frac{ (N \lambda + \lambda_{\mu}) \lambda }{ (N + 1)\lambda + \lambda_{\mu} } \\ \hat{\mu}_{*} &= \hat{m} \\ &= \frac{ \lambda \sum_{n=1}^N x_n + \lambda_{\mu} m }{ N \lambda + \lambda_{\mu} } \end{aligned} \tag{3.62} $$

で計算して、結果を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()

f:id:anemptyarchive:20210316181001p:plain
予測分布:ガウス分布

 観測データが増えると、予測分布が真の分布に近づきます。

・おまけ:アニメーションで推移の確認

 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が既にmlambda_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")

 初期値(事前分布)を含むため、フレーム数の引数framesN + 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の解説は省略…とりあえずこれで動きます……)


f:id:anemptyarchive:20210316181121g:plain
平均パラメータの事後分布の推移:ガウス分布

f:id:anemptyarchive:20210316181256g:plain
予測分布の推移:ガウス分布


参考文献

  • 須山敦志『ベイズ推論による機械学習入門』(機械学習スタートアップシリーズ)杉山将監修,講談社,2017年.

おわりに

 年度内に何とか終わらせたい。コード自体は8割方できてるのですが、そこからブログ用に解説を書くのもわりと大変。

 2021年3月16日は、モーニング娘。'21の北川莉央さん17歳のお誕生日!

 もう1人で歌ってて凄いなぁ。「オリビアを聴きながら」って初めて知りましたけど良い曲ですね。

【次節の内容】

www.anarchive-beta.com