14. AI技術: 機械学習(Deep Learning)入門 2
Photo by Claudio Schwarz 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 を用意するので驚くかもね.
さて,今回は,
- 入力は要素数 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 \}$ に対して
$\displaystyle\mbox{ SoftMax }(\boldsymbol{a})_i = \frac{\exp(a_i)}{\sum_i \exp(a_i)}$
と定義できる. $\exp$ 関数で強引に正の値に変換してから合計値で正規化しているという,シンプルな変換だ. 単調性と確率分布の性質を満たそうとするとたぶんこれが最初の候補だろう. ちなみに,すべての $a_i$ が正ならば, $\exp$ での変換が不要で,合計値で割って単純に正規化したほうが楽だろうな.
あと,これまた同じ回のプログラム中に登場したが,出力の「誤差」を測る方法として出力ベクトル $\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 などのパッケージの使用宣言だ.
|
|
次に、MLDatasets
package の機能で MNIST データをダウンロードし,NN で扱えるように変換しよう.
まずはダウンロードだ.これは一回だけやれば良い.あとは SSD/HDD に保存される.
|
|
次に,これを変数に入れよう.
|
|
raw_imgs
変数に画像データが, labels
変数にその画像がどの数字を描いたものか,が入る.
ただ,raw_imgs
変数は3次元配列だし,画像用の特殊な型(f0r8形式)で扱いにくいので,次のようにして扱いやすくしておこう.
|
|
ダウンロードしたデータをちょっと見ておこう.
それには画像データを画像で見てみるのが良いので,ColorTypes
package を使うことにしよう.
インストールしてない場合はインストールしておこう.
|
|
その後,次のようにすると,行列データを(以下の場合はグレイ表現の)画像で見ることができる.
|
|

ちなみにこれは 0から9 のどの数字かというと,
|
|
5
ということで,正解は 5 だそうだ.
次に,これらを,学習プログラムに渡せるよう,固まりのデータに変換する.
|
|
変換した結果を見ておこう.
特に,Y
が何を意味するのか,よくみるとわかるだろう.
|
|
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
…略…
|
|
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 を構成できる.
|
|
意味は,上の注釈でわかるだろう.
さて,この学習「していない」 NN での出力を念の為に確認しておこう. 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 である確率 」を並べたもの,と解釈することになる. わかりやすいようにグラフで見ておこう.
|
|
これだと,この画像は "2" である確率が一番高いことになり,正解の "5" を当てられていないことがわかる.
同様にもう二つほど見ておこう. データの 2つ目は
|
|

となっていて,明らかに正解は "0" だ.まあ確認しておくか.
|
|
0
ふむ.ではこの画像データを NN に入れると…
|
|
これも当然うまくいってない.
データの3つ目も確認しておこう. (画像をみるのはもう省略して)真値は
|
|
4
となるので "4" が真値で,NN が出す確率は
|
|
となっていてこれももちろんうまくいってない.
さて,では学習に必要な関数の準備をしよう.
|
|
NN の正解率を測ることができるようになったので,学習していない現状の NN での正解率をみておこう.
|
|
0.0945
ふ~む.ランダムな状態の NN での正解率が 9.45% ということで,まあ自然だな(ランダムに数字を一つだせば,正解率は平均で 10% になるはずなので).
ではデータセットの設定と,パラメータをどうやって修正していくかの方法の指定,そして画面表示設定等をしてしまおう.
|
|
これで準備ができたので,早速実行しよう. ただし,一回小さなデータで関数をコンパイルしておく.
|
|
loss(X, Y) = 1.9413911f0
では,大きめのデータで学習させてみよう.
|
|
loss(X, Y) = 0.26127964f0
loss(X, Y) = 0.13625997f0
loss(X, Y) = 0.097146265f0
loss(X, Y) = 0.07342944f0
loss(X, Y) = 0.057304744f0
loss(X, Y) = 0.0455831f0
loss(X, Y) = 0.03663983f0
loss(X, Y) = 0.029523531f0
loss(X, Y) = 0.023770217f0
loss(X, Y) = 0.019059699f0
96.623440 seconds (392.26 k allocations: 116.986 GiB, 4.65% gc time, 0.12% compilation time)
計算時間は CPU 等によってひどく異なるので,私有環境で上の 10倍以上の時間がかかっても不思議ではない.
さて,学習が終了したようなので,結果をチェックしてみよう.
|
|
0.9972
ふむ.正解率は 99.7% か.学習前は 9.5% だったことを考えると,2分弱の学習としてはたいへん上出来だ.
最初の 3つのデータについて,個別に学習結果をチェックしてみよう. まずは 1つ目のデータに対する NN の回答は,
|
|
となる.これだと "5" である確率が80%以上で,正解をきちんと当てていると言えるな.
次に 2つ目のデータだ.
|
|
これだと "0" である確率がほぼ 100% だ.正解を完全に当てている.
3つ目も見てみよう.
|
|
これも "4" である確率が約 90% で,正解をきちんと当てている.
次に,学習に使っていない,テスト用データを対象としてこの NN の性能を見よう. まず,テスト用データのダウンロードと整形だ.
|
|
この NN を適用した場合の精度を見よう.
|
|
0.958
ふむ,未知のデータに対しても約 96% の確率で正解を出せるということだな. どうやらこの 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 も含めて)インストール方法がわかる.まあ,解凍してファイルをいくつかコピーするだけだ.
のインストールを上の順序でしておく必要がある…のだが,実は,下記のCUDA パッケージ1をインストールすると自動でやってくれる.楽だ.
というわけで,Julia で CUDA パッケージを使えるようにインストールしよう.
|
|
ダウンロード等にかなり時間がかかる. 授業中に下記のコードにチャレンジするのであれば,できれば事前にこのインストールをやっておこう.
あとは次のように,必要なデータや計算ルーチンを GPU に渡すようにしてプログラムを書けば良い.
|
|
GPU に渡すデータのサイズをむやみに大きくしないようにしよう.
上の例で言えば,large_dataset
をあまり大きくすると,GPU の挙動が変になったりして,OS までトラブったりするぞ.
これで準備ができた. 念の為に初期の精度を測っておく.
|
|
0.10885
ふむ.平均正解率 10.9% というところで,まあ妥当だ.
では学習させてみよう. ただし,一回小さなデータで関数をコンパイルしておく.
|
|
では,大きめのデータで学習させてみよう. このデータを CPU で学習したときは 100秒近くかかったが…
|
|
loss(X, Y) = 2.194913f0
7.985506 seconds (832.80 k allocations: 53.118 MiB, 1.24% gc time, 0.21% compilation time)
おお! さすが. CPU だと 97秒かかったこの学習が,この環境だとたった 8秒で学習が終わる. 12倍は速くなった,ということだな.
この例で使っているGPUボードは動画用であって計算用途向きではないので,本格的な計算用 GPUボードだったらどれくらい速くなるんだろう,と思ってしまうな.
さて,学習後の NN の精度も見てみよう.
|
|
0.9505833333333333
正解率が 95% ということで,確かによく学習できているようだ.
未知のデータについてもこの NN の能力を確かめておこう.
|
|
0.9443
未知のデータに対しても 94% の正答率だ.たった 8秒の学習なのに,大変よく出来たと言えよう.
ということで,GPU などの特殊なハードウェアの力を借りることができれば,こうした学習も大変速く行えることがわかる.デスクトップ PC 用のグラフィックカードであればそう高いものでもないので,機械学習を少し勉強して見るならばこうしたハードウェアの調達も検討しておくと良いだろう.
レポート
下記要領でレポートを出してみよう.
- e-mail にて,
- 題名を 2021-numerical-analysis-report-14 として,
- 教官宛(アドレスは web の "TOP" を見よう)に,
- 自分の学籍番号と名前を必ず書き込んで,
- 内容はテキストでも良いし,pdf などの電子ファイルを添付しても良いので,
下記の内容を実行して,結果や解析,感想等をレポートとして提出しよう.
- 上の例で NN の中間層を増やしてみよう.ただし,計算時間は増大するので,バランスが厳しいかも.
- nVIDIA のグラフィックカードが刺さっている PC環境を探すなどして,可能ならば GPU での計算例も試してみよう.
- Flux Model Zoo を見て,他の例を試してみよう.ただし,知らないことばかりの場合は無理しなくて良い.
-
少し前までは Julia では NVidia 社の GPU を使うには
CuArrays
package を使うもの,だったが,今はCUDA
package を使え,ということになっている.そのせいか,最新の環境だとCuArrays
package はインストールすらできない. ↩︎