Tensorflow(Keras)で基本の画像分類モデル構築

# はじめに
## 動機
今回画像分類モデルを初めて構築したので、新鮮な気持ちなまま記録を残しておくことは今後のためになるでしょう。

## 使用データ
食品画像データで、ソースは忘れました。ごめんなさい。
50種類のラベル付けがされているやつです。
今回は50種類だと多いので15種類のクラス分けを行います。

## 使用ライブラリ・環境
実行環境は Google Colab Proです。
深層学習ライブラリはTensorflowを使います。
Tensorflow 2.6.0
後は画像処理の基本的な子たち。
importは以下の通り

```Python
import numpy as np
import pandas as pd
import glob
from PIL import Image
import math

import tensorflow as tf
from tensorflow.python.ops.gen_math_ops import imag
from tensorflow.keras import datasets, layers, models
from tensorflow.python.keras.engine.sequential import Sequential
from tensorflow.python.keras.layers.core import Activation
from sklearn.preprocessing import LabelEncoder
```


# 本題
本記事で行うのは
- 基本の画像分類モデルの構築（CNN）
となります。
モデルの評価指標の算出と、結果の可視化を行い、性能を見てみます。

## 画像データの前処理
先にデータ分析を行うときに必ず必要になる前処理について少し触れます。私はここで4時間くらい詰んでました。

画像分類の前処理で必要な最低限のことは以下の通りです。

- ラベル付け
- データをarrayにぶち込む
- データの正規化  
ここまで出来たらあとは非常に簡単です。
順番に説明していきます。

### ラベル付け
分類問題ではまず画像にラベルを付けるところから始まります。
ではどうやってつけていくか？これはディレクトリの構造を利用することで可能となります。
例えば、画像を3種類のクラスに分類したい場合、0，1，2とラベルを付けることになります。
仮に0はイヌ、1はネコ、2はイノシシとした時、フォルダ０にイヌの画像を配置し、フォルダ１にネコの画像を配置し、フォルダ２にイノシシの画像を配置します。
こうすることで、glob関数を使用し全てのデータの読み込みと同時にラベル付けを行うことができます。
初歩的なことですが、ラベルは0から付けるようにしてください。
ラベルを1から付けてると予測ラベルと正解ラベルの数が合わず、lossがNanで出力されておかしなことになるので。

ディレクトリが doubutsu/train/0/イヌ１.jpg のようになっていると仮定します。

```Python
train_dataset = []
train_labels = []
test_dataset = []
test_labels = []
size = 224

for f in glob.glob("doubutsu/*/*/*.jpg"):
  img_data = tf.io.read_file(f)
  img_data = tf.io.decode_jpeg(img_data)
  img_data = tf.image.resize(img_data,[size,size]) # モデルにインプットするデータのサイズ

  if f.split('/')[1] == "train":
      train_dataset.append(img_data) # データ
      train_labels.append(int(f.split("/")[2])) # ラベルを付けます
　elif f.split('/')[1] == "test":
      test_dataset.append(img_data) 
      test_labels.append(int(f.split("/")[2])) # testにラベルがある場合
```
上記のコードは関数で定義してあげると後々すこし楽です。
引数はsizeだけでOK

### 正規化

データを読み込み、リストに入れることができたので次は正規化を行っていきます。画像データは0~255の値なのですが、機械にとって扱いやすいデータにするために0~1の範囲にする必要があるようです。
今回は配列にして255で割るだけ。シンプル

```Python
# 正規化
train_image = np.array(train_dataset)/255
train_labels = np.array(train_labels)
test_image = np.array(test_dataset)/255
test_labels = np.array(test_labels)
```
当然 train_test_split で train と val に分割することもできます。
今回は Cross Validation は使いません。

## 基本の画像分類モデル（CNN）
### モデル定義
まずは基本から。畳み込み層とプーリング層を1つずつ含むモデル。パラメータは結構適当です。
最終出力層で15を指定し、softmax関数を通すことで、15のクラス分けを行っています。

```Python
model = models.Sequential()
model.add(layers.Conv2D(32, (3, 3), activation = 'relu',input_shape = (size,size,3))) # sizeは↑で設定した画像サイズ
model.add(layers.MaxPooling2D((2, 2)))
model.add(layers.Flatten())
model.add(layers.Dense(32, activation = 'relu'))
model.add(layers.Dense(15, activation = 'softmax'))

# 定義したモデルを表示
model.summary()
```
パラメータなど詳しくはこちらのKeras公式ドキュメント
https://keras.io/ja/getting-started/sequential-model-guide/
このように、Tensorflow(Sequentialモデル)ではaddで層を下に重ねていく感じですね。
（複雑なモデルを定義するときはfunctional APIというものがあったり)

### モデルのコンパイル
次にモデルをコンパイルしていきます。
コンパイルとは、optimizer(最適化関数),loss(損失関数),metrics(評価指標)を決定するもので、モデルの学習を行うための準備段階です。
今回は最適化関数をAdam、損失関数をsparse_categorical_crossentropy、評価指標をaccuracyとします。

```Python
model.compile(optimizer = "Adam",
              loss = "sparse_categorical_crossentropy",
              metrics = ['accuracy'])
```
詳しくはこちらのKerasの公式ドキュメント
https://keras.io/ja/models/sequential/

### モデルの学習
ここからモデルの学習に入ります。
モデルを定義し、コンパイルしたら最後はfitです。
学習回数、バッチサイズ、インプットするデータ、評価に使うデータなどを指定して学習開始します。

```Python
epochs = 20 # 後で使うので定義しておく
result=model.fit(train_image,train_labels,
                 batch_size = 32,
                 epochs = epochs,
                 validation_data=(test_image,test_labels))
```
このコードを実行することでこんな感じで出力されます。↓
![キャプチャ.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1777576/20ba39af-e288-9e5f-0749-c8da506f9393.png)
ここまででモデルの学習と評価は完了しました。
では結果をわかりやすく見るために結果を可視化してみましょう。

```Python
import seaborn as sns
import matplotlib.pyplot as plt

def plot(acc, val_acc, loss, val_loss):
  # 可視化関数の定義
  fig,(ax1, ax2)= plt.subplots(1, 2, figsize= (10,4))

  sns.lineplot(x= range(1, epochs+1), y= acc,ax = ax1, label= "train")
  sns.lineplot(x= range(1, epochs+1), y= val_acc,ax = ax1, label= "validation")
  ax1.legend(loc = "upper left")
  ax1.grid()
  ax1.set_xlabel(xlabel= "Epochs", fontsize= 10)
  ax1.set_ylabel(ylabel= "Accuracy", fontsize= 10)

  sns.lineplot(x= range(1, epochs+1), y= loss, ax= ax2, label= "train")
  sns.lineplot(x= range(1, epochs+1),y= val_loss, ax= ax2, label= "validation")
  ax2.legend(loc = 'upper right')
  ax2.grid()
  ax2.set_xlabel(xlabel= "Epochs", fontsize= 10)
  ax2.set_ylabel(ylabel= "loss", fontsize= 10)
  plt.tight_layout()

plot(result.history["accuracy"],
     result.history["val_accuracy"],
     result.history["loss"],
     result.history["val_loss"])
```
出力↓
![キャプチャ.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1777576/89733674-3917-5634-4627-dd99b87a07af.png)

なかなか酷い結果がでましたね。今回は2000枚の画像を15クラスに分類するタスクなので先のモデルでは明らかに力不足であったことがわかります。

ここまででモデルの定義～学習・評価まで完了しました。

### モデルで予測
では最後に作ったモデルで画像のクラスを予測してみます。
実装はとても簡単で、1行です。

```Python
prediction = model.predict(test_image)
```
これでtest_image(画像の配列)のクラスを予測することができます。
predictionsを見てみると、↓のように15個のクラスに分けられています。
これは単純に確率値なので、最も大きな値が予測結果のクラスとなります。
（今回であれば5番目）
![キャプチャ.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1777576/77421c53-046c-450b-e792-6c6f68f50a59.png)
あとは predictions = argmax() などして整数値で出力すれば、完了です。

## おわりに
以上で、基本の画像分類モデル構築～予測結果の出力までを一通り紹介することができました。
ここからさらに層を積み重ねたり、ドロップアウト等の手法を加えたり、究極は事前学習済みモデルをファインチューニングすることで最強のモデルを作ることができます。
ぜひパラメータを色々工夫して事前学習済みモデルを超える最強のモデルを作ってみてください。(鬼)

余談ですが、MovileNetV2を使って同じデータを予測してみると、結果は以下のようになります。
![キャプチャ.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1777576/1aaef8c8-7793-42d5-a697-12e1ca89587f.png)

y軸を合わせてないのでわかりにくいですが、正解率が0.24 → 0.9になってます。
しかもまだ上がりそう、強すぎる。笑