15. 機械学習 入門2

Photo by Roman Synkevych on Unsplash

入門2 機械学習用フレームワークの利用

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

少し要素数などが先週より大きいので,丁寧に準備しよう. ただし,以下では意外に小さい NN を用意するので驚くかもね.

さて,今回は,

  • 入力は要素数 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)}$

と定義できる.まあ,単調性と確率分布の性質を満たそうとするとたぶんこれが一番シンプルな変換だな. ちなみに,すべての $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}$ は情報幾何の世界ではノルム「もどき」として知られている量なのだ.

実際にやってみる

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

まずはいつものように 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
3
# 学習に使うデータ.60000個ある.
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}}:
 0  1  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  0  0  1  0  1  0  0  0  0     0  0  0  0  0  0  1  0  0  0  0  0
 0  0  0  0  0  1  0  0  0  0  0  0  0     0  0  0  1  0  0  0  0  0  0  0  0
 0  0  0  0  0  0  0  1  0  0  1  0  1     0  0  0  0  0  0  0  0  1  0  0  0
…略…

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

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

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

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

1
r = m( X[:,1] )
10-element Array{Float32,1}:
 0.09637651
 0.12438649
 0.08440215
 0.09990795
 0.0857012
 0.11381065
 0.11615292
 0.11861699
 0.09201259
 0.06863261

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

1
2
3
using Plots

bar( 0:9, r )

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

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

1
imgs[2]

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

1
labels[2]
0

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

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

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

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

1
labels[3]
4

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

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

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

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

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.0945

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

ではデータセットの設定と,パラメータをどうやって修正していくかの方法の指定,そして画面表示設定等をしてしまおう.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# データセット.小さめ.
# 計算にたぶん 5分ぐらいかかるので,速くしたければ数字を小さくする.
# もっと大きくすれば結果はかなり良くなるが,時間もかかる.
dataset = repeated((X, Y), 50)

# 損失関数を小さくする方向をどう決めるか.学習をどう行うか.
# ADAM 法がよく使われるようだ.パラメータは適当.
opt = ADAM(0.01)

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

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

1
@time train!(loss, params(m), dataset, opt, cb = throttle(evalcb, 10))
loss(X, Y) = 1.9150476f0
loss(X, Y) = 0.89732283f0
loss(X, Y) = 0.53422815f0
loss(X, Y) = 0.42770475f0
loss(X, Y) = 0.37346128f0
loss(X, Y) = 0.3388004f0
loss(X, Y) = 0.31279433f0
loss(X, Y) = 0.29351607f0
loss(X, Y) = 0.27754518f0
loss(X, Y) = 0.26382518f0
loss(X, Y) = 0.25176015f0
loss(X, Y) = 0.2406455f0
loss(X, Y) = 0.23020238f0
327.118466 seconds (11.85 G allocations: 226.835 GiB, 9.32% gc time)

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

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

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

ふむ.正解率は 93.5% か.学習前は 9.5% だったことを考えると,たった5分の学習としては上出来だな.

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

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

となる.これだと "5" である確率が80%以上で,正解をきちんと当てていると言えるな.

次に 2つ目のデータだ.

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

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

3つ目も見てみよう.

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

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

次に,学習に使っていない,テスト用データを対象としてこの NN の性能を見よう. まず,テスト用データのダウンロードと整形だ.

1
2
3
4
5
6
7
8
# テスト用に使うデータ.10000個.上と内容は重複なしのはず.
imgs_test   = MNIST.images(:test)
labels_test = MNIST.labels(:test)

# 使いやすいように変換.
X_test = hcat(float.(reshape.(imgs_test, :))...) 
Y_test = onehotbatch(labels_test, 0:9)

この NN を適用した場合の精度を見よう.

1
accuracy(X_test, Y_test)
0.9318

ふむ,未知のデータに対しても 93% の確率で正解を出せるということだな. どうやらこの NN の学習はうまくいった,と言ってよいだろう.

おまけ: GPU を使った場合

上記の計算は小さめとはいえ約 5分かかっている. これに対し,特殊なハードウェアの力を借りて計算したらどうなるか,という例を示しておこう. 今回は nVIDIA 社の GPU (まあ,要はグラフィックカードだ) を使ってみる,という例になる.

残念ながら下記の code は nVIDIA 社の GPU がインストールされていない環境では動かない. nVIDIA 社のグラフィックカードが刺さっている環境で試そう.

準備

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

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

のインストールを上の順序でしておく必要がある…のだが,実は,下記のCuArrays パッケージをインストールすると自動でやってくれる.楽だ.

というわけで,Julia で CuArrays パッケージを使えるようにインストールしよう.

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

ダウンロード等にかなり時間がかかる. 授業中に下記のコードにチャレンジするのであれば,できれば事前にこのインストールをやっておこう.

あとは次のように,必要なデータや計算ルーチンを 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
32
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

# 上の例の20倍のデータ量.
dataset = repeated((X, Y), 1000) |> gpu
opt = ADAM()
evalcb = () -> @show(loss(X, Y))

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

1
accuracy(m, oX, oY)
0.10885

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

では学習させてみよう.上の例の 20倍のデータ量があるので,本来なら100分かかるということを頭に入れて実行しよう.

1
@time train!(loss, params(m), dataset, opt, cb = throttle(evalcb, 10))
loss(X, Y) = 2.3821456f0
loss(X, Y) = 0.15087678f0
 15.854575 seconds (4.88 M allocations: 230.561 MiB, 1.07% gc time)

おお! たいへん驚くことに,普通にやると学習に 5x20 = 100分かかると思われるこのデータ量に対して,この環境だとたった15秒で学習が終わる

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

1
accuracy(m, oX, oY)
0.9697666666666667

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

未知のデータについてもこの NN の能力を確かめておこう.

1
2
3
4
5
6
7
8
9
# テスト用に使うデータ.10000個.上と内容は重複なしのはず.
imgs_test   = MNIST.images(:test)
labels_test = MNIST.labels(:test)

# 使いやすいように変換.
X_test = hcat(float.(reshape.(imgs_test, :))...) 
Y_test = onehotbatch(labels_test, 0:9)

accuracy(m, X_test, Y_test)
0.9601

未知のデータに対しても 96% の正答率だ.たった 15秒の学習なのに,大変よく出来たと言えよう.

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

レポート

下記要領でレポートを出してみよう.

  • e-mail にて,
  • 題名を 2020-numerical-analysis-report-15 として,
  • 教官宛(アドレスは web の "TOP" を見よう)に,
  • 自分の学籍番号と名前を必ず書き込んで,
  • 内容はテキストでも良いし,pdf などの電子ファイルを添付しても良いので,

下記の内容を実行して,結果や解析,感想等をレポートとして提出しよう.

  1. 上の例で NN の中間層を増やしてみよう.ただし,計算時間は増大するので,バランスが厳しいかも.

  2. nVIDIA のグラフィックカードが刺さっている PC環境を探すなどして,可能ならば GPU での計算例も試してみよう.

  3. Flux Model Zoo を見て,他の例を試してみよう.ただし,知らないことばかりの場合は無理しなくて良い.