🥅

Pytorch の Dataset、Dataloader の仕組みを理解しながらPytorchをやってみた

2021/11/29に公開

Pytorch の Dataset や Dataloader がよくわからなかったので調べながら画像分類をやってみました。

データセットは kaggle の Cat vs Dog を使っています。

Colab はこちら

kaggle API を使ってデータセットをダウンロードしていますが、kaggle.json ファイルの準備が必要ですので、詳しい手順は以下の記事を参照ください。

https://rikei-bakadikara2021.com/kaggle-google-colab-tutorial/

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() 要素を参照するときに呼ばれる関数

https://dreamer-uma.com/pytorch-dataset/

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 を使ってみます。

変換内容は適当ですが、公式ドキュメントを参考に色々試してみるとよいでしょう。

https://albumentations.ai/docs/examples/serialization/

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 向けの転置
        ]
    )

https://qiita.com/Takayoshi_Makabe/items/79c8a5ba692aa94043f7

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

https://qiita.com/takurooo/items/ba8c509eaab080e2752c

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

https://fastai.github.io/timmdocs/

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でもあまり予測はうまく行ってないですね。

以上になります、最後までお読みいただきありがとうございました。

その他参考情報

https://ohke.hateblo.jp/entry/2019/12/28/230000

https://www.kaggle.com/tomohiroh/pytorch-starter

https://ja.stateofaiguides.com/20211004-pytorch-performance-tips/

https://www.mattari-benkyo-note.com/2021/04/27/pytorch-performance-tuning-guide-1-parameter-grad-none/

https://twitter.com/PyTorch/status/1437838231505096708

Discussion