はじめに
前回の記事で Stable Diffusion を実行しました
せっかくなので、公式のノートブックに載っている他のモデルも動かしてみましょう
今回は ResNet50 などの画像分類モデルを使って、画像に写っているものを識別します
Bumblebee が前処理や後処理をやってくれているので、 Python よりも簡単に AI を使うことができます
このシリーズの記事
- 画像分類: ResNet50 (ここ)
- 画像生成: Stable Diffusion
- 文章の穴埋め: BERT
- 文章の判別: BERTweet
- 文章の生成: GPT2
- 質疑応答: RoBERTa
- 固有名詞の抽出: bert-base-NER
実装の全文はこちら
実行環境
- MacBook Pro 13 inchi
- 2.4 GHz クアッドコアIntel Core i5
- 16 GB 2133 MHz LPDDR3
 
- macOS Ventura 13.0.1
- Rancher Desktop 1.6.2
- メモリ割り当て 12 GB
- CPU 割り当て 6 コア
 
Livebook 0.8.0 の Docker イメージを元にしたコンテナで動かしました
コンテナ定義はこちらを参照
セットアップ
必要なモジュールをインストールして EXLA.Backend で Nx が動くようにします
Mix.install(
  [
    {:bumblebee, "~> 0.1"},
    {:nx, "~> 0.4"},
    {:exla, "~> 0.4"},
    {:kino, "~> 0.8"}
  ],
  config: [nx: [default_backend: EXLA.Backend]]
)
EXLA.Backend で動いていることを確認します
Nx.default_backend()
コンテナで動かしている場合、キャッシュディレクトリーを指定した方が都合がいいです
※詳細は前回の記事を見てください
cache_dir = "/tmp/bumblebee_cache"
モデルのダウンロード
Stable Diffusion のときと同様、モデルファイルを Haggin Face からダウンロードしてきて読み込みます
必要な場合は cache_dir を指定します
{:ok, resnet} =
  Bumblebee.load_model({
    :hf,
    "microsoft/resnet-50",
    cache_dir: cache_dir
  })
{:ok, featurizer} =
  Bumblebee.load_featurizer({
    :hf,
    "microsoft/resnet-50",
    cache_dir: cache_dir
  })
featurizer はモデルに画像を読み込ませるための前処理定義です
ここで指定している ResNet 50 の場合、 https://huggingface.co/microsoft/resnet-50/resolve/main/preprocessor_config.json を読むことになります
preprocessor_config.json は以下のような内容です
これに従って、例えば画像を 224 * 224 にリサイズしたりします
{
  "crop_pct": 0.875,
  "do_normalize": true,
  "do_resize": true,
  "feature_extractor_type": "ConvNextFeatureExtractor",
  "image_mean": [
    0.485,
    0.456,
    0.406
  ],
  "image_std": [
    0.229,
    0.224,
    0.225
  ],
  "resample": 3,
  "size": 224
}
画像分類の実行
画像の準備
Kino.Input.image で分類する画像を用意します
セルを実行して表示される領域に画像ファイルをドラッグ&ドロップしてください
image_input = Kino.Input.image("IMAGE", size: {224, 224})
入力した画像を Nx のテンソルとして読み込みます
image =
  image_input
  |> Kino.Input.read()
  |> then(fn input ->
    input.data
    |> Nx.from_binary(:u8)
    |> Nx.reshape({input.height, input.width, 3})
  end)
Kino.Image.new(image)
手動推論
公式とは順序が逆になりますが、先に手動推論をやってみましょう
まず画像に前処理を掛けます
inputs = Bumblebee.apply_featurizer(featurizer, image)
1 * 224 * 224 * 3 のテンソルになりました
このテンソルを Axon で推論します
outputs = Axon.predict(resnet.model, resnet.params, inputs)
推論結果が 1 * 1000 のテンソルで得られました
今回使っているモデルは画像を 1000 クラスに分類するので、各クラス毎の確信度が得られています
このままの数値は使いづらいので softmax して、 その中でも TOP5 だけを取り出し、スコアとクラス名(ラベル)に変換します
最後に結果が見やすいようにデータテーブルで表示します
outputs.logits
|> Nx.squeeze()
|> Axon.Activations.softmax()
|> Bumblebee.Utils.Nx.top_k(k: 5)
|> then(fn {scores, class_ids} ->
  scores
  |> Nx.to_flat_list()
  |> Enum.zip(Nx.to_flat_list(class_ids))
  |> Enum.map(fn {score, class_id} ->
    [
      label: resnet.spec.id_to_label[class_id],
      score: score
    ]
  end)
end)
|> Kino.DataTable.new()
|> dbg()
ここまでの実装を見て分かる通り、前処理と後処理だけでも結構めんどくさいです
というわけで、 Bumblebee は勝手に前処理と後処理をやってくれます
Nx.Serving による提供
こっちが本来の Bumblebee の使い方です
serving = Bumblebee.Vision.image_classification(resnet, featurizer)
serving
|> Nx.Serving.run(image)
|> then(&Kino.DataTable.new(&1.predictions))
何も考えず Nx.Serving.run に渡せば predictions の中に結果が返ってきます
すごく便利ですね
他のモデル
他のモデルもリポジトリーIDを書き換えるだけで実行できる、とのことなので、関数化して実行してみます
serve_model = fn repository_id ->
  {:ok, model} =
    Bumblebee.load_model({
      :hf,
      repository_id,
      cache_dir: cache_dir
    })
  {:ok, featurizer} =
    Bumblebee.load_featurizer({
      :hf,
      repository_id,
      cache_dir: cache_dir
    })
  Bumblebee.Vision.image_classification(model, featurizer)
end
"facebook/convnext-tiny-224"
|> serve_model.()
|> Nx.Serving.run(image)
|> then(&Kino.DataTable.new(&1.predictions))
"google/vit-base-patch16-224"
|> serve_model.()
|> Nx.Serving.run(image)
|> then(&Kino.DataTable.new(&1.predictions))
"facebook/deit-base-distilled-patch16-224"
|> serve_model.()
|> Nx.Serving.run(image)
|> then(&Kino.DataTable.new(&1.predictions))
モデルによって推論結果が異なります
まとめ
画像分類モデルが簡単に実行できますね!
YOLO は後処理が面倒すぎるので、まだ Bumblebee には入らなさそう









