機械学習: 入門2 with Flux package

機械学習入門2: Flux パッケージを利用してみよう

Flux パッケージ

機械学習をより便利に使えるように,Julia のパッケージに Flux というものがある. 今回はこれを使ってみよう.

ちなみに,Flux の使い方については Flux マニュアル を見るとよいだろう. なお,サンプルが Flux Model Zoo に載っているので,参考にするとよいだろう.

注: 有名な機械学習用ライブラリ/フレームワークとして他に Caffe, Keras (高水準ライブラリ,TensorFlow などの他の低水準ライブラリを下部に使う), TensorFlow, Chainer などが知られている(主に他の言語用だが).

今回のターゲット問題

今回は典型的(かつ有名)な学習問題である「手書き数字の認識問題」をターゲットとしよう. 学習に使える実データとして有名なものに MNIST というものがある.

これは 70,000枚(学習用 60,000枚 + 学習成果テスト用 10,000枚)の手書き数字画像とその「正解」情報からなるデータセットで,各画像は以下のようになっている.

  • 色はモノクロ.濃淡が 0以上1以下の実数で表されている.
  • サイズは 28 $\times$ 28 ドット.
  • 本来のデータは 20 $\times$ 20 ドットの白黒二色のものだが,anti-aliasing アルゴリズムで 0以上1未満の濃淡に変換するなどしている. サイズが大きくなっているのは畳み込みなどの画像処理をしやすくするために各辺に「余白」をつけているのだと思われる. ただ,単に余白を足したのではなく,「大きめのキャンバスの中心に画像の中心をあわせて配置」しているので,余白は必ずしも各辺で 4ドットずつになっていないので要注意.

たとえばその 10枚目のデータを画像として見てみると

となっている(この画像では 0 $\cong$ 黒, 1 $\cong$ 白として表示されている).ちなみにこれは数字の「4」だそうな.

MNIST のデータは本家の web からダウンロードしてもよいが,それだとちょいとした前処理が必要となる. しかし実は,Flux パッケージは綺麗に処理された MNIST データをダウンロードする機能をもっているので,その機能を使おう(どうやるかはプログラムを見ればわかる).

今回用意する neural netowrk

今回は

  • 入力は要素数 768 ($= 28\times 28$) の実数ベクトル
  • 出力は数字 0 ~ 9 に対する「確率」を表すベクトル. つまり,要素数 10 の実数ベクトル $\boldsymbol{p}$で,$0 \leq p_i \leq 1$ かつ $\sum_i p_i = 1$.
  • 通常の密結合 NN を用いる.中間(network)層は 2つ.
  • 1つ目の中間層の出力サイズは 32 で,活性関数は ReLU.
  • 2つ目の中間層の出力サイズは 10 で,活性関数の代わりに softmax 関数で正規化して出力が確率分布として成り立つようにする.(注: 2つ目の中間層の出力が最終出力)

という NN を Flux で用意して,この中に含まれるパラメータを(学習によって)修正することにしよう.

なお,softmax 関数というのは,実数の列(負の実数もOK)を確率分布として解釈できる数列に(各要素は単調に)変換する関数の一つで, ベクトル $\boldsymbol{a} = \{ a_i \}$ に対して

$\displaystyle\mbox{ SoftMax }(\boldsymbol{a})_i = \frac{\exp(a_i)}{\sum_i \exp(a_i)}$

と定義できる.まあ,単調性と確率分布の性質を満たそうとするとたぶんこれが一番シンプルな変換だな. ちなみに,すべての数が正値ならば,単なる定数倍で正規化したほうが楽だろうな.

あと,出力の「誤差」を測る方法として,出力ベクトル $\boldsymbol{y}$ と真値ベクトル $\boldsymbol{z}$ に対する Cross Entropy

$\displaystyle\mbox{ CrossEntropy }(\boldsymbol{y}, \boldsymbol{z}) = - \sum_i \, z_i \, \exp( y_i )$

を使うことにする.
注: 入力独立変数に対して非対称な関数なので入力の順序に注意.順序は定義によるので,プログラムの source を確認しておく必要がある.Flux パッケージの crossentropy 関数は上の順序になっている.

これは,確率分布間の「違い」を数字にする方法の一つで,まあ,ノルム「のようなもの」と思えば良い(実際この量は $\boldsymbol{z}$ のエントロピー + カルバックライブラー情報量 $\mbox{KL}(\boldsymbol{z}, \boldsymbol{y})$で,$\mbox{KL}$ は情報幾何の世界ではノルム「もどき」として知られている量なのだ).

実際にやってみる

あとは少しずつプログラムを作っていくだけだ.

注: ただし一つ注意として,JuliaBox にデフォルトで入っている Flux がうまく動かない(やや古い?)ので,JuliaBox を利用している人は現段階で改めて

1
2
using Pkg
Pkg.add("Flux")

として Flux をインストールし直しておこう(1回だけでよい) もちろん,私有環境で Flux がインストールされてない人もこの作業が必要なのでやっておこう.

まずはいつものように Flux などのパッケージの使用宣言だ.

1
2
3
using Flux, Flux.Data.MNIST, Statistics
using Flux: onehotbatch, onecold, crossentropy, train!, throttle
using Base.Iterators: repeated

次に、MNIST データをダウンロードし,NN で扱えるように変換しよう. まずはダウンロードだ.

1
2
imgs = MNIST.images()
labels = MNIST.labels()

ダウンロードしたデータをちょっと見ておこう. まず,画像データの1つ目を見てみる.

1
imgs[1]

これは 0から9 のどの数字かというと,

1
labels[1]

5

ということで,正解は 5 だそうだ.

次に,これらを変換する.

1
2
3
4
5
6
7
# 「画像 = 行列」となっているデータをベクトルに変換し,
# それを横にくっつけていく
X = hcat(float.(reshape.(imgs, :))...) 

# 正解の値を,0:9 に対応するように,要素が 「真偽値」(= 1 or 0) の
# 10次元ベクトルに変換する
Y = onehotbatch(labels, 0:9)

変換した結果を見ておこう(結果の見た目は Flux のバージョンによって多少異なる).

1
X

784×60000 Array{Float64,2}: 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 … 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 …略…

1
Y

10×60000 Flux.OneHotMatrix{Array{Flux.OneHotVector,1}}: false true false false false … false false false false false false false false true false false false false false false false false false false false false false false false false …略…

では,NN を作ろう. Flux では大変簡単に,以下のように NN を構成できる.

1
2
3
4
5
6
7
8
m = Chain(
  Dense(28^2, 32, relu),
  # 密結合第一層.28^2 の入力を受けて,32 の出力を返す.活性関数は ReLU.
  Dense(32, 10),
  # 密結合第二層.32 の入力を受けて 10 の出力を返す.
  softmax
  # 出力直前に 正規化
  )

意味は,上の注釈でわかるだろう.

さて,この学習「していない」 NN での出力を念の為に確認しておこう. 1つ目のデータを入れるとどういう答えが返ってくるか…

1
r = m( X[:,1] )

Tracked 10-element Array{Float32,1}: 0.08216107f0 0.08204124f0 0.13114497f0 0.12829882f0 0.07081599f0 0.08782198f0 0.11761235f0 0.1471058f0 0.07090792f0 0.0820898f0

これが NN の出力で,この画像が「 0 ~ 9 である確率 」を並べたもの,と解釈することになる. わかりやすいようにグラフで見ておこう.

1
2
3
using Plots

bar( 0:9, r.data )

これだと,この画像は “2” である確率が一番高いことになり,正解の “5” を当てられていないことがわかる.

同様にもう二つほど見ておこう. データの 2つ目は

1
imgs[2]

となっていて,明らかに正解は “0” だ.まあ確認しておくか.

1
labels[2]

0

ふむ.ではこの画像データを NN に入れると…

1
bar( 0:9, m( X[:,2] ).data )

これも(当然?)うまくいってない.

データの3つ目も確認しておこう. (画像をみるのはもう省略して)真値は

1
labels[3]

4

となるので “4” が真値で,NN が出す確率は

1
bar( 0:9, m( X[:,3] ).data )

となっていてこれもうまくいってない.

さて,では学習に必要な関数の準備をしよう.

1
2
3
4
5
# 損失関数.要は出力誤差.今回は Cross Entropy で.
loss(x, y) = crossentropy(m(x), y)

# 正解率.この文脈だと精度ともいう.
accuracy(x, y) = mean(onecold(m(x)) .== onecold(y))

NN の正解率を測ることができるようになったので,学習していない現状の NN での正解率をみておこう.

1
2
# 初期のパラメータの NN だと精度は?
accuracy(X, Y)

0.07628333333333333

ふ~む.ランダムな状態の NN での正解率が 7.6% ということで,まあ自然だな(ランダムに数字を一つだせば,正解率は平均で 10% になるはずなので).

1
2
3
4
5
6
7
8
9
# データセット.
dataset = repeated((X, Y), 200)

# 損失関数を小さくする方向をどう決めるか.学習をどう行うか,ということ.
# ADAM 法がよく使われるようだ.
opt = ADAM()

# 学習が進んでいく途中での画面表示等を設定.
evalcb = () -> @show(loss(X, Y))

これで準備ができたので,早速実行しよう.

1
@time train!(loss, params(m), dataset, opt, cb = throttle(evalcb, 10)) # about 3min 30sec.

loss(X, Y) = 2.2536974f0 (tracked) loss(X, Y) = 1.5546396f0 (tracked) loss(X, Y) = 1.0643252f0 (tracked) loss(X, Y) = 0.7940663f0 (tracked) loss(X, Y) = 0.6253212f0 (tracked) loss(X, Y) = 0.5265094f0 (tracked) loss(X, Y) = 0.46330255f0 (tracked) loss(X, Y) = 0.41968328f0 (tracked) loss(X, Y) = 0.38776076f0 (tracked) loss(X, Y) = 0.3634234f0 (tracked) loss(X, Y) = 0.34418032f0 (tracked) loss(X, Y) = 0.32846916f0 (tracked) loss(X, Y) = 0.315274f0 (tracked) loss(X, Y) = 0.3047265f0 (tracked) loss(X, Y) = 0.29463047f0 (tracked) loss(X, Y) = 0.28560913f0 (tracked) 205.568310 seconds (1.23 M allocations: 86.438 GiB, 47.72% gc time)

注: 計算時間は CPU 等によってひどく異なるので,私有環境で上の 10倍以上の時間がかかっても不思議ではない.

さて,学習が終了したようなので,結果をチェックしてみよう.

1
2
# 学習後の NN の精度.
accuracy(X, Y)

0.92355

ふむ.正解率は 92% か.学習前は 7.6% だったことを考えると上出来だな.

最初の 3つのデータについて,個別に学習結果をチェックしてみよう. まずは 1つ目のデータに対する NN の回答は,

1
bar( 0:9, m( X[:,1] ).data )

となる.これだと “5” である確率が約 70% で,正解をきちんと当てていると言えるな.

次に 2つ目のデータだ.

1
bar( 0:9, m( X[:,2] ).data )

これだと “0” である確率がほぼ 100% だ.正解を完全に当てている.

3つ目も見てみよう.

1
bar( 0:9, m( X[:,3] ).data )

これも “4” である確率が約 90% で,正解をきちんと当てている.

どうやら学習はうまくいった,と言ってよいだろう.

おまけ: GPU を使った場合

上記の計算は約 200秒(約 3分半)かかっている. これに対し,特殊なハードウェアの力を借りて計算したらどうなるか,という例を示しておこう. 今回は nVIDIA 社の GPU (まあ,要はグラッフィクカードだ) を使ってみる,という例になる.
注: 残念ながら下記の code は JuliaBox では動かない.nVIDIA 社のグラッフィクカードが刺さっている環境で動くのでそうした環境で試そう.

準備

nVIDIA 社の GPU を Flux から使うには,事前に

  • グラッフィクカードのデバイスドライバ: まあこれは Flux に関係なくインストールしているだろう.
  • CUDA Toolkit : CUDA という専用言語で GPU を扱うためのライブラリ.nVIDIA製. Windows だと,ダウンロードしてダブルクリックするだけでインストールできる. ただし,ファイルサイズが 2GB を超えるのでダウンロードの際は覚悟しよう.
  • cuDNN : 深層 NN 用ライブラリ.nVIDIA製.cuDNN のインストール方法 を読むと(デバイスドライバと CUDA Toolkit も含めて)インストール方法がわかる.まあ,解凍してファイルをいくつかコピーするだけだ.

のインストールを上の順序でしておく必要がある.まあ,実際にやってみるとこれは実は簡単だ.

次に,Julia で CuArrays パッケージを使えるようにしておく(入ってなければ Pkg.add でインストールする).

あとは次のように,必要なデータや計算ルーチンを GPU に渡すようにしてプログラムを書けば良い.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
using Flux, Flux.Data.MNIST, Statistics
using Flux: onehotbatch, onecold, crossentropy, train!, throttle
using Base.Iterators: repeated
using CuArrays

imgs = MNIST.images()
labels = MNIST.labels()

oX = hcat(float.(reshape.(imgs, :))...)
oY = onehotbatch(labels, 0:9)

X = copy(oX)  |> gpu
Y = copy(oY) |> gpu

m = Chain(
  Dense(28^2, 32, relu),
  Dense(32, 10),
  softmax) |> gpu

loss(x, y) = crossentropy(m(x), y)

function accuracy(m, x, y)
    m = m |> cpu
    r = mean( (onecold(m(x))) .== (onecold(y)))
    m = m |> gpu
    return r
end

dataset = repeated((X, Y), 200) |> gpu
opt = ADAM()
evalcb = () -> @show(loss(X, Y))

これで準備ができた. 念の為に初期の精度を測っておく.

1
accuracy(m, oX, oY)

0.10885

ふむ.平均正解率 10.9% というところで,まあ妥当だ.

では学習させてみよう.

1
@time train!(loss, params(m), dataset, opt, cb = throttle(evalcb, 10))

loss(X, Y) = 2.233282f0 4.308298 seconds (3.96 M allocations: 189.181 MiB, 1.57% gc time)

おお! たいへん驚くことに,(この環境だと) たった 4秒で学習が終わる.

学習後の NN の精度も見てみよう.

1
accuracy(m, oX, oY)

0.9246

正解率が 92.5% ということで,確かに学習できているようだ.

ということで,GPU などの特殊なハードウェアの力を借りることができれば,こうした学習も大変速く行えることがわかる.デスクトップ PC 用のグラッフィクカードであればそう高いものでもないので,機械学習を少し勉強して見るならばこうしたハードウェアの調達も検討しておくと良いだろう.

Report No.14

  1. 上の例で NN の中間層を増やしてみよう.ただし,計算時間は増大するので,バランスが厳しいかも.
  2. Flux Model Zoo を見て,他の例を試してみよう.ただし,知らないことばかりの場合は無理しなくて良い.