はじめに
表題についてちょっと調べた&やってみたのでメモ。 やりたいことは表題の通りなんだけど、少し数式で表現して整理。
- 今、学習データとしての個得られているとする。
- 入力と関数が与えられたとき、出力はに従うとする(ハイパーパラメータを持つ場合もあるが今回は固定値として扱うので表記から省略)。
- そして、この関数はガウス過程に従う確率変数とする(は共分散関数)。
- やりたいのは、個の任意の未知入力が与えられて、事後分布からサンプリングされた関数の出力を得たい。
- 予測分布からのサンプリングも得たいという話もあるけど今回は対象外。
- とりあえず、事後分布からの出力値が得られれば、これからが計算できるので、それっぽいは得られそう。
- 予測分布からのサンプリングも得たいという話もあるけど今回は対象外。
ようするに、学習入力点個に対する関数の出力を並べたベクトルをとし、未知入力点個に対する関数の出力を並べたベクトルをとした場合、
からのMCMCサンプリングはできるのは分かるけど、 からのサンプリングはどうやったらいいんだ?って話。
方法1 ~MCMCサンプリングの時、同時分布からサンプリング~
非常にお世話になっている書籍
のサポートページに、コーシー分布を観測モデルとしたガウス過程回帰のpythonコードgpr-cauchy.py, elliptical.pyがある。これは楕円スライスサンプリングというMCMCサンプリングの一手法を用いて関数出力値の推定を行っているが、これを見るとサンプルを得るところで
(ただし、はという要素を持つの行列、はという要素を持つの行列、はという要素を持つの行列)
からのサンプルを使って更新している。そして、そのサンプルを採択するか否かの尤度計算はで計算、つまり学習入力点だけで行っている*1。
なるほど、確かにこうすればからサンプリングしたことになり、そのサンプルからを捨てれば周辺化することになるからからサンプリングしたことになる。
方法2 ~からのサンプルを用いて からサンプリング~
方法1だと、学習時に評価したい未知入力がわかっている必要があるが、場合によってはそれが困難だったりする。そういう場合はどうするか?
からのサンプルが個 得られているとすると、
と近似できる。ここでガウス過程の性質より、
なので、は全て同じ混合比の混合正規分布で近似できる。従っての乱数を発生させた後、それに紐づく多次元正規分布からサンプリングすればよい。
実装
方法2によるコーシー分布ガウス過程回帰を上記サポートページにあるgpr-cauchy.pyを改変する形でやってみた。
import sys import putil import numpy as np from pylab import * from numpy import exp,log from elliptical import elliptical from numpy.linalg import cholesky as chol def gpr_cauchy (f, param): x,y,gamma,Kinv = param M = len(x) return gpr_cauchy_lik (y[0:M], f[0:M], gamma, Kinv) def gpr_cauchy_lik (y, f, gamma, Kinv): return - np.sum (log (gamma + (y - f)**2 / gamma)) \ - np.dot (f, np.dot(Kinv, f)) / 2 def kgauss (tau,sigma): return lambda x,y: exp(tau) * exp (-(x - y)**2 / exp(sigma)) def kernel_matrix (xx, kernel): N = len(xx) eta = 1e-6 return np.array ( [kernel (xi, xj) for xi in xx for xj in xx] ).reshape(N,N) + eta * np.eye(N) def gpr_mcmc(x, y, iters, xmin, xmax, gamma): N = len(x) K_NN = kernel_matrix(x, kgauss(1,1)) K_NNinv = inv(K_NN) #こうすると多次元正規分布からサンプリングすることになる P = chol (K_NN) f = P @ randn(N) #MCMCサンプルのうち最初の200個は使わない remove_num = 200 S = iters - remove_num f_posterior = np.zeros((f.shape[0], S)) for iter in range(iters): #楕円スライスサンプリング f,lik = elliptical (f, P, gpr_cauchy, (x,y,gamma,K_NNinv)) print ('\r[iter %2d]' % (iter + 1),) if iter >= remove_num: f_posterior[:, iter - remove_num] = f #未知入力点 x_new = np.linspace(xmin, xmax, 100) M = len(x_new) xx = np.hstack((x, x_new)) K = kernel_matrix(xx, kgauss(1, 1)) K_NM = K[:N, N:N + M] K_MM = K[N:N + M, N:N + M] g = np.zeros (len(x_new)) #50個サンプリング sampling_num = 50 for t in range(sampling_num): s = np.random.randint(S) f_n = f_posterior[:, s] mu = K_NM.T@ K_NNinv @ f_n sig = K_MM - K_NM.T @ K_NNinv @ K_NM P = chol(sig) f_m = P @ np.random.randn(M) + mu g = g + f_m plot (x_new, f_m) plot (x, y, 'bx', markersize=14) plot (x_new, g/sampling_num, 'k', linewidth=3) def usage (): print ('usage: gpr-cauchy.py data.xyf iters [output]') sys.exit (0) def main (): xmin = -5 xmax = 5 ymin = -7.5 ymax = 12.5 gamma = 0.2 if len(sys.argv) < 3: usage () else: [x, y, f] = np.loadtxt (sys.argv[1]).T iters = int (sys.argv[2]) gpr_mcmc (x, y, iters, xmin, xmax, gamma) axis ([xmin, xmax, ymin, ymax]) show () if __name__ == "__main__": main ()
楕円スライスサンプリングを行う関数elliptical
については、 サポートページのelliptical.pyから変えてないので省略。また多次元正規分布からはコレスキー分解を用いてサンプリングしている(この辺が参考になる)。これを、
python .\gpr-cauchy_new.py .\gpr-cauchy.dat 1000
として、実行すると以下のような結果が得られた。
うん、いい感じ。
今後やりたいこと
*1:事前分布の方はの同時分布を評価するべきかと思ったがどうなんだろ?実際両方やってみたけど、結果はあまり変わらなかった