2
2

More than 3 years have passed since last update.

犬と猫の分類をPytorchでファインチューニングをしてやってみた

Last updated at Posted at 2021-09-15

はじめに

コチラの書籍でPytorchの勉強をしているのですが、実際に使わないと理解できないと思ったので、Kaggleの犬猫コンペをPytorchを使ってやってみた記録です。

モデルにはefficientnetB7を使ってファインチューニングを行いました。

環境

Google colabを使います。

Pytorch: 1.9.0+cu102
python: 3.7.11

実装の流れ

1.データの用意
2.データの前処理
3.データセットの作成
4.データローダーの作成
5.モデルの構築
6.損失関数、最適化アルゴリズムの定義
7.学習・検証
8.テストデータで推論

となります。1つずつ紹介します。

1. データの用意

今回の犬猫コンペの画像データは容量が重いのでGoogleDriveにアップロードするのもそこからデータ読み込ませるのも時間がかかります。

ですのでcolabのcontent直下に直接Kaggle APIを利用してデータをダウンロードします。

from google.colab import files

uploaded = files.upload()

for fn in uploaded.keys():
  print('User uploaded file "{name}" with length {length} bytes'.format(
      name=fn, length=len(uploaded[fn])))

# Then move kaggle.json into the folder where the API expects to find it.
!mkdir -p ~/.kaggle/ && mv kaggle.json ~/.kaggle/ && chmod 600 ~/.kaggle/kaggle.json

上記コードを実行すると
jsonファイルをアップロードするように言われるのでkaggle.jsonファイルをアップロードします。

Saving kaggle.json to kaggle.json
User uploaded file "kaggle.json" with length 63 bytes

こんな表示がされればokです。

!kaggle competitions download -c dogs-vs-cats-redux-kernels-edition

コンペのDataのページに表示されているコードをそのまま実行すればデータをダウンロードしてくれます。

ただこのままだとzipファイルのままなのでzipファイルを解凍する必要があります。

import zipfile

with zipfile.ZipFile('/content/train.zip') as existing_zip:
    existing_zip.extractall('/content/train_img/')

with zipfile.ZipFile('/content/test.zip') as existing_zip:
    existing_zip.extractall('/content/test_img/')

zipfileモジュールを利用して、train_imgとtest_imgという名前で解凍してます。


データセットの用意の前に必要なものをimportしておきます

import numpy as np
import random
import glob
from PIL import Image
import torch
import torch.nn as nn
import torch.optim as optim
import torch.utils.data as data
import torchvision
from tqdm import tqdm
from torchvision import models, transforms

from sklearn.model_selection import train_test_split

また再現性を担保するため乱数シードも設定します。

torch.manual_seed(46)
np.random.seed(46)
random.seed(46)

櫻坂46が好きなので、乱数シードはいつも46です。

2. データの前処理

前処理を行うImageTransformクラスを定義します。

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(),
                transforms.Normalize(mean,std)
            ]),
            'val': transforms.Compose([
                transforms.Resize((resize,resize)),
                transforms.ToTensor(),
                transforms.transforms.Normalize(mean, std)
            ]),
            'test' : transforms.Compose([
                transforms.Resize((resize,resize)),
                transforms.ToTensor(),
                transforms.transforms.Normalize(mean, std)
            ])
        }

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

trainデータ、valデータ、testデータそれぞれに行う前処理の内容をComposeを使ってまとめ、辞書化しております。

trainデータに関しては150×150にリサイズ、ランダム水平反転、ランダムクロップ、テンソル化、正規化を行なっています。

valデータとtestデータに関しては、水増しは行わず、リサイズとテンソル化、正規化だけ行います。

3.データセットの用意

次にDatasetを作成するDogCatDatasetクラスを定義します。

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

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

    def __getitem__(self, index):
        img_path = self.file_list[index]
        img = Image.open(img_path)

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

        label = img_path.split('/')[-1].split('.')[0]

        if label == 'cat':
            label = 0
        elif label == 'dog':
            label = 1

        return img_trans, label

最終的なスコア提出の際に、犬である確率を提出する必要があるので、ラベルはcatを0,dogを1としています。

普段、画像の読み取りには、cv2を使ってたのでPILのImageに慣れるのに苦労しました。


実際に定義したクラスを使ってデータセットを作成していきます。

train_list = glob.glob('/content/train_img/train/*')
test_path_list = glob.glob('/content/test_img/test/*')

trainデータとtestデータの画像パスを全部取得してリスト化します。

trainデータのうち25%を検証データとするため、画像パスをtrain_test_splitを使って分割します。
分けられたインデックスを使って、trainとvalの画像パスをリスト化します。

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

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)

画像のインデックス番号を分ける使い方は初めてだったのでちょっと感動しました。

len(train_path_list), len(val_path_list), len(test_path_list)
# -> (18750, 6250, 12500)

データの枚数をチェックしておきましょう。

さて画像パスをリスト化できたので、このリストを元にdatasetを作ります。

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

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

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

test_dataset = DogCatDataset(
    file_list = test_path_list, transform = ImageTransform(size, mean, std), phase = 'test')

今回全画像データの平均や標準偏差を計算するのがめんどくさかったので良くみられる、全て0.5で処理しています。

4. データローダーの作成

datasetが作成できれば、ここは簡単です。

batch_size = 64
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,
                    'test': test_dataloader}

後でdataloaderを簡単に取り出しやすいようにまとめて辞書化しておきます。

5. モデル構築

まずPytorchでeffiientnetを使うために学習済みモデルをインストールします

!pip install efficientnet_pytorch

Efficientnet-b7をインスタンス化します。

from efficientnet_pytorch import EfficientNet

model = EfficientNet.from_pretrained('efficientnet-b7')

最後の全結合層を今回は2値分類なので出力ユニットを変更します。

num_ftrs = model._fc.in_features
model._fc = nn.Linear(num_ftrs, 2)

6. 損失関数、最適化アルゴリズムの定義

loss_fn = nn.CrossEntropyLoss()

optimizer = optim.SGD(model.parameters(), lr = 0.1, momentum=0.9)

scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=EPOCHS)

損失関数はCrossEntropyLoss
最適化アルゴリズムはファインチューニングだとモーメンタム付きSGDが良さげとのことなので使用。
また学習率スケジューラーも使用します。

7. 学習・検証

学習を行う関数を定義します。

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)

            if phase == 'train':
                scheduler.step()

            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)

Pytorchだと学習時に必要な情報を自分でカスタマイズできるので、好きですね。

じゃ学習を実行します。

EPOCHS = 100

train_model(model, dataloaders_dict, loss_fn, optimizer, epochs=EPOCHS, scheduler)

100エポック終わらせるのに7時間くらいかかりました。

8. テストデータで推論

labels = []
ids = []
cat_preds = []
dog_preds = []
with torch.no_grad():
    for image_path in tqdm(test_path_list):
        img = Image.open(image_path)
        test_transformer = ImageTransform(size, mean, std)
        img = test_transformer(img, phase = 'test')
        img = img.unsqueeze(0)
        img = img.to(device)
        model.eval()
        output = model(img)
        pred = nn.functional.softmax(output, dim = 1)[:].tolist()
        cat_preds.append(pred[0][0])
        dog_preds.append(pred[0][1])
        ids.append(int(image_path.split('/')[-1].split('.')[0]))

このうちのdog_predsリストに犬である確率が格納されているのでこれを提出します。

import pandas as pd

submit = pd.DataFrame({'id': ids,
                       'label': dog_preds})


submit.sort_values(by='id', inplace=True)

submit.reset_index(drop=True, inplace=True)

submit.to_csv('/content/drive/MyDrive/dog vs cat/dogs-vs-cats-redux-kernels-edition/submission8.csv', index=False)

スコア⇩

スクリーンショット 2021-09-16 2.05.23(2).png

まとめ

普段Tensorflowを使ってますが、Pytorchだとコードは長くなる分、丁寧に記述していくので、理解が深まるなと思った。他の深層学習(GAN、物体検出など)もPytorchを使って実装していきたい。

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2