はじめに
本記事はPhoenix LiveViewとBumblebeeを使用して画像分類を行うWebアプリケーションを構築します
画像分類とは?
画像に何が写っているのかを分類を行う分類器を通して、判別します
画像のどこに何が写っているかを判別するのは物体検知で別のものです
Elixirで画像分類はどうやるのか?
色々出てきました
- Bumblebee -> 学習済みネットワーク構築ライブラリ
- AxonOnnx -> ONNX モデルコンバーター
- Evision -> OpenCV Elixir Binding
- Ortex -> ONNX Runtime Elixir Binding
- EXGBoost -> XGBoost Elixir Binding
今回はBumblebeeを使います
Bumblebeeとは
- 学習済みの機械学習のモデルをAxonで動かすライブラリ
- モデルと学習データをHuggingFaceからダウンロードしていい感じにモデルを構築してくれる
- Nx.Serveを使用してElixirのGenServerのWorkerとして簡単にPhoenixに組み込める
アプリケーションの作成
では実際にBumblebeeをPhoenix Liveviewに組み込んで、サクッと画像分類Webアプリを作ってみます
プロジェクトの作成
アプリ名はclassification
DBは使わないので--no-ecto
オプションをつけます
mix phx.new classification --no-ecto
ライブラリの追加
完了したら移動して以下のように3つのライブラリを追加します
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で以下の設定が必要です
import Config
config :nx, default_backend: EXLA.Backend # 追加
初期ページの作成
最低限動かすにはmountを追加します
defmodule ClassificationWeb.PageLive do
use ClassificationWeb, :live_view
@impl true
def mount(_params, _session, socket) do
socket
|> then(&{:ok, &1})
end
end
アクションサイドバーとファイルのドロップエリアを作っておきます
<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に追加します
defmodule ClassificationWeb.Router do
use ClassificationWeb, :router
...
scope "/", ClassificationWeb do
pipe_through :browser
- get "/", PageController, :home
+ live "/", PageLive
end
...
end
これで初期ページができました
フォームのアップロードイベントの追加
formにphx-changeイベントを追加して、handle_eventでフォームの変更を検知します
<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>
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を完了する
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が完了しない場合は何もしないようにする
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
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を許可します
<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にエンコードして表示
- ファイル名を表示
<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を使う
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はまとめて行う処理の受付時間をそれぞれ指定しています。
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を使用する
推論結果を表示する変数も追加します
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
<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>
最初に作ったサイドバーにボタンを追加します
<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された関連データを全て初期化しています
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
これで完成になります
実行結果
アップロードした画像が 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