13. AI技術:機械学習(Deep Learning)入門 2

建物を組み立てるための足場。機械学習フレームワークの比喩
Photo by Claudio Schwarz on Unsplash

前回はニューラルネットワークを自分で組み立て,学習の仕組みを確かめた.今回はFluxという「足場」を使い,同じ考え方を手書き数字60,000枚の学習へ広げよう.

この回の到達目標

  1. 対応づける前回のNNの要素とFluxの機能を結びつけられる.
  2. 整形する画像とラベルを学習に使える行列へ変換できる.
  3. 学習させるDataLoader・Adam・自動微分を使った学習ループを読める.
  4. 評価する学習用と未知のテスト用データを分けて精度を比較できる.
1 Fluxを知る 2 MNISTを整える 3 NNを学習させる 4 未知データで評価する

1. 機械学習フレームワークを使う

手作りからFluxへ

ここまで感じただろうが,今現在の深層学習には一定の「やりかた」があり,それに沿うならばプログラムはどうしたってかなり似たようなものになる. ならば,そうした点を共通化し,かつ,計算時間の掛かりそうな箇所に高速なライブラリを組み込むような使いやすいフレームワークライブラリがあればユーザは大助かりだ.

そうした需要にこたえ,深層学習用の多くのフレームワークが存在する. Julia についてもやはり存在し,パッケージに Flux というものがあるので今回はこれを使ってみよう.

モデルChainDenseNNの層を組み立てる
データDataLoader小さなbatchに分ける
損失と勾配crossentropywithgradientずれと修正方向を求める
更新Adamupdate!パラメータを改善する
図:Fluxは前回手作業で書いた処理を,役割ごとの部品として提供する.

Fluxパッケージのインストール

(前回までに使用しているので既にインストール済みとは思うが)もしも Flux が未インストールの場合は下記のようにしてインストールしておこう.

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

マニュアルとサンプル

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

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

2. ターゲットは手書き数字の認識

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

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

全70,000枚0〜9の手書き数字
学習用 60,000枚NNのパラメータ更新に使う テスト用 10,000枚学習後の性能確認だけに使う
図:テスト用画像は学習中に見せない.未知の画像に対する性能を公平に測るためである.
  • 色はモノクロ.濃淡が 0以上1以下の実数で表されている.
  • サイズは 28 $\times$ 28 ドット.
  • 本来のデータは 20 $\times$ 20 ドットの白黒二色のものだが,anti-aliasing アルゴリズムで 0以上1未満の濃淡に変換するなどしている. サイズが大きくなっているのは畳み込みなどの画像処理をしやすくするために各辺に「余白」をつけているのだと思われる. ただ,単に余白を足したのではなく,「大きめのキャンバスの中心に画像の中心をあわせて配置」しているので,余白は必ずしも各辺で 4ドットずつになっていないので要注意.

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

MNISTの10枚目に収録された手書き数字4の画像

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

MNIST のデータは本家の web からダウンロードしてもよいが,それだとちょいとした前処理が必要となる. まあこういった有名なデータは MLDatasets パッケージでダウンロードできるので,そのようにしよう(あとで具体的に示そう).

今回用意するneural network

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

さて,今回は,

  • 入力は要素数 784 ($= 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 で用意して,この中に含まれるパラメータを(学習によって)修正することにしよう.

画像28 × 28濃淡の行列
ベクトル化7841列に並べる
Dense + ReLU32特徴を組み合わせる
Dense10数字ごとの得点
softmaxp0, ... ,p9確率分布へ変換
図:1枚の画像が,784個の入力から10種類の確率へ変換される流れ.

なお,12. AI技術: 機械学習(Deep Learning)入門のプログラム中にも登場したが, softmax 関数というのは実数の列(負の実数もOK)を確率分布として解釈できる数列に(かつ,各要素は単調に)変換する関数の一つで, ベクトル $\boldsymbol{a} = \{ a_i \}$ に対して

\begin{equation} \mbox{ SoftMax }(\boldsymbol{a})_i = \frac{\exp(a_i)}{\sum_i \exp(a_i)} \end{equation}

と定義できる. $\exp$ 関数で正の値に変換してから合計値で正規化しているという,シンプルな変換だ. 単調性と確率分布の性質を満たそうとすると,最初の候補になるだろう. すべての $a_i$ が正ならば,単に合計値で割って総和を1にする方法も考えられる.ただし softmax は,負の入力も扱えることや,入力値の差を指数関数によって強調できることが異なる.

あと,これまた同じ回のプログラム中に登場したが,出力の「誤差」を測る方法として出力ベクトル $\boldsymbol{y}$ と真値ベクトル $\boldsymbol{z}$ に対する Cross Entropy

\begin{equation} \mbox{ CrossEntropy }(\boldsymbol{y}, \boldsymbol{z}) = - \sum_i \, z_i \, \log( y_i ) \end{equation}

を使う.

crossentropyの入力順序

予測値と正解値に対して非対称な関数なので,入力の順序に注意しよう.順序は定義によるため,プログラムのsourceを確認する必要がある.Fluxパッケージのcrossentropy関数は上の順序になっている.

3. Fluxで実際にやってみる

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

Step 1:パッケージとMNISTを準備する

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

1using Flux
2using Flux: onehotbatch, onecold, params, crossentropy, train!, throttle
3using DataFrames
4using MLDatasets
5using Statistics
6using Base.Iterators: repeated
7using ProgressMeter
8using Plots

次に、MLDatasets package の機能で MNIST データを使わせていただこう. それには関数 MNIST を呼び出せばよい.

ただし,この関数を初めて呼び出したときにデータが(一回だけ)ダウンロードされるので解説しよう. 実際は,下記のように,このデータについてごく簡単な説明があり,それを理解した上でダウンロードするのか,y/n で尋ねられる. そこで表示中にある stdin> の箇所に "y" (と Enter)を入力しよう. するとダウンロードが始まる.少しだけ待とう.

1MNIST()
This program has requested access to the data dependency MNIST.
which is not currently installed. It can be installed automatically, and you will not see this message again.

Dataset: THE MNIST DATABASE of handwritten digits
Authors: Yann LeCun, Corinna Cortes, Christopher J.C. Burges
Website: http://yann.lecun.com/exdb/mnist/

[LeCun et al., 1998a]
    Y. LeCun, L. Bottou, Y. Bengio, and P. Haffner.
    "Gradient-based learning applied to document recognition."
    Proceedings of the IEEE, 86(11):2278-2324, November 1998

The files are available for download at the offical
website linked above. Note that using the data
responsibly and respecting copyright remains your
responsibility. The authors of MNIST aren't really
explicit about any terms of use, so please read the
website to make sure you want to download the
dataset.

Do you want to download the dataset from ["https://ossci-datasets.s3.amazonaws.com/mnist/train-images-idx3-ubyte.gz", "https://ossci-datasets.s3.amazonaws.com/mnist/train-labels-idx1-ubyte.gz", "https://ossci-datasets.s3.amazonaws.com/mnist/t10k-images-idx3-ubyte.gz", "https://ossci-datasets.s3.amazonaws.com/mnist/t10k-labels-idx1-ubyte.gz"] to "/home/jovyan/.julia/datadeps/MNIST"?
[y/n]
stdin> ■■■■■■

すると次のような出力が出て,どんな形式でダウンロードされたかと,この呼出し方だと学習用データ(60,000個)が得られるということがわかる.

dataset MNIST:
  metadata  =>    Dict{String, Any} with 3 entries
  split     =>    :train
  features  =>    28×28×60000 Array{Float32, 3}
  targets   =>    60000-element Vector{Int64}

次に,これを変数に入れよう.

1# 学習に使うデータ.60000個ある.
2raw_imgs = MNIST().features
3labels   = MNIST().targets

raw_imgs 変数に画像データが, labels 変数にその画像がどの数字を描いたものか,が入る.

ただ,raw_imgs 変数は画像が 90度回転した格好で格納されているので,次のようにして(人間に)見やすいものにしておこう.

1# 扱いやすいように変換しておく.
2data_number = size(labels)[1]
3imgs = [ raw_imgs[:,:,i]' for i in 1:data_number ]

さて,念のためにもダウンロードしたデータをちょっと見ておこう.

もとは画像データなのだから,当然,画像で見てみるのが良いだろう. 次のようにすると,行列データを(以下の場合はグレイ表現の)画像で見ることができる.

1Gray.( imgs[1] )
MNIST学習データの1枚目。手書き数字の5

ちなみにこれは 0から9 のどの数字かというと,

1labels[1]

5

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

Step 2:画像と正解ラベルを行列へ変換する

次に,これらを,学習プログラムに渡せるよう,固まりのデータに変換する.

1# 「画像 = 行列」となっているデータをベクトルに変換し,
2# それを横にくっつけて一つの大きな行列に.
3X = hcat( reshape.(imgs, :)... )
4
5# 正解の値を,0:9 に対応するように,要素が 「真偽値」(= 1 or 0) の
6# 10次元ベクトルに変換する
7Y = onehotbatch(labels, 0:9)

変換した結果を見ておこう. 特に,Y が何を意味するのか,よくみるとわかるだろう.

1X
784×60000 Matrix{Float32}:
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
…略…
1Y
10×60000 OneHotMatrix(::Vector{UInt32}) with eltype Bool:
 ⋅  1  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  …  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅
 ⋅  ⋅  ⋅  1  ⋅  ⋅  1  ⋅  1  ⋅  ⋅  ⋅  ⋅     ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  1  ⋅  ⋅  ⋅  ⋅  ⋅
 ⋅  ⋅  ⋅  ⋅  ⋅  1  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅     ⋅  ⋅  ⋅  1  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅
 ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  1  ⋅  ⋅  1  ⋅  1     ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  1  ⋅  ⋅  ⋅
 ⋅  ⋅  1  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  1  ⋅  ⋅  ⋅     ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅
 1  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  1  ⋅  …  ⋅  ⋅  ⋅  ⋅  ⋅  1  ⋅  ⋅  ⋅  1  ⋅  ⋅
 ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅     ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  1  ⋅
 ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅     1  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅
 ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅     ⋅  1  ⋅  ⋅  ⋅  ⋅  ⋅  1  ⋅  ⋅  ⋅  1
 ⋅  ⋅  ⋅  ⋅  1  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅     ⋅  ⋅  1  ⋅  1  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅
画像60,000枚28 × 28 × 60,000raw_imgs
入力行列 $X$784 × 60,0001列が1枚の画像
ラベル60,000個0〜9labels
正解行列 $Y$10 × 60,0001列がone-hotベクトル
図:Fluxには「特徴 × データ数」の入力行列と,「分類数 × データ数」の正解行列を渡す.

Step 3:FluxでNNを組み立てる

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

1model = Chain(
2  Dense(28^2, 32, relu),
3  # 密結合第1層.28^2 の入力を受けて,32 の出力を返す.活性関数は ReLU.
4  Dense(32, 10),
5  # 密結合第2層.32 の入力を受けて 10 の出力を返す.
6  softmax
7  # 出力直前に 正規化
8  )
Chain(
  Dense(784 => 32, relu),               # 25_120 parameters
  Dense(32 => 10),                      # 330 parameters
  NNlib.softmax,
)                   # Total: 4 arrays, 25_450 parameters, 99.617 KiB.

意味は,プログラム中の注釈と出力でおおよそわかるだろう.

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

1r = model( X[:,1] )
10-element Vector{Float32}:
 0.14702854
 0.060798667
 0.10340613
 0.068249345
 0.12690952
 0.10045575
 0.08257647
 0.10937854
 0.11651743
 0.084679656

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

1bar( 0:9, r )
学習前のNNが1枚目の画像に出した0から9の確率

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

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

1Gray.( imgs[2] )
MNIST学習データの2枚目。手書き数字の0

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

1labels[2]
0

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

1bar( 0:9, model( X[:,2] ) )
学習前のNNが2枚目の画像に出した0から9の確率

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

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

1labels[3]
4

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

1bar( 0:9, model( X[:,3] ) )
学習前のNNが3枚目の画像に出した0から9の確率

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

ちょっと,全体の正解率を見ておこう.

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

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

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

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

Step 4:学習の準備をする

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

1# 最適化アルゴリズムの指定.
2optim = Flux.setup( Flux.Adam(), model )
3
4# データを学習ルーチンにどう取り込むかの指定.
5loader = Flux.DataLoader((X, Y), batchsize = 10, shuffle = true )
6
7# 損失関数も設計しておこう.
8loss( tx, y ) = crossentropy( tx, y )
60,000枚学習データ全体
10枚ずつ1 batch
6,000 batch全体を1周=1epoch
50 epoch順序を混ぜて反復
図:一度に全画像を扱わず,小さなbatchでパラメータを更新しながらデータ全体を何周もする.

Step 5:NNを学習させる

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

 1epochs = 50 # 遅いPCを使う場合はもっと小さい値にすること
 2
 3@showprogress for epoch in 1:epochs
 4
 5  for (x, y) in loader
 6    lossF, grads = Flux.withgradient(model) do m
 7      loss( m(x), y )
 8    end
 9    Flux.update!(optim, model, grads[1])
10  end    
11
12  @show( loss(model(X), Y) )
13end
loss(model(X), Y) = 0.18430202f0
loss(model(X), Y) = 0.12055561f0
Progress:   6%|███                                      |  ETA: 0:04:16
loss(model(X), Y) = 0.11767374f0
Progress:   8%|████                                     |  ETA: 0:03:40
loss(model(X), Y) = 0.093734324f0
…略…
Progress:  96%|████████████████████████████████████████ |  ETA: 0:00:07
loss(model(X), Y) = 0.009560081f0
Progress:  98%|█████████████████████████████████████████|  ETA: 0:00:03
loss(model(X), Y) = 0.0066529927f0
Progress: 100%|█████████████████████████████████████████| Time: 0:02:51
loss(model(X), Y) = 0.008321506f0

普通の(ちょっと良い) PC を使った上の学習には,結局 3分弱かかったということだな. 途中の表示を見ることで,少しずつだがlossの値が小さくなっていくことがわかる.

4. 学習したNNの性能を確かめる

まずは学習用データについて

では早速,学習したニューラルネットワークの性能を見てみよう.

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

ふむ.正解率は 99.7% か.学習前は 11% だったことを考えると,170秒での学習としてはたいへん上出来だ.

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

1bar( 0:9, model( X[:,1] ) )
学習後のNNが1枚目を数字5と高い確率で判定したグラフ

となる.これだと "5" である確率がほぼ100%か? 正解をきちんと当てていると言える.

次に 2つ目のデータだ.

1bar( 0:9, model( X[:,2] ) )
学習後のNNが2枚目を数字0と高い確率で判定したグラフ

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

3つ目も見てみよう.

1bar( 0:9, model( X[:,3] ) )
学習後のNNが3枚目を数字4と高い確率で判定したグラフ

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

入力画像入力された手書き数字5
学習前の予測正解の5を選べていない学習前の確率
学習後の予測正解の5へ確率が集中した学習後の確率
図:画像は同じでも,学習後は正解の「5」へ出力確率が集中する.

次に未知のテスト用データについて

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

 1# テスト用に使うデータ.10000個.上と内容は重複なしのはず.
 2raw_imgs_check = MNIST(:test).features
 3labels_check   = MNIST(:test).targets
 4
 5# 使いやすいように転置.
 6check_number = size(labels_check)[1]
 7imgs_check = [ raw_imgs_check[:,:,i]' for i in 1:check_number ]
 8
 9# Flux 用の形に.
10X_check = hcat( reshape.(imgs_check, :)... )
11Y_check = Flux.onehotbatch(labels_check, 0:9)

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

1accuracy(X_check, Y_check)
0.9647

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

学習前11.0%ほぼ当てずっぽう
学習用データ99.7%学習後
未知のテスト用データ96.5%汎化性能
図:重要なのは,学習に使っていない10,000枚でも高い精度を保っていることだ.

発展・補足編

コラム:MNIST認識精度の歴史

1998年の論文 "Gradient-based learning applied to document recognition," Y.Lecun, et.al., Proc.IEEE, vol.86, no.11, 1998, 2278-2324, doi:10.1109/5.726791 の Fig.9 によると、この MNIST 問題に対し、この時点で既に、SVM の一種で 99.2%, cNN の一種で 99.3% の精度を達成している.

2013年の論文 "Regularization of Neural Networks using DropConnect," Li Wan et.al., PMLR(Proc. Machine Learning Research), vol.28, no.3, 2013, 1058-1066 の Table 3 によると機械学習で 99.79% ぐらいの精度が達成できているようだ. 人間の MNIST の認識精度も 99.8% 程度と聞くので、機械学習は人間レベルに達したと言って良いだろう.

コラム:softmaxとcrossentropyをまとめて計算する

本文では,NNの最終層で softmax を計算し,その確率分布を crossentropy に渡した.この形は処理を段階ごとに観察しやすい.一方,実際の計算では,丸め誤差の影響を抑えるために両者をまとめた Flux.logitcrossentropy を使う方法もある.その場合は,モデル末尾の softmax を外し,最後の Dense 層が出す値(logit)をそのまま損失関数へ渡す.

1model = Chain(
2    Dense(28 * 28, 32, relu),
3    Dense(32, 10)
4)
5
6loss(logits, y) = Flux.logitcrossentropy(logits, y)

予測結果を確率として表示したいときだけ,softmax(model(x)) とすればよい.これは大規模なNNでも役立つ,数値計算上の工夫である.

レポート No.13

提出ファイルについての注意

近年はセキュリティ上の懸念から,実行形式のプログラムなどをメールに添付すると,受信側サーバがメールそのものを拒絶することがある. そういう問題を避けるため,レポートをファイルで提出するときは,実行形式などの危険視されやすい形式を避けよう.

要するに,レポートはPDFファイルにして送るのが良い,と思っておこう.

以下の課題について,自らの将来のスキルアップにつながるように調査と考察を行い,
     学籍番号-氏名-13.pdf
というファイルとしてレポートを作成し、 webフォーム から教官宛に提出しよう.

なお,レポートを $\TeX$ 等で作成したものを印刷した「紙媒体」を教官に直接手渡す形で提出してもよいが、物質によるレポート提出は常に破損や紛失の可能性があるのであまりお勧めはしないぞ.

課題

  1. 上の例で NN の中間層を増やしてみたりして,「未知のデータに対する判定の正解率」をもっと上げられないか試してみよう.ただし,計算時間は増大するので,バランスが厳しいかも.

  2. Flux の GPUサポートマニュアルなどを参考に,可能ならば GPU で高速に計算できないか試行錯誤してみよう. ただし,適切なグラフィックカードが使える環境でないと試せないので,そうした環境にある場合のみのチャレンジだ.

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