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

この記事は
===

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

Tensorflowでの画像認識
===

Inception-v3？
---

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

* [MNIST For ML Beginners](https://www.tensorflow.org/get_started/mnist/beginners)：単なるニューラルネットワークで28x28のモノクロ数字画像の分類
* [Deep MNIST for Experts](https://www.tensorflow.org/get_started/mnist/pros)：CNNで28x28のモノクロ数字画像の分類
* [Convolutional Neural Networks](https://www.tensorflow.org/tutorials/deep_cnn)：CNNで24x24のカラー写真の分類

ただ上記はあくまでお勉強用のサンプルというレベルで、カテゴリ数が増えたりするとまともな精度は出ません。じゃあお勉強用じゃないモデルが公開されてないかというといろいろされてます。その中の１つが[Inception-v3](https://github.com/tensorflow/models/tree/master/research/inception)です。

どんなモデル？
---

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

![inception-v3](https://github.com/tensorflow/models/raw/master/research/inception/g3doc/inception_v3_architecture.png)

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

実装は公開されてる？
---

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

大まかな手順
===

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

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

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

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

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

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

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

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

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

学習画像を入手する
===

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

収集した画像はディレクトリに分けて保管しましょう。ディレクトリ名は数字連番でいいです。
その時の数字は、上記で作成したカテゴリで何行目だったのかと合わせてください。ただし「-」はゼロ行目です。
たとえばチョコレートは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というのを使う必要があります
  * [（別記事）nvidia-dockerでtensorflow(GPU版)を動かす](http://qiita.com/shouta-dev/items/22a5c387cac82bef9f83)


TFRecordの作成
---

### TFRecord形式とは

* トレーニングを行うには、Tensorflowが読み込める形式に画像ファイルを変換する必要があります。
* TensorflowではTFRecord形式という標準データフォーマットが定義されています
* 中身は[Protocol Buffer](https://ja.wikipedia.org/wiki/Protocol_Buffers)上に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コマンドのソースを下記のように一部書き換えることで出力します。

```diff: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のコマンドは[こちら](https://github.com/tensorflow/tensorflow/blob/master/tensorflow/python/tools/freeze_graph.py)にあります。
* ただし、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に自作で下記のようにポーティングして動かしてみることにしました。

```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作れる時代ってすごいですね！