猫の品種判別

# はじめに
はじめまして、普段はフリーターとして働いている者です
今回は受講したAidemyさんの課題として
猫の品種判別アプリを作成したため
ブログの作成をいたします。

# 目次
1.環境と参考にしたサイト
2.画像データセットの準備
3.ライブラリ・クラスの定義
4.学習とモデルの定義
5.テストデータに対する結果を確認
6.実装
7.まとめ

# 1.環境と参考にしたサイト
### 環境
下記のサイトを参考に**Google Colaboratory**で作成しました

### 参考サイト
・[【ネコと機械学習4】Pytorchを使ったネコの品種分類器の実装](https://gogocatslife.com/20220813_catbreedclassifier/#i-2)
・[たったのこれだけ？！ゼロから作る画像認識AIアプリ【TensorFlow × Gradio】](https://www.youtube.com/watch?v=I-JtV2CNWlM)

# 2.画像データセットの準備
データセットはKaggleにあるOxfordのVGGが用意したデータセットを使いました。
・ [Cats and Dogs Breeds Classification Oxford Dataset](https://www.kaggle.com/datasets/zippyz/cats-and-dogs-breeds-classification-oxford-dataset)

ダウンロードしたデータセットをGoogleDriveにアップロードします
下記のコードでGoogleDriveをGoogle Colaboratoryにマウント（連携）します
```py
from google.colab import drive
drive.mount('/content/drive')
```
今回のアプリでは猫品種判別を行いたいのでPythonで一緒に入っている犬と猫のデータを猫の品種を分けるコードを実行し、**catbreed**というフォルダに格納します。
```py
# ネコの画像だけを集めるコード

# ネコの品種をリストアップ
catbreeds=[
    'Abyssinian',
    'Bengal',
    'Birman',
    'Bombay',
    'British_Shorthair',
    'Egyptian_Mau',
    'Maine_Coon',
    'Persian',
    'Ragdoll',
    'Russian_Blue',
    'Siamese',
    'Sphynx',
]

# ネコの画像のパスだけをリストに格納
import glob
imglist = []
for catbreed in catbreeds:
    imgs = glob.glob(f'/content/drive/MyDrive/images/{catbreed}*.jpg') #Google Colabで｢パスをコピー｣をして、glob関数内にCtrl+Vではりつける
    imglist += imgs

# imagesフォルダからネコの画像だけ複製してcatbreedsフォルダに移動
import shutil
new_dir_path = 'catbreeds'

for src in imglist:
    shutil.copy(src, new_dir_path)
```
コードを実行することで、作成した**catbreeds**フォルダにネコの品種12種それぞれ200データ、計2400データがコピーされます。

# 3.ライブラリ・クラスの定義
```py
#必要なライブラリ用意
import numpy as np
import random
import glob
from PIL import Image
import matplotlib.pyplot as plt
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
import torch.utils.data as data
import torchvision
from tqdm import tqdm
from torchvision import models, transforms, datasets
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report
import os
import time
```

乱数シードを固定します

```py
#シードを固定

torch.manual_seed(0)
np.random.seed(0)
random.seed(0)
```

画像前処理を行うためのクラスを定義します
Resize サイズを150px×150px
ToTensor 輝度の上限を 255 → 1 
```py
#データの前処理を行うクラスを定義

class ImageTransform():
    def __init__(self, resize, mean, std):
        self.data_transform = {
            'train': transforms.Compose([
                transforms.Resize((resize, resize)),
                transforms.RandomHorizontalFlip(p=0.5),
                transforms.RandomCrop(size =(150, 150), padding=18),
                transforms.ToTensor(),
            ]),
            'val': transforms.Compose([
                transforms.Resize((resize,resize)),
                transforms.ToTensor(),
            ]),
            'test' : transforms.Compose([
                transforms.Resize((resize,resize)),
                transforms.ToTensor(),
            ])
        }

    def __call__(self, img, phase):
        return self.data_transform[phase](img)
```

画像によってRGBが3つではなく4つになっているものがあるため
img = img.convert(“RGB”)と指定してチャンネルをRGBの3つに限定します。
```py
#データにネコの品種のラベル付けを行うクラスを定義

class CatBreedsDataset(data.Dataset):
    def __init__(self, file_list, phase, breeds,transform = None):
        self.file_list = file_list
        self.transform = transform
        self.phase = phase
        self.breeds = breeds

    def __len__(self):
        return len(self.file_list)

    def __getitem__(self, index):
        img_path = self.file_list[index]
        img = Image.open(img_path)
        img = img.convert("RGB") #RGBチャンネルに限定しておく

        img_trans = self.transform(
            img, self.phase)

        label = img_path.split('/')[-1].split('_')[0]
        if label in breeds.keys():
          label = breeds[label]
        else:
          label = 0


        return img_trans, label
```
品種のラベル付け
```py
breeds ={
    'Abyssinian':0,
    'Bengal':1,
    'Birman':2,
    'Bombay':3,
    'British':4,
    'Egyptian':5,
    'Maine':6,
    'Persian':7,
    'Ragdoll':8,
    'Russian':9,
    'Siamese':10,
    'Sphynx':11,
}
    
breed_paths={}

for key,value in breeds.items():
  breed_paths[key]= glob.glob(f'/content/drive/MyDrive/mystudy/20220806_catbreeds/catbreeds/{key}*.jpg')
```

# 4.学習とモデルの定義
### 4-1.学習
学習データは各品種200なので180を訓練と検証用にします
残りはテストデータにします
```py
#学習データの数を決める

Ntrain = 180
train_list = []

for key,value in breed_paths.items():
  train_list += value[:Ntrain]
```
```py
#学習データを訓練データと検証データに分けるためのパスのリストを定義

train_idx, valid_idx = train_test_split(range(len(train_list)), test_size = 0.25, random_state = 0)

train_path_list = []
val_path_list = []
for index in train_idx:
    i = train_list[index]
    train_path_list.append(i)

for index in valid_idx:
    i = train_list[index]
    val_path_list.append(i)
```
訓練と検証データに用いなかったデータをテストデータにします
```py
#テストデータの数を決める

test_path_list = []

for key,value in breed_paths.items():
  test_path_list += value[Ntrain:]
```
```py
#訓練、検証、テストデータの数を確認

len(train_path_list), len(val_path_list), len(test_path_list)
 #確認した数値 (1620, 540, 240)
```

**CatBreedsDataset**には、品種のラベルを格納した辞書(breeds)を引数に与えます。
```py
#パスのリストからデータセットを読み込む

size = 150
mean = (0.5,0.5,0.5)
std = (0.5,0.5,0.5)

train_dataset = CatBreedsDataset(
    file_list = train_path_list, transform = ImageTransform(size, mean, std), phase = 'train',breeds=breeds)

val_dataset = CatBreedsDataset(
    file_list = val_path_list, transform = ImageTransform(size, mean, std), phase = 'val',breeds=breeds)

test_dataset = CatBreedsDataset(
    file_list = test_path_list, transform = ImageTransform(size, mean, std), phase = 'test',breeds=breeds)
```
正しく読み込めているかの確認
```py
# 画像が正しく読み込めているか確認

breedsdict ={
    0:'Abyssinian',
    1:'Bengal',
    2:'Birman',
    3:'Bombay',
    4:'British_Shorthair',
    5:'Egyptian_Mau',
    6:'Maine_Coon',
    7:'Persian',
    8:'Ragdoll',
    9:'Russian_Blue',
    10:'Siamese',
    11:'Sphynx'
}

num =14 # 画像がランダムに並んでいるので番号を手動で選択

plt.rcParams["font.size"] = 18
plt.figure(figsize=(6, 6))
x, t = train_dataset[num]
exbreed = breedsdict[t]
plt.title(exbreed)
plt.axis('off')
plt.imshow(x.permute(1, 2, 0))
plt.savefig(f'/content/catbreeds{exbreed}.jpg')# プロットしたをファイルを保存する
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3231350/56f09764-9bda-6417-8f6f-8f40035ca2ea.png)
シャムが表示されました

```py
# データをまとめて確認する
plt.figure(figsize=(20, 20))
for n in range(54):
    x, t = train_dataset[n]
    plt.subplot(6, 9, n+1)
    plt.title(train_dataset[n][1])
    plt.axis('off')
    plt.imshow(x.permute(1, 2, 0))
plt.savefig('/content/catbreeds.jpg')
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3231350/0805a41d-65b9-6047-7173-7c32ee4b1482.png)

DataLoaderでデータをミニバッチに分けます。訓練データは1500、バッチサイズを20とすると、バッチ数は75となります。ミニバッチに分けることで学習が局所解に陥る可能性が低くなるらしいです
```py
#データローダーでデータをミニバッチに分ける

batch_size = 20
train_dataloader = data.DataLoader(
    train_dataset, batch_size=batch_size, shuffle = True)

val_dataloader = data.DataLoader(
    val_dataset, batch_size=batch_size, shuffle = False)

test_dataloader = data.DataLoader(
    test_dataset, batch_size=batch_size, shuffle = False)

dataloaders_dict = {'train': train_dataloader,
                    'val': val_dataloader}
```

### 4-2.モデル定義
畳み込みニューラルネットワーク **efficientnet-b7**を使います。
efficientnet-b7は、resnetなど既存モデルにおけるネットワークの
深さや広さなどのパラメータをバランスよく最適化したモデル

まずはGoogleColabolatoryでPytorchのefficientnetモデルを使うために、pip installします。
```py
!pip install efficientnet_pytorch
```

次にモデルをインスタンス化します。今回は12種類の分類なので、モデルの全結合層の出力を12にしておきます。また、学習率(lr)は1e-4ではなかなか学習が進まなかったので、1e-3としました
```py
#Efficientnetモデルのインスタンス化

from efficientnet_pytorch import EfficientNet

model = EfficientNet.from_pretrained('efficientnet-b7')
num_ftrs = model._fc.in_features
model._fc = nn.Linear(num_ftrs, 12) # 12クラスを分類
EPOCHS = 50
loss_fn = nn.CrossEntropyLoss()
optimizer = optim.SGD(model.parameters(), lr = 0.001, momentum=0.9)
scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=EPOCHS)
```

訓練と検証を行うための関数を定義します。pytorchは、訓練と検証を行うために、3重forのような構造を持った関数を定義する必要があり、pytorch lightningを使えばより簡潔に記述することができますが、pytorch lightningでは学習曲線の表示がTensorBoradでしかできなかったりします
```py
#訓練と検証を行う関数を定義

def train_model(model, dataloaders_dict, loss_fn, optimizer, epochs, scheduler):
    history = {'loss':[], 'acc':[], 'val_loss':[], 'val_acc':[]}
    device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
    model = model.to(device)
    for epoch in range(epochs):
        print(f'{epoch + 1} start')
        print('----------')

        for phase in ['train', 'val']:
            if phase == 'train':
                model.train()
            else:
                model.eval()

            epoch_loss = 0.0
            epoch_corrects = 0.0

            #if (epoch == 0) and (phase == 'train'):
            #    continue

            for inputs, labels in tqdm(dataloaders_dict[phase]):
                inputs = inputs.to(device)
                labels = labels.to(device)

                optimizer.zero_grad()

                with torch.set_grad_enabled(phase == 'train'):
                    outputs = model(inputs)
                    loss = loss_fn(outputs, labels)
                    pred_value, pred_label = torch.max(outputs, 1)

                    if phase == 'train':
                        loss.backward()
                        optimizer.step()

                    epoch_loss += loss.item() * inputs.size(0)
                    epoch_corrects += torch.sum(pred_label == labels.data)



            epoch_loss /= len(dataloaders_dict[phase].dataset)*1.0
            epoch_acc = epoch_corrects.double() / len(dataloaders_dict[phase].dataset)*1.0

            print(f'{phase} Loss: {epoch_loss :.4f} Acc : {epoch_acc :.4f}')
            if phase == 'train':
                history['loss'].append(epoch_loss)
                history['acc'].append(epoch_acc)
            else:
                history['val_loss'].append(epoch_loss)
                history['val_acc'].append(epoch_acc)

            if phase == 'train':
                scheduler.step()
    return history
```

学習を開始します。訓練データ1620と検証データ540を50エポックの学習を行うと46分程で終了します。
```py
#学習開始、学習曲線のためにhistoryに格納
%%time
history = train_model(model, dataloaders_dict, loss_fn, optimizer, EPOCHS, scheduler)

#100%|██████████| 81/81 [00:48<00:00,  1.68it/s]
#train Loss: 0.0606 Acc : 0.9895
#100%|██████████| 27/27 [00:07<00:00,  3.81it/s]
#val Loss: 0.5675 Acc : 0.8019
#50 start
#----------
#100%|██████████| 81/81 [00:47<00:00,  1.69it/s]
#train Loss: 0.0548 Acc : 0.9938
#100%|██████████| 27/27 [00:07<00:00,  3.81it/s]val Loss: 0.5667 Acc : 0.7981
#CPU times: user 34min 46s, sys: 9min 34s, total: 44min 21s
#Wall time: 46min 32s
```
学習済みモデルを保存しておきます。一度保存しておけば、テストしたいときにロードするだけです
```py
# 学習済みモデルの保存
modeldir = '/content/drive/MyDrive/'
savedparams = 'catbreedscls_1620data_efficientnet_50epochs_lr0001'
torch.save(model.state_dict(), modeldir+savedparams+'.mt')
```

学習曲線を描くための関数を定義します。
```py
# 学習曲線を描くための関数を定義

def plot_graph(values1, values2, rng, label1, label2, figname):
    plt.plot(range(rng), values1, label=label1)
    plt.plot(range(rng), values2, label=label2)
    plt.title(os.path.splitext(os.path.basename(figname))[0])
    plt.legend()
    plt.grid()
    plt.savefig(figname)
    plt.show()
```
loss curve を描画します
```py
#loss curveを描画

t_losses = history['loss']
t_accus = history['acc']
v_losses = history['val_loss']
v_accus = history['val_acc']

lossfigname = '_loss' + '.jpg'

plot_graph(t_losses, v_losses, EPOCHS, 'loss(train)', 'loss(validate)',modeldir+savedparams+lossfigname)
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3231350/f496bf71-33d6-d8bf-407f-fbf4245960e3.png)

learning curveを描画します
```py
#learning curveを描画

lcfigname = '_learningcurve' + '.jpg'

t_accus_np = [i.to('cpu').detach().numpy().copy() for i in t_accus]
v_accus_np = [i.to('cpu').detach().numpy().copy() for i in v_accus]

plot_graph(t_accus_np, v_accus_np, EPOCHS, 'acc(train)', 'acc(validate)',modeldir + savedparams + lcfigname)
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3231350/428a6ef1-25c1-cfc1-dba3-0a7f5e49c7d1.png)

efficientnet-b7を使ったところ、検証データに対する正解率は80%程になりました
データ拡張などすればもっと高い精度が出せそうです。

# 5.テストデータに対する結果を確認
テストデータに対する結果を計算するので、GPUに接続していなくても大丈夫です。
```py
# ネットワークの準備

model = EfficientNet.from_pretrained('efficientnet-b7').cpu().eval()
num_ftrs = model._fc.in_features
model._fc = nn.Linear(num_ftrs, 12)
# Loaded pretrained weights for efficientnet-b7
```
```py
# 重みの読み込み
modeldir = '/content/drive/MyDrive/'
savedparams = 'catbreedscls_1620data_efficientnet_50epochs_lr0001'
model.load_state_dict(torch.load(modeldir+savedparams+ '.mt', map_location=torch.device('cpu')))
# <All keys matched successfully>
```

```py
#modelを評価状態にする
model.eval()

#テストデータセットを読み込み
# x : 画像、 t : ラベル
x, t = test_dataset[1]

# NNによる出力値の算出
y = model(x.unsqueeze(0))
print(y)
# tensor([[ 7.6970e+00,  6.9904e-01, -2.5003e+00,  6.5394e-01, -9.9504e-01,
#         -1.3742e+00, -1.4093e+00, -2.0889e+00, -2.1842e+00, -1.5930e-01,
#         4.7276e-03,  1.1452e+00]], grad_fn=<AddmmBackward0>)
# 今回は12クラスの分類なので、モデルの出力層から12個の数値が渡されているのがわかります。

# ソフトマックス関数で確率に変換
y = F.softmax(y)
y
# tensor([[9.9542e-01, 9.0959e-04, 3.7101e-05, 8.6948e-04, 1.6715e-04, 1.1441e-04,
#         1.1046e-04, 5.5983e-05, 5.0892e-05, 3.8554e-04, 4.5427e-04, 1.4210e-03]],
#      grad_fn=<SoftmaxBackward0>)
# NNからの出力をソフトマックス関数に入力すると、12品種の予測確率が返ってきます。
```
テストデータに対する結果を表示します。
```py
# テストデータに対する結果を表示

prob = []
prdbreed = []
pred = []
Y = []
for i, (x,y) in enumerate(test_dataloader):
    with torch.no_grad():
        output = model(x)
    #prob += [i.to('cpu').detach().numpy().copy()[0] for i in output]
    #prob += [round(F.softmax(l)[0].to('cpu').detach().numpy().copy()*100,1) for l in output]
    prdbreed += [torch.argmax(F.softmax(l)).to('cpu').detach().numpy().copy() for l in output]
    prob += [round(torch.max(F.softmax(l)).to('cpu').detach().numpy().copy()*100,1) for l in output]

    pred += [int(l.argmax()) for l in output]
    Y += [int(l) for l in y]

print(classification_report(Y, pred))

#              precision    recall  f1-score   support
#
#           0       0.59      0.85      0.69        20
#           1       0.84      0.80      0.82        20
#           2       0.71      0.85      0.77        20
#           3       1.00      0.95      0.97        20
#           4       0.86      0.60      0.71        20
#           5       0.90      0.90      0.90        20
#           6       0.93      0.65      0.76        20
#           7       0.83      1.00      0.91        20
#           8       0.77      0.50      0.61        20
#           9       0.73      0.80      0.76        20
#          10       0.86      0.95      0.90        20
#          11       0.90      0.90      0.90        20

#    accuracy                           0.81       240
#   macro avg       0.83      0.81      0.81       240
#weighted avg       0.83      0.81      0.81       240
```
正解率80%であることがわかります、
ネコの品種によって適合率と再現率、F値が異なっています。
ラベル3のボンベイのF値が1であり、ラベル8のラグドールのF値が0.61と最も低くなっています。

各画像に対する予測結果を表示します
```py
# テストデータに対する予測結果をまとめて表示

plt.rcParams["font.size"] = 18
plt.figure(figsize=(20, 20))
for n in range(12):
    x, t = test_dataset[n]
    plt.subplot(3, 4,n+1)
    plt.title(str(prob[n])+'%')
    plt.title('{} {}%'.format(breedsdict[int(prdbreed[n])],prob[n]))
    plt.axis('off')
    plt.imshow(x.permute(1, 2, 0))
plt.savefig('/content/drive/MyDrive/mystudy.jpg')
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3231350/634f5a5c-58ea-6d01-7cfc-8303fee5df76.png)
ほとんどの画像は正しく分類できています

# 6.実装
判別アプリの実装です
今回は**Gradio**で実装していきます
Gradioを使うためにpip installします
```py
!pip install gradio
```
```py
import torchvision.transforms.functional as F #PyTorchで画像をリサイズ
import gradio as gr

@torch.no_grad()
def predict(img):
    img = Image.fromarray(img.astype("uint8"), "RGB")

    # 前処理
    img = F.resize(img, (150,150)) #高さと幅を指定
    img = F.to_tensor(img)
    img = img.unsqueeze(0)


    # 推論
    output = model(img).squeeze(0)
    probs = nn.functional.softmax(output, dim=0).numpy()

    # ラベルごとの確率をdictとして返す
    return {breedsdict[i]: float(probs[i]) for i in range(len(catbreeds))}

demo = gr.Interface(fn=predict, inputs=gr.inputs.Image(), 
outputs=gr.outputs.Label(num_top_classes=len(catbreeds)))#リストの変数名を指定し長さを取得
    
if __name__ == "__main__":
    demo.launch(share=True)
```

アプリ [URL](https://136e67302a3a1f710d.gradio.live)

# 7.まとめ
今回、何を作成すればいいか思いつかず講師の方との技術カウンセリングで相談させていただき
私が猫を飼っていたので、猫の品種判別を作成しました
分からないところが多すぎて何回も講師の方々に助けていただき完成しました。

特に最後のアプリ実装の時に私が課題にないGradioを選んでしまったためかなり難航しました
今後は、少しずつプログラミングの知識を増やしていきたいと思っています。