ひろしまQuest2020:画像データを使ったレモンの外観分類(ステージ1)チュートリアル

概要¶
こちらのチュートリアルでは、シンプルなモデルを使って、データの学習を行い、予測値を算出し、提出物を作成する方法について説明します。
説明は以下の流れで構成されています。
- 準備
- 分析環境
- ファイル構成
- ライブラリのインポート
- データの読み込みと確認
- データの読み込み
- 画像データの可視化
- 各画像に対してのラベル比率の確認
- モデリング
- データ分割方法の定義
- データセットのクラス定義
- データ前処理の定義
- データローダーの作成
- 損失関数、最適化手法の定義
- モデルの学習
- 層化k分割交差検証によるスコアの算出
- 推論
- 推論用データセットの作成
- 評価用データに対しての推論¶
- 提出物の作成
- 評価スコアの改善に向けて
1.準備¶
分析環境¶
当ファイルのソースコードは、以下の環境で動作することを確認しています。
- OS: Ubuntu 16.04.7
- 言語: Python==3.6.7
- GPU: TITAN X (Pascal)
- ライブラリ
- pandas
- numpy
- Pillow
- torch
- albumentations
- sklearn
- matplotlib
ファイル構成¶
当ファイルは以下のディレクトリ構成のもと実行されることを想定しています。
inputフォルダに今回のデータセットを解凍したものを全て配備しましょう。
- tutorial/
- input/
- train_images/
- train_images.csv
- test_images/
- test_images.csv
- sample_submit.csv
- output/
- src/
- 当チュートリアルの実行ファイル
- input/
ライブラリのインポート¶
必要なライブラリのインポートを行います。
import cv2
import numpy as np
import pandas as pd
import time
from glob import glob
from PIL import Image
from tqdm import tqdm
import torch
import torch.nn as nn
import torchvision.models as models
from torch.optim import Adam
from torch.optim.lr_scheduler import ReduceLROnPlateau
from torch.utils.data import DataLoader, Dataset
from albumentations import Compose, Normalize, Resize, RandomResizedCrop,CenterCrop,HorizontalFlip,VerticalFlip,Rotate,RandomContrast,IAAAdditiveGaussianNoise
from albumentations.pytorch import ToTensorV2
from sklearn.metrics import cohen_kappa_score
from sklearn.model_selection import StratifiedKFold
import matplotlib.pyplot as plt
%matplotlib inline
print(torch.__version__)
print(torch.cuda.is_available())
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
train_images_path_list = sorted(glob('../input/train_images/*.jpg'))
test_images_path_list = sorted(glob('../input/test_images/*.jpg'))
print('================')
print('学習用画像: ')
print(len(train_images_path_list))
print(train_images_path_list[:5])
print('================')
print('精度評価用画像: ')
print(len(test_images_path_list))
print(test_images_path_list[:5])
学習用画像が1102枚、評価用画像が1651枚であることが確認できました。
画像データの可視化¶
画像データの可視化を実行します。
上で取得したファイルパスを用いて画像データを読み込み、可視化してみましょう。
# 画像の読み込み
image_0000 = Image.open('../input/train_images/train_0000.jpg')
# 画像の表示
plt.imshow(image_0000);
指定する画像のファイル名を変えてみると、様々な形状のレモンがあることが確認できます。
各画像に対してのラベル比率の確認¶
学習用画像のラベル比率を確認してみましょう。
train_df = pd.read_csv('../input/train_images.csv')
train_df['class_num'].value_counts().plot.bar(figsize=(10, 3),rot=0);
優良、良、加工、規格外の順番で画像数が少なくなっていることが確認できます。
train_df['fold']=0
kf = StratifiedKFold(n_splits=5,shuffle=True,random_state=0)
for fold,(train_index, test_index) in enumerate(kf.split(train_df, train_df['class_num'])):
print('FOLD{}'.format(fold))
train_df.loc[test_index,'fold']=fold
データセットのクラスを定義¶
torch.utils.data.Datasetクラスを継承したクラスを作成します。
N_CLASSES = 4
class LemonTrainDataset(Dataset):
def __init__(self, df,transform=None):
self.df = df
self.labels = df['class_num']
self.transform = transform
def __len__(self):
return len(self.df)
def __getitem__(self, idx):
file_name = self.df['id'].values[idx]
file_path = f'../input/train_images/{file_name}'
image = cv2.imread(file_path)
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
if self.transform:
augmented = self.transform(image=image)
image = augmented['image']
label = self.labels.values[idx]
target = torch.zeros(N_CLASSES)
target[int(label)] = 1
return image, target
class LemonTestDataset(Dataset):
def __init__(self, df, transform=None):
self.df = df
self.transform = transform
def __len__(self):
return len(self.df)
def __getitem__(self, idx):
file_name = self.df['id'].values[idx]
file_path = f'../input/test_images/{file_name}'
image = cv2.imread(file_path)
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
if self.transform:
augmented = self.transform(image=image)
image = augmented['image']
return image
train_dataset = LemonTrainDataset(train_df)
print(train_dataset.__getitem__(0)[0].shape)
print(train_dataset.__getitem__(0)[1].shape)
データの前処理はこの段階では一切行なっていないため、640*640のサイズの画像であることが確認できます。
データ前処理の定義¶
albumentations
と呼ばれるデータ加工用のライブラリを使用して、画像に対しての前処理を定義します。
画像を見ると中心部分にレモンが写っていることが多いので、画像のサイズを512*512でセンタークロップしたものを使用します。
その他は、基本的なデータオーグメンテーションを定義しています。
HEIGHT=512
WIDTH=512
def get_transforms(*, data):
if data == 'train':
return Compose([
CenterCrop(height=HEIGHT, width=WIDTH, p=1.0),
HorizontalFlip(0.5),
VerticalFlip(0.5),
Rotate(0.5),
RandomContrast(0.5),
IAAAdditiveGaussianNoise(p=0.25),
Normalize(
mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225],
),
ToTensorV2(),
])
elif data == 'valid':
return Compose([
CenterCrop(height=HEIGHT, width=WIDTH, p=1.0),
Normalize(
mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225],
),
ToTensorV2(),
])
データローダーの作成¶
FOLD=0
BATCH_SIZE=16
# データセットの作成
train_dataset = LemonTrainDataset(train_df[train_df['fold']!=FOLD].reset_index(drop=True),transform=get_transforms(data='train'))
valid_dataset = LemonTrainDataset(train_df[train_df['fold']==FOLD].reset_index(drop=True),transform=get_transforms(data='valid'))
# データローダーの作成
train_loader = DataLoader(train_dataset, batch_size=BATCH_SIZE, shuffle=True)
valid_loader = DataLoader(valid_dataset, batch_size=BATCH_SIZE, shuffle=False)
分類モデルの定義¶
ここでは、torchvisionと呼ばれるライブラリに標準で実装されているresnet18モデルを使用します。
またimagenet
と呼ばれる大規模データセットを学習データとしたモデルのパラメータを流用するために、pretrained=True
とすることに注意しましょう。
モデルの出力層は今回のレモンのラベル数となるので、4に変更します。
def get_resnet18_model(trained=True,class_num=N_CLASSES):
model = models.resnet18(pretrained=trained)
model.fc = nn.Linear(512, class_num)
model.to(device)
return model
model = get_resnet18_model(trained=True,class_num=N_CLASSES)
model.fc
損失関数、最適化手法の定義¶
lr = 1e-4
n_epochs = 5
optimizer = Adam(model.parameters(), lr=lr)
scheduler = ReduceLROnPlateau(optimizer, 'min', factor=0.75, patience=4, verbose=True, eps=1e-6)
criterion = nn.BCEWithLogitsLoss()
モデルの学習¶
モデルの学習を行います。
学習を行なっていく中で、スコアの高いモデルのパラメータをfold_best_score.pth
としてoutputフォルダに保存します。
model.to(device)
best_score = 0.
best_thresh = 0.
best_loss = np.inf
for epoch in range(n_epochs):
print('epoch={}'.format(epoch))
start_time = time.time()
model.train()
avg_loss = 0.
optimizer.zero_grad()
tk0 = tqdm(enumerate(train_loader), total=len(train_loader))
for i, (images, labels) in tk0:
images = images.to(device)
labels = labels.to(device)
y_preds = model(images)
loss = criterion(y_preds, labels)
loss.backward()
optimizer.step()
optimizer.zero_grad()
avg_loss += loss.item() / len(train_loader)
model.eval()
avg_val_loss = 0.
preds = []
valid_labels = []
tk1 = tqdm(enumerate(valid_loader), total=len(valid_loader))
for i, (images, labels) in tk1:
images = images.to(device)
labels = labels.to(device)
with torch.no_grad():
y_preds = model(images)
_, y_preds_labels = torch.max(y_preds, 1)
_, labels_new = torch.max(labels, 1)
preds.append(y_preds_labels.to('cpu').numpy())
valid_labels.append(labels_new.to('cpu').numpy())
loss = criterion(y_preds, labels)
avg_val_loss += loss.item() / len(valid_loader)
scheduler.step(avg_val_loss)
preds = np.concatenate(preds)
valid_labels = np.concatenate(valid_labels)
score = cohen_kappa_score(valid_labels, preds, weights="quadratic")
if score>best_score:
best_score = score
print(f' Epoch {epoch+1} - Save Best Score: {best_score:.4f}')
torch.save(model.state_dict(), f'../output/fold{FOLD}_best_score.pth')
層化k分割交差検証によるスコアの算出¶
層化k分割交差検証として分割したデータを使って、スコアを算出してみましょう。
print('qwk={}'.format(cohen_kappa_score(valid_labels, preds,weights="quadratic")))
test_df = pd.read_csv('../input/test_images.csv')
test_dataset = LemonTestDataset(test_df,transform=get_transforms(data='valid'))
test_loader = DataLoader(test_dataset, batch_size=1, shuffle=False)
評価用データに対しての推論¶
tk_test = tqdm(enumerate(test_loader), total=len(test_loader))
preds = []
for i, (images) in tk_test:
images = images.to(device)
y_preds = model(images)
_, y_preds_labels = torch.max(y_preds, 1)
preds.extend(y_preds_labels.to('cpu').numpy())
len(preds)
提出物の作成¶
test_df['preds']=preds
test_df[['id','preds']].to_csv('../output/fold{}_submission.csv'.format(FOLD),index=False,header=None)
4.評価スコア改善に向けて¶
前述したソースコードはごく簡易的な手法を用いているので、工夫次第で更に評価値が改善する余地があります。
以下に、評価改善のための工夫例をいくつか提示します。
- 精度改善のための工夫例
- より高性能なネットワークの採用
- データオーギュメンテーション(データ水増し)の実行
- 画像の拡大・縮小のサイズや手法の変更
- 損失関数・最適化手法の変更
当チュートリアルは以上となります。
皆様のご参加をお待ちしています。
