Elixir Image で機械学習モデルによる画像識別を実行する

## はじめに

Elixir の Image モジュールで画像に写っているものを識別します

内部的には機械学習モデルによる推論が実行されていますが、Image モジュールではシンプルに使えるようになっています

https://github.com/elixir-image/image

実装したノートブックはこちら

https://github.com/RyoWakabayashi/elixir-learning/blob/main/livebooks/vix/classification.livemd

## セットアップ

必要なモジュールをインストールします

```elixir
Mix.install(
  [
    {:image, "~> 0.52"},
    {:bumblebee, "~> 0.5"},
    {:exla, "~> 0.7"},
    {:kino, "~> 0.13"}
  ],
  config: [
    nx: [
      default_backend: EXLA.Backend,
      default_defn_options: [compiler: EXLA]
    ]
  ]
)
```

- [Image](https://github.com/elixir-image/image): 画像処理
- [Bumblebee](https://github.com/elixir-nx/bumblebee): 機械学習による推論実行
- [EXLA](https://github.com/elixir-nx/nx/tree/main/exla): 行列演算の高速化
- [Kino](https://github.com/livebook-dev/kino): Livebook の UI/UX

Nx と Bumblebee、 EXLA の全てがインストールされていないと `Image.Classification` モジュールが読み込めません

Bumblebee をインストールした時点で Nx は付随されるため、省略可能です

## 画像の準備

任意の画像を読み込みます

```elixir
puppies_img = Image.open!("/home/livebook/vix/puppies.png")
```

![スクリーンショット 2024-07-08 23.58.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/0982d827-241e-adf1-1c4d-d39d78e10ecb.png)

## 画像の識別

### ラベル名だけの取得

`Image.Classification.labels` で識別結果のラベル名だけが配列として返ってきます

```elixir
Image.Classification.labels(puppies_img)
```

実行結果

```elixir
["golden retriever"]
```

デフォルトでは確信度 0.5 以上のラベルを返します

第2引数で `min_score: 0.3` のように閾値を指定できます

### ラベル名と確信度の取得

`Image.Classification.classify` でラベルと確信度の組み合わせを配列で取得できます

```elixir
Image.Classification.classify(puppies_img)
```

実行結果

```elixir
%{
  predictions: [
    %{label: "golden retriever", score: 0.545122504234314},
    %{label: "Labrador retriever", score: 0.07027861475944519},
    %{label: "Tibetan mastiff", score: 0.06626683473587036},
    %{label: "flat-coated retriever", score: 0.03985850512981415},
    %{label: "Border collie", score: 0.027414413169026375}
  ]
}
```

`Kino.DataTable` と組み合わせれば、表形式で出力可能です

```elixir
puppies_img
|> Image.Classification.classify()
|> Map.get(:predictions)
|> Kino.DataTable.new()
````

実行結果

![スクリーンショット 2024-07-10 22.00.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/85a4784c-a739-817c-f5c3-26efa5cef904.png)

### 任意モデルでの画像識別

`Image.Classification.classifier` を使うことで、任意の画像識別モデルを使用できます

まずはデフォルトの画像識別モデル（この記事内でこれまで使っていたものと同じモデル）を使ってみます

デフォルトの画像識別モデルは HuggingFace 上に公開されている "microsoft/resnet-50" というモデルです

https://huggingface.co/microsoft/resnet-50

デフォルトなのでオプションを指定しません

```elixir
{_, classifier} = Image.Classification.classifier()
```

実行結果

```elixir
{Nx.Serving,
 [
   serving: %Nx.Serving{
     module: Nx.Serving.Default,
     arg: #Function<1.94186835/1 in Bumblebee.Vision.ImageClassification.image_classification/3>,
     client_preprocessing: #Function<2.94186835/1 in Bumblebee.Vision.ImageClassification.image_classification/3>,
     client_postprocessing: #Function<3.94186835/2 in Bumblebee.Vision.ImageClassification.image_classification/3>,
     streaming: nil,
     batch_size: 10,
     distributed_postprocessing: &Function.identity/1,
     process_options: [],
     defn_options: [compiler: EXLA]
   },
   name: Image.Classification.Server,
   batch_timeout: 100
 ]}
```

以下のようにして画像識別モデルを `Nx.Serving` の形式で取得できます

```elixir
serving = Keyword.get(classifier, :serving)
```

実行結果

```elixir
%Nx.Serving{
  module: Nx.Serving.Default,
  arg: #Function<1.94186835/1 in Bumblebee.Vision.ImageClassification.image_classification/3>,
  client_preprocessing: #Function<2.94186835/1 in Bumblebee.Vision.ImageClassification.image_classification/3>,
  client_postprocessing: #Function<3.94186835/2 in Bumblebee.Vision.ImageClassification.image_classification/3>,
  streaming: nil,
  batch_size: 10,
  distributed_postprocessing: &Function.identity/1,
  process_options: [],
  defn_options: [compiler: EXLA]
}
```

取得した `serving` を使って、以下のように画像識別が実行できます

```elixir
serving
|> Nx.Serving.run(Image.to_nx!(puppies_img))
|> Map.get(:predictions)
|> Kino.DataTable.new()
```

これまでと同じモデルなので結果も同じになります

![スクリーンショット 2024-07-10 22.00.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/85a4784c-a739-817c-f5c3-26efa5cef904.png)

`Image.Classification.classifier` にオプションを指定することで、違うモデルが使用できます

```elixir
{_, classifier} = Image.Classification.classifier(
  model: {:hf, "google/vit-base-patch16-224"},
  featurizer:  {:hf, "google/vit-base-patch16-224"}
)

serving = Keyword.get(classifier, :serving)

serving
|> Nx.Serving.run(Image.to_nx!(puppies_img))
|> Map.get(:predictions)
|> Kino.DataTable.new()
```

実行結果

![スクリーンショット 2024-07-10 22.11.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/04967c4f-114a-e646-0782-b3c21967963d.png)

## まとめ

Bumblebee でも簡単に画像識別を実行できますが、 Image モジュールでは更に簡単に、機械学習モデルを意識せずに画像識別を実行できます

単純な分類をしたいだけの場合、 `Image.Classification.labels` を使えば実装は超シンプルです