ElixirのみでディープラーニングOnnx編：Livebook＋AxonOnnxでResnet画像識別（Python同様Onnx学習済みモデルが利用可）

[ElixirImp／fukuoka.ex／kokura.ex](https://fukuokaex.connpass.com)と[LiveView JP](https://liveviewjp.connpass.com/)の [piacere](https://twitter.com/piacere_ex) です、ご覧いただいてありがとございます :bow:

今回は、ElixirでResnet Onnxモデルを使った画像識別MLをLivebook上で開発します
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/7fafab81-73d2-d51b-06c0-edc8da0aa4cc.png)

ResNetによる画像処理やCNNの基礎的なことを学べるコラムにもなっているため、Python／Elixir問わず、画像処理の基礎を学びたい方の入門編コラムでもありますので、これまでAI・MLやディープラーニングを学ぶ機会を逃した方は、このコラムを活かしてください

なお、Elixirにて機械学習／ディープラーニングするツール ＆ ライブラリについては下記をご参考ください

https://qiita.com/piacerex/items/c406e5737156b9fa114d

# 本コラムの検証環境

本コラムは、以下環境で検証しています（Windows WSL2で実施していますが、Windowsネイティブや実機のUbuntu 18.04、Macでも動作する想定です）

- Windows 11＋WSL2 Ubuntu 18.04
- Elixir 1.13.4 on WSL2 Ubuntu　※<b>[最新版のインストール手順はコチラ](https://qiita.com/piacerex/items/01e89435af0f7a454ad2)</b>
- Livebook 0.6.3 　　　　　　　 ※<b>[最新版のインストール手順はコチラ](https://qiita.com/piacerex/items/0871b54b41128d426da5)</b>

# ResNetの概要

ResNet（Residual Network）は、[「ImageNet」](https://www.image-net.org/)と呼ばれる、1,400万枚以上のカラー写真の教師ラベル付きデータベースサイトで学習した、1,000種類の画像を分類できる学習済みモデルです

2015年、Microsoft Researchによって考案され、[「ILSVRC（ImageNet Large Scale Visual Recognition Challenge）」](https://www.image-net.org/challenges/LSVRC/)というImageNet画像データセットを識別するコンテストで有償したモデルです

[MNISTと同様の多クラス分類](https://qiita.com/piacerex/items/9cf4f328222103458167#mnist%E6%89%8B%E6%9B%B8%E3%81%8D%E6%96%87%E5%AD%97%E8%AD%98%E5%88%A5%E3%81%AE%E6%A6%82%E8%A6%81)によって、1,000種類の各画像にどの程度マッチするかを識別します

学習済みモデルのため、モデルを入手すれば、すぐ利用できる点が便利です

また、このモデルをベースに「転移学習」を行うことで、別の教師ラベル群と紐付けられた画像識別も可能です（これはどこかの回でデモしたいと思います）

## ResNet18のアーキテクチャ

今回、使用するResNet18は、下図のような構成を持つモデルで、224（横ピクセル数） x 224（縦ピクセル数） x 3（チャネル数）の画像をインプットとし、1,000種類の各画像との多クラス分類を行います
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/a2dad6f0-0fee-5e96-2f53-dc8c7ba8a25d.png)

10層のCNN（Convolutional Neural Network：畳み込みニューラルネットワーク）層とプーリング層、全結合層の全18層から構成されます

CNNは、画像識別においてポピュラーなアルゴリズムで、以下2つのパートで構成されます

①畳み込み層
②プーリング層

畳み込み層は、画像内を縦3ピクセル x 横3ピクセルなどの特定サイズのブロック（カーネルと呼ばれます）でフィルタを施す畳み込み演算を、左上から右下まで1ピクセルずつズラしながら行うことで、特徴抽出を行います
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/050699d2-6159-0ba2-e2b9-6cb2c498dab7.png)

なお、アーキテクチャの図に書かれている「stride（ストライド）」とは、この畳み込み演算を1ピクセルずつズラすのでは無く、数ピクセルまとめてズラすことを指し、図の記載では、2ピクセルずつズラすことで、大きな画像からの特徴抽出結果をより小さなサイズとすることで高速化したり、大雑把な特徴抽出を行う効果があります（ResNetの前半の層で多用されているのはこの理由）

プーリング層は、畳み込み層で特徴抽出済みのデータに対して、プーリング用カーネル（畳み込み層とはサイズが異なることがある）を1ピクセルのデータへと圧縮します

ResNetでは、圧縮方法として、「Average Pooling」というカーネル内の値の平均を取る手法を用います（他には「Max Pooling」と言う画像内のブロックの最大を取る手法もメジャーです）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/b6280d56-4b5d-1919-0f87-1cdfb1e7c2c6.png)

全結合層は、上記の層で特徴抽出された結果を元に予測を行う層で、内部的にソフトマックス関数による多クラス分類が行われる点は、MNISTと同じ仕組みです

# Onnxの概要

Onnx（Open Neural Network Exchange）は、機械学習モデルを共通化するためのフォーマットです

これを利用すると、TensorFlow／KerasやPyTorchで学習したモデルを、別の推論エンジン／ライブラリで利用することが可能となります

ElixirのAxonOnnxも、学習済みモデルを読み込み、Axon上のニューラルネットワークとして利用します

ちなみにOnnx化したモデルは、モデル学習時よりも、推論が高速化されるといったメリットがあります

# 最終的なコード

下記のコードのみでResNet Onnxモデルでの画像識別が実現できます（以降の節で各パートの解説をします）

```elixir:ライブラリのロード（100秒程度かかります）
Mix.install([
  {:exla, "~> 0.2"},
  {:axon_onnx, "~> 0.1"},
  {:stb_image, "~> 0.5"}, 
  {:download, "~> 0.0"},
  {:jason, "~> 1.3"}, 
  {:kino, "~> 0.6"}, 
])
```

```elixir:モデルのロードまで（40～60秒程度かかります）
EXLA.set_as_nx_default([:tpu, :cuda, :rocm, :host])

File.rm("imagenet_class_index.json")
classes =
  Download.from("https://s3.amazonaws.com/deep-learning-models/image-models/imagenet_class_index.json")
  |> elem(1)
  |> File.read!
  |> Jason.decode!

File.rm("resnet18-v1-7.onnx")
{model, params} = 
  Download.from("https://media.githubusercontent.com/media/onnx/models/main/vision/classification/resnet/model/resnet18-v1-7.onnx")
  |> elem(1)
  |> AxonOnnx.import
```

```elixir:画像識別する対象をダウンロード
File.rm("greatwhiteshark_157273892.jpg")
data =
  Download.from("https://www.collinsdictionary.com/images/full/greatwhiteshark_157273892.jpg")
  |> elem(1)
  |> File.read!
Kino.Image.new(data, :jpeg)
```

```elixir:画像加工した後、画像識別
nx_image = data
  |> StbImage.read_binary!
  |> StbImage.resize(224, 224)
  |> StbImage.to_nx

nx_channels = nx_image
  |> Nx.axis_size(2)

tensor =
  case nx_channels do
    3 -> nx_image
    4 -> Nx.slice(nx_image, [0, 0, 0], [224, 224, 3])
  end
  |> Nx.divide(255)
  |> Nx.subtract(Nx.tensor([0.485, 0.456, 0.406]))
  |> Nx.divide(Nx.tensor([0.229, 0.224, 0.225]))
  |> Nx.transpose
  |> Nx.new_axis(0)

Axon.predict(model, params, tensor)
|> Nx.flatten
|> Nx.argsort
|> Nx.reverse
|> Nx.slice([0], [5])
|> Nx.to_flat_list
|> Enum.with_index
|> Enum.map(fn {no, index} -> {index, Map.get(classes, to_string(no))} end)
```

実行結果は、以下の通りで、対象画像がどんな種類の画像かが識別されています

正確には、ResNetの学習元となっているImageNetにある1,000種類の画像のいずれかとして識別されたかの結果が出力されています
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/63b1df5b-1c3b-6ab3-8665-4ef71920461b.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/bf2da70b-d9dc-4b45-7595-3d8c6b9e6dd3.png)

# 各パートについて解説

## ①ライブラリのロード

Livebookで下記を実行し、必要ライブラリをロードしてください（Livebookが初めての方は、[基本的な操作をこちらのコラム](https://qiita.com/piacerex/items/533d26c81bada4741422)で学べます）

新しいLivebookだと、下図のようなライブラリロード用のエリアが追加されているので、この中で実行してください（KinoVegaLiteやKinoDBなどをプラグインしたときも、ココに追加されます）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/da68a9d2-d2e9-6780-2b3f-f01477ae81d3.png)

```elixir
Mix.install([
  {:exla, "~> 0.2"},
  {:axon_onnx, "~> 0.1"},
  {:stb_image, "~> 0.5"}, 
  {:download, "~> 0.0"},
  {:jason, "~> 1.3"}, 
  {:kino, "~> 0.6"}, 
])
```

なお、KinoVegaLiteやKinoDBをロードする際は、Kino単独のロードをしなくても、Livebook上での画像やグラフの表示が可能になります

## ②ImageNetの教師ラベルを準備

ResNetで画像識別すると、1,000種類の画像それぞれに、どの程度マッチしたかが返ってくるため、各画像の教師ラベルを準備することで、どの画像にマッチしているかを人が見て分かる表示ができるようになります

```elixir
# 再実行時、Download.from()でeexistエラーになるのを防止
File.rm("imagenet_class_index.json")

# ResNetはImageNetで学習した1,000種類の教師ラベルで画像識別をする
# （Mapでソート順が崩れているが0～999の順になっており、ResNetもその順で識別する）
classes =
  Download.from("https://s3.amazonaws.com/deep-learning-models/image-models/imagenet_class_index.json")
  |> elem(1)
  |> File.read!
  |> Jason.decode!
```

実行結果は以下の通りです（なお、Mapでソート順が崩れていますが、0～999の順で定義されており、ResNetもその順で識別結果を返します）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/808b3ec3-8b61-88bd-2807-a1df3ebcc6af.png)

## ③Resnet Onnxモデルを準備

Resnet Onnxモデルは、ネット上に保存されているので、それをダウンロードして使います

なお最終的なコードでは、パイプを使うことで、変数 ``resnet`` を省略していますが、ここでは説明をカンタンにするために変数化しています

```elixir
# 再実行時、Download.from()でeexistエラーになるのを防止
File.rm("resnet18-v1-7.onnx")

# ResNet Onnxモデルをダウンロード（20秒程度かかります）
resnet =
  Download.from("https://media.githubusercontent.com/media/onnx/models/main/vision/classification/resnet/model/resnet18-v1-7.onnx")
  |> elem(1)
```

ダウンロード後、ローカルに保存されたResNet Onnxモデルを ``AxonOnnx.import()`` でインポートします

```elixir
# ResNet Onnxモデルをインポート（20秒程度かかります）
{model, params} = AxonOnnx.import(resnet)
```

実行結果として、自前で構築したとき同様、インポートされたモデルのサマリが表示されます
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/08438dc6-eb7a-88e3-a188-01705bd5ce60.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/93e7407a-90fe-703c-c9e3-170867d8d16a.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/1116328c-4f4f-0aa8-e580-12d454daa87a.png)

上記サマリは、224 x 224 x 3の画像をインプットとし、10層のCNNと、Average Pooling層、全結合層から構成されるResNetモデル（下図）と同様であることを表しています
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/a2dad6f0-0fee-5e96-2f53-dc8c7ba8a25d.png)

上図は、Livebook上で下記コードにより見ることができます

```elixir
File.rm("f9a8eaaa866c2b783162fe65afdff67171e993fb.png")
Download.from("https://forums.fast.ai/uploads/default/original/3X/f/9/f9a8eaaa866c2b783162fe65afdff67171e993fb.png")
|> elem(1)
|> File.read!
|> Kino.Image.new(:jpeg)
```

それとサマリの中で、「batchnorm」と書かれた、上記ResNetモデルに記載されていない層がありますが、コレは「Batch Normalization」を意味しており、「内部共変量シフト」と呼ばれる、学習が効率的に進まない状況に対し、正規化により学習を安定化／高速化する層になるのですが、ResNetのような深い層を持つモデルでは重要となります

``params`` には、カーネルが入っています
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/3d963a6b-206e-589d-9f32-b58743fc366c.png)

## ④モデルの学習

ResNetは、学習済みモデルのため、モデルの学習は不要です

## ⑤学習済みモデルで予測を実施

実際に、学習済みモデルにて未知データでの予測を行ってみます

### 未知データとなる画像のロード

```elixir
# 再実行時、Download.from()でeexistエラーになるのを防止
File.rm("greatwhiteshark_157273892.jpg")

# 画像識別する対象をロード
data =
  Download.from("https://www.collinsdictionary.com/images/full/greatwhiteshark_157273892.jpg")
  |> elem(1)
  |> File.read!

Kino.Image.new(data, :jpeg)
```

実行結果は、以下の通りです
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/8ca6c27d-9c38-7afe-9e29-716a21706700.png)

次に、この画像をResNetで扱える形（224 x 224 x 3）へと変形し、Nx行列へと変えます

この際、チャンネル数が3では無く、4になっているケースがあるため、3になるよう加工します

なお最終的なコードでは、その後のResNet向け画像加工をまとめてパイプで処理しているため、変数 ``nx_binary`` を省略していますが、ここでは各加工仮定をデバッグするために変数化しています

```elixir
# ResNetにインプットできるよう224 x 224 x 3の画像に加工
nx_image =
  data
  |> StbImage.read_binary!
  |> StbImage.resize(224, 224)
  |> StbImage.to_nx

nx_channels =
  nx_image
  |> Nx.axis_size(2)

nx_binary = 
  case nx_channels do
    3 -> nx_image
    4 -> Nx.slice(nx_image, [0, 0, 0], [224, 224, 3])
  end
```

加工後の画像を確認してみます

```elixir
nx_binary
|> StbImage.from_nx
|> StbImage.write_file("changed.jpg")

File.read!("changed.jpg")
  |> Kino.Image.new(:jpeg)
```

224 x 224 x 3の画像に加工されていることが確認できました
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/275b931b-3f4e-de17-94bd-e546f98157a4.png)

この後、ResNetで扱える画像加工が、以下のように続きます

```elixir
tensor = nx_binary
  |> Nx.divide(255)
  |> Nx.subtract(Nx.tensor([0.485, 0.456, 0.406]))
  |> Nx.divide(Nx.tensor([0.229, 0.224, 0.225]))
  |> Nx.transpose
  |> Nx.new_axis(0)
```

最初に行うのは、ResNetで扱える輝度に下げるために、``Nx.subtract(Nx.tensor([0.485, 0.456, 0.406]))`` を行っているのですが、この加工を下記のようにデバッグしてみましょう

``Nx.divide(255)`` で正規化した後に加工を行っているのですが、このままだとKinoで画像表示できないため、``Nx.multiply(255)`` で非正規化し、画像として扱えるよう ``|> Nx.as_type({:u, 8})`` でf32化されていたデータの小数を除去します

```elixir
nx_binary
|> Nx.divide(255)
|> Nx.subtract(Nx.tensor([0.485, 0.456, 0.406]))
|> Nx.multiply(255)
|> Nx.as_type({:u, 8})

|> StbImage.from_nx
|> StbImage.write_file("changed.jpg")

File.read!("changed.jpg")
  |> Kino.Image.new(:jpeg)
```

輝度が下げられていることが確認できました
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/a063917b-7815-0a8d-76a0-7099a09588e1.png)

他のResNet向け画像加工も、上記のようにデバッグすれば、どのような加工を行ったかが確認できますので、トライしてみてください

その後、``Nx.transpose()`` で横／縦／チャンネルを逆転させ、``Nx.new_axis(0)`` で深さを1段下げます

### 画像識別

ResNet向け画像加工が一通り終わったら、``Axon.predict()`` で、モデルと学習済みステート、未知データ行列を渡すと、予測（画像識別）が行われます

```elixir
# 1,000種類の画像各々にどの程度、適合しているかを予測
predict_per_list =
  Axon.predict(model, params, tensor)
```

以下の通り、各画像とのマッチ率が算出されます
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/7724f3de-7bc7-d475-0763-c14c6056715b.png)

このマッチ率をランク付けし、最もマッチするものから順に並べ替えます

```elixir
# マッチ率をランク付け
rank = predict_per_list
  |> Nx.flatten
  |> Nx.argsort
  |> Nx.reverse
```

以下の通り、最もマッチする画像のインデックスが返ってきます
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/30354c6f-e4c2-49e0-d494-5d2ec278e6c9.png)

最後に、このランク付けに該当する教師ラベルを取得します

```elixir
# 識別した1,000種類の教師ラベルを取得
rank
|> Nx.slice([0], [5])
|> Nx.to_flat_list
|> Enum.with_index
|> Enum.map(fn {no, index} -> {index, Map.get(classes, to_string(no))} end)
```

この実行結果が、画像識別でマッチした順の教師ラベルとなっています
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/c3a04cc9-72e0-a975-c6cf-b05c2dcdbf6a.png)

教師ラベルはそれぞれ、以下表の通りで、いい感じに画像識別されていると思います

教師ラベル|日本語訳|参考画像
-|-|-
tiger_shark|イタチザメ|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/f41151a0-a027-b1eb-1890-64e36c384eef.png)
great_white_shark|ホオジロザメ|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/1f14785e-877b-6c38-32d7-20eabd8bb122.png)
hammerhead|シュモクザメ|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/d7a89c99-8c38-2eec-e2f2-ac28e6abe1aa.png)
electric_ray|シビレエイ|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/3328b547-f803-ec93-4b80-e11a2fcf3b50.png)
dugong|ジュゴン|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/77b35d40-77f8-cbda-4fd2-4d2c35111b95.png)

# 終わり

今回は、ResNet OnnxモデルやCNNについて解説し、Livebookで実際にResNet Onnxモデルによる画像識別を実践しました

Livebook＋Nx＋AxonOnnxを使うことで、PythonのJupyterNotebookやColaboratoryとほぼ同じフィーリングでOnnx学習済みモデルを使ったAI・ML開発が可能であることが実感いただけたでしょうか

また、ResNetによる画像識別は、GPU非搭載PCでも気軽に試すことができる分かりやすいディープラーニング例でもあるので、ディープラーニング入門としてもオススメです

# 主催／運営しているElixirコミュニティ紹介

[**1. ElixirImp** : A place to LOVE the buds in Elixir (Elixir実装の芽を愛でる場)
**2. fukuoka.ex** : Fukuoka local Elixir Community (福岡Elixirコミュニティ)
**3. kokura.ex** : Kokura local Elixir Community (小倉Elixirコミュニティ)](https://fukuokaex.connpass.com)

https://fukuokaex.connpass.com

[**4. LiveView JP** : A place to mob-program in LiveView, LiveBook+Nx+Axon, and elixir-desktop](https://liveviewjp.connpass.com)

https://liveviewjp.connpass.com

[**5. Neos.ex** : A place to connecting Elixir and NeosVR to create a new world](https://elixirjp.slack.com/archives/C02J9C12VMF)

## :ocean::ocean::ocean: Elixir生誕10周年を祝い、"Elixirの現在" に追いつける :ocean::ocean::ocean:

Elixir界隈に激震をもたらした2021年の大変動を活用するコラム群を日々アップデートしています

本コラムも、[第3弾「Elixir／Livebook＋NxでPythonっぽくAI・ML」](https://qiita.com/piacerex/items/e5590fa287d3c89eeebf#%E7%AC%AC3%E5%BC%BEelixirlivebooknx%E3%81%A7python%E3%81%A3%E3%81%BD%E3%81%8Faiml)に追加しています

https://qiita.com/piacerex/items/e5590fa287d3c89eeebf#%E7%AC%AC3%E5%BC%BEelixirlivebooknx%E3%81%A7python%E3%81%A3%E3%81%BD%E3%81%8Faiml

# p.s.このコラムが、面白かったり、役に立ったら…

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/67568d54-6584-ad33-77c7-b3b3afe70287.png) や ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/f5b819ef-d655-ef72-294c-b1a9d2c5b563.png) にて、どうぞ応援よろしくお願いします:bow