この記事について
本記事では、ガウス過程回帰(Gaussian Process Regression)を数式とRコードを用いて、実際に計算しながら理解します。
ガウス過程回帰とは
ガウス過程回帰は「データが多変量正規分布に従う」というシンプルな仮定から、予測値の平均と不確実性(分散)を同時に計算できる手法です。プロセス開発・材料開発・実験自動化などができるベイズ最適化の、内部モデルとしてよく使われます。私自身、ガウス過程回帰を半導体の材料開発で活用してきました。
本記事では以下の内容を解説します:
- ガウス過程モデルの定式化
- カーネル行列の計算方法と意味
- ガウス過程回帰を計算するためのRコード
- ガウス過程回帰の外挿における問題点
統計学の基礎知識(正規分布・行列演算)があれば読めるように書きました。
サンプルデータ
本記事では、下図のサンプルデータ(黒点)を例に計算を進めます。ここで、xは入力変数、yは観測値です。

このデータに対して、ガウス過程回帰モデルを適用し、下図のような予測分布を計算できるようにするのが、本記事の目標です。

Rコード
外部パッケージを使用せず、すべての行列計算を実装していますので、ガウス過程回帰の中身がちゃんとわかる内容になっています。Rコード全文は本記事の付録に含まれていますので、Rの実行環境がある場合はコピペで動かすことができます。
また、Rの実行環境がない場合もShinyliveを使って、こちらのリンクから試すこともできます。

記事の構成
- 1章 ガウス過程回帰とは
- 2章 新しい点での予測値
- 3章 カーネルの計算方法と意味
- 4章 予測分布を実際に計算
- 5章 ガウス過程回帰を計算するRコード
- 6章 外挿の危険性について
最終章の外挿の危険性については、こちらの記事でいかに克服するかを解説しましたので、合わせてお読みください。
www.doe-get-started.com
1. ガウス過程回帰とは
ガウス過程回帰(GP)はデータが「一つの大きな多変量正規分布」に従うというモデルです。
既知の観測値 と予測値
をまとめると、ガウス過程は、
という形で、記述されます。
ここで、、
、
はカーネルと言われる行列で、次節で説明するように、すべて入力
から決まります(観測値
には依存しません)。
各カーネル行列の意味:
: 訓練点どうしのカーネル行列(
)
: 訓練点と予測点のカーネル行列(
)
: 予測点どうしのカーネル行列(
)
2. 新しい点での予測値
今やりたいことは、既知のデータセット が与えられた時、新しい点
における予測値
を計算することです。予測は条件付き分布(eq.2)として計算されます。この式が一番重要です。
難解に思われるかもしれませんが、(eq.2)は(eq.1)に対して条件付き分布の"公式"を当てはめることで導出できるので、気になる方は付録を読んでください。
一旦(eq.2)を認めていただくとして、ここで言いたいことは、 を決めると予測
の
- 平均
- 分散
は一意に定まるということです。
3. カーネル
次に、(eq.2)にカーネル、
、
をどのように計算するのかを説明します。
ガウス過程回帰ではRBFカーネルというカーネルがよく用いられます。
これはカーネル行列の各成分が、2つの入力点とハイパーパラメータ
を使って計算できます。
すべてのの組に対して
を計算しカーネル行列
を構築していきます。
ここまでで、まだカーネルの意味はさっぱり分からないと思いますが、先にどんな計算をするかだけ紹介します。
3.1 カーネルの計算例
入力点として、RBFでカーネルを計算してみます。繰り返しになりますが、カーネルは入力点
だけで決まるもので、出力
は必要ありません。
そのままxに対して計算してもよいのですが、後々のために、xを次の式で標準化し、
に対してカーネルを計算してみましょう。RBFカーネルのハイパーパラメータは固定して、
とします。
となります。すべてのに対して同様に計算すると、
これが(eq.2)で用いるカーネルです。
以下に、カーネル行列を構築するRコードを示します。RBF以外も選べるようになっています。
# ---------- カーネル成分の計算(rbf以外も選択可能) ----------
kernel <- function(xi, xj, type = c("rbf", "linear", "quad", "rbf_linear"),
theta1 = 1, theta2 = 0.4) {
type <- match.arg(type)
switch(type,
rbf = { return(theta1 * exp(-0.5*sum((xi - xj)^2) / theta2)) },
linear = { return(sum(xi*xj) + 1) },
quad = { return((sum(xi*xj) + 1)^2) },
rbf_linear = { return(theta1 * exp(-0.5*sum((xi - xj)^2) / theta2) + sum(xi*xj) + 1) }
)
}
# ---------- カーネル行列の構築 ----------
kernel_matrix <- function(X1, X2 = X1, type = "rbf", theta1 = 1, theta2 = 0.4,
noise = 0, jitter = 1e-5) {
X1 <- as.matrix(X1); X2 <- as.matrix(X2)
N1 <- nrow(X1); N2 <- nrow(X2)
K <- matrix(0, nrow = N1, ncol = N2)
for (i in 1:N1) {
xi <- X1[i, , drop = FALSE]
for (j in 1:N2) {
xj <- X2[j, , drop = FALSE]
K[i, j] <- kernel(xi, xj, type = type, theta1 = theta1, theta2 = theta2)
}
}
if (noise != 0 && identical(X1, X2)) { K <- K + noise * diag(N1) }
return(K)
}
2.2 カーネルの意味
ここまでで、とりあえずカーネル行列は作れるようになりましたね。しかし、カーネルとは一体何を意味しているのでしょうか?本節はかなり抽象的になるので、飛ばしても問題ありません。
今、ある一つの入力 について無限次元の基底
を用意し、入力
に対する出力
を
と表すことにしましょう。すると全データは、
のように書けますから、共分散行列は、
となります。これはというガウス過程です。
以上を一行でまとめると、
のような無限次元の基底の線形結合モデル
と、ガウス過程
が同じものだよ
ということです。
別の言い方をすれば、ガウス過程のカーネルを決めることは、基底関数の性質を決めることだと言えます。
そして、カーネルを決める際、基底
を考える必要はない、というのがカーネルトリックと言われる、まさにトリックですね。
RBFカーネルの場合、Kの各成分は(eq.3)で与えられますが、これは背後で
という関数の無限次元の基底
を用いた線形結合モデル
になっています。
4. 予測分布を実際に計算
前置きが長くなりましたので、何をしたいか思い出しておきましょう。我々は、ガウス過程回帰による新しい点の予測値の平均と分散を、(eq.2)により計算したいのでした。
ここで、
: 訓練点どうしのカーネル行列(
)
: 訓練点と予測点のカーネル行列(
)
: 予測点どうしのカーネル行列(
)
です。
また、はデータが得られるときのノイズを考慮した分散になります。
4.1 サンプルデータ
以下では、サンプルデータを使って、ガウス過程回帰の予測分布を計算していきます。5回の実験を行い、それぞれの入力xにおいて出力y(実験値)が、以下のように得られたとしましょう。
| i | 入力 |
観測値 |
標準化 |
標準化 |
| 1 | −1.0 | −8.200 | −1.2649 | −1.2785 |
| 2 | −0.5 | −1.125 | −0.6325 | −0.2212 |
| 3 | 0.0 | −2.843 | 0.0000 | −0.4779 |
| 4 | +0.5 | +8.697 | +0.6325 | +1.2463 |
| 5 | +1.0 | +5.250 | +1.2649 | +0.7313 |
これをplotすると図のようになります。

を行っておきます。生データと標準化後のデータは下図の通りです。

4.2 ガウス過程回帰による予測平均と分散
まず、カーネルを構築します。前述したカーネルを計算する関数kernel_matrixを利用しましょう。
# 訓練点どうしのカーネル行列(ノイズを含む: eq.2 における K+sigma2*I)
K <- kernel_matrix(X_train, X_train, type=type, theta1=theta1, theta2=theta2, noise=theta3)
# 訓練点と予測点のカーネル行列(ノイズを含まない)
k <- kernel_matrix(X_train, X_pred, type=type, theta1=theta1, theta2=theta2)
# 予測点どうしのカーネル行列(ノイズを含まない)
s <- kernel_matrix(X_pred, X_pred, type=type, theta1=theta1, theta2=theta2)
RBFカーネルのハイパーパラメータは固定して、theta1=1.0、theta2=0.4、sigma2=0.05 とすると
標準化後の入力 に対するK:
4.2.1 内挿点 x*=0 における予測値
Step 1: 標準化(訓練データと同じ統計量を使用)
Step 2: の計算(
と各訓練点
の間のカーネル値)
Step 3: の計算
を求めます。
Step 4: 予測平均の計算
Step 5: 予測分散の計算
Step 6: 元スケールへの逆変換
よって予測値は-1.591 ± 2.037 となります。
4.2.2 外挿点 x*=2 における予測値
Step 1: 標準化
Step 2: の計算(
と各訓練点
の間のカーネル値)
ここで、内挿点の時との違いは、
の成分がほぼゼロであることです。
は共分散ですから、外挿点
と訓練データがほとんど互いに関係がないということを意味します。
Step 3: の計算(x*=0 と共通)
Step 4: 予測平均の計算
Step 5: 予測分散の計算
Step 6: 元スケールへの逆変換
よって予測値は −0.184 ± 6.772 となります。
以下に、x*=0(内挿)と x*=2(外挿)の比較を示します:
| x*=0(内挿) | x*=2(外挿) | |
| z* | 0.0000 | 2.5298 |
| |
1.0000(中央点) | 0.1353(最遠点のみ) |
| |
−0.2909 | −0.0806(≈ 事前平均 0) |
| |
0.0426(小) | 0.9739(≈ 1) |
| |
−1.591 | −0.184(≈ 事前平均 0.356) |
| |
2.037 | 6.772(大) |
外挿点では が小さくなるため、
となり予測平均が標準化空間でゼロ(元スケールで
)に収束します。また
となり予測分散が事前分散まで膨らみます。
5. ガウス過程回帰で新しい点を予測するRコード
以下に、新しい点の予測平均・分散を計算する関数 GP_pred を示します。
# ---------- コレスキー分解により Vx=b を x について解く ----------
chol_solve <- function(L, b) {
y <- forwardsolve(L, b, upper.tri = FALSE, transpose = FALSE)
x <- backsolve(t(L), y, upper.tri = TRUE, transpose = FALSE)
x
}
# ---------- ガウス過程回帰の予測分布(mu, var_y)の計算 ----------
GP_pred <- function(X_train, Y_train, X_pred,
theta1 = 1, theta2 = 0.4, theta3 = 0,
type = c("rbf", "linear", "quad", "rbf_linear")) {
X_train <- as.matrix(X_train)
Y_train <- as.matrix(Y_train)
X_pred <- as.matrix(X_pred)
N <- nrow(X_train); M <- nrow(X_pred)
if (N != length(Y_train)) stop("length(x_train) must equal length(y_train)")
# カーネル行列の構築(theta2 = 0.4 をそのまま渡す)
K <- kernel_matrix(X_train, X_train, type=type, theta1=theta1, theta2=theta2, noise=theta3)
k <- kernel_matrix(X_train, X_pred, type=type, theta1=theta1, theta2=theta2)
s <- kernel_matrix(X_pred, X_pred, type=type, theta1=theta1, theta2=theta2)
# コレスキー分解
L <- t(chol(K))
yy <- chol_solve(L, Y_train) # yy = K^{-1} y
v <- chol_solve(L, k) # v = K^{-1} k_*
mu <- t(k) %*% yy # 予測平均
var_f <- s - t(k) %*% v # 予測分散(関数)
if (abs(theta3) > .Machine$double.eps^0.5) {
var_y <- var_f + theta3 * diag(M)
return(list(mu = mu, var_f = var_f, var_y = var_y))
} else {
return(list(mu = mu, var_f = var_f))
}
}
これまでの関数(kernel_matrixおよびGP_pred)とその他必要な処理すべてを含んだコードを示します。以下のコードをすべてコピペすると、サンプルデータを使ったガウス過程回帰のプロットが得られます。
# -----------------------------------------------------------------------
# Kernel処理関数
# -----------------------------------------------------------------------
chol_solve <- function(L, b) {
y <- forwardsolve(L, b, upper.tri = FALSE, transpose = FALSE)
x <- backsolve(t(L), y, upper.tri = TRUE, transpose = FALSE)
x
}
kernel <- function(xi, xj, type = c("rbf", "linear", "quad", "rbf_linear"),
theta1 = 1, theta2 = 0.4) {
type <- match.arg(type)
switch(type,
rbf = { d2 <- sum((xi - xj)^2); return(theta1 * exp(-0.5*d2 / theta2)) },
linear = { return(sum(xi*xj) + 1) },
quad = { return((sum(xi*xj) + 1)^2) },
rbf_linear = { d2 <- sum((xi - xj)^2); return(theta1 * exp(-0.5*d2 / theta2) + sum(xi*xj) + 1) }
)
}
kernel_matrix <- function(X1, X2 = X1, type = "rbf", theta1 = 1, theta2 = 1, noise = 0, jitter = 1e-5) {
X1 <- as.matrix(X1); X2 <- as.matrix(X2)
N1 <- nrow(X1); N2 <- nrow(X2)
K <- matrix(0, nrow = N1, ncol = N2)
for (i in 1:N1) {
xi <- X1[i, , drop = FALSE]
for (j in 1:N2) { K[i, j] <- kernel(xi, X2[j, , drop = FALSE], type = type, theta1 = theta1, theta2 = theta2) }
}
if (noise != 0 && identical(X1, X2)) K <- K + noise * diag(N1)
return(K)
}
# -----------------------------------------------------------------------
# Gaussian Process処理関数
# -----------------------------------------------------------------------
GP_pred <- function(X_train, Y_train, X_pred, theta1 = 1, theta2 = 0.4, theta3 = 0, type = c("rbf", "linear", "quad", "rbf_linear")) {
X_train <- as.matrix(X_train); Y_train <- as.matrix(Y_train); X_pred <- as.matrix(X_pred)
N <- nrow(X_train); M <- nrow(X_pred)
if (N != length(Y_train)) { stop("length(x_train) must equal length(y_train)") }
K <- kernel_matrix(X_train, X_train, type = type, theta1 = theta1, theta2 = theta2, noise = theta3)
k <- kernel_matrix(X_train, X_pred, type = type, theta1 = theta1, theta2 = theta2)
s <- kernel_matrix(X_pred, X_pred, type = type, theta1 = theta1, theta2 = theta2)
L <- t(chol(K)); yy <- chol_solve(L, Y_train); v <- chol_solve(L, k)
mu <- t(k) %*% yy; var_f <- s - t(k) %*% v
if (abs(theta3) > .Machine$double.eps^0.5) {
var_y <- var_f + theta3 * diag(M)
return(list(mu = mu, var_f = var_f, var_y = var_y))
} else {
return(list(mu = mu, var_f = var_f))
}
}
# -----------------------------------------------------------------------
# Standardize処理関数
# -----------------------------------------------------------------------
standardize_fit <- function(X, Z, y, standardize_X = TRUE, intercept_col = 1L) {
X <- as.matrix(X); Z <- as.matrix(Z); y <- as.numeric(y)
n <- length(y); p <- ncol(X); d <- ncol(Z)
y_mean <- mean(y); y_sd <- sd(y); if (y_sd == 0) y_sd <- 1
y_s <- (y - y_mean) / y_sd
Z_mean <- colMeans(Z); Z_sd <- apply(Z, 2, sd); Z_sd[Z_sd == 0] <- 1
Z_s <- sweep(sweep(Z, 2, Z_mean, "-"), 2, Z_sd, "/")
X_s <- X; X_mean <- rep(0, p); X_sd <- rep(1, p)
if (standardize_X) {
for (j in seq_len(p)) {
if (!is.null(intercept_col) && j == intercept_col) { X_mean[j] <- 0; X_sd[j] <- 1
} else {
X_mean[j] <- mean(X[, j]); X_sd[j] <- sd(X[, j]); if (X_sd[j] == 0) X_sd[j] <- 1
X_s[, j] <- (X[, j] - X_mean[j]) / X_sd[j]
}
}
}
list(X = X_s, Z = Z_s, y = y_s, y_mean = y_mean, y_sd = y_sd,
Z_mean = Z_mean, Z_sd = Z_sd, X_mean = X_mean, X_sd = X_sd,
standardize_X = standardize_X, intercept_col = intercept_col)
}
standardize_apply <- function(Xnew, Znew, std) {
Xnew <- as.matrix(Xnew); Znew <- as.matrix(Znew)
Z_s <- sweep(sweep(Znew, 2, std$Z_mean, "-"), 2, std$Z_sd, "/")
X_s <- Xnew
if (isTRUE(std$standardize_X)) {
p <- ncol(Xnew)
for (j in seq_len(p)) {
if (!is.null(std$intercept_col) && j == std$intercept_col) next
X_s[, j] <- (Xnew[, j] - std$X_mean[j]) / std$X_sd[j]
}
}
list(X = X_s, Z = Z_s)
}
destandardize_y <- function(y_scaled, std) { std$y_mean + std$y_sd * y_scaled }
destandardize_y_sd <- function(sd_scaled, std) { std$y_sd * sd_scaled }# -----------------------------------------------------------------------
# サンプルデータを使ったGaussian Processの計算
# -----------------------------------------------------------------------
theta1 <- 1.0; theta2 <- 0.4; theta3 <- 0.05
x1 <- c(-1, -0.5, 0, 0.5, 1)
y <- c(-8.200343, -1.124568, -2.842514, 8.697183, 5.250363)
X <- cbind(1, x1); Z <- cbind(x1)
std <- standardize_fit(X, Z, y, standardize_X = TRUE, intercept_col = 1L)
X_s <- std$X; Z_s <- std$Z; y_s <- std$y
m <- 100; x1n <- seq(-2, 2, length.out = m)
y_true_line <- 1 + 3*x1n - 0.5*exp(-2*(x1n))
new_s <- standardize_apply(cbind(1, x1n), cbind(x1n), std)
res <- GP_pred(X_train = Z_s, Y_train = y_s, X_pred = new_s$Z,
theta1 = theta1, theta2 = theta2, theta3 = theta3, type = "rbf")
mu_pred <- destandardize_y(res$mu, std)
sd_pred <- destandardize_y_sd(sqrt(diag(res$var_y)), std)
y_lo <- mu_pred - sd_pred; y_hi <- mu_pred + sd_pred
plot(x1n, mu_pred, type = "l", col = "steelblue", lwd = 2.5,
ylim = c(-20, 20), xlab = "x", ylab = "y", main = "GP Prediction")
polygon(c(x1n, rev(x1n)), c(y_lo, rev(y_hi)), col = rgb(0.27, 0.51, 0.71, 0.2), border = NA)
lines(x1n, y_true_line, col = "black", lwd = 2)
points(x1, y, pch = 16, cex = 1.3)
abline(v = c(-1, 1), col = "orange", lty = 2, lwd = 1.5)
legend("topleft",
legend = c("GP mean", "+-1 SD", "True f(x)", "Training data"),
lty = c(1, NA, 1, NA), pch = c(NA, 15, NA, 16),
col = c("steelblue", rgb(0.27, 0.51, 0.71, 0.3), "black", "black"),
pt.cex = c(NA, 2, NA, 1.3), bty = "n")
上記のコードによって得られる予測グラフは下図の通りです。

ガウス過程回帰の計算方法については、本章で終わりです。長文を読んでいただきありがとうございました。
6. ガウス過程回帰による外挿の危険性について
せっかくガウス過程回帰を理解できたところではありますが、ひとつ注意すべき点があります。それは外挿に対する予測の不確かさです。
図3を見てみると、オレンジ色の外挿域では ガウス過程回帰の予測はゼロに収束していっています。
これは、ガウス過程の平均がで計算されるところで、外挿域では
だからです。
ガウス過程回帰は与えらえたデータ付近の局所的な構造にしかフィットしないと言うこともできます。
つまり、人間が物理的な前提知識や、大局的なデータのトレンドを知っていても、予測モデルに反映することができないわけで、ガウス過程回帰は本質的には外挿に弱いモデルです。
ここで、RBFカーネルと線形カーネルを組み合わせたカーネルを使えば、線形項により大域的な傾向をとらえられるのでは?と反論があるかもしれません。実際、図3に対してカーネルを調整すれば、外挿域でもフィットさせることはできます。
しかし、カーネルとして線形項を入れると、基底は明示的に表現していない(カーネルトリック)ので、例えば特定の変数の回帰係数がいくらか、という解釈ができません。
私は、このガウス過程の「外挿に弱い」「解釈が難しい」という課題を解決するのがセミパラメトリックベイズだと考えていて、その解説は別の記事にまとめましたので、根気のある方は本記事と合わせて読んでいただけると嬉しいです。
www.doe-get-started.com
付録
条件付き正規分布の公式
の観測値が与えられたときの
の条件付き分布:
、
、
、
、
、
を代入: