CNNを用いて飯テロ画像を判定してみた

###**目次**
[1.検証内容](#検証内容)
[2.検証動機](#検証動機)
[3.開発環境](#開発環境)
[4.開発言語](#4開発言語)
[5.学習用データセット](#5学習用データセット)
[6.達成目標](#達成目標)
[7.前処理](#2前処理)
[8.学習モデルの作成(料理ラベル全部)](#学習モデルの作成(料理ラベル全部))
[9.学習モデルの作成(料理ラベル１０種類限定)](#学習モデルの作成(料理ラベル１０種類限定))
[10.学習モデルを用いた画像判定](#学習モデルを用いた画像判定)
[11.まとめ（考察）](#まとめ（考察）)
[12.今後の展望](#今後の展望)


###**検証内容**

畳み込みニューラルネットを使ってtwitter上の料理画像を判別できるか検証してみました。
＊判定用の画像はtwitter上の「#飯テロ」から拝借

###**検証動機**
機械学習を学んでおり、その学習の入り口として画像認識をしてみたかった。とにかく学んだことをアウトプットしてみようと思っていました。なんで料理の画像認識なのかというと、自分が食品の会社に勤めているからなだけで素材は何でもよかった。こっちは動機が不純、、、


###**開発環境**

MacBook Pro（GPUなし）
Google colab(ランタイムタイプ:GPU)

###**開発言語**
Python3.7.11
tensorflow 2.5.0
numpy 1.19.5
scikit-learn 0.22.2


###**学習用データセット**
[food101](https://data.vision.ee.ethz.ch/cvl/datasets_extra/food-101/)
101種類の料理がフォルダごとに保存されている


###**達成目標**
オープンデータセットを使い学習済みのモデルを生成し、飯テロ画像を高い精度で認識させる



###前処理
では前処理から、

```python
with open('/content/drive/MyDrive/images-food/labels.txt', 'r') as f:
    labels_list = f.read().splitlines() #splitlinesで改行で読み込む
labels_list = list(map(lambda x : x.lower(), labels_list))
```

food101のデータセットをGooglecolabにアップロードして、text.txtから各食品のラベルリストを作成

```python
import glob 
import cv2

def load_images(dir_path, label, resize_shape, max_n = None):
  image_path_list = glob.glob(dir_path + label + "/*.jpg")
  if max_n is not None and len(image_path_list) > max_n:
    image_path_list = image_path_list[:max_n] 
  image_list = []
  for image_path in image_path_list:
    image = cv2.imread(image_path)
    image = cv2.resize(image, resize_shape)
    image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
    image_list.append(image)
  return image_list, [labels_list.index(label)] * len(image_list)

dir_path = '/content/drive/MyDrive/images-food/'#colab上のimaeges-foodのパスを格納
width = 200 
height = 200
resize_shape = tuple((width, height))
image_list = []
label_list = []
for label in labels_list:
  image_list_temp, label_list_temp = load_images(dir_path, label, resize_shape)
  image_list.extend(image_list_temp)
```

load_images関数の設定
if文を用いて要素数の取り出し条件設定
for文を用いて、画像の読み込み、画像の大きさの変更、BGRから RGBへの色の変換（OpenCVは最初BGRでの読み込みのため）、image_path_listに画像を格納[labels_list.index(label)] * len(image_list)で該当インデックスの分だけ画像 ラベルのタグをそれぞれの画像へ
  
```python
from tensorflow import keras
from tensorflow.keras.datasets import cifar10
from tensorflow.keras.layers import Activation, Conv2D, Dense, Dropout, Flatten, MaxPooling2D
from tensorflow.keras.models import Sequential, load_model
from tensorflow.keras.utils import to_categorical
import numpy as np
import matplotlib.pyplot as plt

print(type(image_list))
print(type(label_list))

X = np.stack(image_list, axis=0)
Y = np.array(label_list)
Y = to_categorical(Y) 

print(type(X))
print(type(Y))
print(X.shape)
print(Y.shape)
```
画像をstack関数で３次元から４次元に変換
ラベルをnumpy配列に変換。その後one-hot表現に変換


次にpickle化。前処理に時間がかかって大変なことがわかったので、
毎回前処理をやらなくてもいいように仕様変更することにしました。

```python
import pickle
# pickle = 複数のオブジェクトを1つのまとまりに保存する事ができる
with open('/content/drive/MyDrive/images-food/dataset.pkl', 'wb') as f:
  pickle.dump((X, Y), f)
```

```python
import pickle
# pickle化したファイルをロード
with open('/content/drive/MyDrive/images-food/dataset.pkl', 'rb') as f:
  X, Y = pickle.load(f)

print(type(X))
print(type(Y))
print(X.shape)
print(Y.shape)
```

次にファイルを使うときは前処理を飛ばしてload関数を使えばOK


####**学習モデルの作成(料理ラベル全部)**

全種類の料理ラベルを100枚ずつ学習させてみました。


```python
from tensorflow import keras
from tensorflow.keras.datasets import cifar10
from tensorflow.keras.layers import Activation, Conv2D, Dense, Dropout, Flatten, MaxPooling2D
from tensorflow.keras.models import Sequential, load_model
from tensorflow.keras.utils import to_categorical
import numpy as np
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from tensorflow.keras.applications.vgg16 import VGG16
from keras.callbacks import EarlyStopping

# トレーニングデータととtestデータを分ける。
X_train, X_test, Y_train, Y_test = train_test_split(
    X, Y, test_size=0.3, random_state=42)

# モデルの定義
model = Sequential()
model.add(Conv2D(32, (3, 3), padding='same',
                 input_shape=X_train.shape[1:]))
model.add(Activation('relu'))
model.add(Conv2D(32, (3, 3)))
model.add(Activation('relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Dropout(0.25))

model.add(Flatten())
model.add(Dense(512))
model.add(Activation('relu'))
model.add(Dropout(0.5))
model.add(Dense(Y_train.shape[1]))
model.add(Activation('softmax'))


# コンパイル
model.compile(loss='categorical_crossentropy',
              optimizer='adam',
              metrics=['accuracy'])

#　callback関数の定義
es = EarlyStopping(monitor = 'val_loss', min_delta = 0.0000, patience = 5)

model.fit(X_train, Y_train, validation_data=(X_test, Y_test), batch_size=256, epochs=100, callbacks = [es])


# 精度の評価
scores = model.evaluate(X_test, Y_test, verbose=1)
print('Test loss:', scores[0])
print('Test accuracy:', scores[1])

# データの可視化（テストデータの先頭の10枚）
for i in range(10):
    plt.subplot(2, 5, i+1)
    plt.imshow(X_test[i])
plt.suptitle("10 images of test data",fontsize=20)
plt.show()

# 予測（テストデータの先頭の10枚）
pred = np.argmax(model.predict(X_test[0:10]), axis=1)
print(pred)

model.summary()
```

結果
Test accuracy: 0.0168
全然ダメでした、、、
epochsを１００回しても精度はこんなものでした。
ならば、転移学習ではどうだろうと次に試みました。

```python
from tensorflow import keras
from tensorflow.keras import optimizers
from tensorflow.keras.datasets import cifar10
from tensorflow.keras.layers import Activation, Conv2D, Dense, Dropout, Flatten, MaxPooling2D
from tensorflow.keras.models import Sequential, load_model
from tensorflow.keras.utils import to_categorical
import numpy as np
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from tensorflow.keras.applications.vgg16 import VGG16
from tensorflow.keras import Input, layers, Model
from keras.callbacks import EarlyStopping

# トレーニングデータととtestデータを分ける。
X_train, X_test, Y_train, Y_test = train_test_split(
    X, Y, test_size=0.3, random_state=42)

# モデルの定義
input_tensor = Input(shape=(200, 200, 3))

vgg16 = VGG16(include_top=False, weights='imagenet', input_tensor=input_tensor)

top_model = Sequential()
top_model.add(Flatten(input_shape=vgg16.output_shape[1:]))
top_model.add(Dense(256, activation='relu'))
top_model.add(Dropout(0.5))
top_model.add(Dense(101, activation='softmax'))

# vgg16とtop_modelを連結
model =Model(inputs=vgg16.input, outputs=top_model(vgg16.output))

#　vgg16による特徴抽出部分の重みは更新されると崩れてしまうので固定
for layer in model.layers[:19]:
    layer.trainable =False


# コンパイル
model.compile(loss='categorical_crossentropy',
              optimizer='adam',
              metrics=['accuracy'])

#　callback関数の定義
es = EarlyStopping(monitor = 'val_loss', min_delta = 0.0000, patience = 5)


# トレーニングデータの学習
model.fit(X_train, Y_train, validation_data=(X_test, Y_test), batch_size=256, epochs=100, callbacks = [es])

# 精度の評価
scores = model.evaluate(X_test, Y_test, verbose=1)
print('Test loss:', scores[0])
print('Test accuracy:', scores[1])

# データの可視化（テストデータの先頭の10枚）
for i in range(10):
    plt.subplot(2, 5, i+1)
    plt.imshow(X_test[i])
plt.suptitle("10 images of test data",fontsize=20)
plt.show()

# 予測（テストデータの先頭の10枚）
pred = np.argmax(model.predict(X_test[0:10]), axis=1)
print(pred)

model.summary()
```
結果
Test accuracy: 0.0106
余計悪くなってしまいました、、、

１つの料理につき100枚の学習では学習不足みたいです。
もっと増やしてみたいが、自分のPCはおろかGooglecoalb上でも時間がかかりすぎて厳しいですね、、、

####**学習モデルの作成(料理ラベル１０種類限定)**

そこで、料理の学習ラベルを１０種類に限定してみようと次にトライ

前回までは、学習用の画像はGooglecolabの容量上１種類につき100枚が限界でしたが、１０種類の料理に絞れば、ファイルに入っている全部の写真を学習モデルに使用できると仮説を立てて取り組んでみました。

```python
from tensorflow import keras
from tensorflow.keras.datasets import cifar10
from tensorflow.keras.layers import Activation, Conv2D, Dense, Dropout, Flatten, MaxPooling2D
from tensorflow.keras.models import Sequential, load_model
from tensorflow.keras.utils import to_categorical
import numpy as np
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from tensorflow.keras.applications.vgg16 import VGG16
from keras.callbacks import EarlyStopping

# トレーニングデータととtestデータを分ける。
X_train, X_test, Y_train, Y_test = train_test_split(
    X, Y, test_size=0.3, random_state=42)

# モデルの定義

model = Sequential()
model.add(Conv2D(32, (3, 3), padding='same',
                 input_shape=X_train.shape[1:]))#前段のX.shapeを利用。入力を(200, 200, 3)で使用
model.add(Activation('relu'))
model.add(Conv2D(32, (3, 3)))
model.add(Activation('relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Dropout(0.25))

model.add(Flatten())
model.add(Dense(512))
model.add(Activation('relu'))
model.add(Dropout(0.5))
model.add(Dense(Y_train.shape[1]))#前段のY_train.shapeを利用。出力を(101)で使用。101はラベルの種類となる
model.add(Activation('softmax'))


# コンパイル
model.compile(loss='categorical_crossentropy',
              optimizer='adam',
              metrics=['accuracy'])

#　callback関数の定義
es = EarlyStopping(monitor = 'val_loss', min_delta = 0.0000, patience = 5)


# トレーニングデータの学習
model.fit(X_train, Y_train, validation_data=(X_test, Y_test), batch_size=256, epochs=100, callbacks = [es])


# 精度の評価
scores = model.evaluate(X_test, Y_test, verbose=1)
print('Test loss:', scores[0])
print('Test accuracy:', scores[1])

# データの可視化
for i in range(10):
    plt.subplot(2, 5, i+1)
    plt.imshow(X_test[i])
plt.suptitle("10 images of test data",fontsize=20)
plt.show()

# 予測
pred = np.argmax(model.predict(X_test[0:10]), axis=1)
print(pred)

model.summary()
```

結果
Test accuracy: 0.165
これでも苦しい結果でした、、、

最終手段として、転移学習ではどうなるか試してみました。

```python

from tensorflow import keras
from tensorflow.keras import optimizers
from tensorflow.keras.datasets import cifar10
from tensorflow.keras.layers import Activation, Conv2D, Dense, Dropout, Flatten, MaxPooling2D
from tensorflow.keras.models import Sequential, load_model
from tensorflow.keras.utils import to_categorical
import numpy as np
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from tensorflow.keras.applications.vgg16 import VGG16
from tensorflow.keras import Input, layers, Model
from keras.callbacks import EarlyStopping

# トレーニングデータとtestデータを分ける。
X_train, X_test, Y_train, Y_test = train_test_split(
    X, Y, test_size=0.3, random_state=42)

# モデルの定義
input_tensor = Input(shape=(200, 200, 3))

vgg16 = VGG16(include_top=False, weights='imagenet', input_tensor=input_tensor)

top_model = Sequential()
top_model.add(Flatten(input_shape=vgg16.output_shape[1:]))
top_model.add(Dense(256, activation='relu'))
top_model.add(Dropout(0.5))
top_model.add(Dense(10, activation='softmax'))

# vgg16とtop_modelを連結
model =Model(inputs=vgg16.input, outputs=top_model(vgg16.output))

# vgg16による特徴抽出部分の重みは更新されると崩れてしまうので固定
for layer in model.layers[:19]:
    layer.trainable =False


# コンパイル
model.compile(loss='categorical_crossentropy',
              optimizer='adam',
              metrics=['accuracy'])


#　callback関数の定義
es = EarlyStopping(monitor = 'val_loss', min_delta = 0.0000, patience = 5)


# トレーニングデータ学習
model.fit(X_train, Y_train, validation_data=(X_test, Y_test), batch_size=256, epochs=100, callbacks = [es])


# 精度の評価
scores = model.evaluate(X_test, Y_test, verbose=1)
print('Test loss:', scores[0])
print('Test accuracy:', scores[1])

# データの可視化
for i in range(10):
    plt.subplot(2, 5, i+1)
    plt.imshow(X_test[i])
plt.suptitle("10 images of test data",fontsize=20)
plt.show()

# 予測（テストデータの先頭の10枚）
pred = np.argmax(model.predict(X_test[0:10]), axis=1)
print(pred)

model.summary()
```
結果
Test accuracy: 0.683
格段に良くなりました！

これなら何とか学習モデルとして使えそう。

そこで、この学習モデルを毎回学習しなくてもいいように、model.save()でモデルを保存

```python
model.save('/content/drive/MyDrive/images-food/tl10')
```

####**学習モデルを用いた画像判定**

10種類の料理に限定し、転移学習をさせた学習モデルを用いて最後にtwitter上の「#飯テロ」にある
料理を正しく判定できるか試してみました。


```python
import tensorflow as tf
import glob 
import cv2
from tensorflow.keras.utils import to_categorical
import numpy as np
import matplotlib.pyplot as plt
from sklearn.metrics import accuracy_score

model = tf.keras.models.load_model('/content/drive/MyDrive/images-food/tl10')

def load_images(dir_path, label, resize_shape, max_n = None):
  image_path_list = glob.glob(dir_path + label + "/*.jpg")
  if max_n is not None and len(image_path_list) > max_n:
    image_path_list = image_path_list[:max_n] 
  image_list = []
  for image_path in image_path_list:
    image = cv2.imread(image_path)
    image = cv2.resize(image, resize_shape)
    image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
    image_list.append(image)
  return image_list, [labels_list.index(label)]

dir_path = '/content/drive/MyDrive/images-food/predict/'
width = 200 
height = 200
resize_shape = tuple((width, height))
image_list = []
label_list = []
for label in labels_list:
  image_list_temp, label_list_temp = load_images(dir_path, label, resize_shape)
  image_list.extend(image_list_temp)
  label_list.extend(label_list_temp)

print(type(image_list))
print(type(label_list))

X = np.stack(image_list, axis=0)
Y_temp = np.array(label_list)
Y = to_categorical(Y_temp) #to_categoricalでone-hot表現

print(type(X))
print(type(Y))
print(X.shape)
print(Y.shape)

# 予測（テストデータの予測）
pred = np.argmax(model.predict(X), axis=1)
print(pred)

# 10行５列に１種類10枚ずつ画像を格納し、その後可視化
fig, ax = plt.subplots(10, 5, figsize = (20, 30))
for row in range(10):
  for col in range(5):
    ax[row, col].imshow(X[col + 5*row])
    ax[row, col].set_title(labels_list[Y_temp[col + 5*row]] + ' -> ' + labels_list[pred[col + 5*row]])
    ax[row, col].set_axis_off()

fig.tight_layout()
plt.show()

# 判定精度（%）
accuracy = accuracy_score(Y_temp, pred) * 100
print("認識精度: {:.1f} %".format(accuracy))

```
画像の上部　：　正解ラベル　-> 判定ラベル
![飯テロ推測結果.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1630001/91496e65-4219-3dbd-0d2b-896c2efb39d5.png)


###**まとめ（考察)**
最終的な精度は74%。初めての画像認識としてはいい結果ではないでしょうか。
誤認識があった画像をそれぞれ確認してみる
寿司:誤認識の画像は餃子と判定されている。色と形が確かに餃子に見えなくもない。故に餃子と言いたいのでしょうか
餃子:誤認識の画像は餃子特有の形が確認できないもの。学習用の写真は色だけでなく、形も写真内に収まっているのが大多数を占める。認識難しかったように思う
ピザ：誤認識の画像はピザが浮いている写真。チーズの線も見えるのでピザとは認識し難いと思う。
ステーキ：誤認識はこのラベルが一番多い。元画像を確認すると、ステーキの他に添え物のサラダがある写真が多く、ステーキであることの判別がサラダ込みの判定になるのではないだろうか
ドーナツ：誤認識の画像は学習用の画像にはあまり見当たらないので、誤判定に至ったのではないでしょうか
チキンカレー：誤認識の写真は寿司となっているが、皿の形やナンの色が巻物やご飯の色にみえなくもない、、、
チーズケーキ：カレーの誤判定。色がまさにカレー。まあ、納得できる

###**今後の展望**
今回初めて画像認識をやってみましたが、その過程は色々と大変で最終的な結果を得られるまで、とても時間がかかりました。その分やりがいも多くもっと学んでみたいと思いました。
画像判定の結果から、今回の学習内容では「写真そのものの構図=料理」と認識されることが多いような気がしました。そうなると、物体検知のような画像中の物体の位置から認識できるような学習に変更することで、より良い精度が期待できるような気がしました。なので、次回は物体検知用いた画像認識に挑戦してみたいと思います。