※このブログではサーバー運用、技術の検証等の費用のため広告をいれています。
記事が見づらいなどの問題がありましたらContactからお知らせください。

<前のページ
【Next.js+Tensorflow+TypeScript】機械学習Webアプリの作成|Part1

【Next.js+Tensorflow+TypeScript】機械学習Webアプリの作成|Part2

react web開発 フロント開発 機械学習 TypeScript next.js

投稿日: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になれることが目的であり、ベストプラクティスでは無いことに注意してください。

環境について

この記事では以下の環境を前提としています。

  • node v10.19.0
  • npm v6.14.4
  • Docker version 19.03.13
  • DockerCompose version 1.25.5
  • bashターミナル

参考


実践

Kerasの環境を構築

まずはtensorflow + Keras + JupyterNotebookの環境を用意します。今回はDockerDockerComposeを使ってみることにします。
イメージはtensorflowのものを使用します。このイメージはJupyterNotebookをデフォルトでサポートしているので非常に便利です。

ではDockerCompose用の定義ファイルを作成しましょう!

./docker-compose.yaml
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のページにアクセスできます。

▲Jupyter Notebookの画面

では動作のテストをしてみます。

New > Python3

で新規ノートブックを開始できます。

ノートブックを開始できたら以下のコードをコピペして実行(Shift+ Enter)してみましょう。

ターミナル
import numpy as np
import tensorflow as tf
from tensorflow import keras

問題なく実行できていればKeras環境の構築は完了です!

訓練済NNモデルの出力

機械学習ライブラリ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/notebookstfjs_modelsというディレクトリが作成され、そこにtensorflow.js用のモデルとパラメータ用データが生成されました。

ローカルPCのワーキングディレクトリを確認すると、以下のようなファイル群の生成が確認できると思います。

./keras_app/notebooks
└── tfjs_models
    ├── group1-shard10of25.bin
    ├── group1-shard11of25.bin
    ︙
    ├── group1-shard9of25.bin
    └── model.json

ではいよいよWebアプリケーションの作成に入ります!

Next.jsアプリケーションの作成

ここでは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のモデルを読み込んで見ましょう。

./next_app/pages/index.tsx
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変数にセットされます。

アプリケーションを実行してブラウザの開発者ツールなどで確認してみると下のようにモデルの内容が確認できると思います。

▲コンソールにモデルの概要が出力される

問題なくモデルの読み込みまで完了できました!

カメラを入力に推論を実行

では最後にカメラの入力を受け取り、先程読み込んだ訓練済モデルでの推論を自動的に行うコンポーネントを作成してみましょう。

./next_app/pages/index.tsx
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アプリは適切な設計ではありません。

このエントリーをはてなブックマークに追加

<前のページ
【Next.js+Tensorflow+TypeScript】機械学習Webアプリの作成|Part1

関連記事

記事へのコメント