Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

手書き文字識別(MNIST)

mnist_20221219.livemd

手書き文字識別(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 手書き文字識別)
  • 多クラス分類

資料

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

学習データと検証データの分割

{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

ⅱ)モデルの学習

学習データとラベルの関係性を学習

資料

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(&amp;Nx.round(&amp;1))
|> dbg

:ok