Juliaで機械学習：深層学習フレームワークFlux.jlを使ってみる　その１：基本編

Julia言語のWebサイトをみると、Fluxなる深層学習フレームワークがある。
https://github.com/FluxML/Flux.jl
GPUも使える、などと書いてある。
"Relax! Flux is the ML library that doesn't make you tensor"（リラックス！Fluxはテンソルを使わない機械学習ライブラリだよ）などとあるので、どんなものか使ってみる。
Juliaの中の人が作っているっぽいので、今後もしかしたら大きくなるかもしれない。

以前、Juliaの別の深層学習フレームワークKnet.jlについて紹介した。
https://qiita.com/cometscome_phys/items/f09e801bc5b3f57f6350

今回は、Fluxの基本的なところをドキュメントに従って解説してみる。
ドキュメントは
http://fluxml.ai/Flux.jl/stable/
こちら。

# バージョン
Julia 0.7.0
Flux 0.6.0

->
Julia 1.1.0
Flux 0.8.3

2019年6月3日追記：Flux.jlがバージョンアップして以下の記事のいくつかは動かなくなっていたようなので、修正。

-> 
Julia 1.6.1
Flux 0.12.6

2021年8月23日追記：Flux.jlがバージョンアップした結果動かなかった部分を修正。paramがparamsになったり、Trackerが無くなったりしたようだ。コードが動かないな、と思ったときは[ドキュメント](
https://fluxml.ai/Flux.jl/stable/models/basics/#Taking-Gradients)
を参照すること。


#インストール
Juliaは0.7.0からは、
]キーを押してPkgモードに切り替えて色々パッケージを入れられるようになった。Fluxもadd Fluxで入る。

#勾配を取る
## 一変数関数
機械学習では、パラメータを最適化するためにパラメータの微分が必要となる。
そこで、Fluxにも微分するライブラリがある。
まず、1変数関数$f(x)$:
$$
f(x) = 3x^2 + 2x + 1
$$
のx微分を計算してみよう。手で簡単に微分できるので、結果としては、
$$
f'(x) = 6x + 2
$$
が欲しい。二階微分は
$$
f''(x) = 6
$$
である。

まず、ライブラリを使用可能にする：

```julia
#using Flux.Tracker
using Flux
f(x) = 3x^2 + 2x + 1
```
~~これはFluxのTrackerライブラリを呼んでいる。~~そして関数を定義した。Juliaではtexとほとんど同じ記法で書ける。
一階微分は

```julia
#f′(x) = Tracker.gradient(f, x; nest = true)[1]
f′(x) = gradient(f, x)[1]
```


と書けばよい。これで、$x=2$での一階微分の値は

```julia
f′(2)
```
で計算できる。

二階微分は

```julia
f′′(x) = gradient(f′, x)[1]
#f′′(x) = Tracker.gradient(f′, x; nest = true)[1]
```

で定義して、

```julia
f′′(2)
```
で計算できる。Juliaでは′記号も使えるので、ほとんどそのまま書ける。

##多変数関数
機械学習ではパラメータで微分を取ることが多い。そしてそのパラメータは式の中にたくさんある場合がある。
これはつまり、多変数関数の偏微分を取ることに他ならない。
関数として、

$$
f(x) = Wx+b
$$
を用意しよう。この関数のパラメータは$W$と$b$であるが、これは、この関数を

$$
f(W,x,b) = Wx+b
$$
と三変数関数とみなすことと等しい。
それぞれの変数での偏微分は

$$
\frac{\partial f}{\partial W} = x ,\;
\frac{\partial f}{\partial x} = W ,\;
\frac{\partial f}{\partial b} = 1 ,\;
$$
となる。
これを計算するには、Fluxでは

```julia
f′(W,x,b) = gradient(f, W, x, b)
#f′(W,x,b) = Tracker.gradient(f, W, x, b)
```
となり、計算は

```julia
f′(2,3,4)
```
でできる。

##パラメータが多い時
機械学習でのニューラルネットワークには数百以上のパラメータ微分が出てくることがある。
これを一々引数に入れるのは大変である。そんな時のために、Fluxには~~param~~ paramsというものがある。
まず、

```julia
#W = param(2) 
#b = param(3)
W = [2]
b = [3]
f(x) = W * x + b
```
として、Wとbを微分可能なパラメータであり、値が2と3であるとする。
そしてそれを

```julia
p = params([W,b])
#params = Params([W, b])
```
としてパラメータにまとめる。
このパラメータに関してのgradient(勾配)を

```julia
x = 4
grads = gradient(() -> f(x)[1],p)
#grads = Tracker.gradient(() -> f(x), params)
```
とする。これで$x=4$での勾配が定義された。
こうすることで、$W$と$b$でのパラメータ微分は

```julia
grads[W] 
grads[b] 
```
で求まる。

上の例だとあまり~~param~~ paramsのご利益を感じられないので、もう少しパラメータの多いものを考えてみよう。
Wを$2 \times 5$行列、$b$を2成分ベクトルとして、
$$
f(x) = W x + b
$$
を考える。ここで$x$は5成分ベクトルである。$f(x)$は2成分ベクトルとなる。
Wとbを乱数で適当に作る。

```julia
W = rand(2, 5)
b = rand(2)
```

そして、式を定義する。

```julia
f(x) = W*x .+ b
```

この式を使った関数：
$$
loss(x,y) = \sum_{i=1}^2 (y_i - [f(x)]_i)^2
$$
を定義しておく：

```julia
function loss(x, y)
  ŷ = f(x)
  sum((y .- ŷ).^2)
end
```

ここで$y$は2成分ベクトルである。
インプットとして5成分ベクトル$x$を乱数とし、アウトプットとして2成分ベクトル$y$を

```julia
x, y = rand(5), rand(2) 
```
用意すると、関数lossは

```julia
loss(x, y)
```
で計算できる。このloss関数は当然Wとbに依存している。そこで、loss関数のW微分とb微分を計算しよう。
これは、Wとbを~~param~~ paramsに変えて：

```julia
#W = param(W)
#b = param(b)
#parameters = Params([W, b])
parameters = params([W, b])
```

gradientを

```julia
gs = gradient(() -> loss(x, y),parameters)
#gs = Tracker.gradient(() -> loss(x, y),parameters)
```
と定義すればよい（注：Julia 0.7.0とFlux 0.6.0で1回目はエラーが出るがもう1回実行するとなぜかうまくいく現象があったので、エラーが出た場合にはgsをもう一回定義してみるといいかもしれない。）。

これで、loss関数のW微分は

```julia
Δ = gs[W]
```
で計算できる。ここで$W$は行列であることに注意。このΔは、Wのそれぞれの成分でloss関数を微分したものであり、Wが$2 \times 5$行列なので、Δも$2 \times 5$行列となる。
もし、loss関数を最小化するようなWが欲しければ、勾配方向に変化させればよい。Wなどのパラメータをアップデートする関数としてupdate!があり、それを使用可能にすれば、

```julia
using Flux: update!
#using Flux.Tracker: update!
update!(W, 0.1Δ)
loss(x, y)
```
とすることで、lossを減らす方向にWを変化させることができる。

# ニューラルネットワークのレイヤーを作る
ここまでで、paramを使うことでパラメータ微分ができることがわかった。
次は、ニューラルネットワークを作ってみよう。
５成分を持つあるインプットのベクトルxがあった時、2成分のアウトプットベクトルf(x)が欲しいと考える。
隠れ層1層のニューラルネットワークは
$$
f(x) = W_2 {\cal F}(W_1 x + b_1)+b_2
$$
と書ける。ここで、${\cal F}(x)$はベクトルの各成分に作用する非線形な関数(活性化関数と呼ばれる)であり、例えばsigmoid（シグモイド）関数：
$$
{\cal F}(x) = \frac{1}{1 + e^{-x}}
$$
などが使われる。$W_1$は$n \times 5$行列で、$n$は隠れ層のニューロンの数である。
ここでは$n=3$としておく。
これをFluxで書くと、

```julia
#W1 = param(rand(3, 5))
#b1 = param(rand(3))
W1 = rand(3, 5)
b1 = rand(3)
layer1(x) = W1 * x .+ b1

#W2 = param(rand(2, 3))
#b2 = param(rand(2))
W2 = rand(2, 3)
b2 = rand(2)
layer2(x) = W2 * x .+ b2

f(x) = layer2(σ.(layer1(x)))
```
となる。ここで、σ(x)はシグモイド関数である。loss関数の時と同じで、このネットワークのパラメータ微分はgradientを使って容易に書ける。

もう少しシンプルな書き方としては、一つのレイヤーをインプットとアウトプットを引数にして

```julia
function linear(in, out)
  #W = param(randn(out, in))
  #b = param(randn(out))
  W = randn(out, in)
  b = randn(out)
  x -> W * x .+ b
end
```
と定義して、

```julia
linear1 = linear(5, 3) 
linear2 = linear(3, 2)
f(x) = linear2(σ.(linear1(x)))
```
としてもよい。ここで、それぞれの層のパラメータWやbはlinear1.Wなどとすれば取り出せる。

また、他の書き方として、Wとbをstructでまとめることで

```julia
struct Affine
  W
  b
end
#Affine(in::Integer, out::Integer) =
#  Affine(param(randn(out, in)), param(randn(out)))
Affine(in::Integer, out::Integer) =
  Affine(randn(out, in), randn(out))
(m::Affine)(x) = m.W * x .+ m.b
```
とレイヤーを定義することもできて、

```julia
linear1  = Affine(5,3)
linear2 = Affine(3,2)
f(x) = linear2(σ.(linear1(x)))
```
とすることもできる。

##Fluxのシンプルな書き方
以上のものは、FluxではDenseというものを使って書くこともできる。Denseは引数が二つの時はレイヤーを、三つの時は三つ目に活性化関数を入れることになる。よって、上記と同じものを作りたければ、

```julia
using Flux
layer1 = Dense(5,3, σ)
layer2 = Dense(3, 2)
f(x) = layer2(layer1(x))
```
でよい。
さらに、これをもっとシンプルに書くこともできて、

```julia
using Flux
f2 = Chain(
  Dense(5, 3, σ),
  Dense(3, 2))
```
でよい。
他にもう一つの書き方もあって、

```julia
using Flux
f3 = Dense(3, 2) ∘ Dense(5, 3, σ)
```
と一行でも書ける。

#次回
次回はパラメータの最適化を使った学習について。