「ガウス過程と機械学習」を3章まで読み終えたので、復習を兼ねてJulia(1.1.0)でガウス過程を実装し、 カーネルのハイパーパラメーターをOptim.jlで推定するところまでをまとめる。数学的に細かい内容は本を読んで欲しい。 図3.23の陸上男子100mの世界記録の回帰モデルを作成することを今回の目標とする。

ガウスカーネルによる回帰: ガウスカーネルによる回帰

ガウスカーネル+線形カーネルによる回帰: ガウスカーネル+線形カーネルによる回帰

任意の有限の入力 \( x_1, \ldots , x_n \) を与えたときに、出力 \( (f(x_1), \ldots , f(x_n)) \) が平均 \( (\mu(x_1), \ldots , \mu(x_n)) \) 分散 \( (k(x_n, x_{nm} )) \) のガウス分布に従う時、 \( f \) をガウス過程と呼び、 \( f \sim \text{GP} (\mu(x), k(x, x^\prime)) \) と書く。そして \( \mu \) を平均関数、 \( k \) をカーネル関数と呼んでいるのであった。

今回は本と同様、簡単のために平均関数が恒等的に0となるものだけを考える。

ガウスカーネルの定義

もっとも基本的なカーネルであるガウスカーネルを定義して、ガウス過程を構成する。ガウスカーネルのカーネル関数は次のものとする。 $$ k(x, x^\prime ) = \exp \left( -\frac{|x-x^\prime|^2}{\theta}\right) $$ 本文では $$ k(x, x^\prime ) = \theta_1 \exp \left( -\frac{|x-x^\prime|^2}{\theta_2}\right) $$ この形で紹介されていたが、後々カーネルの線型結合を考えるのでここでは \( \exp \) の前に係数を付けない前者を採用する。

abstract type Kernel end

function cov(k::Kernel, xs1, xs2)
    # covariance matrix
    n1 = size(xs1, 1)
    n2 = size(xs2, 1)
    c = zeros(n1, n2)
    for i in 1:n1
        for j in 1:n2
            c[i, j] = ker(k, xs1[i, :], xs2[j, :])
        end
    end
    c
end

cov(k::Kernel, xs) = cov(k, xs, xs)


"""
Gaussian Kernel
"""
mutable struct GaussianKernel <: Kernel
     theta::Float64
end

function ker(k::GaussianKernel, x1, x2)
    exp(- sum((x1 - x2).^2) / k.theta)
end

分散共分散行列を計算する cov 関数とガウスカーネルを定義した。 mutable にしたのは、後々パラメーター推定をするときにパラメーターの更新をするため。

using Distributions
using Plots

gk = GaussianKernel(1)
xs = collect(-4:0.5:4)
gk_dist = MvNormal(zeros(Base.length(xs)), cov(gk, xs))
Plots.plot(xs, rand(gk_dist, 5), 
    label = "", xlabel = "x", ylabel = "f",
    linewidth = 2,
    title = "")

\( \theta=1 \)のガウスカーネルから生成されるガウス過程から、入力を-4から4まで0.5ごとに選んだ点とし、サンプルをいくつか取ってみる。

ガウス過程からのサンプル

ガウス過程の定義

上でガウスカーネルを定義した方法には一つ問題があり、例えば点を0.1毎に取ると上手く動かない。

xs = collect(1:0.1:4)
gk_dist = MvNormal(zeros(Base.length(xs)), cov(gk, xs))
Plots.plot(xs, rand(gk_dist, 5), 
    label = "", xlabel = "x", ylabel = "f",
    linewidth = 2,
    title = "")
PosDefException: matrix is not positive definite; Cholesky factorization failed.

Stacktrace:
 [1] checkpositivedefinite at /Users/osx/buildbot/slave/package_osx64/build/usr/share/julia/stdlib/v1.1/LinearAlgebra/src/factorization.jl:11 [inlined]
 [2] #cholesky!#96(::Bool, ::Function, ::LinearAlgebra.Hermitian{Float64,Array{Float64,2}}, ::Val{false}) at /Users/osx/buildbot/slave/package_osx64/build/usr/share/julia/stdlib/v1.1/LinearAlgebra/src/cholesky.jl:153
 [3] #cholesky! at ./none:0 [inlined]
 [4] #cholesky!#97(::Bool, ::Function, ::Array{Float64,2}, ::Val{false}) at /Users/osx/buildbot/slave/package_osx64/build/usr/share/julia/stdlib/v1.1/LinearAlgebra/src/cholesky.jl:185
 [5] #cholesky#101 at ./none:0 [inlined]
 [6] cholesky at /Users/osx/buildbot/slave/package_osx64/build/usr/share/julia/stdlib/v1.1/LinearAlgebra/src/cholesky.jl:275 [inlined] (repeats 2 times)
 [7] Type at /Users/apple/.julia/packages/PDMats/AObTs/src/pdmat.jl:19 [inlined]
 [8] MvNormal(::Array{Float64,1}, ::Array{Float64,2}) at /Users/apple/.julia/packages/Distributions/wY4bz/src/multivariate/mvnormal.jl:196
 [9] top-level scope at In[18]:6

問題が発生した原因は、 cov 関数により生成される分散共分散行列が正定値にならないことである。対策としては、分散共分散行列の対角成分に小さい数を加えて行列が正定値になるようにすれば良い。(1.4のリッジ回帰の説明を参照)

各成分ごとにカーネル関数を計算した結果得られる分散共分散行列に、単位行列の定数倍を加えて最終的に使う分散共分散行列を作るというのは、観測ノイズを考慮した観測モデルを考えるときも同じなので、今回はガウス回帰モデルを次のように定義する。

using LinearAlgebra

"""
Gaussian Process
"""

mutable struct GaussianProcess{K <: Kernel}
    kernel::K
    eta::Float64 # regularization parameter
    GaussianProcess(kernel::K) where {K <: Kernel} = new{K}(kernel, 1e-6)
    GaussianProcess(kernel::K, eta::Real) where {K <: Kernel} = new{K}(kernel, Float64(eta))
end

function cov(gp::GaussianProcess, xs)
    # regularlize
    n = size(xs, 1)
    cov(gp.kernel, xs) + gp.eta * Matrix{Float64}(I, n, n) 
end

function dist(gp::GaussianProcess, xs)
    l = size(xs, 1)
    k = cov(gp, xs)
    MvNormal(zeros(l), k)
end

ここでは、 eta が観測ノイズの項目に相当し、観測値にノイズがないものとして考える場合は分散共分散行列の正則化のため対角成分に1e-6を加えることにする。xs の刻みを細かくしてサンプリングできることを確認しよう。

gp = GaussianProcess(GaussianKernel(1))
xs = collect(-4:0.1:4)
Plots.plot(xs, rand(dist(gp, xs), 5), 
    label = "", xlabel = "x", ylabel = "f",
    linewidth = 2,
    title = "")

ガウス過程からのサンプリング

xs = collect(-4:0.01:4)
Plots.plot(xs, rand(dist(gp, xs), 5), 
    label = "", xlabel = "x", ylabel = "f",
    linewidth = 2,
    title = "")

ガウス過程からのサンプリング)

ノイズ項を入れるとこんな感じ

gp = GaussianProcess(GaussianKernel(1), 0.01)
xs = collect(-4:0.01:4)
Plots.plot(xs, rand(dist(gp, xs), 5), 
    label = "", xlabel = "x", ylabel = "f",
    linewidth = 2,
    title = "")

ガウス過程からのサンプリング(ノイズ項)

同様に定数カーネル、線形カーネルも定義しておこう。(その他のカーネルにも本文には出てくるが、ここでは省略)

"""
Constant kernel
"""
struct ConstantKernel <: Kernel end

function ker(k::ConstantKernel, x1, x2)
    return 1.0
end

"""
Linear kernel
"""
struct LinearKernel <: Kernel end

function ker(k::LinearKernel, x1, x2)
    return 1.0 + dot(x1, x2)
end

LinearKernelのカーネルの実装では、定数項を考慮するために1を加えている。サンプルをプロットするとそれぞれ下のようになる(コードは略)

定数カーネル 線形カーネル

カーネルの定数倍、和

本文3.3.2にあるように、カーネルは組み合わせて使うことができ、カーネルの和・積もまたカーネル関数になる。

今回、ガウスカーネル、ガウスカーネル+線形カーネルを考えるにあたっては、カーネルの定数倍、カーネルの和が定義されていれば十分なので、その二つを定義しておこう。

import Base: +, *

"""
Scalar product
"""
mutable struct KernelScalarProd <: Kernel
    coef::Float64
    kernel::Kernel
end

function ker(k::KernelScalarProd, x1, x2)
     k.coef * ker(k.kernel, x1, x2)
end

"""
Sum
"""
mutable struct KernelSum <: Kernel
    kernel1::Kernel
    kernel2::Kernel
end

function ker(k::KernelSum, x1, x2)
     ker(k.kernel1, x1, x2) + ker(k.kernel2, x1, x2)
end

*(coef::Real, k::Kernel) = KernelScalarProd(Float64(coef), k)
+(k1::Kernel, k2::Kernel) = KernelSum(k1, k2)

こんな風にカーネルの線型結合からガウス過程が定義できるようになった。下は、線形カーネルとガウスカーネルの線型結合を考えた例。

gp = GaussianProcess(2.0 * LinearKernel() 
                    + 0.8 * GaussianKernel(0.01))
xs = collect(-1:0.01:3)
Plots.plot(xs, rand(dist(gp, xs), 10), 
    label = "", xlabel = "x", ylabel = "f",
    linewidth = 2,
    title = "")

線型結合

回帰

サンプリングができたので、次に回帰を行う。

回帰を行おう。本文の後半には、ガウス過程回帰の計算方法を少なくする方法が書いてあるが、まだそこまで読んでいないのでここは素直な方法(本の公式3.8)でガウス過程回帰を定義する。

function predict(gp::GaussianProcess, xtest, xtrain, ytrain)
    k_star = cov(gp.kernel, xtrain, xtest)
    s = cov(gp, xtest)

    k_inv = inv(cov(gp, xtrain))
    k_star_inv = k_star' * k_inv
    mu = k_star_inv * ytrain
    sig = s - k_star_inv * k_star
    MvNormal(mu, sig)
end

まず、パラメーターは既知のものとして、予測分布からのサンプリングと、誤差範囲を示してみよう。

xs = [-0.5, 0.5, 1, 1.4, 3]
ys = [0.7, 1.8, 1.7, 2.3, 1]

gp = GaussianProcess(1.596 * GaussianKernel(6.560), 0.082)

xtest = collect(range(-1, stop=3.5, length=100))
pred = predict(gp, xtest, xs, ys)
qt = mapslices(x -> quantile(x, [0.025, 0.975]), rand(pred, 10000), dims = 2)

Plots.plot(xtest, qt[:, 1], fillrange = qt[:, 2], fillalpha = 0.3,
    label = "", linewidth = 0)
Plots.plot!(xtest, mean(pred), label = "Mean", linewidth = 2, linestyle = :dash)

scatter!(xs, ys, label = "", title = "Posterior distribution")

実は、これだとうまくいかない

PosDefException: matrix is not Hermitian; Cholesky factorization failed.

Stacktrace:
 [1] checkpositivedefinite(::Int64) at /Users/osx/buildbot/slave/package_osx64/build/usr/share/julia/stdlib/v1.1/LinearAlgebra/src/factorization.jl:11
 [2] #cholesky!#97(::Bool, ::Function, ::Array{Float64,2}, ::Val{false}) at /Users/osx/buildbot/slave/package_osx64/build/usr/share/julia/stdlib/v1.1/LinearAlgebra/src/cholesky.jl:182
 [3] #cholesky#101 at ./none:0 [inlined]
 [4] cholesky at /Users/osx/buildbot/slave/package_osx64/build/usr/share/julia/stdlib/v1.1/LinearAlgebra/src/cholesky.jl:275 [inlined] (repeats 2 times)
 [5] Type at /Users/apple/.julia/packages/PDMats/AObTs/src/pdmat.jl:19 [inlined]
 [6] Type at /Users/apple/.julia/packages/Distributions/wY4bz/src/multivariate/mvnormal.jl:196 [inlined]
 [7] predict(::GaussianProcess{KernelScalarProd}, ::Array{Float64,1}, ::Array{Float64,1}, ::Array{Float64,1}) at ./In[24]:10
 [8] top-level scope at In[25]:7

原因は、 predictsig が計算誤差によりSymmetricになっていないのが原因なので、predict を次のように修正する。

function predict(gp::GaussianProcess, xtest, xtrain, ytrain)
   k_star = cov(gp.kernel, xtrain, xtest)
   s = cov(gp, xtest)

   k_inv = inv(cov(gp, xtrain))
   k_star_inv = k_star' * k_inv
   mu = k_star_inv * ytrain
   sig = Symmetric(s - k_star_inv * k_star)
   MvNormal(mu, sig)
end

すると、次のような結果が得られる。

回帰結果

微分を定義する

学習データを \( \mathcal{D}=(\mathbf{X}, \mathbf{y}) \), ハイパーパラメーターを \( \boldsymbol{\theta} \), ハイパーパラメータから計算されるカーネル行列を \( \mathbf{K}_\boldsymbol{\theta} \) とした時に、対数尤度関数

$$ L := -\log | \mathbf{K}_\boldsymbol{\theta} | - \mathbf{y}^T \mathbf{K}_\boldsymbol{\theta}^{-1} \mathbf{y} $$

を最大化するハイパーパラメーターを勾配法で求めよう。\( L \) の偏微分は、

$$ \frac{\partial L}{\partial \theta} = \text{tr} \left( \mathbf{K}_\boldsymbol{{\theta}}^{-1} \frac{\partial \mathbf{K}_\boldsymbol{\theta}}{\partial \theta} \right) + (\mathbf{K}_\boldsymbol{\theta}^{-1} \mathbf{y})^T \frac{\partial \mathbf{K}_\boldsymbol{\theta}}{\partial \theta} (\mathbf{K}_\boldsymbol{\theta}^{-1} \mathbf{y})$$

だった。パラメータ \( \theta \in \boldsymbol{\theta} \) は \( \theta > 0 \) でなくてはならないので、\( \tau = \log \theta \) と変換して \( \tau \) を最適化する。つまり、実際に勾配法で使う偏微分は $$ \frac{\partial L}{\partial \tau} = \frac{\partial L}{\partial \theta} \frac{\partial \theta}{\partial \tau} = \theta \frac{\partial L}{\partial \theta}$$ である。同様に \( \frac{\partial \mathbf{K}_\boldsymbol{\theta}}{\partial \tau} = \theta \frac{\partial \mathbf{K}_\boldsymbol{\theta}}{\partial \theta} \) だから、\( \theta \) の代わりに \( \tau \) を考えて

$$ \frac{\partial L}{\partial \tau} = \text{tr} \left( \mathbf{K}_\boldsymbol{{\theta}}^{-1} \frac{\partial \mathbf{K}_\boldsymbol{\theta}}{\partial \tau} \right) + (\mathbf{K}_\boldsymbol{\theta}^{-1} \mathbf{y})^T \frac{\partial \mathbf{K}_\boldsymbol{\theta}}{\partial \tau} (\mathbf{K}_\boldsymbol{\theta}^{-1} \mathbf{y})$$

を計算する。まずは GaussianKernel, ConstantKernel, LinearKernel の微分を定義する。パラメーターごとの偏微分したもののリストを返すことにする ConstantKernel, LinearKernel はパラメーターを持たないので、空のリストを返しておく。

function logderiv(k::GaussianKernel, x1, x2)
    [ker(k, x1, x2) / k.theta * sum((x1 - x2).^2)]
end

function logderiv(k::ConstantKernel, x1, x2)
    []
end

function logderiv(k::LinearKernel, x1, x2)
    []
end

カーネルの定数倍、和に対して、元のカーネルの微分を利用して微分を定義する。

function logderiv(k::KernelScalarProd, x1, x2)
    [ker(k.kernel, x1, x2), 
     k.coef * logderiv(k.kernel, x1, x2)...]
end

function logderiv(k::KernelSum, x1, x2)
    [logderiv(k.kernel1, x1, x2)..., 
     logderiv(k.kernel2, x1, x2)...]
end

Optim.jlによる最適化

微分を定義したので、Optim.jl で最適化しよう。

まず、カーネルのパラメーターを更新する update! を定義。

function update!(k::GaussianKernel, theta)
    k.theta = Float64(theta)
    k
end

update!(k::ConstantKernel) = k

update!(k::LinearKernel) = k

function update!(gp::GaussianProcess, params...)
    update!(gp.kernel, params[1:end - 1]...)
    gp.eta = params[end]
    gp
end

これを和と定数倍の場合にも延長する。和のカーネルを更新する時に、ぞれぞれのカーネルのパラメーターの数を知る必要がある。Base.length をカーネル、ガウス過程に対して拡張しよう。

Base.length(k::Kernel) = Base.length(fieldnames(typeof(k)))

Base.length(k::KernelScalarProd) = 1 + Base.length(k.kernel)

Base.length(k::KernelSum) = Base.length(k.kernel1) + Base.length(k.kernel2)

Base.length(gp::GaussianProcess) = Base.length(gp.kernel) + 1

これでようやく和と定数倍の場合の update が定義できる。

function update!(k::KernelScalarProd, params...)
    k.coef = params[1]
    update!(k.kernel, params[2:end]...)
    k
end

function update!(k::KernelSum, params...)
    l = Base.length(k.kernel1)
    update!(k.kernel1, params[1:l]...)
    update!(k.kernel2, params[l+1:end]...)
    k
end

対数尤度関数と微分では共通する計算があるので、

Avoid repeating computations
https://julianlsolvers.github.io/Optim.jl/stable/#user/tipsandtricks/#avoid-repeating-computations

を参考にして fg! を定義。Optim.jlは関数の最小化を行うため、fg! では \( -L \) の値と微分を計算している。(ついでに対数尤度も定義しておく)

function logp(gp::GaussianProcess, xs, ys)
    k = cov(gp, xs)
    k_inv = inv(k)
    -log(det(k)) - ys' * k_inv * ys
end

function fg!(gp::GaussianProcess, xs, ys, F, G, params)
    # -logp and gradient
    y = exp.(params)
    update!(gp, y...)
    k = cov(gp, xs)
    k_inv = inv(k)
    k_inv_y = k_inv * ys

    n = size(xs, 1)

    function deriv(d_mat::Matrix{<: Real})
        -(-tr(k_inv * d_mat) + k_inv_y' * d_mat * k_inv_y)
    end

    # gradient
    if G != nothing
        d_tensor = zeros(n, n, Base.length(gp))
        for i in 1:n
            for j in 1:n
                t = logderiv(gp.kernel, xs[i, :], xs[j, :])
                d_tensor[i, j, 1:end - 1] = t
            end
        end
        # eta
        d_tensor[:, :, end] = y[end] .* Matrix{Float64}(I, n, n) 
        G .= mapslices(deriv, d_tensor, dims = [1, 2])[:]
    end

    # log likelihoood
    if F != nothing
        return -(-log(det(k)) - ys' * k_inv * ys)
    end
end

まずは図3.16のデータでハイパーパラメーターを推定しよう。推定したいハイパーパラメータの形は $$ k(\mathbf{x}, \mathbf{x}^\prime \mid \boldsymbol{\theta}) = \theta_1 \exp \left( - \frac{|\mathbf{x} - \mathbf{x}^\prime |^2}{\theta_2} \right) + \theta_3 \delta (\mathbf{x}, \mathbf{x}^\prime) $$ だから、パラメーターを仮置きして下のようにガウス過程を定義する。

gp = GaussianProcess(1.0 * GaussianKernel(1.0), 1.0)

Optim.jlのGradientDescent を使ってパラメーターを推定する。実際のハイパーパラメーターに戻すために、最後に exp を取っている。

using Optim

lower = fill(-30.0, 3)
upper = fill(30.0, 3)

res = optimize(
    Optim.only_fg!((F, G, x) -> fg!(gp, xs, ys, F, G, x)),
    lower, upper, [0.0, 0.0, 0.0], 
    Fminbox(GradientDescent()))

println(res)
pars = Optim.minimizer(res)
println("[theta1, theta2, theta3] = ", exp.(pars))
Results of Optimization Algorithm
 * Algorithm: Fminbox with Gradient Descent
 * Starting Point: [0.0,0.0,0.0]
 * Minimizer: [0.4677728528438338,1.8810363129622452, ...]
 * Minimum: 1.738770e+00
 * Iterations: 3
 * Convergence: true
   * |x - x'| ≤ 0.0e+00: false 
     |x - x'| = 6.21e-08 
   * |f(x) - f(x')| ≤ 0.0e+00 |f(x)|: false
     |f(x) - f(x')| = 9.45e-15 |f(x)|
   * |g(x)| ≤ 1.0e-08: true 
     |g(x)| = 9.23e-09 
   * Stopped by an increasing objective: false
   * Reached Maximum Number of Iterations: false
 * Objective Calls: 353
 * Gradient Calls: 353
[theta1, theta2, theta3] = [1.59643, 6.5603, 0.0819847]

男子100m走の世界記録のデータを使ったハイパーパラメーター推定

長くなったが、最後に、本と同様、男子100m走の世界記録のデータを使ってハイパーパラメーターを推定してみよう。

using CSV
using Dates
using DataFrames

df = CSV.read(IOBuffer(
"Date,Time
1964/10/15,10.06
1968/6/20,10.03
1968/10/13,10.02
1968/10/14,9.95
1983/7/3,9.93
1987/8/30,9.93
1988/8/17,9.93
1988/9/24,9.92
1991/7/14,9.9
1991/8/25,9.86
1994/7/6,9.85
1996/7/27,9.84
1999/6/16,9.79
2002/9/14,9.78
2005/6/14,9.77
2006/5/12,9.77
2006/6/11,9.77
2006/8/18,9.77
2007/9/9,9.74
2008/5/31,9.72
2008/8/16,9.69
2009/8/16,9.58"); dateformat="yyyy/mm/dd")
disallowmissing!(df)
scatter(df.Date, df.Time, label="")

男子100mの世界記録

値を平均0, 分散1となるように正規化する。

using Dates

xs_raw = Dates.value.(df.Date .- Date(0, 1, 1)) ./ 365
xs_mean, xs_std = mean(xs_raw), std(xs_raw)
ys_raw = df.Time
ys_mean, ys_std = mean(ys_raw), std(ys_raw)

xs = (xs_raw .- xs_mean) ./ xs_std
ys = (ys_raw .- ys_mean) ./ ys_std

scatter(xs, ys, label="")

正規化した男子100mの世界記録

LBFGS でハイパーパラメーターを推定する。[0, 0, 0] からスタートすると、

function plot_gp_100m(gp, pars)

    update!(gp, exp.(pars)...)
    x_test = collect(range(-2, stop=2, length=100))
    pred = predict(gp, x_test, xs, ys)
    qt = mapslices(x -> quantile(x, [0.025, 0.975]), rand(pred, 10000), dims = 2)

    # convert
    x_test = x_test .* xs_std .+ xs_mean
    qt = qt .* ys_std .+ ys_mean
    Plots.plot(x_test, qt[:, 1], fillrange = qt[:, 2], fillalpha = 0.3,
        label = "", linewidth = 0)
    Plots.plot!(x_test, mean(pred) .* ys_std .+ ys_mean, label = "", linewidth = 2, linestyle = :dash)
    scatter!(xs_raw, ys_raw, label = "")
end

gp = GaussianProcess(1.0 * GaussianKernel(1.0), 1.0)

res = optimize(
    Optim.only_fg!((F, G, x) -> fg!(gp, xs, ys, F, G, x)),
    fill(-10.0, 3), fill(10.0, 3), [0.0, 0.0, 0.0], 
    Fminbox(LBFGS()))

println(res)
pars = Optim.minimizer(res)
println("[theta1, theta2, theta3] = ", exp.(pars))
println("logp:", logp(gp, xs, ys))

plot_gp_100m(gp, pars)
Results of Optimization Algorithm
 * Algorithm: Fminbox with L-BFGS
 * Starting Point: [0.0,0.0,0.0]
 * Minimizer: [1.4404906589345008,2.6294999978819886, ...]
 * Minimum: -1.486413e+01
 * Iterations: 20
 * Convergence: true
   * |x - x'| ≤ 0.0e+00: true 
     |x - x'| = 0.00e+00 
   * |f(x) - f(x')| ≤ 0.0e+00 |f(x)|: true
     |f(x) - f(x')| = 0.00e+00 |f(x)|
   * |g(x)| ≤ 1.0e-08: false 
     |g(x)| = 5.07e-08 
   * Stopped by an increasing objective: true
   * Reached Maximum Number of Iterations: false
 * Objective Calls: 7536
 * Gradient Calls: 7536
[theta1, theta2, theta3] = [4.22277, 13.8668, 0.102625]
logp:14.864131619224107

[0,0,0]からスタートしたガウスカーネルによる回帰

となって本に載っているのとは別の局所解に収束してしまう。[0, 0, -3] からスタートすると、

res = optimize(
    Optim.only_fg!((F, G, x) -> fg!(gp, xs, ys, F, G, x)),
    fill(-10.0, 3), fill(10.0, 3), [0.0, 0.0, -3.0], 
    Fminbox(LBFGS()))

println(res)
pars = Optim.minimizer(res)
println("[theta1, theta2, theta3] = ", exp.(pars))
println("logp:", logp(gp, xs, ys))

plot_gp_100m(gp, pars)
Results of Optimization Algorithm
 * Algorithm: Fminbox with L-BFGS
 * Starting Point: [0.0,0.0,-3.0]
 * Minimizer: [0.4403584574143354,-1.4741097963164387, ...]
 * Minimum: -1.407396e+01
 * Iterations: 5
 * Convergence: true
   * |x - x'| ≤ 0.0e+00: true 
     |x - x'| = 0.00e+00 
   * |f(x) - f(x')| ≤ 0.0e+00 |f(x)|: true
     |f(x) - f(x')| = 0.00e+00 |f(x)|
   * |g(x)| ≤ 1.0e-08: false 
     |g(x)| = 1.45e-08 
   * Stopped by an increasing objective: true
   * Reached Maximum Number of Iterations: false
 * Objective Calls: 331
 * Gradient Calls: 331
[theta1, theta2, theta3] = [1.55326, 0.228982, 0.0429989]
logp:14.073964533876048

[0,0,-3]からスタートしたガウスカーネルによる回帰

と、本と同様の回帰結果が得られる。(パラメーターの値は本と違ってしまっているが…)

最後に、ガウスカーネル + 線形カーネルによる回帰を行ってみよう。

gp_2 = GaussianProcess(1.0 * ConstantKernel() + 1.0 * LinearKernel() + 1.0 * GaussianKernel(1.0), 1.0)

res = optimize(
    Optim.only_fg!((F, G, x) -> fg!(gp_2, xs, ys, F, G, x)),
    fill(-10.0, 5), fill(10.0, 5), [0.0, 0.0, 0.0, 0.0, -3.0], 
    Fminbox(LBFGS()))

println(res)
pars = Optim.minimizer(res)
println("[theta1, theta2, theta3, theta4, theta5] = ", exp.(pars))
println("logp:", logp(gp_2, xs, ys))

plot_gp_100m(gp_2, pars)
Results of Optimization Algorithm
 * Algorithm: Fminbox with L-BFGS
 * Starting Point: [0.0,0.0,0.0,0.0,-3.0]
 * Minimizer: [-3.591175043240611,-0.6634886622587809, ...]
 * Minimum: -1.970913e+01
 * Iterations: 12
 * Convergence: true
   * |x - x'| ≤ 0.0e+00: true 
     |x - x'| = 0.00e+00 
   * |f(x) - f(x')| ≤ 0.0e+00 |f(x)|: true
     |f(x) - f(x')| = 0.00e+00 |f(x)|
   * |g(x)| ≤ 1.0e-08: false 
     |g(x)| = 6.18e+00 
   * Stopped by an increasing objective: true
   * Reached Maximum Number of Iterations: false
 * Objective Calls: 4960
 * Gradient Calls: 4960
[theta1, theta2, theta3, theta4, theta5] = [0.0275659, 0.515051, 0.110337, 0.0252142, 0.0470559]
logp:19.709131389510937

線形+ガウスカーネルによる回帰

内容をまとめたJupyter Notebook ->
https://nbviewer.jupyter.org/github/matsueushi/notebook_blog/blob/master/gp_blog.ipynb

カーネル部分をjlファイルに分離し、指数カーネルや周期カーネルも定義したレポジトリはこちら ->
https://github.com/matsueushi/gp_and_mlp