投稿日:2021年7月3日
この記事ではKerasに用意された訓練済のモデルEfficientNetをTensorflow.js用に出力し、それをNext.js + TypeScriptのアプリケーション上で読み込んで分類をする方法について詳しく解説しています。
この記事はPart1の続編で、Next.js + TypeScript + Tensorflow.jsで機械学習のWebアプリケーションを作成してみよう、のPart2です。
今回はKerasに用意された訓練済のモデルEfficientNetをTensorflow.js用に出力し、それをNext.js + TypeScriptのアプリケーション上で読み込んで分類をしてみましょう。
この記事の内容はNext.js、TypeScript、Tensorflow.jsになれることが目的であり、ベストプラクティスでは無いことに注意してください。
この記事では以下の環境を前提としています。
まずはtensorflow + Keras + JupyterNotebookの環境を用意します。今回はDocker、DockerComposeを使ってみることにします。
イメージはtensorflowのものを使用します。このイメージはJupyterNotebookをデフォルトでサポートしているので非常に便利です。
ではDockerCompose用の定義ファイルを作成しましょう!
version: "3"
services:
keras_app:
image: tensorflow/tensorflow:2.4.2-jupyter
volumes:
- ./keras_app/notebooks:/tf/notebooks
ports:
- 8888:8888
ここではホスト(ローカルPC)の./keras_app/notebooksというディレクトリをコンテナの/tf/notebooksというディレクトリにマウントしています。
コンテナの/tf/notebooksディレクトリはtensorflow/tensorflowのDockerイメージがJupyterNotebookのデフォルトのルートとして指定しているディレクトリです。つまり、そこにマウントすることでJupyterNotebook上で生成したファイルをホストの./keras_app/notebooksにそのまま同期できるというわけです。
では起動させてみましょう。
$ sudo docker-compose up -d
出力されるurlにアクセスすると以下の様にJupyter Notebookのページにアクセスできます。
では動作のテストをしてみます。
New > Python3
で新規ノートブックを開始できます。
ノートブックを開始できたら以下のコードをコピペして実行(Shift+ Enter)してみましょう。
import numpy as np
import tensorflow as tf
from tensorflow import keras
問題なく実行できていればKeras環境の構築は完了です!
機械学習ライブラリKerasには訓練済のニューラルネットワークモデルが用意されています。今回はこの中からEfficientNetを使ってみます。
from tensorflow.keras.applications import EfficientNetB0
# モデルをインスタンス化
model = EfficientNetB0(weights = "imagenet")
これだけでモデルの構築は完了です!
構造を見てみましょう。
構造を見るにはモデルインスタンスのsummary()メソッドを呼び出します。
model.summary()
Model: "efficientnetb0"
__________________________________________________________________________________________________
Layer (type) Output Shape Param # Connected to
==================================================================================================
input_5 (InputLayer) [(None, 224, 224, 3) 0
__________________________________________________________________________________________________
rescaling_3 (Rescaling) (None, 224, 224, 3) 0 input_5[0][0]
__________________________________________________________________________________________________
normalization_3 (Normalization) (None, 224, 224, 3) 7 rescaling_3[0][0]
[...]
top_dropout (Dropout) (None, 1280) 0 avg_pool[0][0]
__________________________________________________________________________________________________
predictions (Dense) (None, 1000) 1281000 top_dropout[0][0]
==================================================================================================
Total params: 5,330,571
Trainable params: 5,288,548
Non-trainable params: 42,023
これを見ると入力は(None, 224, 224, 3)、出力は(None, 1000)となっています。これは入力が224×224のRGB(3次元)画像、出力は1000次元のクラスタリングということを意味しています。
Noneは同時に読み込む入力が不定数であることを意味しています(同時に複数の枚数の画像を読み込める)。
pythonのKerasのモデルをそのままJavaScriptのTensorflowで読み込むことはできません。
modelをtensorflow.jsに読み込める形で生成しましょう。
tensorflow.js用のモデルの生成にはtensorflowjsというpythonパッケージが必要になります。
JupyterNotebookでは以下の様に!をつけることでNotebook上からシェルコマンドを実行できます。
今回はこれを使ってインストールしてみましょう。
!pip install tensorflowjs
ではこのパッケージを使ってモデルを生成してみましょう。
# pythonコードでjsモデルを出力
import tensorflowjs as tfjs
# tfjs_modelsディレクトリにモデルデータを出力
tfjs.converters.save_keras_model(model, "tfjs_models")
これでコンテナの/tf/notebooks/、そしてそこにマウントされているホストの./keras_app/notebooksにtfjs_modelsというディレクトリが作成され、そこにtensorflow.js用のモデルとパラメータ用データが生成されました。
ローカルPCのワーキングディレクトリを確認すると、以下のようなファイル群の生成が確認できると思います。
./keras_app/notebooks
└── tfjs_models
├── group1-shard10of25.bin
├── group1-shard11of25.bin
︙
├── group1-shard9of25.bin
└── model.json
ではいよいよWebアプリケーションの作成に入ります!
ここではNext.js+TypeScriptでアプリケーションを作成しましょう。
$ mkdir next_app
$ cd next_app
$ npm init
$ npx create-next-app --typescript app
次に、先程生成したtensorflow.js用に変換した訓練済EfficientNetモデルデータをブラウザ上で読み取らせる方法について考えましょう。方法はいくつかありますが、今回はサーバーから静的ファイルとして配信する方法でやってみましょう。
Next.jsで静的ファイルを配信するためにはpublicディレクトリに対象ファイルを配置します。
$ cp -r -f ./keras_app/notebooks/tfjs_models ./next_app/app/public
↑ここではコマンドで移動しましたが、普通に画面上からコピー&ペーストでOKです!
さあ、配信設定が問題なくできているか確認してみましょう。
$ npm run dev
この状態でブラウザからhttp://localhost:3000/tfjs_models/model.jsonにアクセスしてみてください。
以下のようにmodel.json内の内容が表示されましたか?
されていれば設定は完璧です!
次にブラウザ上でモデルを読み込みんでみます。
ブラウザ上でtensorflow.jsを実行するには@tensorflow/tfjsパッケージが必要になります。
まずはこれをインストールしましょう。
$ cd app
$ npm install --save @tensorflow/tfjs
では静的サーバーから訓練済EfficientNetのモデルを読み込んで見ましょう。
import * as tf from '@tensorflow/tfjs';
import { useState, useEffect, useRef} from "react";
export default function Home() {
// Modelをstateで定義
const [model, setModel] = useState<tf.LayersModel|null>(null);
// TODO:デバッグ情報は公開時には消去
// modelが読み込めたらコンソールに情報を出力
useEffect(()=>{
console.log(model);
},[model]);
// モデルは一度だけ読み込む
useEffect(()=>{
// モデルを静的サーバーから読み込む
tf.loadLayersModel('/tfjs_models/model.json').then(model => {
// stateにセット
setModel(model);
});
},[]);
return (
<div>
{model==null ? "読み込み中です…" : "準備完了しました…"}
</div>
)
}
このコンポーネントの中ではtensorflow.jsのモデルをmodelという名前でstateで定義しています。モデルの読み込みにはloadLayersModel()というメソッドを使います。このメソッドは非同期なので読み込みが終わったタイミングでthen()内のsetModel()が実行され、モデルがmodel変数にセットされます。
アプリケーションを実行してブラウザの開発者ツールなどで確認してみると下のようにモデルの内容が確認できると思います。
問題なくモデルの読み込みまで完了できました!
では最後にカメラの入力を受け取り、先程読み込んだ訓練済モデルでの推論を自動的に行うコンポーネントを作成してみましょう。
import * as tf from '@tensorflow/tfjs';
import { useState, useEffect, useRef } from "react";
export default function Home() {
// [ ref ] ビデオを参照するRef
const videoRef = useRef<HTMLVideoElement>(null);
// [ state ] 訓練済NNモデル
const [model, setModel] = useState<tf.LayersModel|null>();
// [ state ] 推論の結果
const [predictionResult, setPredictionResult] = useState<number>(0);
// モデルは初回一度だけ読み込む
useEffect(()=>{
// モデルを静的サーバーから読み込む
tf.loadLayersModel('/tfjs_models/model.json').then(model => {
// stateにセット
setModel(model);
});
},[]);
/**
* ビデオの読み込み、モデルの読み込みが完了したら推論スタート
*/
useEffect(()=>{
if(videoRef.current == null || model == null){
return;
}
// <video>の参照を取得
let video = videoRef.current;
// <video>にカメラのストリームを入力
getVideo(video).then(video => {
//<video>への入力が完了したら推論
video.onloadeddata = () => {
// 初回に1回だけ推論
execEstimate(video, model);
// 推論を1秒ごとにスケーリング
setInterval(() => {
execEstimate(video, model);
},1000);
}
});
},[model, videoRef]);
/**
* カメラの入力をvideoに流し込む
* 今回のNNモデルの入力は224 * 224 * 3なのでそれに合わせる
*/
const getVideo = (video: HTMLVideoElement):Promise<HTMLVideoElement> => {
console.log("getVideo");
return navigator.mediaDevices
.getUserMedia({ video: { width: 224, height: 224 } })
.then(stream => {
video.srcObject = stream;
video.play();
return video;
})
};
/**
* 推論の実行
* @returns
*/
const execEstimate = (video:HTMLVideoElement, model:tf.LayersModel): void => {
// <video>に出力されている画像をTensorとして取得
let input = tf.browser.fromPixels(video);
// Batch=1(1枚の画像のみ)の入力を生成
let inputReshaped = input.reshape([1,224,224,3]);
// モデルで推論
let prediction = model.predict(inputReshaped) as tf.Tensor<tf.Rank>;
// 出力は0~1の値なので、0~100の値に変更
let predictionHundredFold = prediction.dataSync<"float32">().map((value, index, array) => {
return value * 100;
});
let predictionArgMax = tf.argMax(predictionHundredFold).dataSync<"int32">();
setPredictionResult(predictionArgMax[0]);
}
return (
<div>
<video ref={videoRef}></video>
{model==null ? "読み込み中です…" : `推測ラベル:${predictionResult}`}
</div>
)
}
ここでは新しくvideoRefという<video>へのrefと、訓練済NNモデルで推論した結果を値として保持するpredictionResultというstateを定義しました。
<video>への参照をrefとして定義することで、この変数越しに好きなタイミングで<video>の値を読み取ることができます。
predictionResultはmodelの出力をそのまま保持するのではなく、modelの出力配列のうち最も値の大きなクラスのインデックス(0~999)を保持します。
<video>はもともと動画を埋め込むためのタグで、それ自身にカメラの入力を受け取る機能が実装されているわけではありません。そこでgetVideo()でカメラの入力を受け取り、それをそのまま<video>に流す処理が実装してあります。
execEstimate()がモデルから推論を行う関数です。ここについて、詳しく解説すると長くなるので、今回は
『へ〜。こう書くとなんか推論できるんだ〜。』
程度に考えてください。
開発用サーバーを起動して実行するとカメラが起動して推論がスタートします。
試しにバナナを見せてみました。
動いた!!
バナナは818あたりに分類されていそうですね!
※もちろん、実際にこのモデルの訓練データにバナナが含まれているかどうかはわかりません。訓練済モデルは一般的にそのまま使うことはなく、途中の層のパラメータや転移学習をしてからの分類が一般的です。
※今回のアプリケーションをそのままサーバーにデプロイして動かすと、訓練済モデルのダウンロードに毎回100MBくらいダウンロードしてしまいます。当然そのようなWebアプリは適切な設計ではありません。