はじめに
今までに幾度かテーマにしていますが、回帰とよばれる問題があります。
例えば、車の速度とブレーキをかけてから車が止まるまでの距離について、データから両者の関係を把握したい、というような場合。最も単純な関係式でこの目的を果たそうとする場合、一般には以下に示す単回帰が用いられるでしょう。
こんな需要もあるかもしれません。
上のモデルより少し難しいモデルですが、
上記の内容について理解が追い付かない場合は、こちらのサイトなどを覗いてみてください。分かりやすくまとめられています。
分析の目的が変数間の関係把握であるならば、上述のモデルで事足りる場合も多いと思います。しかし「将来得られる可能性のある任意の値
これまでに得られたデータ
今回は、上記のような期待に応えるモデルとしてガウス過程を取り上げ、 データに対し柔軟な回帰曲線を得られるようにすることを最終目標とします。
参考図書はこちらです。 こちらのブログも大いに参考にさせていただきました。RもいいですがJuliaも面白そうです。影響を受けてJuliaの参考書を買ってしまいました。
本記事の構成は以下の通りです。
ガウス過程の定義
ガウス過程の導出
ガウス過程について特にわかりやすいと思った参考図書の記載を抜粋してガウス過程について説明します。
この節の理解には多変量正規分布(
以下の線形回帰モデルを考えます。
特徴ベクトルの要素数を
特徴ベクトルとは機械学習分野の用語で、変換処理によって抽出されるデータの特徴を要素に持つベクトルです。
とおけます。
さらに式(1)を
とおけます。
ここで
とまとめられます。
式(3)を使えば、基底関数を自由に設定したり、基底関数の次元を増やすことでほとんど任意の形の関数が表現できそうです。しかし、
そこで、パラメータwの周辺化消去(期待値をとって積分消去)を考えます。
これによって
式(5)がガウス過程を定義する式です。これは式(3)と基本的に同じ機能をもつため、式(3)と同様、ほとんどどんな形でも表現できそうです。ただし、線形回帰モデル式(3)にあったパラメータ
ガウス過程の直感的な性質
ガウス過程の性質を決定する共分散行列について見ていきます。
とおくと、この
なので、これは
つまり、
カーネル関数
これを特徴ベクトルから計算しようとすると、複雑な計算が要求されます。そうではなく、式(6)を計算するための関数として、カーネル関数を用います。また、カーネル関数を用いて内積を計算することをカーネルトリックと呼びます。
カーネル関数は、何らかの無限次元の特徴ベクトル空間における2点の内積を表現できる、という特徴をもつ関数で、それぞれに異なる無限次元特徴ベクトルを定義する様々なカーネル関数があります。
例えば、カーネル関数の1種、ガウスカーネル
の場合、
となる特徴ベクトルは、第
となります(
これは、
を証明することで確認できます。証明はこちらのサイトで見れます。
なお、通常のパラメータとは設定目的の異なる、モデルの複雑さを決定するパラメータはハイーパーパラメータ(超母数)などと呼ばれます。以降、カーネル関数のパラメータもハイパーパラメータと呼ぶことにします。
ガウス過程のシミュレーション
ガウス過程の概要を確認したので、ガウス過程に従う確率変数がどのような挙動をするのか、いくつかのカーネル関数を例に見てみます。
シミュレーションの前に、カーネル行列(
# カーネル行列を作成する関数を定義
# 引数 kernel:この後に定義するカーネル関数を指定 x:トレーニングデータ
# 引数 par:kernelが要求するハイパーパラメータ delta:誤差の要素。ハイパーパラメータとは別で指定することにした
kernel_cov <- function(kernel, x,par, delta){
NL <- length(x)
Sigma <- matrix(NA, nrow=NL, ncol=NL)
for(i in 1:(NL-1)){
Sigma[i,i] <- kernel(x[i],x[i],par) + delta
for(j in (i+1):NL){
Sigma[i,j] <- kernel(x[i],x[j],par)
Sigma[j,i] <- Sigma[i,j]
}
}
Sigma[NL,NL] <- kernel(x[NL],x[NL],par) + delta
return(Sigma)
}
# シミュレーションのための自作関数を定義
GP_sim <- function(kernel, par){
p <- ggplot() + theme_bw(base_size = 11)
i <- 0
xs = seq(-4,4,0.05)
repeat{
i <- i + 1
if(i == 5) break
p <- p + geom_line(data=data.frame(x = xs,
y = MASS::mvrnorm(1, mu=rep(0,length(xs)),
kernel_cov(kernel=kernel,x=xs, par=par,delta=1e-8))),
aes(x=x,y=y), colour=as.factor(i))
}
plot(p)
}
ガウスカーネルのシミュレーション
前の節で紹介したガウスカーネルのシミュレーションです。改めてガウスカーネルを再定義します。
ガウスカーネルは、下のようにrateパラメータ
## ガウスカーネルの定義 kernel_cov()で使用できるように、引数は(x1,x2,par)でないといけない
## カーネルのハイパーパラメータはすべてparで指定する
Gaussian_kernel <- function(x1, x2, par){
return(par[1]*exp(-(x1 - x2)^2/par[2]))
}
GP_sim(Gaussian_kernel, par=c(1,1))
ガウスカーネルをカーネル関数に設定したガウス過程は、無限回微分可能な滑らかな曲線になるそうです。
線形カーネルのシミュレーション
線形カーネルは、ハイパーパラメータを持たないカーネル関数です。
線形カーネルは、
## 線形カーネルを定義。ハイパーパラメータは無し
linear_kernel <- function(x1,x2,par){
return(1 + t(x1)%*%x2)
}
GP_sim(linear_kernel, par=NULL)
シミュレーションでも直線が引けることが確認できます。
Matern3カーネルのシミュレーション
Maternカーネルは、ガウスカーネルの無限回微分可能という前提がモデル化において強すぎるという主張から提案されたカーネルです。
Maternカーネルを用いたガウス過程から生成される関数は、「
Matern3カーネルは以下のようになります。
このMatern3カーネルを
# Matern3カーネルを定義
Matern3_kernel <- function(x1,x2, par){
return((1 + sqrt(3)*abs(x1-x2)/par[1])*exp(-sqrt(3)*abs(x1-x2)/par[2]))
}
GP_sim(Matern3_kernel,par=c(1,1))
ガウスカーネルと比べていびつな曲線になっていることが分かります。
ガウスカーネルと線形カーネルの結合
カーネル関数は組み合わせて使うことも可能です。 ここではガウスカーネルと線形カーネルを組み合わせてみます。
上の式において、
# ガウスカーネル+線形カーネルの定義
Gaussian_plus_Linear_kernel <- function(x1,x2,par){
return(par[1] * exp(-(x1 - x2)^2/par[2]) + par[3] * (1 + t(x1)%*%x2))
}
GP_sim(Gaussian_plus_Linear_kernel,c(0.8,0.07,2))
ガウスカーネルと線形カーネルがうまく組み合わさっていることが分かります。
実際に回帰・パラメータ推定を実行
ひととおりシミュレーションしたので実践です。
まずは参考図書と同じ、陸上男子100mの世界記録のデータを準備します。
# データの準備
x <- as.Date(x=c("1964/10/15","1968/6/20","1968/10/13","1968/10/14","1983/7/3","1987/8/30","1988/8/17",
"1988/9/24","1991/7/14","1991/8/25","1994/7/6","1996/7/27","1999/6/16","2002/9/14",
"2005/6/14","2006/5/12","2006/6/11","2006/8/18","2007/9/9","2008/5/31","2008/8/16","2009/8/16"))
y <- c(10.06,10.03,10.02,9.95,9.93,9.93,9.93,9.92,9.9,9.86,9.85,9.84,9.79,9.78,9.77,9.77,9.77,9.77,9.74,9.72,9.69,9.58)
# 標準化
x <- (as.numeric(x)-mean(as.numeric(x)))/sd(as.numeric(x))
y <- (y-mean(y))/sd(y)
library(ggplot2)
p <- ggplot(data=data.frame(x=x,y=y), aes(x=x,y=y)) + theme_bw(base_size=11) + geom_point()
p
分析にあたって、まずはハイパーパラメータの最適な値を推定します。
推定方法にはハイパーパラメータを少しずつ変えてその事後分布を推定するMCMCも使えますが、ここでは計算量の少ない効率的な方法として勾配法を用います。
簡略化のため、ガウス過程のハイパーパラメータを
対数尤度は、
です。
式(7)を最大にする
式(7)を計算する自作関数を以下で定義します。
# ガウス過程モデルの対数尤度に比例する値を計算する関数を定義
GP_L <- function(x, y, par, kernel){
return(-log(det(kernel_cov(kernel=kernel,x=x,par=exp(par[-length(par)]),delta=exp(par[length(par)]))))
-t(y)%*%
solve(kernel_cov(kernel=kernel,x=x,par=exp(par[-length(par)]), delta=exp(par[length(par)])))%*%y)
}
式(7)を最大にするoptim()
を使います。参考図書では式(7)をoptim()
では引数gr
で一階偏微分関数を指定でき、簡単に定義できるなら明示的に指定した方が良いのですが、指定しなくても勝手に微分してくれるので今回は横着します。
参考図書には、以下のガウス過程の予測分布の公式が記載されていますので、この公式に倣って予測分布を計算する自作関数を定義します。
ここで
を要素に持つカーネル行列です。
# 予測値の算出のための関数定義
# 返り値は各予測点の平均と共分散行列
my_predict <- function(x_test, x_train, y_train, kernel, par){
kernel_cov_star <- function(x1,x2,par,kernel){
NL <- length(x1)
ML <- length(x2)
Sigma <- matrix(NA, nrow=NL, ncol=ML)
for(i in 1:NL){
for(j in 1:ML){
Sigma[i,j] <- kernel(x1=x1[i], x2=x2[j],par=par[-length(par)])
}
}
return(Sigma)
}
K <- kernel_cov(kernel=kernel,x=x_train, par=par[-length(par)], delta=par[length(par)])
k_star <- kernel_cov_star(kernel=kernel, x1=x_train, x2=x_test,par=par)
k_2star <- kernel_cov(kernel=kernel, x=x_test, par=par[-length(par)], delta=par[length(par)])
mu <- t(k_star) %*% solve(K) %*% y_train
sigma <- k_2star - t(k_star) %*% solve(K) %*% k_star
return(list(mu, sigma))
}
ガウスカーネルを使った回帰
上で定義した関数を使い、カーネル関数にガウスカーネルを設定して回帰を実行してみます。
#自作関数GP_L()を使って対数尤度を計算 対数尤度を最大化するハイパーパラメータを推定
res_optim1 <- optim(par=c(0,0,0),fn=GP_L,kernel=Gaussian_kernel, x=x,y=y,control = list(fnscale=-1), method="BFGS")
par <- exp(res_optim1$par)
# 推定したパラメータの値を確認
par
## [1] 1.55308949 0.22898734 0.04299662
#自作関数my_predict()を使って予測分布の平均・ 共分散行列を求める
pre <- my_predict(x_test = seq(-2.0,2.0,0.01),x_train=x, y_train=y, kernel=Gaussian_kernel,par=par)
# 結果の描画
library(ggplot2)
p <- ggplot() + theme_bw(base_size=11) +
geom_line(data=data.frame(x=seq(-2.0,2.0,0.01),y=pre[[1]]),aes(x=x,y=y))
p <- p + geom_point(data=data.frame(x=x,y=y),aes(x=x,y=y))
p <- p + geom_ribbon(data=data.frame(x=seq(-2.0,2.0,0.01),ymin=pre[[1]]-2*sqrt(diag(pre[[2]])), ymax=pre[[1]]+2*sqrt(diag(pre[[2]]))),
aes(x=x,ymax=ymax,ymin=ymin),fill="blue",alpha=0.5)
p
最適化したカーネル関数は以下になります。
青い領域はガウス事後分布の
これまた不思議な予測曲線が得られました。結果を観察すると、観測値の乏しい範囲では誤差範囲が広く推定されており、期待値も平均(
ハリボーはこう考えました。
データの少ない部分は曖昧に推定するというのは、現実的な判断じゃあないか>🦔
しかしかめきちはこう言っています。
将来世界記録のタイムが伸びるという推定はあまりにもおかしいかめ>🐢
ガウスカーネル+線形カーネルで回帰
かめきちの意見を踏まえ、カーネル関数でガウスカーネルと線形カーネルを組み合わせてみます。
# ガウスカーネル+線形カーネを使って回帰
# 自作関数GP_L()を使って尤度を計算 尤度を最大化するハイパーパラメータを推定
res_optim2 <- optim(par=c(0,0,0,0),fn=GP_L,kernel=Gaussian_plus_Linear_kernel,
x=x,y=y,control = list(fnscale=-1), method="BFGS")
par <- exp(res_optim2$par)
# 推定したパラメータの値を確認
par
## [1] 0.11098682 0.02720031 0.46865763 0.04742707
# 自作関数my_predict()を使って予測分布の平均・ 共分散行列を求める
pre <- my_predict(x_test = seq(-2.0,2.0,0.01),x_train=x, y_train=y,
kernel = Gaussian_plus_Linear_kernel,par=par)
# 結果の描画
library(ggplot2)
p <- ggplot() + theme_bw(base_size=11) +
geom_line(data=data.frame(x=seq(-2.0,2.0,0.01),y=pre[[1]]),aes(x=x,y=y))
p <- p + geom_point(data=data.frame(x=x,y=y),aes(x=x,y=y))
p <- p + geom_ribbon(data=data.frame(x=seq(-2.0,2.0,0.01),ymin=pre[[1]]-2*sqrt(diag(pre[[2]])), ymax=pre[[1]]+2*sqrt(diag(pre[[2]]))),
aes(x=x,ymax=ymax,ymin=ymin),fill="blue",alpha=0.5)
p
It’s so brilliant>🐢
最適化したカーネル関数は以下になります。
タイムが短縮される全体的な傾向が線形カーネルでとらえるとともに、細かい変動をガウスカーネルで表現できています。
勾配法の初期値を変えるとどうなるでしょう。
res_optim3 <- optim(par=c(0,0.4,0,0),fn=GP_L,kernel=Gaussian_plus_Linear_kernel,
x=x,y=y,control = list(fnscale=-1), method="BFGS")
par = exp(res_optim3$par)
par
## [1] 0.22313442 2.81135418 0.50883359 0.09808983
~省略~
最適化したカーネル関数は以下になります。
ハイパーパラメータの値が変化し、結果も全く異なります。勾配法によるハイパーパラメータ推定では初期値によって結果が異なることも多く、局所最適解が多い場合はその傾向が強いです。
このような場合、MCMCで推定すると大域的局所解に近づいてくれます。
情報量基準の確認
モデルの良さを表す数値的指標として、各種の情報量基準が提案されています。 ここでは、以下で定義されるAICを計算してみます。 (AICは正しい使い方というのがあるようで私はAICを誤用しているかもしれません。誤りがあればご指摘を!)
# optim()で対数尤度にマイナスをかけているので、ここでは対数尤度にマイナスをかけない
# optim()で最大化したGP_L()は、実際の対数尤度ではなく対数尤度に比例する値なので、ここで正確な対数尤度を計算する
my_AIC <- function(res_optim, x){
return(2*(res_optim$value/2 + length(x)/2 * log(2*pi))+2*length(res_optim$par))
}
cat(sprintf("AIC of \n res1 = %.3f\n res2 = %.3f\n res3 = %.3f",my_AIC(res_optim1,x), my_AIC(res_optim2,x), my_AIC(res_optim3,x)))
## AIC of
## res1 = 60.507
## res2 = 68.203
## res3 = 65.642
AICは、小さい値をとるモデルほど良いモデルと考えます。結果を見ると、一番初めのガウスカーネルを使ったモデルが最もAICが小さいです。また、ガウスカーネル・線形カーネルの2モデルの比較では、後のモデルの方がAICが小さいです。
これらの結果から、ガウスカーネルを使ったモデルが最も良いモデルであることが示唆されますが、AICはほかのモデルを完全否定するものではないので、最終的にどのモデルを選択するかは技術者判断と言えます。
私としては最後の結果を支持したいところです…。
まとめ
今回はガウス過程について説明しました。ガウス過程は理論が難しいですが、結果が合理的で、黒魔術かと疑ってしまうような技術です。
その応用範囲も広く、空間統計の分野では割と以前から活用されていたようです。また構造計算で利用される有限要素法においてもデータを完璧に補間するモデルとして広く利用されているそうです。
私はこれまで以下の活用方法を確認しています。
- 一般化線形モデルの線形予測子をガウス過程に置き換え、柔軟なモデルに豹変させる
- 空間統計において空間的自己相関を考慮したモデルを構築する
ちなみにガウス過程を一般化線形モデルに活用したりするような場合、事後分布は単純なガウス分布ではないため、MCMC等の近似推論法が必要となります。私の大好きなMCMCです。
また、大規模データに対しガウス過程を含むモデルを素直に計算すると、計算量が膨大となってしまいます。そのため、様々な近似手法が提案されているようです。参考図書の後半でその近似手法が述べられているので今後勉強したいと思っています。
ガウス過程の活用例も今後の記事で紹介できればと思っています。楽しみ。