決定的には定まらない観測値をモデル化するのに用いる
さいころを振る前にも各回で出る目を確率変数として書ける
$N$ 回振るとそれぞれの確率変数の観測値が得られる
試行のたびに得られる観測値は変わる
事前知識や、手元にあるデータから推定できるか否か、という観点で選ぶ
得られた観測値から、確率変数の従う規則を推測したい
サンプル $\mathcal{D}$ が得られる確率は $$ \ell(\theta) = p(\mathcal{D} ; \theta) = \prod_{n=1}^N p(x_n;\theta) $$ (独立に同じ分布に従っていると仮定したため)
最尤推定量 $\hat{\theta}$ は $$ \hat{\theta} = \arg\max_{\theta} \ell(\theta) = \arg\max_{\theta}\prod_{n=1}^N p(x_n; \theta) $$ と定義される
ただし計算の簡単のため&実装上の問題から負の対数尤度 $$ \mathcal{L}(\theta) = - \log \ell(\theta) = -\sum_{n=1}^N \log p(x_n;\theta) $$ を使うことが多い。 $$ \hat{\theta} = \arg\max_{\theta} \ell(\theta) = \arg\min_{\theta} \mathcal{L}(\theta) $$ であることに注意。
# ロボットをクラスを使って書いてみる
class Robot: # 以下の一段下がっているところがクラス
def __init__(self, state, memory): # 初期状態を与えたときにオブジェクトを作る関数(名前が __init__ と決まっている)
'''
self = オブジェクトを指す。 self.state は、オブジェクトの state という変数を指す。
下の命令は、 self.state に state の値を代入することを表す
'''
self.state = state
self.memory = memory
def stand_up(self): # オブジェクトへの命令の引数には必ず self を入れる
self.state = 'stand up' # 内部状態を更新できる
def sit_down(self):
self.state = 'sit down'
def memorize(self, memory): # オブジェクトへ命令する際に、引数を渡すこともできる
self.memory = memory
def get_state(self):
return self.state
def get_memory(self):
return self.memory
my_robot_1 = Robot('sleep', 'nothing') # クラス名(__init__の引数) と書くとそのクラスのオブジェクトが生成される
print(my_robot_1.get_state()) # オブジェクト.メソッド名(selfを省略した引数たち) と書くと、オブジェクトに命令できる
print(my_robot_1.get_memory()) # 初期化に用いた状態がセットされている
sleep nothing
my_robot_1.stand_up() # 内部状態が更新される
print(my_robot_1.get_state())
my_robot_1.memorize('餃子を食べたい') # 内部状態が更新される
print(my_robot_1.memory)
stand up 餃子を食べたい
my_robot_1 = Robot('stand up', 'nothing')
my_robot_2 = Robot('sleep', 'ラーメン食べたい')
my_robot_1.sit_down() # ロボット1の状態を変えてもロボット2の状態には影響を与えない
print(my_robot_1.get_state())
print(my_robot_2.get_state())
sit down sleep
補助的なものを持っていてもよい
まずは正規分布クラスを実装してみよう
import numpy as np
class Gaussian:
def __init__(self, dim):
'''コンストラクタ(みたいなもの)
オブジェクトを作るときに初めに実行される。
内部状態の初期化に使う
'''
self.dim = dim
'''
self = オブジェクトを指す。 self.dim は、オブジェクトの dim という変数を指す。
上の命令は、 self.dim に dim の値を代入することを表す
'''
self.set_mean(np.random.randn(dim)) # オブジェクトの mean という変数をランダムに初期化
self.set_cov(np.identity(dim))
def set_mean(self, mean):
if mean.shape != (self.dim,): # 間違ったサイズの配列で内部状態を更新しようとすると
raise ValueError('input shape inconsistency') # エラーを上げてプログラムを終了させる
self.mean = mean
def set_cov(self, cov):
if cov.shape != (self.dim, self.dim):
raise ValueError('input shape inconsistency')
if np.linalg.eigvalsh(cov)[0] <= 0:
raise ValueError('covariance matrix must be positive semidefinite.')
self.cov = cov
def log_pdf(self, X):
''' 確率密度関数の対数を返す
Parameters
----------
X : numpy.array, shape (sample_size, dim)
Returns
-------
log_pdf : array, shape (sample_size,)
'''
return 0
def fit(self, X):
''' X を使って最尤推定をする
Parameters
----------
X : numpy.array, shape (sample_size, dim)
'''
pass
def sample(self, sample_size):
''' 現状のパラメタを使って `sample_size` のサイズのサンプルを生成する
Parameters
----------------
sample_size : int
Returns
-----------
X : numpy.array, shape (sample_size, dim)
各行は平均 `self.mean`, 分散 `self.cov` の正規分布に従う
'''
pass
log_pdf
を書く手順を解説します¶sample_size
x dim
の arraysample_size
の arraylog_pdf(self, X)
の実装¶$$ \log p(x \mid \mu, \Sigma) = -\dfrac{1}{2} (x - \mu)^\top \Sigma^{-1} (x - \mu) - \dfrac{D}{2}\log 2\pi - \dfrac{1}{2} \log |\Sigma| $$
$\log$ は np.log
, log determinant は np.linalg.slogdet
で計算できるので
dim = 5
cov = np.identity(dim) # とりあえず単位行列で計算できるか確かめる
- 0.5 * dim * np.log(2 * np.pi) - 0.5 * np.linalg.slogdet(cov)[1]
-4.594692666023363
dim = 10
x = np.ones(dim)
mean = np.zeros(dim)
cov = 2.0 * np.identity(dim)
centered_x = x - mean
print(centered_x)
[1. 1. 1. 1. 1. 1. 1. 1. 1. 1.]
cov_inv_centered_x = np.linalg.solve(cov, centered_x)
print(cov_inv_centered_x)
[0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5]
-0.5 * (centered_x @ cov_inv_centered_x)
-2.5
dim = 3
sample_size = 10
X = np.arange(sample_size * dim).reshape(sample_size, dim)
mean = np.ones(dim)
cov = 2.0 * np.identity(dim)
centered_X = X - mean
print(centered_X)
[[-1. 0. 1.] [ 2. 3. 4.] [ 5. 6. 7.] [ 8. 9. 10.] [11. 12. 13.] [14. 15. 16.] [17. 18. 19.] [20. 21. 22.] [23. 24. 25.] [26. 27. 28.]]
cov_inv_centered_X = np.linalg.solve(cov, centered_X.T).T
print(cov_inv_centered_X)
[[-0.5 0. 0.5] [ 1. 1.5 2. ] [ 2.5 3. 3.5] [ 4. 4.5 5. ] [ 5.5 6. 6.5] [ 7. 7.5 8. ] [ 8.5 9. 9.5] [10. 10.5 11. ] [11.5 12. 12.5] [13. 13.5 14. ]]
-0.5 * np.sum(centered_X * cov_inv_centered_X)
-1928.75
import numpy as np
class Gaussian:
def __init__(self, dim):
'''コンストラクタ(みたいなもの)
オブジェクトを作るときに初めに実行される。
内部状態の初期化に使う
'''
self.dim = dim
'''
self = オブジェクトを指す。 self.dim は、オブジェクトの dim という変数を指す。
上の命令は、 self.dim に dim の値を代入することを表す
'''
self.set_mean(np.random.randn(dim)) # オブジェクトの mean という変数をランダムに初期化
self.set_cov(np.identity(dim))
def set_mean(self, mean):
if mean.shape != (self.dim,): # 間違ったサイズの配列で内部状態を更新しようとすると
raise ValueError('input shape inconsistency') # エラーを上げてプログラムを終了させる
self.mean = mean
def set_cov(self, cov):
if cov.shape != (self.dim, self.dim):
raise ValueError('input shape inconsistency')
if np.linalg.eigvalsh(cov)[0] <= 0:
raise ValueError('covariance matrix must be positive semidefinite.')
self.cov = cov
def log_pdf(self, X):
''' 確率密度関数の対数を返す
Parameters
----------
X : numpy.array, shape (sample_size, dim)
Returns
-------
log_pdf : array, shape (sample_size,)
'''
centered_X = X - self.mean
cov_inv_centered_X = np.linalg.solve(self.cov, centered_X.T).T
log_pdf = - 0.5 * self.dim * np.log(2 * np.pi) - 0.5 * np.linalg.slogdet(self.cov)[1] - 0.5 * np.sum(centered_X * cov_inv_centered_X)
return log_pdf
my_gaussian = Gaussian(2)
X = np.zeros((10, 2))
my_gaussian.set_mean(np.zeros(2))
my_gaussian.log_pdf(X)
-1.8378770664093453
fit
を完成させよ。sample_size
x dim
の array X
self.mean
と self.cov
を X
で計算した最尤推定量で更新するsample
を完成させよ。sample_size
self.mean
、分散self.cov
の正規分布に従う乱数import numpy as np
class Gaussian:
def __init__(self, dim):
'''コンストラクタ(みたいなもの)
オブジェクトを作るときに初めに実行される。
内部状態の初期化に使う
'''
self.dim = dim
'''
self = オブジェクトを指す。 self.dim は、オブジェクトの dim という変数を指す。
上の命令は、 self.dim に dim の値を代入することを表す
'''
self.set_mean(np.random.randn(dim)) # オブジェクトの mean という変数をランダムに初期化
self.set_cov(np.identity(dim))
def log_pdf(self, X):
''' 確率密度関数の対数を返す
Parameters
----------
X : numpy.array, shape (sample_size, dim)
Returns
-------
log_pdf : array, shape (sample_size,)
'''
if X.shape[1] != self.dim: # 入力の形をチェックしています
raise ValueError('X.shape must be (sample_size, dim)')
return -0.5 * np.sum((X - self.mean) * (np.linalg.solve(self.cov, (X - self.mean).T).T), axis=1) \
-0.5 * self.dim * np.log(2.0 * np.pi) - 0.5 * np.linalg.slogdet(self.cov)[1]
def fit(self, X):
''' X を使って最尤推定をする
Parameters
----------
X : numpy.array, shape (sample_size, dim)
'''
if X.shape[1] != self.dim: # 入力の形をチェックしています
raise ValueError('X.shape must be (sample_size, dim)')
self.set_mean(np.mean(X, axis=0))
self.set_cov((X - self.mean).T @ (X - self.mean) / X.shape[0])
def sample(self, sample_size):
''' 現状のパラメタを使って `sample_size` のサイズのサンプルを生成する
Parameters
----------------
sample_size : int
Returns
-----------
X : numpy.array, shape (sample_size, dim)
各行は平均 `self.mean`, 分散 `self.cov` の正規分布に従う
'''
return np.random.multivariate_normal(self.mean, self.cov, size=sample_size)
def set_mean(self, mean):
if mean.shape != (self.dim,):
raise ValueError('input shape inconsistency')
self.mean = mean
def set_cov(self, cov):
if cov.shape != (self.dim, self.dim):
raise ValueError('input shape inconsistency')
if np.linalg.eigvalsh(cov)[0] <= 0:
raise ValueError('covariance matrix must be positive semidefinite.')
self.cov = cov
以下のように分解してみるとわかる $$ \begin{bmatrix}x_1 & x_2 & \dots & x_N\end{bmatrix} = \begin{bmatrix}x_1 & 0 & \dots & 0\end{bmatrix} + \begin{bmatrix}0 & x_2 & \dots & 0\end{bmatrix} + \cdots + \begin{bmatrix}0 & 0 & \dots & x_N\end{bmatrix} $$
my_model = Gaussian(2) # my_model というオブジェクトが出来た
print(my_model.mean, my_model.cov) # 平均、共分散行列を持っている
[-1.09289089 0.15135944] [[1. 0.] [0. 1.]]
my_model_1 = Gaussian(2) # 他のオブジェクトも作れる
print(my_model_1.mean, my_model_1.cov) # 平均はランダムに初期化されるため my_model とは異なる
[-0.67672032 -0.71912055] [[1. 0.] [0. 1.]]
X = np.random.multivariate_normal(np.array([1.0, 2.0]), np.array([[1.0, 0.9], [0.9, 4.0]]), size=100)
import matplotlib.pyplot as plt
plt.scatter(X[:, 0], X[:, 1])
plt.show()
my_model.fit(X) # X で最尤推定をして、 mean, cov を更新する
print(my_model.mean, my_model.cov)
[0.88266934 1.91770907] [[0.70260287 0.46085926] [0.46085926 3.38136057]]
# サンプルサイズを大きくすると、真値に近くなる
X = np.random.multivariate_normal(np.array([1.0, 2.0]), np.array([[1.0, 0.9], [0.9, 4.0]]), size=10000)
import matplotlib.pyplot as plt
plt.scatter(X[:, 0], X[:, 1])
plt.show()
my_model.fit(X)
print(my_model.mean, my_model.cov)
[1.00993174 2.02038778] [[0.99788835 0.89904189] [0.89904189 3.99284042]]
# サンプリングを試してみる
sample = my_model.sample(10000)
plt.scatter(sample[:, 0], sample[:, 1])
plt.show()
my_model.mean = np.array([0, 0, 0]) # オブジェクトの変数に直接アクセスすることもできるが、おかしな値に設定してもエラーが出ない
my_model.set_mean(np.array([0, 0, 0])) # メソッドの中で配列のサイズのチェックを行なっているため、ちゃんとエラーが出て止まる
--------------------------------------------------------------------------- ValueError Traceback (most recent call last) <ipython-input-26-d630bfc41f7b> in <module>() ----> 1 my_model.set_mean(np.array([0, 0, 0])) # メソッドの中で配列のサイズのチェックを行なっているため、ちゃんとエラーが出て止まる <ipython-input-17-1dcee8c239e9> in set_mean(self, mean) 58 def set_mean(self, mean): 59 if mean.shape != (self.dim,): ---> 60 raise ValueError('input shape inconsistency') 61 self.mean = mean 62 ValueError: input shape inconsistency
my_model.set_mean(np.array([0, 0]))
my_model.set_cov(np.identity(2))
np.exp(my_model.log_pdf(np.array([[0,0]]))) # 1次元の Normal distribution だと 0.4 くらいなので、二次元だと0.16くらいのはず
array([0.15915494])
class Dice:
def __init__(self, n_faces):
self.n_faces = n_faces
self.set_prob(np.ones(self.n_faces) / self.n_faces)
def set_prob(self, prob_array):
if not np.allclose(prob_array.sum(), 1.0):
raise ValueError('prob_array must be normalized.')
if prob_array.shape != (self.n_faces,):
raise ValueError(f'prob_array must be of shape ({self.n_faces},)')
self.prob_array = prob_array
def fit(self, X):
'''
Parameters
----------
X : array, shape (n_faces, )
X[i] は i 番目の目が出た回数を表す
'''
self.set_prob(X / X.sum())
def sample(self, n_trials):
'''
Parameters
----------
n_trials : int
さいころを振る回数
Returns
------
X : array, shape (n_faces,)
X[i] は i 番目の目が出た回数を表す
'''
return np.random.multinomial(n_trials, self.prob_array)
from scipy.linalg import eigh
class PCA:
def __init__(self, input_dim, n_components):
self.input_dim = input_dim
self.n_components = n_components
self.set_encoder(np.eye(self.input_dim)[:self.n_components, :])
def set_encoder(self, encoder):
if encoder.shape != (self.n_components, self.input_dim):
raise ValueError(f'encoder must have shape {(self.n_components, self.input_dim)}')
if np.abs(encoder @ encoder.transpose() - np.eye(self.n_components)).max() > 1e-4:
raise ValueError('encoder must be orthonormal.')
self.encoder = encoder
def fit(self, X):
eig_val, eig_vec = eigh(X.T @ X)
self.set_encoder(eig_vec[:, -self.n_components:].transpose())
def transform(self, X):
return X @ self.encoder.transpose()
def inverse_transform(self, z):
return z @ self.encoder
import matplotlib.pyplot as plt
from sklearn.datasets import fetch_olivetti_faces
# データを取得
dataset = fetch_olivetti_faces()
num_examples, row_size, col_size = dataset['images'].shape
X = dataset['data']
# 平均0にしておく(しなくてもまあ大丈夫だけど)
X_mean = X.mean(axis=0)
X_centered = X - X_mean
n_components=20
pca = PCA(X.shape[1], n_components)
pca.fit(X_centered)
z = pca.transform(X_centered)
X_rec = pca.inverse_transform(z) + X_mean
idx = 190
f, (ax1, ax2) = plt.subplots(1, 2)
ax1.imshow(dataset['images'][idx], cmap=plt.cm.gray) # 左が元の画像
ax2.imshow(X_rec[idx].reshape(row_size, col_size), cmap=plt.cm.gray) # 右が再構成画像
plt.show()