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

Photo by Claudio Schwarz on Unsplash

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

Flux パッケージ

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

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

Flux パッケージのインストール (VIORAS 環境では不要!)

個人環境等で Flux が未インストールの場合は下記のようにしてインストールしておこう.

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



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

  機械学習用ライブラリで Julia 用のものとしては、他に

  • Lux (NNモデルパラメータは自分で更新するなどフレームワークに近いか?高速らしい),
  • MLJ (これははっきりとしたフレームワーク)

などがある. また、

という、科学技術用機械学習ライブラリがあり、大変充実している様子だ. なお、科学技術用機械学習について興味があるならば、

などが大変参考になるだろう.

また、(この分野で有名な) Python 用の機械学習用ライブラリ/フレームワークとしては Keras, TensorFlow, PyTorch, JAX などが知られている.

今回のターゲット問題

今回は典型的(かつ有名)な学習問題である「手書き数字の認識問題」をターゲットとしよう. 学習に使える実データとして有名なものに 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 を用意するので驚くかもね.

さて,今回は,

  • 入力は要素数 784 ($= 28\times 28$) の実数ベクトル
  • 出力は数字 0 ~ 9 に対する「確率」を表すベクトル. つまり,要素数 10 の実数ベクトル $\boldsymbol{p}$で,$0 \leq p_i \leq 1$ かつ $\sum_i p_i = 1$.
  • 通常の密結合 NN を用いる.中間(network)層は 3つ.
  • 1つ目の中間層の出力サイズは 512 で活性化函数は ReLU,
  • 2つ目の中間層の出力サイズは 256 で活性化函数は ReLU,
  • 3つ目の中間層の出力サイズは 10 で,活性関数の代わりに softmax 関数で正規化して出力が確率分布として成り立つようにする… のがやりたいことだが、ここの値には手を加えずにそのまま出力する. というのも、あとでどうせ softmax の出力値と真値を crossentropy(後述) に入れた値で比較するので、crossentropy と softmax の合成である logitcrossentropy をあとで使うことにする. その方が誤差も小さいし、計算も速いのだ.

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

なお,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$ が正ならば, $\exp$ での変換が不要で,合計値で割って単純に正規化したほうが楽だろうな.

あと,これまた同じ回のプログラム中に登場したが,出力の誤差相当を計算するために、確率分布間の距離もどきを測れる、出力ベクトル $\boldsymbol{y}$ と真値ベクトル $\boldsymbol{z}$ に対する Cross Entropy

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

を使う.

入力独立変数に対して非対称な関数なので入力の順序に注意.目くじら立てるほどの本質的な違いはないと思ってもまあいいが.順序は定義によるので,プログラムの source を確認しておく必要がある.Flux パッケージの crossentropy 関数は上の順序になっている.

実際にやってみる

ライブラリ, データとニューラルネットワークの準備

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

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

1using Flux
2using MLDatasets
3using Statistics
4using ProgressMeter
5using 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] )

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

1labels[1]

5

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

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

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

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

1model = Chain(
2  Dense(28^2, 512, relu), # 1層目
3  Dense(512, 256, relu),  # 2層目
4  Dense(256, 10)          # 3層目
5  # 出力直前に softmax を使わずに、Loss計算のときに softmax と crossentropy の合成関数である logitcrossentropy を使った方が良い
6  )
Chain(
  Dense(784 => 512, relu),              # 401_920 parameters
  Dense(512 => 256, relu),              # 131_328 parameters
  Dense(256 => 10),                     # 2_570 parameters
)                   # Total: 6 arrays, 535_818 parameters, 2.044 MiB.

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

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

1r = softmax( model( X[:,1] ) )
10-element Vector{Float32}:
 0.052046895
 0.10186538
 0.092209764
 0.082437664
 0.12592654
 0.06235168
 0.18899253
 0.08749943
 0.07551988
 0.13115025

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

1bar( 0:9, r )

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

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

1Gray.( imgs[2] )

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

1labels[2]
0

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

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

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

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

1labels[3]
4

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

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

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

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

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

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

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

ふ~む.ランダムな状態の NN での正解率が 7.4% ということで,少し低いがまあこんなものかな(ランダムな出力でも,正解率は平均で 10% になるはずなので).

ニューラルネットワーク学習の準備

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

 1# 最適化アルゴリズムの指定.
 2optim = Flux.setup( Flux.Adam(), model )
 3
 4# データを学習ルーチンにどう取り込むかの指定.
 5loader = Flux.DataLoader((X, Y), batchsize = 2048, shuffle = true )
 6
 7# 損失関数も設計しておこう.
 8loss( tx, y ) = Flux.logitcrossentropy( tx, y )
 9# 上で述べたように、この logitcrossentropy は
10# crossentropy と softmax の合成関数とみなせる.

ニューラルネットワーク学習

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

 1epochs = 500 # 遅いPCを使う場合はもっと小さい値にすること
 2sq_loss = Float64[] # loss の値がどう変化するかあとでプロットしたいので記録する.
 3
 4@showprogress 1.0 for epoch in 1:epochs 
 5  # @showprogress の後に数字sを入れると、s秒毎に表示する.
 6  # ループ数が大きすぎるときなどはこうしないと @showprogress の挙動が不安定になる.
 7  for (x, y) in loader
 8    lossF, grads = Flux.withgradient(model) do m
 9      loss( m(x), y )
10    end
11    Flux.update!(optim, model, grads[1])
12    push!(sq_loss, lossF)
13  end
14end
Progress: 100%|█████████████████████████████████████████| Time: 0:10.07

普通の PC を使った上の学習には,結局10分ほどかかったということだな. 授業中にこれを試すのは大変なので、たとえば上の code 中の epochs = 500epochs = 50 などに直すなどして、あまり待たなくて良いようにして実行しよう.

あとせっかくなので、学習プロセスで誤差(loss)がどのように変化したか見ておこう.

1plot(sq_loss, xlabel = "learning iteration", ylabel = "loss", legend = :false)

このグラフの縦軸は一種の誤差相当なので、グラフが下がれば下がるほど良いことになる. そして、最終的に15,000回の学習が行われているのだが、結構最初の方で学習が十分になされているように見える.

  約 15,000回の学習、というのは、次のようにカウントされる.

  • (データ 60,000個)/(batch サイズ2048) = 約30 となり、1 epoch あたり 30回の学習が行われる.
  • 全体で 500 epoch を指定したので、30 x 500 = 15,000 となる.

う~ん、最初の方のおおよそ 1,000回分だけ見てみようか(あと、最初の方の値が大きすぎるので少し捨てて).

1plot(10:1000, sq_loss[10:1000], xlabel = "learning iteration", ylabel = "loss", legend = :false)

どうやら、今回は 1000回ぐらいの学習で十分だったようだ. NN の初期パラメータが乱数で決まるのでこの数字は変化するだろうが、それでもまあ(念の為に3倍にして) 3000回ぐらい学習すれば十分そうだ. つまり、epochs = 100 ぐらいできっと十分なのだろう.

ニューラルネットワーク性能チェック: まずは学習用データについて

次に,学習したニューラルネットワークの、学習用データに関する性能を見てみよう.

1accuracy(X, Y)
1.0

おお! なんと正解率が 100% になっている. 学習前は 7.4% だったことを考えると,10分の学習としてはたいへん上出来だ.

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

1bar( 0:9, softmax( model( X[:,1] ) ) )

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

次に 2つ目のデータだ.

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

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

3つ目も見てみよう.

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

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

ニューラルネットワーク性能チェック: 未知の検証用データについて

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

 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)

この未知の検証用データを学習済みのニューラルネットワークで判定した場合の精度を見よう.

1accuracy(X_check, Y_check)
0.9827

ふむ,未知のデータに対しても約 98.3% の確率で正解を出せるということだな. どうやらこのニューラルネットワークの学習はたいへんにうまくいった,と言ってよいだろう.

ちなみに…

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% 程度と聞くので、機械学習は人間レベルに達したと言って良いだろう.

おまけ: GPU を使った場合

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

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

ちなみに,阪大情報教育システムや VIORAS(JupyterHubサービス環境)では 肝心の GPU ハードウェアが無いので,実際に CUDA の機能を使おうとしたときに Info: The GPU function is being called but the GPU is not accessible. などのメッセージが出て使えないことがわかるぞ.

準備

NVidia 社の GPUボードが入っている PC で GPU を Flux から使うには,事前に

  • グラフィックカードのデバイスドライバのインストール.
    ↑ ただし、これは既に行っているだろうから無視して良い

と、 下記のように、CUDA パッケージと cuDNN パッケージのインストールが必要である. ちなみに、この2つのパッケージをインストールすると、Julia 用の CUDAライブラリが自動的にダウンロード・インストールされる1

1using Pkg
2Pkg.add("CUDA")
3Pkg.add("cuDNN")

この2つのパッケージのインストールが済んでいれば、nVIDIA の GPU が使える環境では、たとえば次のように必要なデータや計算ルーチンを GPU に渡すようにしてプログラムを書けば良い.

  GPU を使うとき、知っておくと良い知識がある.それは下記の通り.

  • 倍精度近似実数型(Float64)変数の計算速度は、CPU では単精度計算速度の半分ぐらい.つまり、そんなに遅くない. しかし、GPU では良くて半分、下手すると数十分の一などになる.つまり、GPU での倍精度計算は「贅沢」である.
  • 逆に、多くの GPU では単精度変数(Float32)用の機構が数多く実装されている.これを使うのが賢い.
  • さらに、新しい GPU には単精度の計算をさらに速くする特殊な機構(TensorCoreなど)があったりする.これを明示的に指定できるならしたほうが速い.
  • GPU では半精度変数(Float16)の計算はさらに速いが、機械学習ではこれは学習による変化が無くなってしまう「勾配消失」を起こすことがあるので、これを使うのは機械学習に詳しくなってから(実際、下記のプログラムで f32 とあるところを f16 とすると、初期乱数によるが、勾配消失現象を見ることができる).

上記の知識を踏まえ、単精度変数の利用を前提にした準備部分が以下の通り.

 1ENV["CUDA_TF32_ALLOWED"] = "1"
 2# nVIDIA GPU の TF32 型を有効にする.デフォルトで ON になっているはずだが念の為.
 3
 4using Flux
 5using MLDatasets
 6using Statistics
 7using ProgressMeter
 8using Plots
 9using CUDA
10using cuDNN
11
12CUDA.math_mode!(CUDA.FAST_MATH)
13# GPU において TF32 型などの計算の高速化を許可する
14
15raw_imgs = MNIST().features
16labels   = MNIST().targets
17data_number = size(labels)[1]
18imgs = [ raw_imgs[:,:,i]' for i in 1:data_number ]
19
20oX = hcat(float.(reshape.(imgs, :))...) |> f32
21oY = Flux.onehotbatch(labels, 0:9) |> f32
22
23X = copy(oX)  |> gpu
24Y = copy(oY) |> gpu
25
26model = Chain(
27  Dense(28^2, 512, relu), # 32 -> 512 に拡大
28  Dense(512, 256, relu),  # 層を追加
29  Dense(256, 10)
30  # 出力直前に softmax を使わずに、Loss計算のときに softmax と crossentropy の
31  # 合成関数である logitcrossentropy を使った方が良い
32  )  |> f32 |> gpu
33
34# 最適化アルゴリズムの指定.
35optim = Flux.setup( Flux.Adam(), model )
36
37# データを学習ルーチンにどう取り込むかの指定.
38loader = Flux.DataLoader((X, Y), batchsize = 2048, shuffle = true )
39
40# 損失関数も設計しておこう.
41loss( tx, y ) = Flux.logitcrossentropy( tx, y )
42
43# model の正解率
44accuracy(sX, sY) = mean(Flux.onecold(softmax(model(sX))) .== Flux.onecold(sY))

学習前の(学習用データに対する)正解率は下記の通り. まあ、0.1 前後になるはずなので、納得だ.

1accuracy(X, Y)
0.1039

そして実行は以下の通りだ(強引に @showprogress を使っているので warning が出るがまあ動く).

 1#### 学習
 2epochs = 500 # 遅いPCを使う場合はもっと小さい値にすること
 3sq_loss = Float32[] |> gpu
 4# loss の値がどう変化するかあとでプロットしたいので記録する.
 5
 6@showprogress 1.0 for epoch in 1:epochs
 7  # @showprogress の後に数字sを入れると、s秒毎に表示する.
 8  # ループ数が大きすぎるときなどはこうしないと @showprogress の挙動が不安定になる.
 9
10  for (x, y) in loader
11    lossF, grads = Flux.withgradient(model) do m
12      loss( m(x), y )
13    end
14    Flux.update!(optim, model, grads[1])
15    push!(sq_loss, lossF)
16  end
17end
18
19sq_loss = sq_loss |> cpu
Progress: 100%|█████████████████████████████████████████| Time: 0:00:50

という感じで、50秒ほどで学習が終了する. なんと CPU版の12倍以上速いことになる. まあ実はもっと速いはずだが、今回作った NN はかなり小さいのであまり速度差が見えないというところがあるだろう.

さて、CPU 版と同様に、学習プロセスで誤差(loss)がどのように変化したか見ておこう.

1plot(sq_loss, xlabel = "learning iteration", ylabel = "loss", legend = :false)

まあやっぱり、15,000回の学習は過剰だったようだ.最初の 1,000回ほどを見ておこう.

1plot(10:1000, sq_loss[10:1000], xlabel = "learning iteration", ylabel = "loss", legend = :false)

やはり 1,000回ほどの学習で十分なようだな. その場合、GPU での学習は (50秒/15 =) 3~4秒で終わることになりそうだ.

そしてその結果だが、

1accuracy(X, Y)
1.0

となり、やはり学習用データに対しては 100%の精度が得られる.

そして、CPU版のときと同様に

 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 用の形に.
10oX_check = hcat( reshape.(imgs_check, :)... )  |> f32
11oY_check = Flux.onehotbatch(labels_check, 0:9) |> f32
12
13X_check = copy(oX_check) |> gpu
14Y_check = copy(oY_check) |> gpu

として検証用データを用意し、このデータで正解率を検証すると以下の通り.

1accuracy(X_check, Y_check)
0.9828

つまり、未知のデータに対して 98.3% ほどの正解率であることになる. たった 50秒の学習でこれならば満足してよいだろう(しかも、たぶん 3~4秒の学習で十分だ).

レポート 14

以下の課題について能う限り賢明な調査と考察を行い,
     学籍番号-氏名-14.pdf
というファイルとしてレポートを作成し、 webフォーム から教官宛に提出しよう.

なお,レポートを「紙媒体」で教官に直接手渡す提出もありだが、物質によるレポート提出は破損や紛失の可能性があるのであまりお勧めしないぞ.

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

まあ要するに,レポートは pdf ファイルで提出するのが良い と思っておこう.

  一応、レポートやメールには,自分の 所属・学年・学籍番号・氏名 を必ず書いておこう.なにかと安全だ.

課題

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

  2. GPU 計算ができる グラフィックボードが搭載されている PC等があるならば、上の授業資料などを参考に,GPU での高速計算を試してみよう. もちろん、こうした適切な環境でないと試せないので,そうした環境にある場合のみのチャレンジだ.

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

  1. 多くの場合は CUDA toolkit というライブラリを OS にインストールする事前作業が必要だが、Julia のこの2つのパッケージはそれを独自にやってくれる、というわけだ. ↩︎