アプリとサービスのすすめ

アプリやIT系のサービスを中心に書いていきます。たまに副業やビジネス関係の情報なども気ままにつづります

Node.jsでサーバを構築し、Tensorflow.jsでCNNの画像分類してみた【機械学習・Javascript】

今回はgoogle colaboratory(colab)で訓練したCNNの訓練済みモデルとtensorflow.jsを使い、Node.jsのサーバ上で画像分類してみた。

Node.jsを使ってjavascript用のサーバを立てたり、javascriptを使ったりと学ぶことが多かった。colab上で訓練→javascript上でmodelの読み込み→予測結果表示までの過程を備忘録もかねてまとめた。


目次
・この記事の概要

1.CNNで訓練・モデルの保存

2.tensorflowjs_converterで保存したモデルの変換

3.Node.jsで立てたサーバ上でモデルの読み込み

4.アップロードした画像を予測し、正解率を表示


この記事の概要


やったことを簡単にまとめると、まず「adidas, nike, new balance, vans」の4つのスニーカーを判別する画像分類モデルをCNNで作った。

自分のPCのスペックがカスなので、google colaboratoryを使って訓練。保存したモデル(h5ファイル)をtensorflowjs_converterでjavascript上で使えるように変換。


Node.jsでサーバを構築し、javascript用のサーバ上で変換した学習したモデルを読み込み、ローカルからアップロードした画像を予測し、各正解率を表示する。
その作業過程の中でまとめたいこと、知識などを時事系列でまとめてく。

ちなみにNode.jsのサーバ上にアクセスした時の画面はこんな感じ。
f:id:trafalbad:20190514234945p:plain



1.CNNで訓練・モデルの保存

自分のPCのスペックが心配なので、CNNの訓練はGPUが無料で使える神ツール Google Colaboratory(以下’colab’) を使った。

colabの使い方記事はこちらの記事を参考にした。


訓練に使ったpythonコード

tf.enable_resource_variables()

model = models.Sequential()
model.add(layers.Conv2D(32, (3, 3), activation='relu',
                        input_shape=(100, 100, 3)))
model.add(layers.MaxPooling2D((2, 2)))
model.add(layers.Conv2D(64, (3, 3), activation='relu'))
model.add(layers.MaxPooling2D((2, 2)))
model.add(layers.Conv2D(128, (3, 3), activation='relu'))
model.add(layers.MaxPooling2D((2, 2)))
model.add(layers.Conv2D(128, (3, 3), activation='relu'))
model.add(layers.MaxPooling2D((2, 2)))
model.add(layers.Flatten())
model.add(layers.Dropout(0.5))
model.add(layers.Dense(512, activation='relu'))
model.add(layers.Dense(4, activation='softmax'))

model.compile(loss='categorical_crossentropy',
              optimizer='Adam',
              metrics=['acc'])

# fit_generatorで画像を増幅
train_datagen = ImageDataGenerator(
    rescale=1./255,
    rotation_range=40,
    width_shift_range=0.2,
    height_shift_range=0.2,
    shear_range=0.2,
    zoom_range=0.2,
    horizontal_flip=True,)

history = History()
callback=[]
callback.append(history)
callback.append(ModelCheckpoint(filepath='/Users/downloads/tfjs/ep{epoch:02d}model.h5', verbose=0, save_best_only=False,
                save_weights_only=False, mode='auto', period=1))


train_generator = train_datagen.flow(image, labels)

init = tf.global_variables_initializer()
sess = tf.InteractiveSession()
sess.run(init)
history = model.fit_generator(train_generator,  
                                  steps_per_epoch=100, callbacks=callback, epochs=50)

keras.CallbackのModelCheckpointメソッドでepochごとに拡張子がh5のファイルを保存する。colabではモデルの訓練・保存のみ行った。

tensorflowjsでのモデル変換はローカルで仮想環境を構築してやった。



2.tensorflowjs_converterで保存したモデルの変換

作業してるとtensorflowとtensorflowjs(以下’tfjs’)のバージョンが合わないとダメとか書いてある記事が多かった。

しかし、今回はtensorflowとtfjs共に、最新versionでモデル変換できた。
ちなみにtfjsを使うには、tfjsセットアップ用サイトでscriptタグのリンクをコピペしてhtmlに貼り付けるだけ。


ググってると、

・訓練モデルをsaved_model形式でpbファイルに保存
javascript上で読み込めるようにtensorflowjs_converterで変換
javascript上ではtf.loadGraphModel()で読み込む

やり方をしてる記事が結構多かった。

自分はそのやり方だと、エラーでつまづいた。結局、次のやり方でやった。

・普通にCNNで訓練してh5ファイルを保存
・tensorflowjs_converterで変換
javascript上ではtf.loadLayersModel()で読み込む



まず、jupyterで仮想環境を作り、tfjsとかをinstall。

$ pip install --upgrade pip
$ pip install tensorflow keras tensorflowjs


その後tfjsのtensorflowjs_converterを使い、以下のコマンドでh5ファイルを変換。

$ tensorflowjs_converter --input_format keras --output_format tfjs_layers_model ep02model.h5 web_model/

input_formatはkeras、 output_formatはtfjs_layers_model。

変換後はh5ファイルがバイナリファイル(group1-shard1of2.bin)とJSONファイル(model.json)に変換される。

f:id:trafalbad:20190514235124p:plain




3.Node.jsで立てたサーバ上でモデルの読み込み

javascriptの作業環境としてはgoogle chromeが人気だけど、モデルの読み込みとかは、エラーが発生し、かなり面倒。

変換モデルを読み込むためには、Google chrome用に「web server for chrome」でサーバを立ててやる必要があったり、拡張機能を追加しなければならなかった。(モデルを読み込む際の拡張機能についてはこのサイトの動画に詳しく載ってる)



結局、Node.jsでローカルにサーバ立てて、safarijavascriptの動作確認をした。

Node.jsはサーバサイドで動くJavaScriptのことで、Javascript用のサーバを構築できる。
Node.jsはNode.jsのサイトからダウンロード。そうすれば、npmコマンドが使えるようになる。



後はサーバ用のファイル(server.js)を用意して、Node.js用パッケージをダウンロードし、サーバを起動。

$ npm install
$ npm start

起動後のサーバには「http://localhost:8080」でアクセスできる。


server.js

let express = require("express");
let app = express();

app.use(express.static("./static"));

app.listen(process.env.PORT || 8080, function(){
    console.log("Serving at 8080")
});


そして、サーバ上ではtf.loadLayersModel()でさっきのJSONファイル(model.json)を読み込んだ。

let model;
async function loadModel() {
	model=await tf.loadLayersModel(`http://localhost:8080/web_model/model.json`);
	document.getElementById('predict-button').classList.remove('is-loading');
};


4.アップロードした画像を予測し、正解率を表示

サーバにアクセスして画像をアップロードしたときの画面はこんな感じ
f:id:trafalbad:20190514235232p:plain

今回は、コード内でtf.tidy()を使い、tfjsで使うGPUの不要なメモリを掃除してる。

まずローカルのnikeのスニーカー画像をアップロード。htmlではcanvasタグに表示

<!--画面表示領域-->
<canvas id="canvas" width="100" height="100" style="border: 2px solid;"></canvas>

<!--アップロードボタン-->
<label for="image"><b>画像</b></label>
<input type="file" accept="image/*" id="getfile">
var file = document.querySelector('#getfile');
const inputWidth = inputHeight = 100;

document.getElementById('getfile').onchange = function(e) {
  var img = new Image();
  img.onload = draw;
  img.src = URL.createObjectURL(this.files[0]);};

function draw() {
  var canvas = document.getElementById('canvas');
  canvas.width = this.width;
  canvas.height = this.height;
  var ctx = canvas.getContext('2d');
  ctx.drawImage(this, 0,0);}


・「Start」ボタンでモデルの読み込み

$("#start-button").click(function(){
	loadModel();
});


・「Predict」ボタンで予測し、各正解率を表示。

function getAccuracyScores() {
	var score = tf.tidy(() => {
		// resize
		const tmpcanvas = document.getElementById('canvas').getContext('2d');
    var image = tmpcanvas.getImageData(0, 0, inputWidth, inputHeight)
		var tensor = tf.browser.fromPixels(image).resizeNearestNeighbor([inputWidth, inputHeight]).toFloat();
		var offset = tf.scalar(255);
		var tensor_iamge = tensor.div(offset).expandDims();
		return model.predict(tensor_iamge);
	  });

	return score;
}

function prediction() {
	var accuracyScores = getAccuracyScores();
	const accuraylists = accuracyScores.data();
	var index = 0
	accuraylists.then(function(e){
		const elements = document.querySelectorAll(".accuracy");
		elements.forEach(el => {
    el.parentNode.classList.remove('is-selected');
    const rowIndex = Number(el.dataset.rowIndex);
    if (index===rowIndex){
			el.parentNode.classList.add('is-selected');
		}
		el.innerText = e[index];
		index++;
	  });
	});
}

・他にも、アップロード前の画面にリセットする「Resetボタン」、画像を左右に90度回転させるボタンも設置



サーバにアクセスしてから、予測して正解率を表示するまではこんな感じ。
f:id:trafalbad:20190515003014g:plain



完成までかなり大変だったけど、colab、Javascript、Node.js、tensorflowjsとか、かなり得るものが多かった。

今回の記事は、上のスキルがペーペーの人間がNode.jsのサーバ上で画像分類できるまでを備忘録を兼ねて書いた。



参考記事
・画像アップロードブラウザでローカル画像をリサイズしてアップロード

・画像のresetClearing an HTML file upload field via JavaScript