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

Photo by Claudio Schwarz on Unsplash

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

Flux パッケージ

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

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

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

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

1using Pkg
2pkg.add("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 からダウンロードしてもよいが,それだとちょいとした前処理が必要となる. まあこういった有名なデータは MLDatasets パッケージでダウンロードできるるので,そのようにしよう(あとで具体的に示そう).

今回用意する 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)層は 2つ.
  • 1つ目の中間層の出力サイズは 32 で,活性関数は ReLU.
  • 2つ目の中間層の出力サイズは 10 で,活性関数の代わりに softmax 関数で正規化して出力が確率分布として成り立つようにする.(注: 2つ目の中間層の出力が最終出力)

という 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 Flux: onehotbatch, onecold, params, crossentropy, train!, throttle
3using DataFrames
4using MLDatasets
5using Statistics
6using Base.Iterators: repeated

次に、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 ]

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

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

1using Plots
2Gray.( imgs[1] )

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

1labels[1]

5

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

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

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  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅

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

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

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

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

1r = m( X[:,1] )
10-element Vector{Float32}:
 0.12151853
 0.05944286
 0.046448864
 0.044172812
 0.07843999
 0.17664199
 0.122673266
 0.17259233
 0.06293902
 0.11513027

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

1using Plots
2
3bar( 0:9, r )

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

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

1Gray.( imgs[2] )

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

1labels[2]
0

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

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

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

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

1labels[3]
4

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

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

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

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

1# 損失関数.要は出力誤差.今回は Cross Entropy で.
2loss(x, y) = crossentropy(m(x), y)
3
4# 正解率.この文脈だと精度ともいう.
5accuracy(x, y) = mean(onecold(m(x)) .== onecold(y))

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

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

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

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

 1# データセット.大きくすれば結果は良くなるが,時間もかかる.
 2# コンパイル用に小さなデータを,本番用に大きなデータを用意しておく.
 3small_dataset = repeated((X, Y), 1)
 4large_dataset = repeated((X, Y), 500)
 5
 6# 損失関数を小さくする方向をどう決めるか.学習をどう行うか.
 7# ADAM 法がよく使われるようだ.パラメータは適当.
 8opt = ADAM(0.01)
 9
10# 学習が進んでいく途中での画面表示等を設定.
11# cb とは call back のこと.
12evalcb = () -> @show(loss(X, Y))

これで準備ができたので,早速実行しよう. ただし,一回小さなデータで関数をコンパイルしておく.

1train!(loss, params(m), small_dataset, opt, cb = throttle(evalcb, 10))
loss(X, Y) = 2.016057f0

では,大きめのデータで学習させてみよう.

1@time train!(loss, params(m), large_dataset, opt, cb = throttle(evalcb, 10))
loss(X, Y) = 1.5961612f0
loss(X, Y) = 0.20899498f0
loss(X, Y) = 0.13912505f0
loss(X, Y) = 0.101281166f0
loss(X, Y) = 0.07625387f0
loss(X, Y) = 0.05789004f0
loss(X, Y) = 0.044593994f0
loss(X, Y) = 0.0390607f0
loss(X, Y) = 0.028697725f0
loss(X, Y) = 0.022385016f0
 98.348538 seconds (111.47 k allocations: 127.451 GiB, 4.95% gc time)

計算にかかる時間は,乱数で作られた初期状態の値や計算環境の CPUの能力等によってかなり異なるぞ.

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

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

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

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

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

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

次に 2つ目のデータだ.

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

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

3つ目も見てみよう.

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

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

次に,学習に使っていない,テスト用データを対象としてこの 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.9576

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

レポート

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

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

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

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

まあ要するに,レポートは pdf ファイルにして,それをメールに添付して送るのが良い ということだと思っておこう.

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

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

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