JuliaによるGauss過程回帰 (MCMCでハイパーパラメータ決定)
初っ端ですがGauss過程回帰を実装してみます。 今回は全面的にこちらの書籍を参考にしていきます。
1. 理論
理論に関しては初学者の自分がへたに解説する間でもないと思いますので、ざっくりといきます。何か間違っていたらコメント欄で優しく教えてください。
1.1. Gauss過程とは
いま観測をを用いて回帰することを考えます。
の予測を とおき、の基底を並べた特徴ベクトル を , 重みベクトル を とおくとこのようにかけます。
ベクトルで書くと
例えばとして多項式や動径基底関数などを考えるわけですが、例えば動径基底函数に対応する は次元の呪いによってのオーダーで指数的に増えていってしまい、簡単に計算量が爆発します(の基底関数が横に指数的にどんどん長くなっていく)。
これはあまり嬉しくないので、重みがのような確率分布から生成されたとして、これ積分消去できたらいいなと考えます。いま単純のためとするとの共分散は
つまり となって は平均0の多変量Gauss分布に従います。そして狙った通りは消えてくれ、高次元の計算をわざわざする必要はなくなりました。
この確率過程をGauss過程 (Gaussian process)と呼びます。正確な定義は以下のようになります。
あらゆる自然数 で、入力に対応する出力
が平均), 共分散行列とする多変数Gauss分布に従うとき、これをGauss過程(Gaussian process) と呼ぶ。
ただしは要素に関して以下が成り立つ行列である( カーネル行列(kernel matrix)あるいはグラム行列(Gram matrix)という)。
このGram行列というやつが重要です。上の例では の定数倍を無視しての部分をとして議論します。
Gram matrixがすごいのは、kernelの式さえうまく指定すれば特徴ベクトル(基底ベクトル)の計算をいちいちしないでよくなるという点です(詳細はMLPガウス本を参照してください)。これをカーネルトリック (Kernel trick)といい、須くこのtrickを用いる手法をkernel法といいます。
Kernelの式は勿論なんでもいいわけではなく、「うまく」指定しなくてはいけませんが、これはがGauss分布の共分散として機能するようにしてくださいねということで、つまりを計算したとき対称かつ正定値にならなくてはなりません。
Gauss過程に用いるkernelにはいろいろと種類がありますが、一番オーソドックスなRBF kernel (radial basis function kernel, Gaussian kernel)を示します。
RBF kernelは、の値が近かったら必然的に値が大きくなります。つまり「入力が近かったら出力も近くなる」という「類似度」の意味が込められているというわけですね。このほかにも指数カーネル(exponential kernel)、周期カーネル(periodic kernel)などがあります。
Kernelに関しての議論は難しいらしいのでこれ以上あまり深入りしないことにしますが(再生核Hilbert空間での内積とか)、これらのkernelより作られるGram matrixは正定値であることが保証されています(証明はあんまり理解していない)。よく考えたらすごいですね。そしてKernelどうしのの和・積なども半正定値性を持ちます。これも普通にすごいことなので証明をきちんと理解したいです......
さて、もう一度という式に立ち返ってみます。この式のお気持ちとしては、入力を決めると、出力の各次元の予測はからまで全ての実数値の個数次元(=無限次元)のGauss分布からサンプリングされるということです。函数の値が無限次元のGauss分布から出てくるなんてちょっと面白い考え方ですね。
1.2. Gauss過程回帰
Gauss過程を回帰問題に適応するにはどうしたらよいでしょう。ということですが、上で見た入力とそれぞれの観測をがあるとき、新たな入力とその予測値を考えます。
すると、もとの観測と新しい予測を合わせたは、もとの入力と新しい入力を合わせたから計算できるGram matrix (次正方行列)を共分散行列に持つGauss過程です。新しい入力を付け加えてもGauss過程というのがミソですね。
このとき
が成り立つので(証明はMLP本に加えPRMLのはじめの方にも載っている)、Gauss過程での予測は
として
同様に、がベクトルの場合も
要は、「インプットと観測データが与えられたとき、インプットと観測の間の函数を、どの程度の信頼がおけるか、という情報も込めて予測できる」ということなので、どの程度の精度をもって予測できるかわかるのは非常に嬉しいですよね。
Kernel函数ですが、今回の実験にはこちらのkernelを使用します(はKronecker's deltaで、Gram matrixの対角項に加える誤差項を意味します)。
意味としては、いま簡単のためと仮定してやっていましたが、きちんと誤差項を入れたを考えると、共分散行列の対角項に誤差の分散が足され、上が導けます(詳しくはMLP本を参照)。
なのでKronecker's deltaの部分はの値が等しければ1というわけではなく対角項が1と返ってくるだけです。
2. 実装
Juliaを使って実装します。
(これからこのブログではJuliaをメインで使って実装していこうと思います。メインストリームと言えばPythonでしょうが、個人的に少し書きにくいのと、自分がひねくれている性格ので一番人気のあるやつはやめときたいというだけです。)
データセットとしてはMLP本のサポートページからダウンロードできるデータセットに加えて、自分で適当にデータを作ってやってみます。
として、としてエラーを加えて得た200サンプルです。プロットはこんな感じです。
そして、せっかくならなにか実データでやってみたいということで、少し前に「ベテルギウスBetelgeuseが暗くなっている」というニュースを読んで気になっていたので、データをAmerican Association of Variable Star Observers(AAVSO)からダウンロードしてやってみました。
2.1. Markov Chain Monte Carlo (MCMC)を用いたハイパーパラメータの決定
いざ実装!と行きたいわけですが、その前にやっておかなくてはいけないことがあります。これがハイパーパラメータ(hyperparameters)の推定です。 ガウス過程での「学習」はkernelの式の中のハイパーパラメータを通してのみ行われます。
この式の中のですね。ハイパーパラメータの値を調整しないとどうなるでしょうか。結果を一部先取りしてしまいますが、としてやってみます。
方法自体は正しいにもかかわらず、over-fittingしてイケてない回帰になっていますね。というわけで、ハイパーパラメータは是非いい感じにしたい。
このためにパラメータから尤度を計算し、尤度を最大化する方法をとります。
よってをとって対数尤度にすれば
上記を最大化すればいいわけです。よく用いられる手法としてMarkov chain Monte Carlo (MCMC)や、勾配法 gradient methodがあります。
MLP本では、この式の勾配が解析的に求められるので、後者の解説がされていました。しかしながら今回は、勾配法は局所解に陥りやすいが、MCMCならそれを避けながら、解析的に求められない問題にも適用可能だということでこちらで実装してみます。本当は、MCMCを以前授業でやったことがあっただけです(MCMCだけでも記事が書けるのでいずれ書こうと思います)
MCMCの詳しい説明は今回踏み込みませんが、サンプリングの難しい確率分布からMarkov連鎖を用いてうまいことサンプリングする手法です。 今回は最もクラシカルなMetropolis-Hastings法を用いて試みます。本当はMarkov連鎖が収束するとか詳細釣り合い条件とかいろいろ議論しないといけないのですが、手順だけ書いておきます。
- 初期値を決定する。
- について以下を繰り返す。
- をより発生させる。ここでは
- 発生させたに対して、採択確率で採択し、それ以外の場合は棄却してを再度採用する。
- をより発生させる。ここでは
2.2. GPR実装
回帰の実装自体はそんなに難しくありません。
## Load data using LinearAlgebra using DelimitedFiles using Distributions using Plots gr() data = readdlm("gpr.dat",'\t',Float64); N = 5; #number pf training data points A = data[1:N,:]; xtrain = A[:,1]; ytrain = A[:,2]; ## Define Gaussian Kernel function GaussianKernelMat(x1, x2, θ1, θ2) N1 = size(x1,1); N2 = size(x2,1); x_d = x1 * ones(1,N2) - (x2 * ones(1,N1))'; x_log = - x_d .^2 / θ2; return θ1 * (exp(1) * ones(N1,N2)) .^ x_log; end ## Detemine hyperparameters via MCMC (Metropolis-Hastings method) function LogLikelihood(x,y,θ1,θ2,θ3) Nx = length(x); K_θ = GaussianKernelMat(x, x, θ1, θ2) + θ3 * Matrix(1.0I,Nx,Nx); return LLH = - logdet(K_θ) - y' * inv(K_θ) * y; end step_n = 100000; #step number of MCMC burn_in = 5000; #steps at the beginning to discard θ1_0 = 1.0; θ2_0 = 1.0; θ3_0 = 0.2; #initial values ϕ_ts = [log(θ1_0) log(θ2_0) log(θ3_0); zeros(step_n - 1,3)]; #ϕ represents logθ L_ts = [LogLikelihood(xtrain,ytrain,θ1_0,θ2_0,θ3_0); zeros(step_n -1 ,1)]; step_w = 1.0; #step width of one MH step for t = 2:step_n ϕ_tmp = ϕ_ts[t-1,:] + step_w * randn(3,1); θ_tmp = (exp(1) * ones(Float64,3)) .^ ϕ_tmp; K = GaussianKernelMat(xtrain, xtrain, θ_tmp[1],θ_tmp[2]) + θ_tmp[3] * Matrix(1.0I,N,N); r = rand(Float64); ratio = exp(LogLikelihood(xtrain,ytrain,θ_tmp[1],θ_tmp[2],θ_tmp[3]) - LogLikelihood(xtrain,ytrain,exp(ϕ_ts[t-1,1]),exp(ϕ_ts[t-1,2]),exp(ϕ_ts[t-1,3]))); if ratio > r ϕ_ts[t,:] = ϕ_tmp; L_ts[t] = LogLikelihood(xtrain,ytrain,θ_tmp[1],θ_tmp[2],θ_tmp[3]); else ϕ_ts[t,:] = ϕ_ts[t-1,:]; L_ts[t] = L_ts[t-1]; end end #Cut burn-in steps ϕ_ts = ϕ_ts[burn_in+1:end,:]; L_ts = L_ts[burn_in+1:end,:]; #take out maximal values ff = findmax(L_ts); ci = ff[2]; argmax_ϕ = ϕ_ts[ci[1],:]; #ϕ values at the maximal likelihood argmax_θ = (exp(1) * ones(Float64,3)) .^ argmax_ϕ; #θ values at the maximal likelihood #range of input xtest = -1:0.01:3.5; xlen = length(xtest); #estimate mean of y K = GaussianKernelMat(xtrain,xtrain,argmax_θ[1],argmax_θ[2]) + argmax_θ[3] * Matrix(1.0I,N,N); k_star = GaussianKernelMat(xtrain,xtest,argmax_θ[1],argmax_θ[2]); y_est = k_star' * inv(K) * ytrain; k_double_star = argmax_θ[1] + argmax_θ[3]; #variance of y y_var = k_double_star * ones(xlen,1) - diag(k_star' * inv(K) * k_star); y_sig = y_var .^(1/2); #lastly, figure plot plot(xtest, y_est, fillrange=[y_est-2y_sig], fillalpha=0.3, c=:orange) plot!(xtest, y_est, fillrange=[y_est+2y_sig], fillalpha=0.3, c=:orange) plot!(xtrain, ytrain, seriestype=:scatter)
3. 結果
3.1. 回帰結果
はじめに、MLPガウス本のデータを使って実験です。全データ10点のうちはじめの5点のみ使って回帰した結果は以下のようになり、MLPガウス本p.91の図3.20(a)を再現することができました。ハイパーパラメータの値はで、これも同本に記載のある勾配法を用いた値()と少しずれがあるものの大方一致しました(MCMCのステップ幅をせばめればもう少しこの値に近くかもしれません)。
次に10点すべてを用いて行なった回帰を示します。MLP本p.91の図3.20(b)にあたります。ハイパーパラメータはで、これも本の値とほぼ一致しました。複雑な回帰がいい感じでできていることがわかります。
はじめにつくった自作のサンプルデータでも同じことをやりました。
MCMCのおかげでハイパーパラメータも推定でき、データ点が多くても精度良く予測してくれます。データ点の近くでは分散が小さく自信のある様子を、すこし外れたところでは分散が大きく自信のない程度も教えてくれています。
最後に、Betelgeusでの実験はこの通りです。2010年から直近10年の結果を使用し、1年のうち2ヶ月分の平均を6回とって使っています。
滑らかな関数では回帰できており、dimmingの様子もわかりますが、いまいちなんとも言えないですねぇ。まぁ、平均0のGauss過程でGauss kernelをそのまま使っているのでしょうがないでしょう。もう少し平均やkernelに工夫をすればマシになるかもしれません。
3.2. MCMCの様子
今回は3パラメータありますので、パラメータがMH法で動いた様子を3D scatter plotで表現しました。MLP本のデータ10点で学習したときのものです。
確率分布の山の高そうなところに点が集まっているのが分かります。
4. おまけ (MATLAB ソースコード)
研究室ではいつもMATLABを使っているので、自分としてはこちらの方が書きやすいです。 今回Juliaはほぼ初めてでしたが、バグ取り中にエラーの場所を確認する意味でMATLABに移植して同じことをしていたため、副産物的にMATLABコードもできてしまいました(Juliaでやる意味)。こちらも載せるだけ載せておきます。Built inの函数などを使って書けばもっと少なくかけると思います。
dir = which('sample_data_200.txt'); data = readmatrix(dir); %load('~/Desktop/machine_learning/gpbook_data/gpr.dat') %data = gpr; N = 100; A = data(1:N,:); xtrain = A(:,1); ytrain = A(:,2); t1_0 = 1.0; t2_0 = 1.0; t3_0 = 0.2; %% Metropolis Hastings for hyperparameter determination MCMCsteps = 100000; burn_in = 5000; p_ts = [log(t1_0) log(t2_0) log(t3_0); zeros(MCMCsteps - 1,3)]; L_ts = [LogLikelihood(xtrain,ytrain,t1_0,t2_0,t3_0); zeros(MCMCsteps -1 ,1)]; step_w = 0.5; for t = 2:MCMCsteps p_tmp = p_ts(t-1,:) + step_w * randn(1,3); t_tmp = (exp(1) * ones(1,3)) .^ p_tmp; K = GaussianKernelMat(xtrain, xtrain, t_tmp(1),t_tmp(2)) + t_tmp(3) * eye(N); while any(eig(K) <= 0) % limit parameters for K to be positive definite p_tmp = p_ts(t-1,:) + step_w * randn(1,3); t_tmp = (exp(1) * ones(1,3)) .^ p_tmp; K = GaussianKernelMat(xtrain, xtrain, t_tmp(1),t_tmp(2)) + t_tmp(3) * eye(N); end r = rand; ratio = exp(LogLikelihood(xtrain,ytrain,t_tmp(1),t_tmp(2),t_tmp(3))... - LogLikelihood(xtrain,ytrain,exp(p_ts(t-1,1)),exp(p_ts(t-1,2)),exp(p_ts(t-1,3)))); if ratio > r p_ts(t,:) = p_tmp; L_ts(t) = LogLikelihood(xtrain,ytrain,t_tmp(1),t_tmp(2),t_tmp(3)); else p_ts(t,:) = p_ts(t-1,:); L_ts(t) = L_ts(t-1); end end p_ts = p_ts(burn_in+1:end,:); L_ts = L_ts(burn_in+1:end,:); [maxL, ind] = max(L_ts); argmax_t = (exp(1) * ones(1,3)) .^ p_ts(ind,:); %% Draw figure xtest = [-20:0.1:200]'; xlen = length(xtest); K = GaussianKernelMat(xtrain,xtrain,argmax_t(1),argmax_t(2)) + argmax_t(3) * eye(N); k_star = GaussianKernelMat(xtrain,xtest,argmax_t(1),argmax_t(2)); y_est = k_star' * inv(K) * ytrain; k_double_star = argmax_t(1) + argmax_t(3); y_var = k_double_star * ones(xlen,1) - diag(k_star' * inv(K) * k_star); y_sig = y_var .^(1/2); figure; h = plot(xtest,y_est); hold on scatter(xtrain,ytrain,'x'); ar = area(xtest,[y_est - 2*y_sig, 2*y_sig, 2*y_sig]); hold off set(h,'Color',[0.8500 0.3250 0.0980], 'LineWidth',1.0); set(ar(1),'FaceColor',[1 1 1],'LineStyle','None'); set(ar(2),'FaceColor',[0.8500 0.3250 0.0980],'LineStyle','None','FaceAlpha',0.25); set(ar(3),'FaceColor',[0.8500 0.3250 0.0980],'LineStyle','None','FaceAlpha',0.25); % plot(xtest, y_est, fillrange=[y_est-2y_sig], fillalpha=0.3, c=:orange) % plot!(xtest, y_est, fillrange=[y_est+2y_sig], fillalpha=0.3, c=:orange) % plot!(xtrain, ytrain, seriestype=:scatter) %% Define functions function GK = GaussianKernelMat(x1, x2, t1, t2) N1 = size(x1,1); N2 = size(x2,1); x_d = x1 * ones(1,N2) - (x2 * ones(1,N1))'; x_log = - x_d .^2 / t2; GK = t1 * ((exp(1) * ones(N1,N2)) .^ x_log); end function LLH = LogLikelihood(x,y,tt1,tt2,tt3) Nx = length(x); K_theta = GaussianKernelMat(x, x, tt1, tt2) + tt3 * eye(Nx); % LLH = - log(det(K_theta)) - detrend(y') * inv(K_theta) * detrend(y); LLH = - log(det(K_theta)) - y' * inv(K_theta) * y; end
5. 参考文献
- 『ガウス過程と機械学習』持橋大地・大羽成征 MLP
- 本ページトップにリンクあり
- 本ページトップにリンクあり
- Qiita記事『pythonでガウス過程を実装する。パラメータの調整はマルコフ連鎖モンテカルロ法(MCMC)を使う』
『日本統計学会創立 75 周年記念出版 21 世紀の統計科学 < Vol. III > 数理・計算の統計科学』 国友直人・山本拓 監修 東京大学出版会
- ネットで無料ダウンロードできます。素晴らしいpdfです。11章のMCMCの説明を参照しました。
6. さいごに・感想
Gaussian processを実装してすこしでも気持ちをわかろうと努力してみました。 回帰では、結局kernel函数の選択がモデル選択に相当すると思われるので、結構重要ですね。
今回はは全部の平均をでやってましたが、これは要は回帰の精度に自信のないデータから遠く離れた点はなんでもかんでも平均ゼロを予測するよーということで、モデルによってはあまり正しくないといえますので、改善の余地があります。
次回はではハイパーパラメータの決定として勾配法でやってみようかと思います。 またデータが増えるごとに精度が良くなる様子をみるため、アニメーション化も行います(予定)。
今回はJulia素人が頑張って作った感満載で、また数式やコードじたいも本格的にブログに書いたのでえっらい時間がかかりました...... 研究します......