Pytorch の Dataset、Dataloader の仕組みを理解しながらPytorchをやってみた
Pytorch の Dataset や Dataloader がよくわからなかったので調べながら画像分類をやってみました。
データセットは kaggle の Cat vs Dog を使っています。
Colab はこちら
kaggle API を使ってデータセットをダウンロードしていますが、kaggle.json ファイルの準備が必要ですので、詳しい手順は以下の記事を参照ください。
Dataset の作成
テーブルデータのDataset の作成
import pandas as pd
import category_encoders as ce
import torch
titanic_df = pd.read_csv("https://raw.githubusercontent.com/datasciencedojo/datasets/master/titanic.csv")
oe = ce.OrdinalEncoder(cols=titanic_df.select_dtypes(include="object") ,handle_unknown='impute')
titanic_df = oe.fit_transform(titanic_df)
y = torch.Tensor(titanic_df['Survived'].values)
X = torch.Tensor(titanic_df.drop('Survived', axis=1).values)
# Datasetを作成
table_dataset = torch.utils.data.TensorDataset(X, y)
dataset の中身を出力
train_line, target_line = table_dataset[0][0], table_dataset[0][1]
print("説明変数 ", train_line)
print("目的変数 ", target_line)
出力:
説明変数 tensor([ 1.0000, 3.0000, 1.0000, 1.0000, 22.0000, 1.0000, 0.0000, 1.0000,
7.2500, 1.0000, 1.0000])
目的変数 tensor(0.)
パラメーターの設定
バッチサイズはメモリで処理できる範囲なら大きい方が処理速度が上がるらしいです。
BATCH_SIZE = 64 # 適宜変更
SIZE = 512 # 適宜変更
RESIZE = 256 # 適宜変更
EPOCHS = 100 # 適宜変更
特に深い意味はないですが、学習データは 512 サイズの画像を 256 に切り抜き、テストデータは 256 の画像を拡張せずそのまま使っています。
Transform (画像変換、画像拡張) なしの画像 の 自作Dataset class の作成
詳細は省略しますが、このような DataFrame型のデータを用意します。
from torch.utils.data import Dataset
from PIL import Image
class MyDataset(Dataset):
def __init__(self, train_df, input_size):
super().__init__()
self.train_df = train_df
image_paths = train_df["path"].to_list()
self.input_size = input_size
self.len = len(image_paths)
def __len__(self):
return self.len
def __getitem__(self, index):
image_path = self.train_df["path"].to_list()[index]
# 画像の読込
image = Image.open(image_path) # 画像ファイルの読込
image = image.resize(self.input_size) # リサイズ
image = np.array(image).astype(np.float32).transpose(2, 1, 0) # Dataloader で使うために転置する
# ラベル (0: cat, 1: dog)
label = self.train_df["label"].to_list()[index]
# カテゴリ (cat, dog)
category = self.train_df["category"].to_list()[index] # カテゴリ名の設定
return image, label, category
引数に 画像ファイルのパスとラベル(0:猫、1:犬)、カテゴリ(cat, dog)の入ったDataFrame型のデータ(train_df)、画像ファイルのサイズを設定します。
カテゴリはなくても問題ないので、削除してもOKです。
def len() len()を使った時に呼ばれる関数
def getitem() 要素を参照するときに呼ばれる関数
Dataset の作成
image_dataset = MyDataset(
train,
(SIZE, SIZE),
)
Dataset の出力
image, label, category = image_dataset[0]
print(image.shape, type(image), label, category, len(image_dataset))
plt.imshow(image.transpose(2, 1, 0).astype(np.uint8))
Transform (画像変換、画像拡張) ありの自作 Dataset Class の定義
class MyDataset(Dataset):
def __init__(self, train_df, input_size, transform=None):
super().__init__()
self.train_df = train_df
image_paths = train_df["path"].to_list()
self.input_size = input_size
self.len = len(image_paths)
self.transform = transform
def __len__(self):
return self.len
def __getitem__(self, index):
image_path = self.train_df["path"].to_list()[index]
# 画像の読込
image = Image.open(image_path)
image = image.resize(self.input_size)
image = np.array(image)
if self.transform:
transformed = self.transform(image=image)
image = transformed['image']
else:
image = np.array(image).astype(np.float32).transpose(2, 1, 0)
# ラベル (0: cat, 1: dog)
label = self.train_df["label"].to_list()[index]
# ラベル (0: cat, 1: dog)
category = self.train_df["category"].to_list()[index]
return image, label, category
画像変換・拡張の設定
便利な画像変換・拡張ライブラリの albumentations を使ってみます。
変換内容は適当ですが、公式ドキュメントを参考に色々試してみるとよいでしょう。
import torchvision.transforms as transforms
import albumentations
from albumentations.pytorch import ToTensorV2
transformer = albumentations.Compose(
[
albumentations.RandomCrop(width=RESIZE, height=RESIZE), # ランダムな切り抜き
albumentations.HorizontalFlip(p=0.5), # 左右反転
albumentations.RandomBrightnessContrast(p=0.2), # ランダムに明るさとコントラストを変更
ToTensorV2(), # Tensor 型への変換とPytorch 向けの転置
]
)
Dataset の作成
image_dataset = MyDataset(
train,
(SIZE, SIZE),
transform=transformer
)
Dataset の中身を出力
image, label, category = image_dataset[0]
print(image.shape, type(image), label, category, len(image_dataset))
plt.imshow(image.numpy().transpose(2, 1, 0))
学習データと評価データに分割
下記の設定では端数処理の関係で元データのサイズと、引数に設定する学習データと評価データのサイズの合計が一致しないとエラーになる場合もあるので、その場合は一致するよう調整してやりましょう。
train_dataset, valid_dataset = torch.utils.data.random_split(
image_dataset,
[int(len(image_dataset)*0.7), int(len(image_dataset)*0.3)]
)
Dataloader を使ったバッチデータの作成
from torch.utils.data import DataLoader
# 学習用Dataloader
train_dataloader = DataLoader(
train_dataset,
batch_size=BATCH_SIZE,
shuffle=True,
num_workers=2,
drop_last=True,
pin_memory=True
)
# 評価用Dataloader
valid_dataloader = DataLoader(
train_dataset,
batch_size=BATCH_SIZE,
shuffle=True,
num_workers=2,
drop_last=True,
pin_memory=True
)
pin_memory=True は高速化に役立つそうです。
Detaloader の中身の出力
tmp = train_dataloader.__iter__()
batch1, label1, category1 = tmp.next()
batch2, label2, category2 = tmp.next()
print(batch1.shape, label1, category1)
print(batch2.shape, label2, category2)
# 画像を出力
plt.figure(figsize = (10, 10))
for i in range(batch1.shape[0]):
plt.subplot(batch1.shape[0]//4+1, 4, i+1)
plt.title(f"image {i}")
plt.axis("off")
plt.imshow(batch1[i, :, :, :].numpy().transpose(2, 1, 0))
plt.tight_layout()
学習
timm (Pytorch-Image-Models) を使った モデルの作成
import timm
model = timm.create_model(
"vgg16",
pretrained = False,
num_classes = len(train["label"].unique())
)
DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
model = model.to(DEVICE)
print(DEVICE)
最適化関数、Loss関数の設定
from torch.optim import Adam
import torch.nn as nn
optimizer = Adam(model.parameters())
criterion = nn.CrossEntropyLoss()
学習
for epoch in tqdm(range(EPOCHS)):
# 学習
model.train()
train_loss = 0
for batch, label, category in tqdm(train_dataloader):
for param in model.parameters():
param.grad = None
batch = batch.float()
batch = batch.to(DEVICE)
label = label.to(DEVICE)
preds = model(batch)
loss = criterion(preds, label)
loss.backward()
optimizer.step()
# Validation
model.eval()
valid_loss = 0
with torch.inference_mode():
for batch, label, category in tqdm(valid_dataloader):
batch = batch.float()
batch = batch.to(DEVICE)
label = label.to(DEVICE)
preds = model(batch)
loss = criterion(preds, label)
valid_loss += loss.item()
valid_loss /= len(valid_dataloader)
print(epoch, "Loss:", valid_loss)
optimizer.zero_grad() の代わりに
for param in model.parameters():
param.grad = None
を、
with torch.no_grad(): の代わりに
with torch.inference_mode():
を使うと高速化に役立つそうです。
推論
テストデータ用のDataset class の定義
テストデータなので学習データと違い、正解ラベルに相当する部分はありません。
また、データ拡張もおこなっていません。
class TestDataset(Dataset):
def __init__(self, test_df, input_size):
super().__init__()
self.train_df = train_df
image_paths = train_df["path"].to_list()
self.input_size = input_size
self.len = len(image_paths)
def __len__(self):
return self.len
def __getitem__(self, index):
image_path = self.train_df["path"].to_list()[index]
# 入力
image = Image.open(image_path)
image = image.resize(self.input_size)
image = np.array(image).astype(np.float32).transpose(2, 1, 0)
image = torch.from_numpy(image.astype(np.float32)).clone()
return image
テストデータ用の Dataset の作成
test_dataset = TestDataset(
test_path,
(64, 64),
)
image = test_dataset[0]
print(image.shape, type(image), len(test_dataset))
テストデータ用の Dataloader の作成
test_dataloader = DataLoader(
test_dataset,
batch_size=BATCH,
shuffle=True,
num_workers=2,
pin_memory=True
)
推論の実行
# 推論
model.eval()
preds_list = []
with torch.no_grad():
for image in tqdm(test_dataloader):
for param in model.parameters():
param.grad = None
image = image.float()
image = image.to(DEVICE)
preds = model(image)
preds = preds.to('cpu')
preds_list = preds_list + preds.argmax(dim=1).tolist() # 予測結果をlistに格納
予測結果を確認
import collections
print(len(preds_list), collections.Counter(preds_list))
出力: 12500 Counter({0: 12500})
学習済みモデルを使ってないですし精度を狙ったわけではないので仕方ないですが、エポック数100でもあまり予測はうまく行ってないですね。
以上になります、最後までお読みいただきありがとうございました。
その他参考情報
Discussion