【ネコと機械学習2】Pytorchを使ったネコ判別器の作り方

PytorchとGoogleColabを使った実装について

先日、ネコ判別器の記事を書きました(【ネコと機械学習】ネコ判別器で我が家の4ニャンズがネコである確率を計算してみた)

本記事では、PytorchとGoogle Colabolatoryを使ってネコ判別器をつくるための説明をしていきたいと思います。

環境 : GoogleColabolatoryを使う

ネコ判別器を作るためのプログラミングはGoogle Colaboratoryで行いました。GoogleColabolatoryはGoogleのアカウントを持っていれば、無料で利用することができます。GoogleColabolatoryではJupyterNotebookでPythonを記述できるためとても便利です。


また、GoogleColabolatoryでは、無料でGPUを使うことができるので、画像分類を勉強したい初心者にはもってこいです。無料のため使用量に制限がありますので、上限に達したら時間をおいて再接続しましょう。


GoogleColabolatoryにはデータを保存することはできないため、Google Driveをマウントしてファイルのアップロードや保存を行います。GoogleDriveもGoogleアカウントがあれば無料で使えます。Google様に感謝…!

データセットの用意 : Kaggleのdogs-vs-catsを使う

イヌとネコの画像はKaggleのdogs-vs-catsを使います。もしかするとユーザー登録が必要かもしれません。
https://www.kaggle.com/c/dogs-vs-cats/data
trainフォルダには犬猫それぞれ12500枚、合計25000枚の画像が、testフォルダには10000枚の画像があります。このファイルをGoogleDrive上にアップロードしておきます。

Pytorchによるネコ判別器のコード

ライブラリのインストールやデータセットの読み込み

必要なライブラリをインストールします。GoogleColabではこれらのライブラリが標準でインストール済みなので、「pip install –」は今回は必要ありません。

#必要なライブラリ用意
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

乱数シードを固定します。

#シードを固定

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

画像前処理を行うためのクラスを定義します。今回扱う全てのデータは以下の処理を行います。
Resize : サイズを150px×150pxにリサイズ
ToTensor : 輝度の上限を 255 → 1 にする

#データの前処理を行うクラスを定義

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)

データにネコとイヌのラベル付けを行うクラスを定義します。。

#データにネコ=0、イヌ=1のラベル付けを行うクラスを定義

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

KaggleからGoogleDriveに保存しておいた「dog-vs-cats」内の「train」フォルダ内にある画像データを読み込み、パスをリストとして格納します。今回はネコとイヌの画像を分けてパスを取得します。
glob関数内のtrainフォルダまでのパスは、Google Colab上で「パスをコピー」をクリックして、glob関数内にCtrl+Vではりつけることで取得できます。

#ネコとイヌの画像を分けてロード

train_list_cat = glob.glob('/content/drive/MyDrive/dogs-vs-cats/train/cat.*')
train_list_dog = glob.glob('/content/drive/MyDrive/dogs-vs-cats/train/dog.*')

学習に使うデータの数を決めます。Kaggleのデータはネコとイヌそれぞれ12500枚、合計25000枚ありますが、全てを使うと計算に時間がかかってしまうので、それぞれ1000枚ずつ、合計2000枚を学習に使うデータとしたいと思います。
また学習に使うデータは訓練データと検証データにわけます。今回は訓練データ数:検証データ数=75%:25%となるように分けました。

#学習に使うデータの数を決める

Ntrain = 1000
train_list = train_list_cat[:Ntrain]+train_list_dog[:Ntrain]

#学習データを訓練データと検証データに分けるためのパスのリストを定義

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)

次にテスト用のデータを作成します。Kaggleのデータセットにも「test」フォルダがありますが、Kaggleにsubmitする際のテストを行うためのデータセットであるため、画像に正解ラベルがついていません。そのため、ここでは「train」フォルダ内のデータをテストデータとして扱うようにします。

# 学習データからテストデータを生成するためのパスのリストを定義
# (Kaggleのテストデータには正解ラベルがないため)

Ntest = 6
test_path_list = train_list_cat[Ntrain:Ntrain+Ntest]+train_list_dog[Ntrain:Ntrain+Ntest]

# 訓練、検証、テストデータの数を確認

len(train_path_list), len(val_path_list), len(test_path_list)

訓練、検証、テストデータをそれぞれ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')

dataset内の各データはPytorchのTensor型であり、データの形が(channel:0, width:1, height:2)でありこのままではmatplotlibで表示するためには、permuteメソッドによって(height:0, width:1, channel:2)に変更します

# 画像が正しく読み込めているか確認

plt.imshow(test_dataset[3][0].permute(1,2,0));

データをまとめて確認します

# データをまとめて確認する
plt.figure(figsize=(10, 10))
for n in range(12):
    x, t = test_dataset[n]
    plt.subplot(3, 4, n+1)
    plt.title(test_dataset[n][1])
    plt.axis('off')
    plt.imshow(x.permute(1, 2, 0))

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

#データローダーでデータをミニバッチに分ける

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層の隠れ層をもつニューラルネットワークを作成しました。畳み込みとプーリングを4回行うCNN(Convolutional Neural Network)です。
各隠れ層は以下の処理を行っています。
conv : 畳み込み
relu : relu関数
BatchNorm2d : バッチ正規化処理
max_pool2d : 最大値pooling

# モデルの定義
# 4層の隠れ層をもつニューラルネットワークを定義

class Net(nn.Module):
    def __init__(self):
        super().__init__()
        self.conv = nn.Conv2d(in_channels=3, out_channels=32, kernel_size=3, padding=1)
        self.conv2 = nn.Conv2d(in_channels=32, out_channels=64, kernel_size=3, padding=1)
        self.conv3 = nn.Conv2d(in_channels=64, out_channels=128, kernel_size=3, padding=1)
        self.conv4 = nn.Conv2d(in_channels=128, out_channels=256, kernel_size=3, padding=1)
        self.bn1 = nn.BatchNorm2d(32)
        self.bn2 = nn.BatchNorm2d(64)
        self.bn3 = nn.BatchNorm2d(128)
        self.bn4 = nn.BatchNorm2d(256)
        self.fc = nn.Linear(256*9*9, 2)

    def forward(self, x):
        h = self.conv(x)
        h = F.relu(h)
        h = self.bn1(h)
        h = F.max_pool2d(h, kernel_size=2, stride=2)
        h = self.conv2(h)
        h = F.relu(h)
        h = self.bn2(h)
        h = F.max_pool2d(h, kernel_size=2, stride=2)
        h = self.conv3(h)
        h = F.relu(h)
        h = self.bn3(h)
        h = F.max_pool2d(h, kernel_size=2, stride=2)
        h = self.conv4(h)
        h = F.relu(h)
        h = self.bn4(h)
        h = F.max_pool2d(h, kernel_size=2, stride=2)
        h = h.view(-1, 256*9*9)
        h = self.fc(h)
        return h

モデルのインスタンス化を行います。学習回数(epoch数)は多いほど収束しやすいですが、時間との兼ね合いで決めます。Colabでは長時間操作がないと実行が止まったり、深夜は他のユーザーの使用量も増えるなどで実行が中断されることが多いので、計算時間は長くなりすぎない方が良いと思います。
lrは学習率で誤差逆電波の際の勾配にかかる係数です。大きいとなかなか収束する方向に向かえないので、0.0001など小さい方が良い様です。

# モデルのインスタンス化

model = Net()
EPOCHS = 200
loss_fn = nn.CrossEntropyLoss()
optimizer = optim.SGD(model.parameters(), lr = 0.0001, momentum=0.9)
scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=EPOCHS)

訓練と検証を行うための関数を定義します。pytorchの難点は、訓練と検証を行うために、3重forのような構造を持った関数を定義する必要があり、初心者にはハードルが高いことです。pytorch lightningを使えばより簡潔に記述することができますが、pytorch lightningでは学習曲線の表示がTensorBoradでしかできなかったりと自由度が小さくなる部分もあるため、一長一短です。

#訓練と検証を行う関数を定義

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

学習を開始します。1500データの学習と500データの検証を200エポック学習を行うと40分程かかります。

#学習開始。学習曲線のためにhistoryに格納
%%time
history = train_model(model, dataloaders_dict, loss_fn, optimizer, EPOCHS, scheduler)
実行時間。上記コードを実行したときのものではないので注意

学習済みモデルを保存しておきます。一度保存しておけば、テストしたいときにロードするだけでOKです。

# 学習済みモデルの保存
modeldir = '/content/drive/MyDrive/mystudy/20220726_forblog/'
savedparams = 'dogcat_kaggle2000data_mynn_layer2'
torch.save(model.state_dict(), modeldir+savedparams+'.mt')

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

# 学習曲線を描くための関数を定義

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 を描画します。200エポック学習を行うことで損失関数が小さくなっていることがわかります。

#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)

learning curveを描画します。accuracyは正解率であり、200エポックの学習によって、テストデータに対してはおよそ90%の正解率であることがわかります。

#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)

モデルのロードとテスト

これ以降は作成したモデルを使ってテストを行っていきます。これ以降の処理はCPUで行うため、GPUに接続していなくても問題ありません。(接続していても問題ありませんが、modelをcpuで読み込むのを忘れないでください)
以下のコードを実行する際は、modeldirが学習時に保存したものと同じかどうか注意してください。

# ネットワークの準備
model = Net().cpu().eval()

# 重みの読み込み
modeldir = '/content/drive/MyDrive/mystudy/20220726_forblog/'
savedparams = 'dogcat_kaggle2000data_mynn_2'
model.load_state_dict(torch.load(modeldir+savedparams+ '.mt', map_location=torch.device('cpu')))

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

kaggleのテストデータを一つだけ読み込み、画像データとラベルを格納します。画像データを読み込んだモデルに入力することで、NNによる出力値が変数yに格納されます。

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

# NNによる出力値の算出
y = model(x.unsqueeze(0))
print(y)
# --> tensor([[ 1.6474, -1.0637]], grad_fn=<AddmmBackward0>)

yはNNの出力値であり、ソフトマックス関数に渡すことでネコとイヌの確率を計算してくれます。

# ソフトマックス関数で確率に変換
y = F.softmax(y)
y
# --> tensor([[0.9377, 0.0623]], grad_fn=<SoftmaxBackward0>) 94% ネコ

テストデータに対する結果を表示します。今回作成したモデルは単純な構造ですが、75%程の正解率があるそうです。

# テストデータに対する結果(混同行列)を表示

prob = []
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]
    pred += [int(l.argmax()) for l in output]
    Y += [int(l) for l in y]

print(classification_report(Y, pred))

テストデータの画像とネコである確率値を合わせて出力します。

# テストデータに対する予測結果をまとめて表示
plt.rcParams["font.size"] = 18
plt.figure(figsize=(20, 20))
for n in range(len(test_dataset)):
    x, t = test_dataset[n]
    plt.subplot(3, 4,n+1)
    #plt.title(train_dataset[n][1])
    plt.title(str(prob[n])+'%')
    plt.axis('off')
    plt.imshow(x.permute(1, 2, 0))
plt.savefig('/content/drive/MyDrive/mystudy/20220726_forblog/nyans.jpg')

大きくはっきりと写っている画像は高い確率で分類出来ていますが、小さく写っている画像などがうまく正解できていないのがわかります。

自分で用意した画像のテスト(我が家の4ニャンズを判別)

ここからは、自前画像の読み込み方法を書いていきます。我が家の4ニャンズの画像でテストしてみます。
MyDrive直下にフォルダを作り、その中に「cat.1.jpg」「cat.2.jpg」と、Kaggleの訓練データと同じ感じで名前をつけておくことで、今回作成したプログラムをそのまま使うことが出来ます。

# 自前の画像をロード
mycat_list = glob.glob('/content/drive/MyDrive/mystudy/20220726_forblog/mycat/cat.*')

# リサイズなど画像の前処理
mytest_dataset = DogCatDataset(
    file_list = mycat_list, transform = ImageTransform(size, mean, std), phase = 'test')

mytest_dataloader = data.DataLoader(
    mytest_dataset, batch_size=batch_size, shuffle = False)

自前のテストデータに対する結果を表示します。

# 自前のテストデータに対する結果を表示

prob = []
pred = []
Y = []
for i, (x,y) in enumerate(mytest_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]
    pred += [int(l.argmax()) for l in output]
    Y += [int(l) for l in y]

print(classification_report(Y, pred))
# 自前テストデータに対する予測結果をまとめて表示
plt.rcParams["font.size"] = 18
plt.figure(figsize=(20, 20))
for n in range(len(mytest_dataset)):
    x, t = mytest_dataset[n]
    plt.subplot(1, 4,n+1)
    #plt.title(train_dataset[n][1])
    plt.title(str(prob[n])+'%')
    plt.axis('off')
    plt.imshow(x.permute(1, 2, 0))
plt.savefig('/content/drive/MyDrive/mystudy/20220726_forblog/mycat.jpg')

今回は4ニャンズみんなネコと分類されました。前回はシエル君だけイヌと分類されてしまいましたが、今回のモデルでは100%ネコと分類されました。シエル君、よかったね(=・ω・=)

今後について

今回は、私が自分で定義した単純な構造のニューラルネットワークでネコ判別器を作ったのですが、正解率80%程度とそれなりに高い精度が出せてしまうのが現代技術の凄い所だと感心しました。

より簡単に高い精度を出すためには、既に先人が作ったモデルを活用する「ファインチューニング」を行うのが有効です。私は2022年8月下旬にE資格の受験を控えており、E資格のシラバスによると画像認識ではResNetやEfficientNetが出題されるようなので、これらについて体験し、余裕があればまた記事を書きたいと思います。

ここまで読んでくださってありがとうございます。参考になれば幸いです。

参考サイト様

本ページのコードは以下のサイト様を参考に作成させて頂きました。

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

【Keras】ディープラーニング【犬猫判別1】

プログラムを関数にまとめて、実行結果をグラフにプロットしよう

PyTorchで学習済みモデルを元に自前画像をtrainしてtestするまで

1 COMMENT

さんちゃん

シエルくんもちゃんと(?)ネコで良かったですね!
なかなかすごい技術です^ ^

返信する

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

CAPTCHA