LoginSignup
7
4

LiveViewとBumblebeeでサクッと画像分類Webアプリを作ってみる

Last updated at Posted at 2023-07-25

はじめに

本記事はPhoenix LiveViewとBumblebeeを使用して画像分類を行うWebアプリケーションを構築します

画像分類とは?

画像に何が写っているのかを分類を行う分類器を通して、判別します

スクリーンショット 2023-07-25 20.45.41.png

画像のどこに何が写っているかを判別するのは物体検知で別のものです

スクリーンショット 2023-07-25 20.47.05.png

Elixirで画像分類はどうやるのか?

色々出てきました

  • Bumblebee -> 学習済みネットワーク構築ライブラリ
  • AxonOnnx -> ONNX モデルコンバーター
  • Evision -> OpenCV Elixir Binding
  • Ortex -> ONNX Runtime Elixir Binding
  • EXGBoost -> XGBoost Elixir Binding

今回はBumblebeeを使います

Bumblebeeとは

スクリーンショット 2023-07-25 15.54.06.png

  • 学習済みの機械学習のモデルをAxonで動かすライブラリ
  • モデルと学習データをHuggingFaceからダウンロードしていい感じにモデルを構築してくれる
  • Nx.Serveを使用してElixirのGenServerのWorkerとして簡単にPhoenixに組み込める

アプリケーションの作成

では実際にBumblebeeをPhoenix Liveviewに組み込んで、サクッと画像分類Webアプリを作ってみます

プロジェクトの作成

アプリ名はclassification
DBは使わないので--no-ectoオプションをつけます

mix phx.new classification --no-ecto

ライブラリの追加

完了したら移動して以下のように3つのライブラリを追加します

  • stb_image -> 画像をデータに変換
  • exla -> 行列計算(Nx)を高速化
  • bumblebee -> 学習済みモデル構築
mix.exs
defmodule Classification.MixProject do
  use Mix.Project

  ...
  defp deps do
    [
      ...
      {:stb_image, "~> 0.6"},
      {:bumblebee, "~> 0.3.0"},
      {:exla, ">= 0.0.0"}
    ]
  end
end

Exlaの設定

ExlaをPhoenixで使用するために config.exsで以下の設定が必要です

config/config.exs
import Config

config :nx, default_backend: EXLA.Backend # 追加

初期ページの作成

最低限動かすにはmountを追加します

lib/classification_web/live/page_live.ex
defmodule ClassificationWeb.PageLive do
  use ClassificationWeb, :live_view

  @impl true
  def mount(_params, _session, socket) do
    socket
    |> then(&{:ok, &1})
  end
end

アクションサイドバーとファイルのドロップエリアを作っておきます

lib/classification_web/live/page_live.html.heex
<div class="flex h-screen">
  <div>
    Actions
  </div>
  <div class="w-[32em] h-1/2 mx-auto p-4 border-2 rounded-lg shadow-lg">
    <form id="upload-form" class="h-full">
      <div class="h-full">
        <label>
          <input
            type="file"
            class="opacity-0 h-1/2"
          />
          <h1 class="-mt-6 text-center text-4xl">
            Select or Drop File
          </h1>
        </label>
      </div>
    </form>
  </div>
</div>

ページができたらroutingに追加します

lib/classification_web/router.ex
defmodule ClassificationWeb.Router do
  use ClassificationWeb, :router
  ...
  scope "/", ClassificationWeb do
    pipe_through :browser
-   get "/", PageController, :home
+   live "/", PageLive
  end
  ...
end

これで初期ページができました

スクリーンショット 2023-07-25 21.27.38.png

フォームのアップロードイベントの追加

formにphx-changeイベントを追加して、handle_eventでフォームの変更を検知します

lib/classification_web/live/page_live.html.heex
<div class="flex h-screen">
  <div>
    Actions
  </div>
  <div class="w-[32em] h-1/2 mx-auto p-4 border-2 rounded-lg shadow-lg">
-    <form id="upload-form" class="h-full">
+    <form id="upload-form" class="h-full" phx-change="upload">
      <div class="h-full">
        <label>
          <input
            type="file"
            class="opacity-0 h-1/2"
          />
          <h1 class="-mt-6 text-center text-4xl">
            Select or Drop File
          </h1>
        </label>
      </div>
    </form>
  </div>
</div>
lib/classification_web/live/page_live.ex
defmodule ClassificationWeb.PageLive do
  use ClassificationWeb, :live_view

  @impl true
  def mount(_params, _session, socket) do
    socket
    |> then(&{:ok, &1})
  end

+  @impl true
+  def handle_event("upload", _params, socket) do
+    {:noreply, socket}
+  end
end

ファイルアップロード設定

画像ファイルのバイナリデータとファイル名を格納する変数を準備

  • アップロード設定
    • アクセス名を:imageに設定
    • accept -> 許可する画像形式に jpg jpeg pngを設定
    • progress -> アップロード時にhandle_progress/3を実行する
    • auto_upload -> submitを押さずにuploadを完了する
lib/classification_web/live/page_live.ex
defmodule ClassificationWeb.PageLive do
  use ClassificationWeb, :live_view

  @impl true
  def mount(_params, _session, socket) do
    socket
+    |> assign(:upload_file, nil)
+    |> assign(:filename, nil)
+    |> allow_upload(
+      :image,
+      accept: ~w(.jpg .jpeg .png),
+      progress: &handle_progress/3,
+      auto_upload: true
+    )
    |> then(&{:ok, &1})
  end

+  def handle_progress(:image, entry, socket)
+      when entry.done? == false,
+      do: {:noreply, socket}
 ...
end

handle_progressはuploadが完了しない場合は何もしないようにする

lib/classification_web/live/page_live.ex
defmodule ClassificationWeb.PageLive do
  use ClassificationWeb, :live_view

  @impl true
  def mount(_params, _session, socket) do
    ...
  end

+  def handle_progress(:image, entry, socket)
+      when entry.done? == false,
+      do: {:noreply, socket}
 ...
end

完了した場合は以下の処理を行う

  • バイナリデータを取り込んで、ファイルを破棄する
  • entriesなので先頭を取得
  • バイナリをassign
  • ファイル名をassign
lib/classification_web/live/page_live.ex
defmodule ClassificationWeb.PageLive do
  use ClassificationWeb, :live_view

  @impl true
  def mount(_params, _session, socket) do
    ...
  end

  def handle_progress(:image, entry, socket)
      when entry.done? == false,
      do: {:noreply, socket}

+  def handle_progress(:image, entry, socket) do
+    upload_file =
+      consume_uploaded_entries(socket, :image, fn %{path: path}, _entry -> File.read(path) end)
+      |> hd()
+
+    socket
+    |> assign(:upload_file, upload_file)
+    |> assign(:filename, entry.client_name)
+    |> then(&{:noreply, &1})
+  end
...
end

Drag and Dropで画像のアップロードを許可する

現在だとクリックしてファイルを選択しますがphx-drop-targetでdropを許可します

lib/classification_web/live/page_live.html.heex
<div class="flex h-screen">
  <div>
    Actions
  </div>
  <div class="w-[32em] h-1/2 mx-auto p-4 border-2 rounded-lg shadow-lg">
    <form id="upload-form" class="h-full" phx-change="upload">
-      <div class="h-full">
+      <div class="h-full" phx-drop-target={@uploads.image.ref}>
        <label>
-          <input
-            type="file"
-            class="opacity-0 h-1/2" />
+          <.live_file_input 
+            class="opacity-0 h-1/2" type="file"
+            upload={@uploads.image}
+          />
          <h1 class="-mt-6 text-center text-4xl">
            Select or Drop File
          </h1>
        </label>
      </div>
    </form>
  </div>
</div>

アップロードした画像を表示する

アップロードした画像は通常live_img_previewで表示できるのですが、consume_upload_entriesを実行してentryにアクセスできないので別の方法で表示します

画像データがある場合

  • フォームを非表示にする
  • 画像データをBase64にエンコードして表示
  • ファイル名を表示
lib/classification_web/live/page_live.ex
<div class="flex h-screen">
  <div>
    Actions
  </div>
-  <div class="w-[32em] h-1/2 mx-auto p-4 border-2 rounded-lg shadow-lg">
+  <div class={
+    "w-[32em] h-1/2 mx-auto p-4 border-2 rounded-lg shadow-lg"
+    <> if is_nil(@upload_file), do: "", else: " hidden"
+  }>
    <form id="upload-form" class="h-full" phx-change="upload">
      <div class="h-full" phx-drop-target={@uploads.image.ref}>
        <label>
          <.live_file_input 
            class="opacity-0 h-1/2" type="file"
            upload={@uploads.image}
          />
          <h1 class="-mt-6 text-center text-4xl">
            Select or Drop File
          </h1>
        </label>
      </div>
    </form>
  </div>
+  <%= if @upload_file do %>
+    <figure>
+      <img
+        alt=""
+        class="w-full ml-4"
+        src={"data:image/png;base64,#{Base.encode64(@upload_file)}"}
+      />
+      <figcaption>File Name:<%= @filename %></figcaption>
+    </figure>      
+  <% end %>
</div>

モデルのWorkerを作る

モデルの設計図と学習済みデータをダウンロードしてきて、モデルを構築します

モデルはMicrosoftのresnet-50を使用します
load_modelでモデルの設計図と学習済みデータを読み込みます
load_featurizerでPythonの画像のデータをモデルに流し込むための前処理と結果を使いやすい形に整形する後処理のコードをElixirのコードに変換します
Vision.image_classificationで分類器を作成します
オプションはそれぞれ以下を指定しています

  • 結果のトップ1のみを返す
  • 最大で10の分類のリクエストをまとめて行う
  • 行列演算の高速化にExlaを使う
lib/classification/worker.ex
defmodule Classification.Worker do
  def build_model() do
    {:ok, model_info} = Bumblebee.load_model({:hf, "microsoft/resnet-50"})
    {:ok, featurizer} = Bumblebee.load_featurizer({:hf, "microsoft/resnet-50"})

    Bumblebee.Vision.image_classification(model_info, featurizer,
      top_k: 1,
      compile: [batch_size: 10],
      defn_options: [compiler: EXLA]
    )
  end
end

Phoenix起動時にWorkerも起動するように設定

起動時にPhoenix以外のアプリケーションを一緒に起動する時は
application.exの起動するアプリ一覧のchildrenに追加します

BumblebeeやAxonのモデルをWorkerとして起動する場合はNx.Servingを使用します
servingは提供するモデル、nameはPhoenixから使う際のモジュール名、batch_timeはまとめて行う処理の受付時間をそれぞれ指定しています。

lib/classification/application.ex
defmodule Classification.Application do
  @moduledoc false

  use Application

  @impl true
  def start(_type, _args) do
    children = [
      # Start the Telemetry supervisor
      ClassificationWeb.Telemetry,
      # Start the PubSub system
      {Phoenix.PubSub, name: Classification.PubSub},
      # Start Finch
      {Finch, name: Classification.Finch},
      # Start the Endpoint (http/https)
-      ClassificationWeb.Endpoint
+      ClassificationWeb.Endpoint,
+      {Nx.Serving,
+       serving: Classification.Worker.build_model(),
+       name: Classification.Serving,
+       batch_timeout: 100}
    ]

    # See https://hexdocs.pm/elixir/Supervisor.html
    # for other strategies and supported options
    opts = [strategy: :one_for_one, name: Classification.Supervisor]
    Supervisor.start_link(children, opts)
  end
  ...
end

application.exを更新したら phoenixを再起動しましょう

LiveViewからWorkerを使用する

推論結果を表示する変数も追加します

lib/classification_web/live/page_live.ex
defmodule ClassificationWeb.PageLive do
  use ClassificationWeb, :live_view

  @impl true
  def mount(_params, _session, socket) do
    socket
    |> assign(:upload_file, nil)
    |> assign(:filename, nil)
+   |> assign(:ans, "")
+   |> assign(:score, "")
    |> allow_upload(
      :image,
      accept: ~w(.jpg .jpeg .png),
      progress: &handle_progress/3,
      auto_upload: true
    )
    |> then(&{:ok, &1})
  end
  ...
end
lib/classification_web/live/page_live.html.heex
<div class="flex h-screen">
  ...
  <%= if @upload_file do %>
    <figure>
      <img
        alt=""
        class="w-full ml-4"
        src={"data:image/png;base64,#{Base.encode64(@upload_file)}"}
      />
      <figcaption>File Name:<%= @filename %></figcaption>
+     <h1><%= "Anser:#{@ans}" %></h1>
+     <h1><%= "Score: #{@score}" %></h1>
    </figure>      
  <% end %>
</div>

最初に作ったサイドバーにボタンを追加します

lib/classification_web/live/page_live.html.heex
<div class="flex h-screen">
  <div>
    Actions
+    <div class="flex flex-col gap-2">
+      <button
+        class="rounded-lg bg-blue-500 px-5 py-2.5 text-center text-white"
+        phx-click="predict"
+      >Predict</button>
+      <button 
+        class="rounded-lg border bg-white px-5 py-2.5 text-center text-gray-700"
+        phx-click="clear"
+      >Clear</button>
+    </div>
  </div>
  ...
</div>

各イベントを実装します

predictは以下のようなことやっています

  • StbImage.read_binary!でバイナリをStbImage形式のデータに変換
  • StbImage.to_nxでStbImage形式のデータををNx形式のデータに変換
  • Nx.Serving.batched_runでApplicationで指定したServingにデータを渡して分類のリクエストに追加する
  • 分類結果をassign

clearはassignされた関連データを全て初期化しています

lib/classification_web/live/page_live.ex
defmodule ClassificationWeb.PageLive do
  use ClassificationWeb, :live_view

  ...
  @impl true
  def handle_event("upload", _params, socket) do
    {:noreply, socket}
  end

+  def handle_event("predict", _params, socket) do
+    tensor =
+      socket.assigns.upload_file
+      |> StbImage.read_binary!()
+      |> StbImage.to_nx()
+
+    %{predictions: [%{label: ans, score: score}]} =
+      Nx.Serving.batched_run(Classification.Serving, tensor)
+
+    socket
+    |> assign(:ans, ans)
+    |> assign(:score, score)
+    |> then(&{:noreply, &1})
+  end
+
+  def handle_event("clear", _params, socket) do
+    socket
+    |> assign(:upload_file, nil)
+    |> assign(:filename, "")
+    |> assign(:ans, "")
+    |> assign(:score, "")
+    |> then(&{:noreply, &1})
+  end

  ...
end

これで完成になります

実行結果

スクリーンショット 2023-07-25 23.30.24.png

アップロードした画像が bee eater(ハチクイ)という鳥と無事分類されました

最後に

いかがでしたでしょうか?
大半はLiveViewから画像をどう渡すかで、機械学習の部分は20行にも満たない小さなコードでした
Bumblebeeは他にも色々なモデルが使えるのでぜひいろいろ試してみてください
本記事は以上になりますありがとうございました

参考

https://github.com/elixir-nx/bumblebee
https://github.com/elixir-nx/axon_onnx
https://github.com/cocoa-xu/evision
https://github.com/elixir-nx/ortex
https://github.com/acalejos/exgboost
https://github.com/elixir-nx/stb_image
https://github.com/elixir-nx/nx/tree/main/exla
https://huggingface.co/
https://hexdocs.pm/phoenix_live_view/uploads.html#content

7
4
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
4