32
39

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

[Node.js]とりあえず使ってみよう ~画像認識を基にした画像検索をアプリケーションを作ってみよう~

Last updated at Posted at 2018-05-25

前段

こんにちは、とりあえず使って見てからではないと。。勉強進まない。と思っている僕です。
何事もまずは使って見てからと考えてます。
その僕がNodeJsを始めるにあたって参考にした記事とそのコードをこちらに書かせていただきます。
参考にさせていただきました皆様ありがとうございました。

僕のNodejsを学ぶ目的

普段はPythonが大好きです、
好きです。
ただ、普段使いはFlaskでしたが、世界にはどんな技術があるのか知りたくなったので、以前ちょっとだけ触ったことのあるNode.jsに再度手をつけて見ました。
ですので、あくまで目標はPythonのアプリケーションをNodejsと連携しながら見せ方を作っていくということです。
自分の好きなことを織り交ぜながら勉強を進めていくとモチベが上がりますよね!

Node.JS??

こちらなどをご覧ください!
https://qiita.com/hshimo/items/1ecb7ed1b567aacbe559

Hello World

var sys = require('util');
var http = require('http');

var server = http.createServer(
	function (request, response) {
		response.writeHead(200, {'Content-Type':'text/plain'});
		response.write('Hello World!');
		response.end();
	}).listen(3000);
	sys.log('Server running at http://localhost:3000/');
$ node filename.js

で動きます。
あとはlocalhost:3000にアクセスして見てください!

Node.js with Express

もうちょっと高度なことをしたくなりました。
まあ、hello worldだけではあまりnodejsの面白さがわからなかったのです。。
ということで、実際のウェブアプリケーションのごとく、俊足でRestAPIを作ってみようと思いました。
https://qiita.com/etet-etet/items/1c65b934dbe7fc33490b

$ npm install -g express
$ npm install -g express-generator

この二つを入れることにより、エイリアスを自動で作ってくれます、
以降はなんと

$ express sampleapp

このコマンドだけで、Node.jsのプロジェクトセットアップを行えてしまいます。
Screen Shot 2018-05-25 at 19.49.47.png

では、まずは出来上がったこのディレクトリにあるapp.jsを叩いて見ましょう!

$ node app.js

そして、localhost:3000にアクセスをすると!
Screen Shot 2018-05-25 at 19.50.52.png

Done!!

ただ、これができてもあまり嬉しくはないので、このスクリプト等の説明は先ほどのURLに任せますが、僕はなんとかPythonを叩かないと、寂しさのあまり飛び出しそうになったので、ここから無理やりPythonを叩く方向へと進みたいと思います。

ライブラリ:python-shell

まだ生まれたてのnode.js研修生の僕なりに調べてたどり着いた答えがこれでした。
https://github.com/extrabacon/python-shell
このライブラリを使って、Node.jsとPythonをうまく連携させていきたいと思います。
では、早速先ほど作ったsampleプロジェクトのフォルダーの中にpythonというフォルダーを追加して見ましょう!その上で、その中に下記のpython様を置いておいてください。
ここに対して、Nodejsからアクセスをして、データの相互連携をしてみます。

import sys

def hello():
	message = sys.stdin.readlines()
	return message[0]

if __name__ == '__main__':
	print(hello())

めっちゃ簡単ですね、
しかし、ここでミソなのが、nodejsからくるデータの受け取り方です。
結構はまりました。
解決策はこちらのサイトでした、
https://www.sohamkamani.com/blog/2015/08/21/python-nodejs-comm/
記事中部のこの辺りです!
Screen Shot 2018-05-25 at 19.56.44.png
これもできないのにPython好きとかいうなし。
という言葉はさておきまして、、

Nodejs -> python
はstdinを使用して、データが送信されてきます。
ですので、python側でもそれを受け取る準備をしました!
さて、本題のnodejsのスクリプトについては下記のようです。

// node_py.js
var {PythonShell} = require('python-shell');
var pyshell = new PythonShell('./python/my_script.py');

// sends a message to the Python script via stdin
pyshell.send('Hello World!');

pyshell.on('message', function (message) {
  // received a message sent from the Python script (a simple "print" statement)
  console.log(message);
});

// end the input stream and allow the process to exit
pyshell.end(function (err,code,signal) {
  if (err) throw err;
  console.log('The exit code was: ' + code);
  console.log('The exit signal was: ' + signal);
  console.log('finished');
  console.log('finished');
});

さあ、準備ができました!

$ node node_py.js

# output
Hello World!

The exit code was: 0
The exit signal was: null
finished
finished

これで完璧ですね!
NodejsとPythonは仲がいいようでした!

実は、、、

僕のしたかったことは、実はPythonで作成をしたmnistデータを使用したCNNモデルをnode.jsと連携をさせたかったのです。
ですので、こっからが勝負ですね!
さて、まずは、Nodejsでのviewの操作についてです。
大きくメインどころは二つの種類のテンプレートが用意されてます。

jade vs ejs

https://www.quora.com/Whats-the-best-view-engine-for-node-js-ejs-jade-or-handlebars
詳しくはこちらをご覧ください!
お好きな方を!

僕はHTMLが好きなので、ejsを選びました。

var createError = require('http-errors');
var express = require('express');
var path = require('path');
var cookieParser = require('cookie-parser');
var logger = require('morgan');

var indexRouter = require('./routes/index');
var usersRouter = require('./routes/users');

var app = express();

// show image files
app.use(express.static('images'));

// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');

app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));

app.use('/', indexRouter);
app.use('/users', usersRouter);

// catch 404 and forward to error handler
app.use(function(req, res, next) {
  next(createError(404));
});

// error handler
app.use(function(err, req, res, next) {
  // set locals, only providing error in development
  res.locals.message = err.message;
  res.locals.error = req.app.get('env') === 'development' ? err : {};

  // render the error page
  res.status(err.status || 500);
  res.render('error');
});

module.exports = app;

index.ejsという名前のファイルをviewsフォルダーの配下に設置をしましょう!

<html>
<body>
    <h1>New Project</h1>
</body>
</html>

その上で、再度nodejsを実行してもらえれば下記のような画面が出てきます!
成功ですね!
Screen Shot 2018-05-25 at 20.21.14.png

さて、これでHTMLを作っていく下準備ができました。

HTML on Node server

<html>
<body>
    <h1>New Project</h1>
    <input type="text" class="mytext" required autofocus autocomplete='false'/>
</body>
<script>
	$.ajax({
	url: "/anyurl",
	data: { value: $('.mytext').val() },
	method: "POST",
	success: (data) => {
		console.log(data)
		// outputs "SUCESSSSS"
	}
});
</script>
</html>

ノードラン!!

Screen Shot 2018-05-25 at 20.24.07.png テキストエリアをつけられました!

テキストをインプットとしてコンソールに吐き出してみよう!

次に先ほど作成したテキストエリアからインプとを受け付けられるようにしてみましょう!
ちょっと詰まったところがあるのでそこも書きます。

まずはスクリプト!

var createError = require('http-errors');
var express = require('express');
var path = require('path');
var cookieParser = require('cookie-parser');
var logger = require('morgan');
var bodyParser = require('body-parser');

var indexRouter = require('./routes/index');
var usersRouter = require('./routes/users');

var app = express();

// show image files
app.use(express.static('images'));

// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');

app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));

app.use('/', indexRouter);
app.use('/users', usersRouter);

// catch 404 and forward to error handler
app.use(function(req, res, next) {
  next(createError(404));
});

// error handler
app.use(function(err, req, res, next) {
  // set locals, only providing error in development
  res.locals.message = err.message;
  res.locals.error = req.app.get('env') === 'development' ? err : {};

  // render the error page
  res.status(err.status || 500);
  res.render('error');
});

// getting post request
app.use(express.bodyParser());
app.post('/', (req, res) => {
	console.log(res.body.data);
	// outputs input's value
	res.json("SUCESSSSS");
})

module.exports = app;
<html>
<body>
    <h1>New Project</h1>
<form action="/" method="GET">
    <input type="text" name="mytext" required />
    <input type="submit" value="Submit" />
</form>
</body>
</html>

ここでノードをラン!ですが、
エラーが出ます。

どうやら、もう古い書き方をしてしまっていたようです。
その箇所はbodyParser()の部分ですね、クライアントサイドからのインプットを処理する部分でまず、ポストされたリクエストをパースする部分のライブラリが古かったようです。
こちらを参照しました。
Error: Most middleware (like bodyParser) is no longer bundled with Express
https://stackoverflow.com/questions/25550819/error-most-middleware-like-bodyparser-is-no-longer-bundled-with-express

正しいスクリプト

var createError = require('http-errors');
var express = require('express');
var path = require('path');
var cookieParser = require('cookie-parser');
var logger = require('morgan');
var bodyParser = require('body-parser');

var indexRouter = require('./routes/index');
var usersRouter = require('./routes/users');

var app = express();

// show image files
app.use(express.static('images'));

// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');

app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));

app.use('/', indexRouter);
app.use('/users', usersRouter);

// catch 404 and forward to error handler
app.use(function(req, res, next) {
  next(createError(404));
});

// error handler
app.use(function(err, req, res, next) {
  // set locals, only providing error in development
  res.locals.message = err.message;
  res.locals.error = req.app.get('env') === 'development' ? err : {};

  // render the error page
  res.status(err.status || 500);
  res.render('error');
});

// getting post request
app.use(bodyParser.json());
app.post('/', (req, res) => {
	console.log(res.body.data);
	// outputs input's value
	res.json("SUCESSSSS");
})

module.exports = app;

出来上がり!
Screen Shot 2018-05-25 at 20.48.32.png

さあ、そろそろゴールが見えてまいりましたね!

やりたいこと整理

ここで一旦やりたいことを整理します。
kerasを使用して、作ったpythonのCNNのモデルがあります、
こいつは分類器ですが、逆に使えば欲しい画像を検索させることもできますよね?
先ほど作ったテキストエリアに1 ~ 9の数字を入れると、モデルが画像を探して、一枚それと思しきものを回答してくれる。
これを作りたかったのです。

ちょっと仕組みをきちんと説明しますと、、
そんな大きいものではなく、簡素なのですが下記になります。

  1. モデルをトレーニング・検証実施
  2. モデルに先ほどの検証データの一部を与えてそのラベルを再度予測させる
  3. 2の結果をもとに予測に使った画像とそのラベルをもとに public/images/ 配下に label名称.png などのように画像を保存する
  4. 3の結果 public/images/ 配下には 0.png から 9.pngの画像が揃う
  5. これをクライアント側でインプットとして入力された物毎にURIを変更して参照してもらう
    というのが僕の構想です!

モデル作成

今回はgoogleが最近出したサービス, google colabを使用したいと思います。
何を隠そう、無料でGPU k8が使えてしまうのです!
Azureだとあんなに課金を。。。。。おっと誰か来ました!

コードはこちらに共有いたしましたのでご覧ください!
https://drive.google.com/file/d/1Y5DiJ_xcMpK_MsLzEtd0yrYsoDFDtoyI/view?usp=sharing

特に詰まったのはデータのupload/download周りですね、今回はデータをアップすることはないのですが、
やはり作ったモデルをdownloadするときに一工夫必要でした!
ぜひご覧ください。
こちらを完了して、作ったモデルを含むディレクトリ構成が下記のようにセットをしました。

Screen Shot 2018-05-26 at 11.52.43.png 本来ならきちんとモデル等のフォルダーは分けたほうがいいかもしれません。 気にせず進みます。
# cnn_mnist.py
import keras
from keras.datasets import mnist
from keras.models import Sequential
from keras.layers import Dense, Dropout, Flatten
from keras.layers import Conv2D, MaxPooling2D
from keras.models import model_from_json
from keras import backend as K
import numpy as np
import matplotlib.pyplot as plt

batch_size = 128
num_classes = 10
epochs = 5

# input image dimensions
img_rows, img_cols = 28, 28

# the data, split between train and test sets
(x_train, y_train), (x_test, y_test) = mnist.load_data()

if K.image_data_format() == 'channels_first':
    x_train = x_train.reshape(x_train.shape[0], 1, img_rows, img_cols)
    x_test = x_test.reshape(x_test.shape[0], 1, img_rows, img_cols)
    input_shape = (1, img_rows, img_cols)
else:
    x_train = x_train.reshape(x_train.shape[0], img_rows, img_cols, 1)
    x_test = x_test.reshape(x_test.shape[0], img_rows, img_cols, 1)
    input_shape = (img_rows, img_cols, 1)

x_train = x_train.astype('float32')
x_test = x_test.astype('float32')
x_train /= 255
x_test /= 255
print('x_train shape:', x_train.shape)
print(x_train.shape[0], 'train samples')
print(x_test.shape[0], 'test samples')

# convert class vectors to binary class matrices
y_train = keras.utils.to_categorical(y_train, num_classes)
y_test = keras.utils.to_categorical(y_test, num_classes)

model = Sequential()
model.add(Conv2D(32, kernel_size=(3, 3),
                 activation='relu',
                 input_shape=input_shape))
model.add(Conv2D(64, (3, 3), activation='relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Dropout(0.25))
model.add(Flatten())
model.add(Dense(128, activation='relu'))
model.add(Dropout(0.5))
model.add(Dense(num_classes, activation='softmax'))

model.compile(loss=keras.losses.categorical_crossentropy,
              optimizer=keras.optimizers.Adadelta(),
              metrics=['accuracy'])

model.fit(x_train, y_train,
          batch_size=batch_size,
          epochs=epochs,
          verbose=1,
          validation_data=(x_test, y_test))
score = model.evaluate(x_test, y_test, verbose=0)

# save a model architecture
from keras.utils import plot_model
plot_model(model, to_file='model.png')

print('Test loss:', score[0])
print('Test accuracy:', score[1])

# serialize model to JSON
model_json = model.to_json()
with open("model.json", "w") as json_file:
    json_file.write(model_json)
# serialize weights to HDF5
model.save_weights("model.h5")
print("Saved model to disk")
# img_saver.py
import keras
from keras.datasets import mnist
from keras.models import model_from_json
from keras import backend as K
import numpy as np
import matplotlib.pyplot as plt

batch_size = 128
num_classes = 10
epochs = 5
index_range = 30

# input image dimensions
img_rows, img_cols = 28, 28

checker = True

# load json and create model
json_file = open('model.json', 'r')
loaded_model_json = json_file.read()
json_file.close()
loaded_model = model_from_json(loaded_model_json)
# load weights into new model
loaded_model.load_weights("model.h5")
print("Loaded model from disk")

while (checker):
  # the data, split between train and test sets
  (_, _), (x_test, y_test) = mnist.load_data()
  index = np.random.randint(len(x_test))
  x_test = x_test[index:index+index_range]
  y_test = y_test[index:index+index_range]

  if K.image_data_format() == 'channels_first':
      x_test = x_test.reshape(x_test.shape[0], 1, img_rows, img_cols)
      input_shape = (1, img_rows, img_cols)
  else:
      x_test = x_test.reshape(x_test.shape[0], img_rows, img_cols, 1)
      input_shape = (img_rows, img_cols, 1)

  x_test = x_test.astype('float32')
  x_test /= 255
  print(x_test.shape[0], 'test samples')
  
  integers = set()
  fig = plt.figure()

  for i in range(index_range):
    a = loaded_model.predict(x_test[i].reshape(1, 28, 28, 1))
    integers.add(np.argmax(a))
    plt.imshow(x_test[i].reshape(28, 28), cmap='gray')
    fig.savefig('../public/images/{}.png'.format(np.argmax(a)))
  
  if integers == set((0,1,2,3,4,5,6,7,8,9)):
    checker = False
    print("You've got all integers from 0 to 9!")

これでついに、Python側のモデル作成と、そのモデルをもとに、今回クライアントから受けとる数値に紐づく、予測された画像をHTML上で返すことための画像の生成ができます!!

ラスト!

長い間お付き合いいただきましてありがとうございました!
そろそろ終了のお時間です!
これまでの集大成を!!
。。。。
貴重な土曜日を無駄にしました。。
理由は簡単です、app.js内部でのroutingがうまくいかなくて、、調べても調べても回答に行き当たらず、過去に買った教科書を引っ張って来て探してました。
回答はいたって簡単、routingをする際にはapp.jsではなく、きちんとrouting先のjsファイルに記載をしなくてはならないということですね。
例えば、localhost:3000/ には基本的にindex.ejs(もし、view engineをejsにしていたらですよ!) が出てくるようにデフォで記載があります。

var indexRouter = require('./routes/index');
var app = express();
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');
app.use('/', indexRouter);

ここですね、こちらが言っているのは、現在いるdirectoryの配下にあるviewsを基本ディレクトリとして認識してくださいと、nodejsで作ったサーバーに対して宣言していることになります。色々と探してもらえればあるかとは思いますが、expressの古いバージョンではもっと明確に記述をしてありました。

app.get('/', function(req, res) {
         res.render('pages/index'});
    });

このような形です。ところが最近のでは、これがapp.jsへの依存性を減らす目的で、index.jsに記載をされるようになりました。
ですので、以降例えば、今回僕がしたいことのように、index.ejs上で作ったボタンを発火機材にサーバーに対してpythonのapiを叩くようにget requestを送ろうものなら、さっきの設定が当然app.jsにされているのでapi handling等に関しては全てindex.jsに記載をしないといけなかったのです。説明が不得手ですいません。。。
でも、無事見つけられて良かったです。

なので、記事冒頭に作ったmyscript.pyとの連携に関しては、

var bodyParser = require('body-parser');
var express = require('express');
var router = express.Router();

/* GET home page. */
router.get('/', function(req, res, next) {
  res.render('index', { title: 'Express' });
});

var runPython = function() {
	var {PythonShell} = require('python-shell');
	var pyshell = new PythonShell('./python/my_script.py');
	// sends a message to the Python script via stdin
	pyshell.send('Hello World!');

	pyshell.on('message', function (message) {
	  // received a message sent from the Python script (a simple "print" statement)
	  console.log(message);
	});

	// end the input stream and allow the process to exit
	pyshell.end(function (err,code,signal) {
	  if (err) throw err;
	  console.log('The exit code was: ' + code);
	  console.log('The exit signal was: ' + signal);
	});
}

// getting get request
router.use(bodyParser.json());
router.get('/a', (req, res) => {
	res.send('hello!')
	runPython()
});

module.exports = router;

のようにindex.jsを描いてください!

兎にも角にも少々の修正を経て、やっと終わりました!

Untitled.gif

python部分のもじゅーる:https://github.com/Rowing0914/simple_CNN_mnist
全体:https://github.com/Rowing0914/Integer_Image_Search

お付き合いいただきましてありがとうございました!

32
39
3

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
32
39

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?