Juliaで機械学習：深層学習フレームワークKnet.jlを使ってみる

これまで、JuliaでTensorFlowを使った機械学習についてまとめてきた。
今回は、
Juliaで使える深層学習フレームワークKnet(カイネットと発音するらしい)を使ってみる。
https://github.com/denizyuret/Knet.jl
JuliaでTensorFlow.jlを使った記事
"JuliaでTensorFlow その4: 線形基底関数を用いた回帰"
https://qiita.com/cometscome_phys/items/92dba9f82cd58d877ec5
でやったことを、Knet.jlを使ってやってみる。

# 利点
1. 速いらしい
2. GPUも使えるらしい
3. Pythonを使わずにJuliaだけで書ける

# 欠点
1. 文献が日本語英語含めてほとんどない

#バージョン
Julia 0.6.2
Knet 0.9.0

#再現すべき関数
これまで書いてきた記事で何度も登場しているが、再掲しておく。

ここでは、ある関数
$$
y = a_0 x+ a_1 x^2 + b_0 + 3cos(20x)
$$
という関数を考える。ここで、最後のcosはノイズのようなものとして考えており、$a_0$と$a_1$と$b_0$によって得られる二次関数を得ることが目的となる。
データを100点作っておく。

```julia:test.jl
n = 100
x0 = linspace(-2,2,n)
a0 = 3.0
a1= 2.0
b0 = 1.0
y0 = zeros(Float32,(1,n))
f(x0) = a0.*x0 + a1.*x0.^2 + b0 + 3*cos.(20*x0)
y0[:] = f(x0)
```

グラフは

![comparison_0.png](https://qiita-image-store.s3.amazonaws.com/0/246113/a812d46c-55d2-fe37-b206-a31661a405ed.png)
となる。

上のデータをフィッティングする際には、
$$
y = \sum_{k=0}^{k_{\rm max}} a_k x^k + b_0 
$$
という形を考える。ここでは、$x^k$を基底関数として線形回帰をしていることになる。
詳しくは、
JuliaでTensorFlow その4: 線形基底関数を用いた回帰
https://qiita.com/cometscome_phys/items/92dba9f82cd58d877ec5
を参照。

# インプットデータの生成
ここはこれまでの記事とほとんど同じである。

```julia:test.jl
function make_φ(x0,n,k)
    φ = zeros(Float32,k,n)
    for i in 1:k
        φ[i,:] = x0.^(i-1)
    end
    return φ
end
k = 4
φ = make_φ(x0,n,k)
```
データが入ってある引数は配列の一番右であることに注意。ここでは、nがデータの数となるが、PythonでのTensorFlowではこれは一番左の引数であった。PythonのTensorFlowでは、つまり、行列とベクトルの積を$x W$と書いていた。
Knetでは、よく見られる形である$W x$の形式で書くことにする。

# モデルの構築
次に、モデルを構築する。Knetでは、学習すべき量はwと書き、リスト形式で全てを持つようにするようだ。
今回は、重み$W$とバイアス$b$がある。$W$は$1 \times k$の行列である。よって、学習すべき量を

```julia:test.jl
w = Any[ones(Float32,(1,k)),ones(Float32,1)]
```
とする。一つ目が重み、二つ目がバイアスとなる。
次に、このwを使って、値を予言するための関数：

```julia:test.jl
function predict(w,x)
    y = w[1]*x .+w[2]
    return y
end
```
を作る。TensorFlowではここにグラフの構築が入っていた。
さらに、loss関数を

```julia:test.jl
loss(w,x,y) = mean(abs2,y-predict(w,x))
```
その勾配を

```julia:test.jl
lossgradient = grad(loss)
```
と定義しておく。
Knetでは、計算した勾配からwをアップデートする関数update!が用意されており、
トレーニングは

```julia:test.jl
function train(model, data, optim)
    for (x,y) in data
        grads = lossgradient(model,x,y)
        update!(model, grads, optim)
    end
end
```
と書くことができる。ここで、optimは最適化のための関数で、今回はAdamを使うことにする。
このtrainを繰り返し呼ぶことで学習が進む。

# 学習
さて、学習をしてみよう。一つ一つデータを入れて学習させてもよいが、やはりランダムバッチ学習がしたい。
そこで、minibatch関数を使って、

```julia:test.jl
dtrn = minibatch(φ,y0,10,shuffle=true)
```
としてみよう。ここでは、10個ランダムにデータを取ってきている。
具体的な学習のプロセスは

```julia:test.jl
o = optimizers(w, Adam)
for i in 1:2000
    train(w,dtrn,o)
    if i%100 == 0 
        println(loss(w,φ,y0))
    end
end
```
で実行できる。ここで、最適化にAdamを使うため、optimizers(w, Adam)を呼んだ。

全体のコードは

```julia:Knettest.jl
n = 100
x0 = linspace(-2,2,n)
a0 = 3.0
a1= 2.0
b0 = 1.0
y0 = zeros(Float32,(1,n))
f(x0) = a0.*x0 + a1.*x0.^2 + b0 + 3*cos.(20*x0)
y0[:] = f(x0)

function make_φ(x0,n,k)
    φ = zeros(Float32,k,n)
    for i in 1:k
        φ[i,:] = x0.^(i-1)
    end
    return φ
end
k = 4
φ = make_φ(x0,n,k)

w = Any[ones(Float32,(1,k)),ones(Float32,1)]

function predict(w,x)
    y = w[1]*x .+w[2]
    return y
end

loss(w,x,y) = mean(abs2,y-predict(w,x))
lossgradient = grad(loss)

function train(model, data, optim)
    for (x,y) in data
        grads = lossgradient(model,x,y)
        update!(model, grads, optim)
    end
end

dtrn = minibatch(φ,y0,10,shuffle=true)

o = optimizers(w, Adam)
for i in 1:2000
    train(w,dtrn,o)
    if i%100 == 0 
        println(loss(w,φ,y0))
    end
end

ye = predict(w,φ)

using Plots
ENV["PLOTS_TEST"] = "true"

pls = plot(x0,[y0[1,:],ye[1,:]],marker=:circle,label=["Data","Estimation"])
savefig("comparison_2.png")
```

となる。
学習の結果、

```
Any[Float32[0.481912 2.94451 2.05044 0.0198765], Float32[0.481912]]
```

という重みとバイアスが得られ、グラフは

![comparison_2.png](https://qiita-image-store.s3.amazonaws.com/0/246113/917c0228-b868-0fbe-c642-3ff8e7476508.png)

となる。