ラーメン二郎を識別する人工知能の中身

  • 27
    いいね
  • 0
    コメント

この記事は

  • 前にラーメン二郎を識別するチャットボットを作ったのですが、そのバックエンドではディープラーニングで画像分類をしています
  • 具体的には、Tensorflow上で動く画像識別モデルInception-v3を使って、独自カテゴリ約1300種類の画像を分類しています
  • 結構はまりどころが多く、動くところまで持っていくのはそれなりに面倒だったので、手順を残しておきます

Tensorflowでの画像認識

Inception-v3?

GoogleのディープラーニングフレームワークTensorflowのチュートリアルではいくつかの画像認識のサンプルが出てきます

ただ上記はあくまでお勉強用のサンプルというレベルで、カテゴリ数が増えたりするとまともな精度は出ません。じゃあお勉強用じゃないモデルが公開されてないかというといろいろされてます。その中の1つがInception-v3です。

どんなモデル?

とにかく下図のように深い深いCNNです。もはや曼荼羅。宇宙の神秘を感じます。

inception-v3

ちなみにImageNetの画像識別で93.9%(Top-5)の精度を誇ります。

実装は公開されてる?

  • Google自らが普通にgithubで公開しています
  • ちなみにこっちにはInception-v4とかInception-ResNet-v2とか更に後発で高精度なモデルも公開されていますが、どうもリファレンス実装ぽい印象で、上記のリポジトリのInception-v3実装の方が実践的みたいです
  • たとえばディストーションという学習データそのものにノイズを入れたりクリッピングをしたりして精度を上げる手法があるのですが、そういう部分は後者の実装には含まれてないような感じです

大まかな手順

今回の画像認識は大まかに下記の順序でおこないます。

  1. 分類したいカテゴリを決める
  2. 学習画像を入手する
  3. モデルをトレーニングする
  4. モデルの精度を評価する
  5. モデルを使ってコマンドで画像を分類する
  6. モデルを使ってWebアプリで画像を分類する

ちなみに世に公開されているディープラーニング系の実装でありがちなのは4までしか対応していないというパターンです。
このInception-v3では一応5までは提供されてはいるのですが、undocumentedな情報がたくさん必要で、実際に動かすところまではかなり時間を要しました。
実装を公開してくれているような本気の研究者からすると、精度の数値が大事で実際の分類処理なんてたいして興味がないんでしょうけどね。。

分類したいカテゴリを決める

今回のテーマは料理の分類です。
で、料理のカテゴリを定義する必要があるんですが、これが結構大変でして。。

初めはUEC FOOD 256というモデルのカテゴリを参考にしていたのですが、このデータセット後半になればなるほど超絶マニアックなアジア料理ばかりになっていき、もはや日本語翻訳不可なレベルになってきます。

なので、和・洋・中・エスニック 世界の料理がわかる辞典だとかの複数の料理名が公開されているようなサイトを参考にして手作業で作っていきました。名寄せみたいな作業も発生するのでとにかく地道です。

で、カテゴリを決めたら、1カテゴリ1行のテキストファイルを作成しておいてください。あとで使います。
今回でいうと下記のような料理名が列挙されたファイルになります。
冒頭は「-」にしてください。これはゼロ行目を作ってるようでとりあえずおまじないだと思ってください。

food_name.txt
-
チョコレート
クッキー
煮物
缶詰
丼物
梅干し
鍋物
キムチ
茹で卵
...

学習画像を入手する

上記で決めた分類カテゴリに合わせた学習画像を入手します。
1カテゴリあたり最低でも数十枚、できれば数百枚必要です。

収集した画像はディレクトリに分けて保管しましょう。ディレクトリ名は数字連番でいいです。
その時の数字は、上記で作成したカテゴリで何行目だったのかと合わせてください。ただし「-」はゼロ行目です。
たとえばチョコレートは1ディレクトリに、茹で卵は9ディレクトリにいれましょう。

ちなみにどうやってこんなに大量の画像を集めるのでしょう?
・・・人手で1枚1枚集めたということにさせてください。
(ヒント:エンドツーエンドテストツール)

モデルをトレーニングする

トレーニングをします。現実的な分量のトレーニングにはNVIDIAのGPUマシンが必須です。
CPUでは1年たっても終わりません。

GPUマシンを入手したら、Tensorflow/Inception-v3をセットアップしていきます。

バージョン等

  • Ubuntu14
  • Python2.7
  • Tensorflow1.0(GPU)
  • cuda8.0/ cudnn5

セットアップ

Python/Tensorflow

  • PythonやTensorflowは適当にいれてください
  • Python2.7とpipがOSのパッケージマネージャで入るならそれが一番簡単
sudo apt-get -y update
sudo apt-get -y upgrade
sudo apt-get -y install build-essential git python-pip libfreetype6-dev libxft-dev libncurses-dev libopenblas-dev gfortran python-matplotlib libblas-dev liblapack-dev libatlas-base-dev python-dev python-pydot linux-headers-generic linux-image-extra-virtual unzip python-numpy swig python-pandas python-sklearn unzip wget pkg-config zip g++ zlib1g-dev libcurl3-dev
sudo pip install -U pip
sudo apt-get -y install software-properties-common python-software-properties

pip install --upgrade tensorflow-gpu

bazel

  • bazelというGoogle謹製のビルドツールも必要です
  • Java依存なのでJavaも適当に入れます
sudo add-apt-repository -y ppa:webupd8team/java
sudo apt-get -y update
echo debconf shared/accepted-oracle-license-v1-1 select true | sudo debconf-set-selections
echo debconf shared/accepted-oracle-license-v1-1 seen true | sudo debconf-set-selections
sudo apt-get -y install -y oracle-java8-installer

echo "deb [arch=amd64] http://storage.googleapis.com/bazel-apt stable jdk1.8" | sudo tee /etc/apt/sources.list.d/bazel.list
curl https://storage.googleapis.com/bazel-apt/doc/apt-key.pub.gpg | sudo apt-key add -
sudo apt-get -y update
sudo apt-get -y install bazel
sudo apt-get -y upgrade bazel

inception-v3

  • inception-v3は単にgithubからcloneするだけです
git clone https://github.com/tensorflow/models.git
  • bazelでinceptionのコマンドをビルドしておきます
cd models/inception
bazel build inception/imagenet_train
bazel build inception/imagenet_eval
bazel build inception/build_image_data

cuda/cudnn

  • それからGPUを使うためにNVIDIAのcudaとcudnnをインストールする必要があります
  • これがNVIDIAのユーザ登録してブラウザからダウンロードしたり・・・結構面倒です
  • これらが初めからセットアップされたNVIDIAオフィシャルのdocker imageが存在してなぜかユーザ登録等不要で使えるので今回はdockerを使いました
  • dockerをラッピングしたnvidia-dockerというのを使う必要があります

TFRecordの作成

TFRecord形式とは

  • トレーニングを行うには、Tensorflowが読み込める形式に画像ファイルを変換する必要があります。
  • TensorflowではTFRecord形式という標準データフォーマットが定義されています
  • 中身はProtocol Buffer上にGraphDefというフォーマットで記述されているみたいです

画像ディレクトリの準備

  • 前のステップで画像ファイルを収集してあると思います
  • その画像から2~3枚、バリデーション(精度測定)用の画像を選定して別によけておきます
    • 学習画像の中に精度測定用画像が入っていると、過剰に高精度と判定されてしまうためです。まあ問題集と全く同じ問題がテストで出題されるようなものです。
  • ディレクトリ構成的には下記の感じです(outputに学習データ、output_validationにバリデーションデータを入れる)
    • サブディレクトリのカテゴリ番号はバリデーション側も合わせてください
food_data/
| output/
| | 1/                                                                                                                                                                                           | | | abc.jpg
| | | def.jpg
| | | ...(大量)
| | 2/                                                                                                                                                                                           | | | ghi.jpg
| | | jkl.jpg
| | | ...(大量)
| output_validation/
| | 1/                                                                                                                                                                                           | | | xxx.jpg
| | | yyy.jpg
| | | ...(2~3枚)
| | 2/                                                                                                                                                                                           | | | zzz.jpg
| | | aaa.jpg
| | | ...(2~3枚)
  • ちなみに下記のような使い捨てのスクリプト書くと楽です
(1..1300).each do |category|
  system "mkdir -p output_validation/#{category}"

  Dir::glob("output/#{category}/*.*").each_with_index do |f, index|
    if index < 3
      system "mv #{f} output_validation/#{category}"
    end
  end
end

ラベルファイルの作成

  • ここでラベルファイルを作ります
ruby -e "(1..1300).each{|i|puts i}" > /path/to/food_data/labels.txt
  • カテゴリとディレクトリ名のマッピングのファイルなのですが、今回はディレクトリ名そのものを番号にしたので、単なる数値の連番が書かれたファイルになります

TFRecord生成

  • で、下記のコマンドで実行します。
mkdir /path/to/tfrecord
bazel-bin/inception/build_image_data \
  --train_directory=/path/to/food_data/output \
  --validation_directory=/path/to/food_data/output_validation \
  --output_directory=/path/to/tfrecord \
  --labels_file=/path/to/food_data/labels.txt \
  --train_shards=128 \
  --validation_shards=24 \
  --num_threads=8
  • そうするとoutput_directoryで指定した/path/to/tfrecordに下記のような感じでTFRecord形式のファイルが出力されます。
$ ls

train-00000-of-00128
train-00001-of-00128
...
train-00128-of-00128

validation-00000-of-00024
validation-00001-of-00024
...
validation-00024-of-00024
  • --train_shardsで指定した数だけファイルが分割されて生成されてます

トレーニング実施

  • 入力データが出来上がったのでいよいよトレーニングを実施します
  • 下記のコマンドで開始です
bazel-bin/inception/imagenet_train \
  --num_gpus=1 \
  --batch_size=32 \
  --train_dir=/path/to/train \
  --data_dir=/path/to/tfrecord \
  --max_steps=1000000
  • --num_gpusで使用するGPUの数を指定します
  • --data_dirでさっき生成したTFRecordの入ったディレクトリを指定します
  • --train_dirにトレーニング結果が出力されます
  • --max_stepsはトレーニング回数を指定します。上記では100万回を指定してますがGPUパワーがしょぼい場合は10万回くらいにしておいた方が無難かもしれません。

下記の感じで学習が進んでいきます

I tensorflow/core/common_runtime/gpu/gpu_device.cc:975] Creating TensorFlow device (/gpu:0) -> (device: 0, name: TITAN X (Pascal), pci bus id: 0000:01:00.0)
I tensorflow/core/common_runtime/gpu/gpu_device.cc:975] Creating TensorFlow device (/gpu:1) -> (device: 1, name: TITAN X (Pascal), pci bus id: 0000:02:00.0)
2017-08-07 00:51:24.866351: step 0, loss = 13.13 (6.2 examples/sec; 5.134 sec/batch)
2017-08-07 00:51:37.296765: step 10, loss = 14.37 (65.2 examples/sec; 0.491 sec/batch)
2017-08-07 00:51:41.645983: step 20, loss = 15.50 (76.6 examples/sec; 0.418 sec/batch)
2017-08-07 00:51:46.107883: step 30, loss = 14.19 (66.7 examples/sec; 0.480 sec/batch)
2017-08-07 00:51:50.549572: step 40, loss = 14.93 (74.9 examples/sec; 0.427 sec/batch)
2017-08-07 00:51:54.900733: step 50, loss = 13.79 (76.3 examples/sec; 0.419 sec/batch)
2017-08-07 00:51:59.368989: step 60, loss = 13.10 (73.3 examples/sec; 0.437 sec/batch)
2017-08-07 00:52:04.061172: step 70, loss = 13.26 (62.3 examples/sec; 0.513 sec/batch)
2017-08-07 00:52:08.413045: step 80, loss = 13.75 (76.4 examples/sec; 0.419 sec/batch)
2017-08-07 00:52:12.753432: step 90, loss = 13.06 (74.9 examples/sec; 0.427 sec/batch)

1000step進むごとにcheckpointと呼ばれる一連のファイルが--train_dirで指定したディレクトリに出力されていきます。

※tensorflowの1.0より前のバージョンでは.ckptファイルというファイルが出力されていたのですが、最近はdata/index/metaというファイルがバラバラに出力されるようになったみたいです

$ ls /path/to/train

checkpoint
model.ckpt-999999.data-00000-of-00001
model.ckpt-999999.index
model.ckpt-999999.meta
  • ちなみに途中でCtrl+cで処理を中断してもそこまでの学習結果が--train_dirに残るので無駄にはなりません
  • ただし、もう一度トレーニングのコマンドを実行すると中身が空っぽにもどるので注意してください
  • 続きからトレーニングしたい場合は --pretrained_model_checkpoint_path というオプションを付けて実行します

モデルの精度を評価する

トレーニングが完了したら、モデルの精度がどの程度か確認しましょう。

  • 下記のコマンドで精度測定を開始します。
bazel-bin/inception/imagenet_eval \
  --data_dir=/path/to/tfrecord/ \
  --checkpoint_dir=/path/to/train/
  • 下記のような出力で精度が表示されます。
I tensorflow/core/common_runtime/gpu/gpu_device.cc:975] Creating TensorFlow device (/gpu:0) -> (device: 0, name: TITAN X (Pascal), pci bus id: 0000:01:00.0)
I tensorflow/core/common_runtime/gpu/gpu_device.cc:975] Creating TensorFlow device (/gpu:1) -> (device: 1, name: TITAN X (Pascal), pci bus id: 0000:02:00.0)
Successfully loaded model from /path/to/train/model.ckpt-999999 at step=999999.
2017-08-07 00:55:46.156266: starting evaluation on (validation).
2017-08-07 00:55:49.228195: [20 batches out of 1563] (208.3 examples/sec; 0.154sec/batch)
2017-08-07 00:55:51.520711: [40 batches out of 1563] (279.2 examples/sec; 0.115sec/batch)
2017-08-07 00:55:53.801133: [60 batches out of 1563] (280.7 examples/sec; 0.114sec/batch)
2017-08-07 00:55:56.091643: [80 batches out of 1563] (279.4 examples/sec; 0.115sec/batch)
...
2017-08-07 00:58:51.410843: [1560 batches out of 1563] (265.9 examples/sec; 0.120sec/batch)
2017-08-07 00:58:51.767264: precision @ 1 = 0.6387 recall @ 5 = 0.6882 [50016 examples]
  • 上記の場合、Top1(上位1位が正答)で63%、Top5(上位5位内に正答があった)で68%の精度ということになります

モデルを使ってコマンドで画像を分類する

精度がある程度出ていることは確認できました。いよいよこのモデルに実際の画像を分類させてみましょう。
(冒頭で書きましたがここからの作業がけっこう面倒です)

checkpointファイルのfreeze

画像分類の実施は上記でトレーニングした結果得られたcheckpointファイルでは行えません。
checkpointをfreezeする必要があります。

pbファイルの生成

  • checkpointをfreezeするにあたり、inception-v3のモデル構造がどうなっているかをProtocolBuffer/GraphDef形式で定義したgraph.pbというファイルが必要です。
  • このファイルを入手するにあたり上記で実行したimagenet_evalコマンドのソースを下記のように一部書き換えることで出力します。
models/inception/inception/inception_eval.py
@@ -63,6 +63,8 @@ def _eval_once(saver, summary_writer, top_1_op, top_5_op, summary_op):
     summary_op: Summary op.
   """
   with tf.Session() as sess:
+    tf.train.write_graph(sess.graph.as_graph_def(), "/tmp", "graph.pb")
+
     ckpt = tf.train.get_checkpoint_state(FLAGS.checkpoint_dir)
     if ckpt and ckpt.model_checkpoint_path:
       if os.path.isabs(ckpt.model_checkpoint_path):
  • bazelでビルドし直してもう一度evalします。この時、--batch_size=1と指定することを忘れないでください。これは1度に予測する画像が1枚という意味合いになります。
cd models/inception
bazel build inception/imagenet_eval
bazel-bin/inception/imagenet_eval \
  --data_dir=/path/to/tfrecord/ \
  --checkpoint_dir=/path/to/train/ \
  --batch_size=1
  • 上記コマンドを実行すると、/tmp/graph.pbというファイルが出力されます
  • もっと正当にgraph.pbを生成するコードを自作してもいいのですがとりあえずのところこの手順が一番楽です

tensorflowリポジトリのclone

  • freezeのコマンドはこちらにあります。
  • ただし、pipでインストールしたTensorflowにはなぜかこのコードが含まれていません
  • なのでgit cloneしてソースからTensorflowをコンパイルして動かす必要があります
git clone https://github.com/tensorflow/tensorflow.git
cd tensorflow
git checkout r1.0
./configure

freezeの実施

下記コマンドを実行することで、frozen_graph.pbというファイルを入手することができます。

bazel build tensorflow/python/tools:freeze_graph
bazel-bin/tensorflow/python/tools/freeze_graph \
  --input_graph=/tmp/graph.pb \
  --input_checkpoint=/path/to/train/model.ckpt-999999 \
  --output_graph=/path/to/train/frozen_graph.pb \
  --output_node_names=inception_v3/logits/predictions

分類の実施

これでようやく分類の準備ができました。下記のコマンドで分類を実行してみてください。

cd tensorflow
bazel build tensorflow/examples/label_image:label_image
bazel-bin/tensorflow/examples/label_image/label_image \
  --graph=/path/to/train/frozen_graph.pb \
  --labels=/path/to/food_name.txt \
  --output_layer=inception_v3/logits/predictions:0 \
  --input_layer=batch_processing/Reshape:0 \
  --image=/path/to/chahan.jpg
  • --imageで予測したいファイルを指定します
  • --labelsで指定するのは最初に作成した料理名が列挙されたテキストファイルになります

実行すると下記のように予測結果が出力されます。

I tensorflow/examples/label_image/main.cc:207] チャーハン (9): 0.724515
I tensorflow/examples/label_image/main.cc:207] ナシゴレン (227): 0.146054
I tensorflow/examples/label_image/main.cc:207] ピラフ (3): 0.0281593
I tensorflow/examples/label_image/main.cc:207] プリン (178): 0.009696
I tensorflow/examples/label_image/main.cc:207] 固ヤキソバ (202): 0.0092466

72%の確率でチャーハンと言っていますね。

モデルを使ってWebアプリで画像を分類する

上記ではコマンドラインで画像分類を実施しました。
最後にWebアプリからこの機能を呼び出す方法をご紹介します。

分類スクリプトのPythonへのポーティング

まず上記分類スクリプトはC++で書かれているようなので、そのままWebアプリに組み込むのは厳しいです。いまどきCのCGIってのもなんですし。。
ここはWebアプリ関係のライブラリも充実しているpythonから呼び出したいところです。

で、PythonのC/C++バインディングか何かを利用すれば呼び出せそうな気もしますがちょっと取り回しが面倒そうです。
また、上記スクリプトをシェル経由で叩くという手もありますが、ライブラリ読み込み部分のオーバーヘッドがバカになりません。

そこで今回は上記スクリプトをpythonに自作で下記のようにポーティングして動かしてみることにしました。

inception_predictor.py
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

import os.path
import re
import sys
import tarfile

import numpy as np
from six.moves import urllib
import tensorflow as tf

class InceptionPredictor(object):

  __instance = None

  @classmethod
  def instance(cls):
    if cls.__instance == None:
      cls.__instance = InceptionPredictor()
    return cls.__instance

  def __init__(self):
    self.model_dir = '/path/to/model_dir'
    self.model_file = 'frozen_graph.pb'
    self.label_file = '/path/to/labels.txt'
    self.num_top_predictions = 12
    self.output_name = 'normalized'
    self.create_graph()
    self.create_resize_graph()
    self.load_label()

  def load_label(self):
    self.label_map = {}
    with open(self.label_file, 'r') as f:
      labels = f.readlines()
      index = 0
      for index, label in enumerate(labels):
        self.label_map[index] = label.strip()

  def create_graph(self):
    # Creates graph from saved graph_def.pb.
    with tf.gfile.FastGFile(os.path.join(
        self.model_dir, self.model_file), 'rb') as f:
      graph_def = tf.GraphDef()
      graph_def.ParseFromString(f.read())
      _ = tf.import_graph_def(graph_def, name='')

  def create_resize_graph(self):
    # Creates graph from saved GraphDef.
    # create_graph()

    # resizing image porting from main.cc
    wanted_channels = 3
    input_width = 299
    input_height = 299
    input_mean = 128
    input_std = 128

    # build resizing graph
    self.image_ph = tf.placeholder(tf.string, name='image_ph')
    file_reader = tf.read_file(self.image_ph, name='file_reader')
    image_reader = tf.image.decode_jpeg(file_reader, channels=wanted_channels, name='jpeg_reader')

    float_caster = tf.cast(image_reader, tf.float32, name='float_caster')
    dims_expander = tf.expand_dims(float_caster, 0)
    resized = tf.image.resize_bilinear(dims_expander, [input_height, input_width], name='size')
    result = tf.div(tf.subtract(resized, [input_mean]), [input_std], name=self.output_name)

  def predict(self, image):
    # run resizing graph
    if not tf.gfile.Exists(image):
      tf.logging.fatal('File does not exist %s', image)

    resized_tensor = None
    with tf.Session() as resize_sess:
      resized_tensor = resize_sess.run(self.output_name + ":0", feed_dict={self.image_ph: image})

    with tf.Session() as sess:
      prediction_tensor = sess.graph.get_tensor_by_name('inception_v3/logits/predictions:0')
      predictions = sess.run(prediction_tensor,
                             feed_dict={'batch_processing/Reshape:0': resized_tensor})
      predictions = np.squeeze(predictions)
      results = []

      top_k = predictions.argsort()[-self.num_top_predictions:][::-1]
      for node_id in top_k:
        human_string = self.label_map[node_id]
        score = predictions[node_id]
        results.append({'id': node_id, 'human_string': human_string, 'score': float(score)})
      return results

Webアプリへの組み込み

この記事ではPythonのWebアプリ関連のライブラリの説明はしません。
自由にflaskだとかgunicornだとか組み合わせてWebアプリ化してください。

Webアプリから上記のポーティングしたソースをimportした上で、下記のようにすれば予測結果を配列で取得することができます。

predictor = InceptionPredictor()
result = predictor.predict('./data/katsudon.jpg')
print(result)

終わりに

後半、かなり面倒なところはありましたが、公開されている実装で最先端のディープラーニング技術を利用することができます。
とにかく大量のラベリングされた画像とGPUマシンさえあれば、誰でもAI作れる時代ってすごいですね!