可愛い鳥とかっこいい鳥をResNet50(pytorch)で分類してみた
画像認識の勉強のためにResNet50を使った鳥の二値分類を試してみました。
感覚としてどれくらいのデータがあれば精度の良い識別器になるのか、ということが気になっていたので、pretrain済みのモデルを使用した場合/使用していない場合で精度を比較してみました。
※プログラムはこちらの記事を参考にさせていただきました。
検証条件
- Python Version:3.9
- フレームワーク:Pytorch 1.11.0
- 学習アルゴリズム:ResNet50
- 学習データ:「可愛い鳥」と「かっこいい鳥」の二種類
- 事前学習モデル:PytorchのResNet50で利用できるものを使用
利用するPythonモジュールは以下の通り↓
import torch
import torchvision.models as models
from torchvision import transforms
import os
from PIL import Image
import numpy as np
import matplotlib.pyplot as plt
import torch.utils.data as data
import torch.nn as nn
import torch.optim as optim
import random
今回のお題
今回は「可愛い鳥」と「かっこいい鳥」の二値分類をお題として選定しました。


主にOpen Images Datasetというフリーのデータセットから抽出してきたものを使っています。学習と検証に用いたデータ量は以下の通りです。
データ種別 | 画像数 |
---|---|
可愛い鳥 | 1253枚 |
かっこいい鳥 | 1438枚 |
お試しサイト ※2022/9/18追記
実際に画像の判定を試せるサイトを公開しました!↓
http://react-demo-dataflake.s3-website-ap-northeast-1.amazonaws.com
※コスト削減のため裏で動かしているAWS Sagemakerをserverlessにしているため、1回目の判定時にエラーが出やすくなっています。もう一度判定ボタンを押せば正常に判定されると思います。
データセットの準備
可愛い鳥とかっこいい鳥のデータはそれぞれフォルダを分けて保存します。
./
├── 01_train #学習データを格納
│ ├── 01_kawaii #可愛い鳥データ
│ └── 02_kakkoii #かっこいい鳥データ
├── 02_machine_learning #学習や予測用スクリプトを格納
└── 03_testset #予測の際に使うテストセットを格納
学習データと検証データの作成
データセットを学習データと検証データに分けます。8割を学習データに回すよう記載していますが、22行目をいじれば、学習と検証データの割合値を変えられます。
def make_filepath_list(file_path):
"""
学習データ、検証データそれぞれのファイルへのパスを格納したリストを返す
Returns
-------
train_file_list: list
学習データファイルへのパスを格納したリスト
valid_file_list: list
検証データファイルへのパスを格納したリスト
"""
train_file_list = []
valid_file_list = []
for top_dir in os.listdir(file_path):
file_dir = os.path.join(file_path, top_dir)
file_list = os.listdir(file_dir)
random.shuffle(file_list)
# 各データごとに8割を学習データ、2割を検証データとする
num_data = len(file_list)
num_split = int(num_data * 0.8)
train_file_list += [os.path.join(file_path, top_dir, file).replace('\\', '/') for file in file_list[:num_split]]
valid_file_list += [os.path.join(file_path, top_dir, file).replace('\\', '/') for file in file_list[num_split:]]
return train_file_list, valid_file_list
#---------------データの前処理---------------#
# 画像データへのファイルパスを格納したリストを取得する
file_path = '../01_train/'
train_file_list, valid_file_list = make_filepath_list(file_path)
print('学習データ数 : ', len(train_file_list))
print('検証データ数 : ', len(valid_file_list))
前処理
画像を扱う機械学習では前もって画像サイズを整えたり、データオーグメンテーションで画像の反転、回転などで汎用性を高めるのが一般的です。
class ImageTransform(object):
"""
入力画像の前処理クラス
画像のサイズをリサイズする
Attributes
----------
resize: int
リサイズ先の画像の大きさ
mean: (R, G, B)
各色チャンネルの平均値
std: (R, G, B)
各色チャンネルの標準偏差
"""
def __init__(self, resize, mean, std):
self.data_trasnform = {
'train': transforms.Compose([
# データオーグメンテーション
transforms.RandomHorizontalFlip(),
# 画像をresize×resizeの大きさに統一する
transforms.Resize((resize, resize)),
# Tensor型に変換する
transforms.ToTensor(),
# 色情報の標準化をする
transforms.Normalize(mean, std)
]),
'valid': transforms.Compose([
# 画像をresize×resizeの大きさに統一する
transforms.Resize((resize, resize)),
# Tensor型に変換する
transforms.ToTensor(),
# 色情報の標準化をする
transforms.Normalize(mean, std)
])
}
def __call__(self, img, phase='train'):
return self.data_trasnform[phase](img)
# 動作確認
img = Image.open('../01_train/01_kawaii/3016821299a39ec3.jpg')
# リサイズ先の画像サイズ
resize = 300
# 今回は簡易的に(0.5, 0.5, 0.5)で標準化
mean = (0.5, 0.5, 0.5)
std = (0.5, 0.5, 0.5)
transform = ImageTransform(resize, mean, std)
img_transformed = transform(img, 'train')
plt.imshow(img)
plt.show()
plt.imshow(img_transformed.numpy().transpose((1, 2, 0)))
plt.show()
注意点:データに白黒画像が混じっていると動かない
カラー画像を前提にしたプログラムなので、データに白黒画像が混じっているとエラーが出てうまく動きません。白黒画像をカラー化する処理を事前に走らせてください。
※カラー化するスクリプトを参考に載せておきます↓
from PIL import Image
import numpy as np
import glob
def colorize_files(file_path):
file_list = glob.glob(file_path+'*')
for item in file_list:
img = Image.open(item)
color = img.convert("RGB")
color.save(item) #★元の画像が上書きされるので注意!
# カラー化したい画像データを入れたフォルダを指定する
file_path = './color_file/'
# カラー化処理関数
colorize_files(file_path)
DatasetとDataLoaderの作成
Pytorchで学習するにあたっては学習しやすいようにDatasetとDataLoaderを利用することが多いです。今回もこの2つを利用して学習を回していきます。
Dataset
class BirdDataset(data.Dataset):
"""
鳥のDataseクラス。
PyTorchのDatasetクラスを継承させる。
Attrbutes
---------
file_list: list
画像のファイルパスを格納したリスト
classes: list
鳥の特徴のラベル名
transform: object
前処理クラスのインスタンス
phase: 'train' or 'valid'
学習か検証かを設定
"""
def __init__(self, file_list, classes, transform=None, phase='train'):
self.file_list = file_list
self.transform = transform
self.classes = classes
self.phase = phase
def __len__(self):
"""
画像の枚数を返す
"""
return len(self.file_list)
def __getname__(self, index):
return self.file_list[index]
def __getitem__(self, index):
"""
前処理した画像データのTensor形式のデータとラベルを取得
"""
# 指定したindexの画像を読み込む
img_path = self.file_list[index]
img = Image.open(img_path)
# 画像の前処理を実施
img_transformed = self.transform(img, self.phase)
# 画像ラベルをファイル名から抜き出す
# ★ここの処理は各ファイルパスからフォルダ名(=クラス名)を抽出する処理です
label = self.file_list[index].split('/')[2]
# ラベル名を数値に変換
label = self.classes.index(label)
return img_transformed, label
# クラス名
bird_classes = [
'01_kawaii', '02_kakkoii'
]
#resize, mean, stdは前処理の項で定義したものを利用
# Datasetの作成
train_dataset = BirdDataset(
file_list=train_file_list, classes=bird_classes,
transform=ImageTransform(resize, mean, std),
phase='train'
)
valid_dataset = BirdDataset(
file_list=valid_file_list, classes=bird_classes,
transform=ImageTransform(resize, mean, std),
phase='valid'
)
上記53行目のクラス辞書はデータを格納したフォルダ名と連動しています↓
./
└── 01_train #学習データを格納
├── 01_kawaii #可愛い鳥データ
└── 02_kakkoii #かっこいい鳥データ
今回はフォルダ名とクラス名を同じにするルールで記載していますので、他のルールでフォルダ名を決めている場合はうまく動かなくなるのでご注意ください。
DataLoader
# バッチサイズの指定
batch_size = 64
# DataLoaderを作成
train_dataloader = data.DataLoader(
train_dataset, batch_size=batch_size, shuffle=True)
valid_dataloader = data.DataLoader(
valid_dataset, batch_size=32, shuffle=False)
#batch_sizeは一度のイテレーションで学習させるデータの量です
#shuffleはデータ利用時にランダムにデータを並べ替えるかどうか
# 辞書にまとめる
dataloaders_dict = {
'train': train_dataloader,
'valid': valid_dataloader
}
DataLoaderにはDatasetクラスを継承した先ほどのBirdDatasetクラスとバッチサイズ、shuffleの有無を規定します。バッチサイズは大きくするほど一般的にはバッチサイズを大きくするほど学習精度は高まりやすいですが、求められるメモリサイズが大きくなるので自分のスペックに合わせた値を設定しましょう。
アルゴリズムなどの設定
ネットワークはResnet50を使用します。
# ネットワークの設定
model_ft = models.resnet50(pretrained = True)
# 最終ノードの出力を2に変更する
model_ft.fc = nn.Linear(model_ft.fc.in_features, 2)
# GPUの利用有無の設定
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(device)
net = model_ft.to(device)
# 損失関数に交差エントロピーを利用
criterion = nn.CrossEntropyLoss()
# 最適化に関しては、いくつかのパターンを調べた結果、下記が一番結果がよかった
optimizer = optim.SGD(net.parameters(),lr=0.01,momentum=0.9)
Pytorchではデフォルトでresnet50のネットワークが用意されているので
model_ft = models.resnet50(pretrained = True)
と1行でOKです。
また、pretrainモデルもpytorch内に用意されており、 pretrained = True
で設定可能です。今回はpretrainモデルを使う場合と使わない場合で実験をするため、 pretrained = False
の場合も含めて2回学習を回しました。
その他GPUの利用有無、損失関数、最適化処理の設定もしていますが、こちらはよく使われる設定を使用しています。
学習
ここから学習フェーズです。CPUだと1epochでも20分くらいかかります。
train時とvalid時でそれぞれLossとAccを表示させるようprint文を仕込んでいます。
# エポック数
num_epochs = 5
for epoch in range(num_epochs):
print('Epoch {}/{}'.format(epoch+1, num_epochs))
print('-------------')
for phase in ['train', 'valid']:
if phase == 'train':
# モデルを訓練モードに設定
net.train()
else:
# モデルを推論モードに設定
net.eval()
# 損失和
epoch_loss = 0.0
# 正解数
epoch_corrects = 0
# DataLoaderからデータをバッチごとに取り出す
for inputs, labels in dataloaders_dict[phase]:
# optimizerの初期化
optimizer.zero_grad()
# 学習時のみ勾配を計算させる設定にする
with torch.set_grad_enabled(phase == 'train'):
inputs, labels = inputs.to(device), labels.to(device)
outputs = net(inputs)
# 損失を計算
loss = criterion(outputs, labels)
# ラベルを予測
_, preds = torch.max(outputs, 1)
# 訓練時はバックプロパゲーション
if phase == 'train':
# 逆伝搬の計算
loss.backward()
# パラメータの更新
optimizer.step()
# イテレーション結果の計算
# lossの合計を更新
# PyTorchの仕様上各バッチ内での平均のlossが計算される。
# データ数を掛けることで平均から合計に変換をしている。
# 損失和は「全データの損失/データ数」で計算されるため、
# 平均のままだと損失和を求めることができないため。
epoch_loss += loss.item() * inputs.size(0)
# 正解数の合計を更新
epoch_corrects += torch.sum(preds == labels.data)
# epochごとのlossと正解率を表示
epoch_loss = epoch_loss / len(dataloaders_dict[phase].dataset)
epoch_acc = epoch_corrects.double() / len(dataloaders_dict[phase].dataset)
# モデルをepochごとにsave
if phase == 'valid':
torch.save(net.state_dict(), 'model-'+str(epoch)+'_'+phase+'.pth')
print('{} Loss: {:.4f} Acc: {:.4f}'.format(phase, epoch_loss, epoch_acc))
pretrainありの場合の結果
pretrainありの場合は、Epoch1からAccが0.8を超えています。Epoch5ではvalidでもAccが0.95を超えています。
Epoch 1/5
-------------
train Loss: 0.2829 Acc: 0.8587
valid Loss: 0.1689 Acc: 0.9369
Epoch 2/5
-------------
train Loss: 0.0997 Acc: 0.9717
valid Loss: 0.1922 Acc: 0.9295
Epoch 3/5
-------------
train Loss: 0.0697 Acc: 0.9809
valid Loss: 0.2592 Acc: 0.9239
Epoch 4/5
-------------
train Loss: 0.0258 Acc: 0.9935
valid Loss: 0.1373 Acc: 0.9573
Epoch 5/5
-------------
train Loss: 0.0076 Acc: 0.9977
valid Loss: 0.1594 Acc: 0.9536
pretrainなしの場合の結果
pretrainなしの場合はEpoch5でもAccが0.7と低精度になっています。
Epoch 1/5
-------------
train Loss: 2.8234 Acc: 0.5121
valid Loss: 11.3417 Acc: 0.4731
Epoch 2/5
-------------
train Loss: 1.0381 Acc: 0.5692
valid Loss: 0.9440 Acc: 0.5714
Epoch 3/5
-------------
train Loss: 0.7322 Acc: 0.6106
valid Loss: 0.6481 Acc: 0.6605
Epoch 4/5
-------------
train Loss: 0.8654 Acc: 0.6120
valid Loss: 1.8762 Acc: 0.6605
Epoch 5/5
-------------
train Loss: 0.8254 Acc: 0.6348
valid Loss: 1.1411 Acc: 0.6716
テストセットの推定結果
テストセットとして、学習/検証データとして使ったOpen Images Datasetではなく、ネット上から選んだ可愛い鳥とかっこいい鳥の画像を推定してみます。
推定用のプログラムは以下の通りです。基本的には学習時に利用したクラスや関数を利用して推論を行います。
class ImageTransform(object):
def __init__(self, resize, mean, std):
self.data_trasnform = {
'train': transforms.Compose([
# データオーグメンテーション
transforms.RandomHorizontalFlip(),
# 画像をresize×resizeの大きさに統一する
transforms.Resize((resize, resize)),
# Tensor型に変換する
transforms.ToTensor(),
# 色情報の標準化をする
transforms.Normalize(mean, std)
]),
'valid': transforms.Compose([
# 画像をresize×resizeの大きさに統一する
transforms.Resize((resize, resize)),
# Tensor型に変換する
transforms.ToTensor(),
# 色情報の標準化をする
transforms.Normalize(mean, std)
])
}
def __call__(self, img, phase='train'):
return self.data_trasnform[phase](img)
#---------------モデルの読み込み---------------#
# 学習済みモデルの読み込み
model_path = './non-weight/model-4_valid.pth' #epoch5のpretrainなしモデル
#model_path = './with-weight/model-4_valid.pth'#epoch5のpretrainありモデル
model_ft = models.resnet50()
model_ft.fc = nn.Linear(model_ft.fc.in_features, 2)
model_ft.load_state_dict(torch.load(model_path))
# GPUの利用
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(device)
net = model_ft.to(device)
net.eval()
# リサイズ先の画像サイズ
resize = 300
# 今回は簡易的に(0.5, 0.5, 0.5)で標準化
mean = (0.5, 0.5, 0.5)
std = (0.5, 0.5, 0.5)
#---------------評価---------------#
folder = '../03_testset/01_kawaii/'#可愛い鳥のテストセット
#folder = '../03_testset/02_kakkoii/'#かっこいい鳥のテストセット
files = glob.glob(folder+'*')
for item in files:
print(item)
time.sleep(1)
img = Image.open(item)
transform = ImageTransform(resize, mean, std)
img_transformed = transform(img, 'valid').unsqueeze(0)#次元を追加。batch化の代わり
pred = []
with torch.no_grad():
output = net(img_transformed)
_, preds = torch.max(output, 1)
pred += [int(l.argmax()) for l in output]
print(pred)#[0]だと「可愛い」判定、[1]だと「かっこいい」判定です
m = nn.Softmax(dim=1)
print(m(output))#可愛いとかっこいい判定の度合いを表す評価値です。
各8枚ずつ推定した結果を以下に示します。
可愛い鳥
pretrainあり/なしに関わらず8枚中7枚を「可愛い」画像として判定できていました。
※赤文字が正解判定、青文字が不正解判定となります。

preなし:可愛(0.69)

preなし:可愛(0.62)

preなし:可愛(0.69)

preなし:格良(0.51)

preなし:可愛(0.63)

preなし:可愛(0.62)

preなし:可愛(0.61)

preなし:格良(0.55)
かっこいい鳥
pretrainなしでは全て「可愛い」と判定されており、明らかに学習不足であることがわかりました。pretrainありでは全ての画像を「かっこいい」と正しく判定できています。

preなし:可愛(0.52)

preなし:可愛(0.66)

preなし:可愛(0.64)

preなし:可愛(0.64)

preなし:可愛(0.53)

preなし:可愛(0.59)

preなし:可愛(0.69)

preなし:可愛(0.65)
推定結果まとめ
PretrainありとPretrainなしモデルでの正解率をまとめると下記の通りです。
予想していた以上にpretrainモデルの効果が高いな、と感じました。
モデル種別 | 可愛い:正解率 | かっこいい:正解率 |
---|---|---|
Pretrainありモデル | 88% | 100% |
Pretrainなしモデル | 88% | 0% |
プログラムや学習データなどは下記のリポジトリで公開しています。
https://github.com/nakamura10432/bird_classification_git
それではまた。