手書き文字識別(MNIST)
Mix.install([
{:nx, "~> 0.4"},
{:axon, "~> 0.3"},
{:exla, "~> 0.4"},
{:scidata, "~> 0.1"},
{:table_rex, "~> 3.1"},
{:kino_vega_lite, "~> 0.1.7"}
])
概要
- 教師あり学習(MNIST 手書き文字識別)
- 多クラス分類
資料
- Elixir でディープラーニング ①:手書き文字識別(MNIST)を Livebook + Axon で by piacerex
- Elixir 生誕 10 周年祭 ■ 第 3 弾:Elixir/Livebook + Nx で Python っぽく AI・ML by piacerex
MNIST 手書き文字識別
- 手書き文字画像が、0 ~ 9 の数字のうち、どの数字により適合するかを識別する
-
MNIST データセット
- 60,000 文字分のデータ
- 手書き文字画像(1 文字は 28 x 28 ピクセル)
- それぞれの画像が何の数字であるかが 0 ~ 9 で入っているラベル
ⅰ)学習データと検証データの準備
60000 文字分データをダウンロードし、学習データを検証データに分割
MNIST データセットをダウンロード
{datas_raw, labels_raw} = Scidata.MNIST.download()
手書き文字画像データを 1 文字ずつで扱えるようにする
{data_bins, type, shape} = datas_raw
# 60,000文字分の手書き文字画像のバイナリ
<<_::binary>> = data_bins
# 型
{:u, 8} = type
# 文字数、バイナリの次元数、横ピクセル数、縦ピクセル数
{60000, 1, 28, 28} = shape
datas =
data_bins
|> Nx.from_binary(type)
# 一文字分の手書き文字画像データは、784ピクセル(28 x 28)の1次元行列
|> Nx.reshape({60000, 28 * 28})
|> Nx.divide(255.0)
|> dbg()
:ok
ラベルを 1 文字ずつで扱えるようにする
{label_bins, type, shape} = labels_raw
# 60,000文字分の手書き文字画像のラベル
<<_::binary>> = label_bins
{:u, 8} = type
{60000} = shape
labels =
label_bins
|> Nx.from_binary(type)
# 1文字ずつで扱えるように分解
|> Nx.new_axis(-1)
# ラベルを数値からクラス行列に変形
|> Nx.equal(Nx.tensor(Enum.to_list(0..9)))
|> dbg()
:ok
手書き文字画像をヒートマップ表示
index_input = Kino.Input.number("Index", default: 0)
index = Kino.Input.read(index_input)
datas[index]
# ヒートマップ表示できるよう、28 x 28の2次元行列に変形
|> Nx.reshape({28, 28})
|> Nx.to_heatmap()
|> dbg()
# ラベルを確認
labels[index]
|> inspect()
|> IO.puts()
:ok
学習データと検証データの分割
- 手書き文字データとラベルの両方を、学習データ 80%、検証データ 20%に分割
-
バッチ学習
- 学習速度・並列度アップ
- 過学習回避
- 【人工知能】機械学習で行われる学習方法について。バッチ・ミニバッチ・オンライン学習。
{train_datas, test_datas} =
datas
# 32文字ごとにバッチ化
|> Nx.to_batched(32)
# 80%をtrainに、20%をtestに
|> Enum.split(round(60000 * 0.8 / 32))
|> dbg
{train_labels, test_labels} =
labels
# 32文字ごとにバッチ化
|> Nx.to_batched(32)
# 80%をtrainに、20%をtestに
|> Enum.split(round(60000 * 0.8 / 32))
|> dbg
:ok
N 番目のバッチの全 32 文字の手書き文字画像をヒートマップ表示
batch_index_input = Kino.Input.number("Batch index", default: 0)
batch_index = Kino.Input.read(batch_index_input)
for test_data <- test_datas do
test_data[batch_index]
|> Nx.reshape({28, 28})
|> Nx.to_heatmap()
end
batch_index = Kino.Input.read(batch_index_input)
for test_label <- test_labels do
test_label[batch_index]
end
ⅱ)モデルの学習
学習データとラベルの関係性を学習
資料
- Axon
- 活性化関数(Activation function)とは?
- [活性化関数]ソフトマックス関数(Softmax function)とは?
- Dropout:ディープラーニングの火付け役、単純な方法で過学習を防ぐ
- 機械学習入門者向け 分類と回帰の違いをプログラムを書いて学ぼう
- deep learning の基礎(Early Stopping)
require Axon
# 入力層
model =
Axon.input("input", shape: {nil, 784})
# 中間層
# * 計算効率の良いReLUを活性化関数
# * ランダムでニューラルネットワークの間引きを行うことで過学習回避を実現するドロップアウト
|> Axon.dense(128, activation: :relu)
|> Axon.dropout()
# 出力層
# * 他クラス分類に向いているソフトマックス関数
|> Axon.dense(10, activation: :softmax)
Axon.Display.as_table(model, Nx.template({1, 784}, :f32))
|> IO.puts()
モデルの学習
- Axon.Loop モジュールにある学習機能を使って、モデルの学習を行う
- 学習試行回数であるエポック数は、5 ~ 10 回にして、精度向上したいが、今回は Axon を GPU と比べて実行が遅い CPU モードで動かすことから、3 回のみとする
epochs_input = Kino.Input.number("Epochs", default: 3)
epochs = Kino.Input.read(epochs_input)
# 損失関数
# * 多クラス分類のための「カテゴリカル交差エントロピー」
loss_function = :categorical_cross_entropy
# 最適化関数
# * 収束が速く、使い勝手が良い「Adam」に、パラメータの自由度を制限するWeight decayを追加した「AdamW」
optimizer = Axon.Optimizers.adamw(0.005)
trained_state =
model
|> Axon.Loop.trainer(loss_function, optimizer)
# 計算過程を表示するための処理
|> Axon.Loop.metric(:accuracy, "Accuracy")
# バッチが回るたびに同エポック内の正解率(Accuracy)と学習誤差(loss)を表示更新
|> Axon.Loop.handle(:iteration_completed, fn %Axon.Loop.State{} = state ->
[
"\r",
[
"Epoch: #{Nx.to_number(state.epoch)}",
"Batch: #{Nx.to_number(state.iteration)}"
]
|> Enum.concat(
for {k, v} <- state.metrics do
"#{String.capitalize(k)}: #{:io_lib.format('~.5f', [Nx.to_number(v)])}"
end
)
|> Enum.intersperse(", ")
]
|> IO.write()
# ここで{:halt_loop, state}を返却すると、学習を中断できる
# 正解率や誤差の値で早期に学習を打ち切る「Early Stopping」が可能
{:continue, state}
end)
# init_stateに空マップを指定することで、学習モードとなる
|> Axon.Loop.run(
Stream.zip(train_datas, train_labels),
%{},
epochs: epochs,
compiler: EXLA
)
ⅲ)検証データによる評価
予測とラベルの一致率を確認
手書き文字画像全量での正解率チェック
model
# 正解率をチェック
|> Axon.Loop.evaluator()
|> Axon.Loop.metric(:accuracy, "Accuracy")
# init_stateに学習済みモデルを指定することで、予測モードとなる
|> Axon.Loop.run(
Stream.zip(test_datas, test_labels),
trained_state,
compiler: EXLA
)
1 文字ずつの予測の一致チェック
index_input = Kino.Input.number("Index", default: 0)
index = Kino.Input.read(index_input)
Enum.at(test_datas, index)[index]
|> Nx.reshape({28, 28})
|> Nx.to_heatmap()
|> dbg()
Enum.at(test_labels, index)[index]
|> inspect()
|> IO.puts()
:ok
index = Kino.Input.read(index_input)
test_data = Enum.at(test_datas, index) |> dbg
prediction =
model
|> Axon.predict(trained_state, test_data)
|> dbg
prediction[index]
|> Nx.map(&Nx.round(&1))
|> dbg
:ok