TensorFlowを用いた画像分類

# はじめに
この記事では、TensorFlowを用いて画像分類する方法を説明します。[こちら](https://www.kaggle.com/datasets/hasibalmuzdadid/shoe-vs-sandal-vs-boot-dataset-15k-images)のデータを利用して、ブーツ、サンダル、靴の画像分類を行います。なお、本記事ではGoogle Colaboratoryを使用することを前提にしています。

# ファイルのアップロードと解凍
[こちら](https://www.kaggle.com/datasets/hasibalmuzdadid/shoe-vs-sandal-vs-boot-dataset-15k-images)のデータをダウンロードできたら、Google Colaboratoryにアップロードします。(下記の写真を参考にして下さい。)
![1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1543066/9493009b-9ef1-9a5c-9d43-3a683b18488f.png)

アップロードが完了すると以下のようにzipファイルが表示されます。なお、アップロードにはそこそこ時間がかかります。
![2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1543066/782b61ac-dc9d-85eb-bef3-c2b43e0e1801.png)
以下のコードで、zipファイルを解凍します。
```py
import zipfile

local_zip = '/content/archive.zip'
zip_ref = zipfile.ZipFile(local_zip, 'r')
zip_ref.extractall()

zip_ref.close()
```
zipファイルを解凍すると、以下のようにShoe vs Sandal vs Boot Datasetフォルダが現れます。Shoe vs Sandal vs Boot Datasetフォルダの中を見ると、さらにBoot、Sandal、Shoeフォルダに分かれています。Bootフォルダの中にはブーツの写真、Sandalフォルダの中にはサンダルの写真、Shoeフォルダの中には靴の写真が格納されています。
![3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1543066/a78af19d-1c86-2ffc-b41a-89176952b327.png)

# 学習用データと評価用データに分ける
画像分類する機械学習モデルを作るにあたって、データを学習用データと評価用データに分ける必要があります。そこで、osモジュールを使って学習用データを入れるためのディレクトリと評価用データを入れるためのディレクトリを作成します。
```py
import os

os.makedirs('./training/Boot')
os.makedirs('./training/Sandal')
os.makedirs('./training/Shoe')
os.makedirs('./validation/Boot')
os.makedirs('./validation/Sandal')
os.makedirs('./validation/Shoe')
```
以下のようにtrainingフォルダとvalidationフォルダが作成されました。それぞれのフォルダはBoot、Sandal、Shoeフォルダに分かれています。
![4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1543066/46a34c16-4403-4a74-5106-7485d7707977.png)
Shoe vs Sandal vs Boot Datasetフォルダの中ｎ入っている画像ファイルをtrainingフォルダ、validationフォルダに移動させる必要があります。そこで以下のような関数を定義します。
```py
import random
from shutil import copyfile

def split_data(SOURCE_DIR, TRAINING_DIR, VALIDATION_DIR, SPLIT_SIZE):

  all_img_list = os.listdir(SOURCE_DIR) # SOURCE_DIR内にあるファイルをall_img_listに格納する
  all_img_random_list = random.sample(all_img_list, len(all_img_list)) # all_img_listの中にあるファイルをシャッフルして、all_img_random_listに格納する
  train_img_list = all_img_random_list[:int(len(all_img_list)*SPLIT_SIZE)] # SPLIT_SIZE=0.9の場合は、9割のファイルをtrain_img_listに格納する
  val_img_list = all_img_random_list[int(len(all_img_list)*SPLIT_SIZE):] # SPLIT_SIZE=0.9の場合は、残り1割のファイルをval_img_listに格納する

  for i in train_img_list:
    copyfile(os.path.join(SOURCE_DIR, i), os.path.join(TRAINING_DIR, i)) # train_img_listに格納されているファイルをSOURCE_DIRからTRAINING_DIRに移動させる
  
  for i in val_img_list:
    copyfile(os.path.join(SOURCE_DIR, i), os.path.join(VALIDATION_DIR, i)) # val_img_listに格納されているファイルをSOURCE_DIRからVALIDATION_DIRに移動させる
```
この関数は、SOURCE_DIRにあるファイルをSPLIT_SIZEの分だけTRAINING_DIRに移動させ、残りのファイルをVALIDATION_DIRに移動させる関数です。
この関数を利用して、データを学習用データと評価用データに分けます。
```py
Boot_SOURCE_DIR = "./Shoe vs Sandal vs Boot Dataset/Boot"
Sandal_SOURCE_DIR = "./Shoe vs Sandal vs Boot Dataset/Sandal"
Shoe_SOURCE_DIR = "./Shoe vs Sandal vs Boot Dataset/Shoe"

TRAINING_DIR = "./training"
VALIDATION_DIR = "./validation"

TRAINING_Boot_DIR = os.path.join(TRAINING_DIR, "Boot/")
VALIDATION_Boot_DIR = os.path.join(VALIDATION_DIR, "Boot/")

TRAINING_Sandal_DIR = os.path.join(TRAINING_DIR, "Sandal/")
VALIDATION_Sandal_DIR = os.path.join(VALIDATION_DIR, "Sandal/")

TRAINING_Shoe_DIR = os.path.join(TRAINING_DIR, "Shoe/")
VALIDATION_Shoe_DIR = os.path.join(VALIDATION_DIR, "Shoe/")

split_size = .9

split_data(Boot_SOURCE_DIR, TRAINING_Boot_DIR, VALIDATION_Boot_DIR, split_size)
split_data(Sandal_SOURCE_DIR, TRAINING_Sandal_DIR, VALIDATION_Sandal_DIR, split_size)
split_data(Shoe_SOURCE_DIR, TRAINING_Shoe_DIR, VALIDATION_Shoe_DIR, split_size)
```

# ImageDataGeneratorを使ってデータの前処理をする
前の章まででデータを学習用と評価用に分けることができましたが、このままモデリングすることはできず前処理をする必要があります。具体的には以下のような前処理が必要です。
・画像のデータは0~255の数値で構成されているため、255で割って正規化する必要がある。
・画像のサイズを一定にする必要がある。
・データを全て使って学習すると時間がかかりすぎるため、データをミニバッチという小さなグループに分割する必要がある。
・画像のデータに対してラベル付けを行う必要がある。(例えばブーツの画像データに対して、ブーツを表すラベルを対応づける必要がある。)
TensorFlowのImageDataGeneratorという機能を使えば、このような前処理を簡単に行うことができます。
```py
from tensorflow.keras.preprocessing.image import ImageDataGenerator

def train_val_generators(TRAINING_DIR, VALIDATION_DIR):
  train_datagen = ImageDataGenerator(rescale=1.0/255.0)

  train_generator = train_datagen.flow_from_directory(directory=TRAINING_DIR,
                                                      batch_size=20,
                                                      target_size=(150, 150))


  validation_datagen = ImageDataGenerator(rescale=1.0/255.0)

  validation_generator = validation_datagen.flow_from_directory(directory=VALIDATION_DIR,
                                                                batch_size=20,
                                                                target_size=(150, 150))
  return train_generator, validation_generator

train_generator, validation_generator = train_val_generators(TRAINING_DIR, VALIDATION_DIR)
```
ImageDataGeneratorを使うと、flow_from_directoryメソッドのdirectoryに指定されたディレクトリの配下にあるディレクトリ名に基づいてラベル付けを行います。例えば、directoryにtrainingディレクトリを指定した場合、その配下にあるBootディレクトリの中にあるファイルには[1, 0, 0]というラベルがつけられ、Sandalディレクトリの中にあるファイルには[0, 1, 0]というラベルがつけられ、Shoeというディレクトリの中にあるファイルには[0, 0, 1]というラベルがつけられます。画像データとラベルの関係を学習することが目標になります。

# モデリング
モデルは畳み込み層とプーリング層を3回繰り返した後、平滑化層を通って、最後に全結合層を2回繰り返す構造にします。ブーツとサンダルと靴の3種類に分類することが目標なので、最後の全結合層のニューロンの数は3つにする必要があります。また、最適化アルゴリズムはAdam、損失関数はcategorical_crossentropy、評価関数はaccuracyを指定します。
```py
import tensorflow as tf
from tensorflow.keras.optimizers import Adam

def create_model():
  model = tf.keras.models.Sequential([ 
    tf.keras.layers.Conv2D(16, (4,4), activation='relu', input_shape=(150, 150, 3)),
    tf.keras.layers.MaxPooling2D(2,2),
    tf.keras.layers.Conv2D(32, (4,4), activation='relu'),
    tf.keras.layers.MaxPooling2D(2,2), 
    tf.keras.layers.Conv2D(64, (4,4), activation='relu'), 
    tf.keras.layers.MaxPooling2D(2,2),
    tf.keras.layers.Flatten(),
    tf.keras.layers.Dense(512, activation='relu'), 
    tf.keras.layers.Dense(3, activation='softmax')  
  ])

  
  model.compile(optimizer=Adam(learning_rate=0.001),
                loss='categorical_crossentropy',
                metrics=['accuracy']) 

  return model

model = create_model()
```

# 学習
model.fitで学習を行います。
```py
history = model.fit(train_generator,
                    epochs=15,
                    verbose=1,
                    validation_data=validation_generator)
```
学習結果はhistoryに保存されており、属性にアクセスすることで学習用データ、評価用データに対する正解率と学習用データ、評価用データに対する誤差を確認することができます。
なお、japanize_matplotlibを使うことで日本語を表示できるようになります。japanize_matplotlibを使用するには、事前に!pip install japanize_matplotlibでインストールしておく必要があります。
```py
import matplotlib.pyplot as plt
import japanize_matplotlib

acc = history.history['accuracy']
val_acc = history.history['val_accuracy']
loss = history.history['loss']
val_loss = history.history['val_loss']

epochs = range(len(acc))

plt.plot(epochs, acc, 'r', label='学習用データの正解率')
plt.plot(epochs, val_acc, 'b', label='評価用データの正解率')
plt.title('正解率')
plt.legend()

plt.figure()

plt.plot(epochs, loss, 'r', label='学習用データの誤差')
plt.plot(epochs, val_loss, 'b', label='評価用データの誤差')
plt.title('誤差')
plt.legend()

plt.show()
```
学習用データ、評価用データともに良い精度が出せているようです。
![5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1543066/502ff21a-b899-9e1f-fa59-7630f3d6a15a.png)
![6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1543066/966b5015-7cc0-50b2-8934-591d6c1b38bd.png)

# モデルの保存と読み込み
model.saveでモデルを保存することができます。
```py
model.save('image-classification.h5')
```
さらに、load_modelを使って保存したモデルを読み込むことができます。
```py
model_load = tf.keras.models.load_model('image-classification.h5')
```
最後に、画像をアップロードして分類をしてみます。以下のコードを実行するとファイルの選択を求められるため、ブーツやサンダルの写真をアップロードして、きちんと分類できるか確かめて見て下さい！
```py
import numpy as np
from google.colab import files
from tensorflow.keras.utils import load_img, img_to_array

uploaded = files.upload()

for fn in uploaded.keys():
 
  path = '/content/' + fn
  img = load_img(path, target_size=(150, 150))
  x = img_to_array(img)
  x /= 255
  x = np.expand_dims(x, axis=0)

  classes = model_load.predict(x, batch_size=10)
  index = np.argmax(classes[0])
  if index == 0:
    print('Boot')
  elif index == 1:
    print('Sandal')
  elif index == 2:
    print('Shoe')
```

# おわりに
最後まで読んで下さりありがとうございました！