はじめての画像分類器

こんにちは。業務ではエンジニアリングから離れていて、やったことで書けるものあんまりないんですが、
ちょっと時間ができたので自分でちょっと勉強した内容をまとめておこうと思います。
アドベントカレンダー怖くないよ！チュートリアルやっただけの記事でも書いていいんだよ！ってことでひとつ...

# 想定読者
機械学習のことを多々耳にするものの、あまり実装に触れたことがなかったので触れてみます。
なので想定読者は機械学習に触れたことがないけど興味があって、機械学習の実装を覗いてみようという方々です。
熟練の方々は温かい目で、ミスってる部分の指摘をいただけると助かります :smile_cat: 

# テーマ
とっつきやすいテーマが無いと目指すべきゴールがわからず道に迷いますね。
ということで今回は画像を与えたときに、写っているものが何かを分類する、
画像分類器を作りましょう。

なんでもいいですが、何の分類器にするかは最初に決めておきましょう。
私は最近カレーにハマり続けているので、スパイスの画像分類器を作ることを目的にしてみます。

![IMG_2034.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1979/97493458-4ffa-7d55-e7a1-652bf72b042d.jpeg)


[弊ブログ](https://sugaret.hatenablog.com/entry/2019/11/28/234511)もよろしくお願いします

# 前提知識
画像分類器はキャッチーなテーマなので、チュートリアル記事なんかもたくさんあるわけですが、何の前提知識も無いまま読み始めるより、以下のような記事でサラッと全体を把握しておくことをおすすめします。

[数学知識もいらないゼロからのニューラルネットワーク入門](https://jp.techcrunch.com/2017/04/15/20170413neural-networks-made-easy/)
[ニューラルネットワークの「基礎の基礎」を理解する　～ディープラーニング入門｜第1回](https://www.imagazine.co.jp/%E3%83%8B%E3%83%A5%E3%83%BC%E3%83%A9%E3%83%AB%E3%83%8D%E3%83%83%E3%83%88%E3%83%AF%E3%83%BC%E3%82%AF%E3%82%92%E7%90%86%E8%A7%A3%E3%81%99%E3%82%8B%E3%80%80%EF%BD%9E%E3%83%87%E3%82%A3%E3%83%BC%E3%83%97/)

あと、専門用語っぽい言葉には参考リンクを付けておきますのでそちらも参照ください。

# チュートリアルをやる
tensorflow（てんそるふろー *1）転移学習(*2)のチュートリアルをやっていきます。
[Transfer learning with TensorFlow Hub](https://www.tensorflow.org/tutorials/images/transfer_learning_with_hub)ってやつですね。

2019/12のバージョンの項目に合わせて書いていますが、変更があった場合はいい感じに読み替えてもらえると良いと思います。
それぞれのセクションのコードは転載していますが、一部を除いて結果は載せておりませんのでご自身で実行しながら読んでみてください。

(*1)tensorflowとは機械学習のライブラリ群の一つです、googleが作ってます。[wiki](https://ja.wikipedia.org/wiki/TensorFlow)
(*2)転移学習とは別のタスクで学習したモデルを他のタスクに流用する手法です。
今回のケースだと本当はかなり大量のデータを用意しないと精度が上がらないのを、
かなり少ない枚数で解決出来るみたいな感じです。
プログラミング言語を1つ習熟すると、2つ目の習熟が早い〜みたいなノリですたぶん。[参考](https://jinbeizame.hateblo.jp/entry/kelpnet_transfer)


## Setup
前提としてこのチュートリアルはGoogleColaboratory(以下Colab *3)を使用しているので、上部の「Run in Google Colab」をクリックして実行できるようにしましょう。
Colabの画面が開いたら、上から順番に実行していってみましょう。コードの左上の[]のあたりにカーソルを合わせると実行ボタンが出てくるはず。

```python
from __future__ import absolute_import, division, print_function, unicode_literals

import matplotlib.pylab as plt

import tensorflow as tf
```
このあたりは諸々ライブラリのインポートをやっていますね。
`__future__ ..` の記述があるのはpython2-3の互換性を保つための記述。
`import matplotlib.pylab as plt` はあとで使用するプロットのためののライブラリ。


```python
!pip install -q -U tf-hub-nightly
import tensorflow_hub as hub

from tensorflow.keras import layers
```
`!pip install -q -U tf-hub-nightly` の記述はpythonのライブラリのインストール。
`tensorflow_hub` とは事前学習済みのモデルを簡単に利用できるプラットフォーム（tensorflow hub）を利用するためのライブラリ。
`tensorflow.keras` とは機械学習のライブラリで、googleのエンジニアが作ったようだがtensorflow以外からも使用できるもの。

テキスト的にパチパチ書くだけで仮想マシン上でこんなに簡単に実行出来るってすごい。


(*3)Colaboratory は、完全にクラウドで実行される Jupyter ノートブック環境です。設定不要で、無料でご利用になれます。
Colaboratory を使用すると、コードの記述と実行、解析の保存や共有、強力なコンピューティング リソースへのアクセスなどをブラウザからすべて無料で行えます。
[Colaboratoryへようこそ](https://colab.research.google.com/notebooks/welcome.ipynb?hl=ja)より


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1979/027de34b-cac9-b15f-d5e2-15387899b96a.png)
ちなみに、こんなエラーが出たときは支持に従って[RESTART RUNTIME]のボタンを押して、もっかい再生ボタンを押すと良いです。

![curry_ko.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1979/aae8384c-26fb-28ae-a38f-a56f40d292be.png)

## An ImageNet classifier
学習済みモデルを利用して、画像分類をやってみましょう、という項目です。

### Download the classifier
```python
classifier_url ="https://tfhub.dev/google/tf2-preview/mobilenet_v2/classification/2" #@param {type:"string"}
```
学習済み分類器のURLです。チュートリアルに書いてる通り、[ここに置いてあるやつ](https://tfhub.dev/s?module-type=image-classification&q=tf2)だったらなんでもいいよーとのことです。

```python
IMAGE_SHAPE = (224, 224)

classifier = tf.keras.Sequential([
    hub.KerasLayer(classifier_url, input_shape=IMAGE_SHAPE+(3,))
])
```
ニューラルネットワークの層として学習済みモデルを突っ込みます。
与えられる画像のフォーマットを教えておく必要があるようなので、（width, height, channel）で与えます。
channel（色数）の3は後で追加してますね。深い意味はあるんだろうか。

### Run it on a single image
準備ができたところで、適当な画像で判別させてみましょう。

```python
import numpy as np
import PIL.Image as Image

grace_hopper = tf.keras.utils.get_file('image.jpg','https://storage.googleapis.com/download.tensorflow.org/example_images/grace_hopper.jpg')
grace_hopper = Image.open(grace_hopper).resize(IMAGE_SHAPE)
grace_hopper
```
[grace_hopper氏](https://ja.wikipedia.org/wiki/%E3%82%B0%E3%83%AC%E3%83%BC%E3%82%B9%E3%83%BB%E3%83%9B%E3%83%83%E3%83%91%E3%83%BC)の画像を取得しています。

```python
grace_hopper = np.array(grace_hopper)/255.0
grace_hopper.shape
```
型変換をしているようです。

```python
result = classifier.predict(grace_hopper[np.newaxis, ...])
result.shape
```
画像のデータに1次元追加して予測を走らせてみると、1001個の要素のベクトルの配列（正確にはndarrayかな）が帰ってきます。

```python
predicted_class = np.argmax(result[0], axis=-1)
predicted_class
```
この1001の要素はそれぞれのクラス（今回の場合は類推される要素）の確率を表しているので、最大の確率のものを取り出してみます。
653という値が帰ってきます。これが今回の分類器が類推したモノを表しています。

### Decodethe predictions

```python
labels_path = tf.keras.utils.get_file('ImageNetLabels.txt','https://storage.googleapis.com/download.tensorflow.org/data/ImageNetLabels.txt')
imagenet_labels = np.array(open(labels_path).read().splitlines())
```
先程の結果が何を示しているのかを探るために、ラベルをダウンロードして配列に詰めておきます。
URLを直接アクセスするとすぐわかりますが、先程の1001の項目に対応しています。

```python
plt.imshow(grace_hopper)
plt.axis('off')
predicted_class_name = imagenet_labels[predicted_class]
_ = plt.title("Prediction: " + predicted_class_name.title())
```
![output_uzziRK3Z2VQo_0.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1979/401eb898-f0a7-7657-ca25-d455d7e60416.png)

画像表示ライブラリを使って、さっきの653番目の画像が何だったのかを出力します。
無事、Military Uniformという結果が出ましたね。めでたし。

![nut_hakkaku.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1979/755efb03-e947-cd0d-e0a1-99906decbada.png)


## Simple transfer learning
ここからが本番です。今のは「こんな感じで学習済み分類器って使えるぞ〜」でしたが、
次は「自分が集めてきた画像で分類器作るぞ〜」です。

### Dataset
今回のチュートリアルでは花の画像を使います。最後にこのデータセットを適当に入れ替えて、別の分類器にしましょう。

```python
data_root = tf.keras.utils.get_file(
  'flower_photos','https://storage.googleapis.com/download.tensorflow.org/example_images/flower_photos.tgz',
   untar=True)
```
データセットのダウンロード

```python
image_generator = tf.keras.preprocessing.image.ImageDataGenerator(rescale=1/255)
image_data = image_generator.flow_from_directory(str(data_root), target_size=IMAGE_SHAPE)
```
画像群を上手く処理するためにImageDataGenaratorに突っ込みます。
ちなみに画像の分類も先述のURLから画像群をダウンロードしてみるとわかりますが、
ディレクトリごとに特定の画像を集めている感じになってます。

```python
for image_batch, label_batch in image_data:
  print("Image batch shape: ", image_batch.shape)
  print("Label batch shape: ", label_batch.shape)
  break
```
image_dataをイテレートしてimage_batchとlabel_batchの配列をつくります。

### Run the classifier on a batch of images
学習後の状態と比較するために、学習していない状態で分類してみます。

```python
result_batch = classifier.predict(image_batch)
result_batch.shape
```
さっきと同じく予測してみましょう。

```python
predicted_class_names = imagenet_labels[np.argmax(result_batch, axis=-1)]
predicted_class_names
```
32件の分類結果が出ました。画像と照らし合わせて確認します。

```python
plt.figure(figsize=(10,9))
plt.subplots_adjust(hspace=0.5)
for n in range(30):
  plt.subplot(6,5,n+1)
  plt.imshow(image_batch[n])
  plt.title(predicted_class_names[n])
  plt.axis('off')
_ = plt.suptitle("ImageNet predictions")
```
![output_IXTB22SpxDLP_0.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1979/cd610f49-c0e9-58a5-7957-47e44cbfe9b2.png)


結果を見るとかなりかけ離れてますね。
1001の対象物から選んでいるからと言うのもありそうですが、全然正解していません。

### Download the headless model
ということで分類器を自分で作ってみましょう。

```py

feature_extractor_url = "https://tfhub.dev/google/tf2-preview/mobilenet_v2/feature_vector/2" #@param {type:"string"}
```
例によってtensorflow hubから学習済みモデルをダウンロードしてきます。extractorは抽出機っぽいですね。

```py

feature_extractor_layer = hub.KerasLayer(feature_extractor_url,
                                         input_shape=(224,224,3))
```

モデルに突っ込むためのlayerを作成。

```py

feature_batch = feature_extractor_layer(image_batch)
print(feature_batch.shape)
```
こいつは1280のベクトルをそれぞれの画像に対して返してくれるとのこと。
様々な要素から画像の分類を試みるんですね。

```py

feature_extractor_layer.trainable = False
```
今回は抽出機の部分はチューニングしないので、学習されないように指定しておきます。

### Attach a classification head
```py

model = tf.keras.Sequential([
  feature_extractor_layer,
  layers.Dense(image_data.num_classes, activation='softmax')
])

model.summary()
```

さっき作った抽出機のインスタンスを分類器に突っ込んでいます。
作ったモデルのサマリーの表示もしていますね。

```py

predictions = model(image_batch)
```

```py
predictions.shape
```
tensorのshape（形状）を確認しています。

### Train the model

```py

model.compile(
  optimizer=tf.keras.optimizers.Adam(),
  loss='categorical_crossentropy',
  metrics=['acc'])
```
モデルをコンパイルします。
optimizer（最適化アルゴリズム）:色々種類があるみたいです。 [参考](https://keras.io/ja/optimizers/)
loss（損失関数）:損失が出来るだけ小さいように調整するために学習に使用される関数。これも色々あるみたいです。[参考](https://keras.io/ja/losses/)
metrics（評価関数）:モデルの性能を評価するために使う値だが、損失関数のように学習には使われないもの。これも同じくいろいろある模様。 [参考](https://keras.io/ja/metrics/)

```py
class CollectBatchStats(tf.keras.callbacks.Callback):
  def __init__(self):
    self.batch_losses = []
    self.batch_acc = []

  def on_train_batch_end(self, batch, logs=None):
    self.batch_losses.append(logs['loss'])
    self.batch_acc.append(logs['acc'])
    self.model.reset_metrics()
```
損失関数、評価関数の値がどの様になっているかを観測するための関数定義です。

```py
steps_per_epoch = np.ceil(image_data.samples/image_data.batch_size)

batch_stats_callback = CollectBatchStats()

history = model.fit_generator(image_data, epochs=2,
                              steps_per_epoch=steps_per_epoch,
                              callbacks = [batch_stats_callback])
```
fit_generatorで学習を開始します。
学習回数などなどを指定してますね。[参考](https://keras.io/ja/models/sequential/)
学習には多少時間がかかります。

```py
plt.figure()
plt.ylabel("Loss")
plt.xlabel("Training Steps")
plt.ylim([0,2])
plt.plot(batch_stats_callback.batch_losses)
```
![output_A5RfS1QIIP-P_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1979/7e2c626e-ce1f-62a7-0c1c-aaadf78cf09d.png)

損失関数の推移です。徐々に下がっていっていてよさげですね。

```py
plt.figure()
plt.ylabel("Accuracy")
plt.xlabel("Training Steps")
plt.ylim([0,1])
plt.plot(batch_stats_callback.batch_acc)
```
![output_3uvX11avTiDg_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1979/8ee11add-566e-876d-1a50-59c0ebeb6716.png)


評価関数の推移です。これは上がっていっているのでいい感じ。

### Check the predictions
いよいよ分類してみましょう。

```py
class_names = sorted(image_data.class_indices.items(), key=lambda pair:pair[1])
class_names = np.array([key.title() for key, value in class_names])
class_names
```
実際に存在するクラスを取り上げます。存在するのは5種類だけですね。

```py
predicted_batch = model.predict(image_batch)
predicted_id = np.argmax(predicted_batch, axis=-1)
predicted_label_batch = class_names[predicted_id]
```
先程やったようにpredictして、最大値のものを取ってきてラベルにします。

```py
label_id = np.argmax(label_batch, axis=-1)
```
正解のラベルを確保しておきます

```py
plt.figure(figsize=(10,9))
plt.subplots_adjust(hspace=0.5)
for n in range(30):
  plt.subplot(6,5,n+1)
  plt.imshow(image_batch[n])
  color = "green" if predicted_id[n] == label_id[n] else "red"
  plt.title(predicted_label_batch[n].title(), color=color)
  plt.axis('off')
_ = plt.suptitle("Model predictions (green: correct, red: incorrect)")
```
![output_wC_AYRJU9NQe_0.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1979/23a7818b-7e87-049e-3c6d-411091fbdb21.png)


正解のときは緑、失敗のときは赤で出力されるようになっています。
だいたい正解してるんじゃないでしょうか。よさげ。

この後もチュートリアルはちょっとだけ続いて、学習したモデルのexportなどが書かれているんですが、今回使うのはここまでなので、これ以降は割愛しておきます。

![cooking_cumin_seed.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1979/b8644d26-1985-173b-62b1-fa3f01d905ed.png)


# 自分だけの分類器造り

さて、チュートリアルで出てきたコードを利用して自分だけの分類器を作っていきましょう。今回はColab上で動くところまででゴールにしておきます。

## 画像収集
画像分類には画像の収集が欠かせません。

### 画像のダウンロード
[google-images-download](https://github.com/hardikvasa/google-images-download)が便利です。（ダウンロードした画像のライセンスには注意しましょう）

pip経由でインストールできるので、macであれば以下のように使えると思います。

```
pip install google_images_download
googleimagesdownload --keywords "探したい画像"
```

### 画像の選別
ダウンロードしてきた画像をそのまま使おうとすると、ゴミが多くて使えません。
ので、画像を1枚ずつ見ながら変な画像を取り除いていきましょう。
この作業が一番大変でした..

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1979/2bc5db1b-35e9-5df7-9d2f-5a9b537bb6b5.png)
最終的にはこんな感じでディレクトリごとにきれいになった特定の画像が集まっている状態を作りましょう。

### パッケージしてアップロード
とりあえずColabからdownload出来るようにtarで圧縮したファイルをどこかにuploadしておきましょう。私はGoogleDriveに上げときました。
GoogleDriveの場合はtar.gzをuploadした上で、共有可能なリンクを取得し、そのID部分を以下リンクと差し替えてみましょう。
`https://drive.google.com/uc?export=download&id=XXXXXXXXXXXXXXXXXXXXX`

## Colabのコードに反映
こちらに分類機作る部分だけを抜き出したColabファイルを用意したので、xxxxのところを差し替えてみてください。
[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/gist/NaoyaTabakomori/089f66284d583ff9c908e819c11727bc/.ipynb#forceEdit=true&sandboxMode=true&scrollTo=IwBLpCtK-s8f)


上手く動きましたでしょうか。

私の手元ではこんな感じ。
![image (85).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1979/e059416c-75c9-1c82-c022-2772b04701d7.png)

![spice_clove.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1979/d49988c6-83af-d88c-6c80-817f9a26eeda.png)


# おわりに
なんとなーく機械学習の上辺を体験できましたが、分類器のイメージだと自分で撮った写真が分類されてほしい感じありますよね。
手元に環境構築したりして、もうちょっとカジュアルに触れる何か〜を作ってみたい気持ちがあります。がんばるぞ。