Machine Learning

画像処理におけるデータローダ【PyTorch】

データローダを作成するには

  1. データセットの作成
  2. データセットからデータローダを作成

の手順で行います。

データローダの概要

ミニバッチ学習を行う際にデータローダをよく利用します。

ミニバッチ学習の構造は下記です。

この1エポックの構造を作るのがデータローダです。

アノテーション

データローダを利用するにはアノテーション済みのデータが必要です。
アノテーションとは分類のことです。特に教師あり学習の場合はフォルダで画像を正確に分類しておく必要があります。

画像ファイルやフォルダの名前は何でも構いません。

教師あり学習

教師あり学習の場合は下記のようなディレクトリ構造にしておきます。
ルートフォルダのパスを読み込むことで自動で画像に対してラベルを設定してくれます。

ルート
  │
  ├─ クラス_01
  │     ├─ クラス_01 の画像1
  │     ├─ クラス_01 の画像2
  │     └─ クラス_01 の画像3
  │
  ├─ クラス_02
  │     ├─ クラス_02 の画像1
  │     ├─ クラス_02 の画像2
  │     └─ クラス_02 の画像3
  │
  └─ クラス_03
        ├─ クラス_03 の画像1
        ├─ クラス_03 の画像2
        └─ クラス_03 の画像3

教師なし学習

GAN などで利用するには分類は必要ないため、1つのフォルダに画像をまとめます。
この時も ルートフォルダのパスを読み込みます

ルート
  │
  └─ フォルダ
        ├─ 画像1
        ├─ 画像2
        ├─ 画像3
        ├─ 画像4
        ├─ 画像5
        └─ 画像6

データセットの作成

データセットは正解とデータがセットになったものです。

訓練 / 検証用データを分けていない場合

import torchvision.transforms as transforms
from torchvision import datasets

# 学習データのパス
data_path = 'hoge/hoge_train'

# オーグメンテーション
transform = transforms.Compose([
    transforms.Resize((128, 128)),
    transforms.ToTensor()
])

# データセットの作成
dataset = datasets.ImageFolder(data_path, transform)

オーグメンテーションは下記を参考にしてください。

今回はリサイズとテンソルへの変換のみ行っています。

trainData_path = 'hoge/hoge_train'は下記のルートのフォルダに当たります。

ルート
  │
  ├─ クラス_01
  │     ├─ クラス_01 の画像1
  │     ├─ クラス_01 の画像2
  │     └─ クラス_01 の画像3
  │
  ├─ クラス_02
  │     ├─ クラス_02 の画像1
  │     ├─ クラス_02 の画像2
  │     └─ クラス_02 の画像3
  │
  └─ クラス_03
        ├─ クラス_03 の画像1
        ├─ クラス_03 の画像2
        └─ クラス_03 の画像3

訓練 / 検証用データを分けている場合

訓練用と検証用にデータを分けている場合は下記のように別々にデータセットを用意します。

import torchvision.transforms as transforms
from torchvision import datasets

# 学習データのパス
train_path = 'hoge/hoge_train'
test_path  = 'hoge/hoge_test'

# オーグメンテーション
transform = transforms.Compose([
    transforms.Resize((128, 128)),
    transforms.ToTensor()
])

# データセットの作成
train_dataset = datasets.ImageFolder(train_path, transform)
test_dataset  = datasets.ImageFolder(test_path, transform)

これは下記のように訓練用と検証用で既にデータを分けている場合です。

訓練用
  │
  ├─ クラス_01
  │     ├─ クラス_01 の画像1
  │     ├─ クラス_01 の画像2
  │     └─ クラス_01 の画像3
  │
  ├─ クラス_02
  │     ├─ クラス_02 の画像1
  │     ├─ クラス_02 の画像2
  │     └─ クラス_02 の画像3
  │
  └─ クラス_03
        ├─ クラス_03 の画像1
        ├─ クラス_03 の画像2
        └─ クラス_03 の画像3

検証用
  │
  ├─ クラス_01
  │     ├─ クラス_01 の画像1
  │     ├─ クラス_01 の画像2
  │     └─ クラス_01 の画像3
  │
  ├─ クラス_02
  │     ├─ クラス_02 の画像1
  │     ├─ クラス_02 の画像2
  │     └─ クラス_02 の画像3
  │
  └─ クラス_03
        ├─ クラス_03 の画像1
        ├─ クラス_03 の画像2
        └─ クラス_03 の画像3

公開されているデータを利用する場合

下記は CIFAR10 のデータセットを用意した場合のコードです。

from torchvision.datasets import CIFAR10
import torchvision.transforms as transforms

# オーグメンテーション
transform = transforms.Compose([
    transforms.Resize((128, 128)),
    transforms.ToTensor()
])

train_dataset = CIFAR10("./data", train=True, download=True, transform=transform_train)
test_dataset  = CIFAR10("./data", train=False, download=True, transform=transform_test)

データローダの作成

一番重要になるのが バッチサイズです。
バッチサイズでデータローダの構造が決まります。

イテレーションはバッチサイズを決めれば出ます。
エポックはデータローダでは設定しません。

データローダの作成コードは下記です。

# シード値の固定
pl.seed_everything(0)

batch_size   = 10
train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True, drop_last=True)

データローダに設定できる引数です。

batch_sizeバッチサイズ
shuffleランダムに順番を変更するかどうか
drop_last端数のデータを削除するか

shuffleはデータの順序をランダムにします。
この順序のシード値は下記のコードで固定できます。

pl.seed_everything(0)

訓練 / 検証用データを分けていない場合

訓練と検証用データを分けていない場合はここでデータセットを分割して訓練用データセットと検証用データセットを作ります。

import torch
import torchvision.transforms as transforms
from torchvision import datasets
from torch.utils.data import DataLoader
import pytorch_lightning as pl

# 学習データのパス
data_path = 'hoge/hoge_train'

# バッチサイズ
batch_size = 1

# オーグメンテーション
transform = transforms.Compose([
    transforms.Resize((128, 128)),
    transforms.ToTensor()
])

# データセットの作成
dataset = datasets.ImageFolder(data_path, transform)


# 学習データに使用する割合
n_train_ratio = 60

# 割合から個数を出す
n_train = int(len(dataset) * n_train_ratio / 100)
n_val   = int(len(dataset) - n_train)


# 学習データと検証データに分割
train, val = torch.utils.data.random_split(dataset, [n_train, n_val])


# Data Loader
train_loader = torch.utils.data.DataLoader(train, batch_size, shuffle=True, drop_last=True)
val_loader = torch.utils.data.DataLoader(val, batch_size)

データセットの分割 | random_split

# 学習データと検証データに分割
train, val = torch.utils.data.random_split(dataset, [n_train, n_val])

random_splitを使用することでデータセットをランダムに分割できます。
分割するためには引数で分割数を指定する必要があります。

分割数を割合から導出しているのが下記の部分です。

# 学習データに使用する割合
n_train_ratio = 60

# 割合から個数を出す
n_train = int(len(dataset) * n_train_ratio / 100)
n_val   = int(len(dataset) - n_train)

こちらもデータローダの作成と同じでpl.seed_everything(0)でシード値を固定します。

学習・検証・テストで3分割する
# 学習データと検証データに分割
train, val, test = torch.utils.data.random_split(dataset, [n_train, n_val, n_test])

例えば上記のコードにすると訓練、検証、テストで3分割できます。

訓練 / 検証用データを分けている場合

import torchvision.transforms as transforms
from torchvision import datasets
from torch.utils.data import DataLoader
import pytorch_lightning as pl

# 学習データのパス
train_path = 'hoge/hoge_train'
test_path  = 'hoge/hoge_test'

# オーグメンテーション
transform = transforms.Compose([
    transforms.Resize((128, 128)),
    transforms.ToTensor()
])

# データセットの作成
train_dataset = datasets.ImageFolder(train_path, transform)
test_dataset  = datasets.ImageFolder(test_path, transform)

# シード値の固定
pl.seed_everything(0)

# DataLoaderの設定
batch_size   = 10
train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True, drop_last=True)
test_loader  = DataLoader(test_dataset, batch_size=batch_size, shuffle=False, drop_last=True)

データローダの構造

print('データローダのタイプ', type(train_loader))
print('全体のデータ量 : ',len(dataset))
print('イテレーション : ', len(train_loader))

データローダのタイプ <class 'torch.utils.data.dataloader.DataLoader'>
全体のデータ量 :  100
イテレーション :  20

データローダの構造概要

データローダの構造は下記のようになっています。

上記の構造のため、len(data_loader)でイテレータの数を取得できます。

画像とラベルを取り出す

ミニバッチを取り出すには下記のように取り出します。

# データロダからバッチを取り出す
image_list, label_list = iter(train_loader).next()

print('画像の枚数   : ', len(image_list))
print('ラベルの個数 : ', len(label_list))
print('画像        : ', image_list[0].shape)
print('ラベル      : ', label_list[0])

image_list と label_list にはそれぞれバッチ数分のデータが入っています。

画像の枚数   :  3
ラベルの個数 :  3
画像        :  torch.Size([3, 128, 128])
ラベル      :  tensor(1)

データローダを回す

実際にデータローダを回してみます。
学習する際にはエポック数でイテレーションを回しますので、データローダをエポック数の for 文で繰り返します。

# エポック で回す
epoch = 2

for i in range(epoch):

    print('\n\nエポック数 : ', i + 1)
    print('---------------------------------------------------------')

    # ミニバッチ(x, t)を取り出す
    for j, (x, t) in enumerate(train_loader):

        print('イテレーション数 : ', j + 1)
        print('  バッチ数  : ', len(x))
        print('  画像    : ', x.shape)
        print('  ラベル   : ', t, '\n')
エポック数 :  1
---------------------------------------------------------
イテレーション数 :  1
  バッチ数  :  14
  画像    :  torch.Size([14, 3, 128, 128])
  ラベル   :  tensor([3, 0, 8, 2, 6, 1, 9, 9, 4, 7, 6, 7, 7, 2]) 

イテレーション数 :  2
  バッチ数  :  14
  画像    :  torch.Size([14, 3, 128, 128])
  ラベル   :  tensor([3, 6, 9, 2, 6, 0, 0, 5, 1, 9, 7, 5, 2, 5]) 

イテレーション数 :  3
  バッチ数  :  14
  画像    :  torch.Size([14, 3, 128, 128])
  ラベル   :  tensor([5, 9, 0, 5, 8, 4, 4, 1, 7, 8, 6, 7, 4, 6]) 

イテレーション数 :  4
  バッチ数  :  14
  画像    :  torch.Size([14, 3, 128, 128])
  ラベル   :  tensor([2, 1, 0, 0, 4, 5, 8, 9, 2, 6, 0, 5, 9, 8]) 



エポック数 :  2
---------------------------------------------------------
イテレーション数 :  1
  バッチ数  :  14
  画像    :  torch.Size([14, 3, 128, 128])
  ラベル   :  tensor([6, 0, 7, 8, 4, 9, 6, 3, 6, 4, 4, 0, 7, 2]) 

イテレーション数 :  2
  バッチ数  :  14
  画像    :  torch.Size([14, 3, 128, 128])
  ラベル   :  tensor([3, 0, 5, 0, 7, 3, 1, 9, 8, 4, 5, 1, 7, 2]) 

イテレーション数 :  3
  バッチ数  :  14
  画像    :  torch.Size([14, 3, 128, 128])
  ラベル   :  tensor([6, 9, 7, 2, 4, 9, 5, 5, 6, 0, 5, 8, 5, 7]) 

イテレーション数 :  4
  バッチ数  :  14
  画像    :  torch.Size([14, 3, 128, 128])
  ラベル   :  tensor([0, 2, 2, 9, 1, 8, 0, 5, 8, 9, 1, 4, 6, 6]) 

enumerate 関数

データローダは enumerate を利用して回します。

for j, (x, t) in enumerate(train_loader):

enumerate 関数は要素とインデックスを同時に取得できる関数です。

a = ['a', 'b', 'c', 'd']
b = list(enumerate(a))
print(b)
[(0, 'a'), (1, 'b'), (2, 'c'), (3, 'd')]

上記のようにリストの中身とインデックスがタプルでリスト化されます。

つまり、

print(list(enumerate(train_loader)))

とした場合、

[(0, [[1バッチの画像リスト], [1バッチのラベルリスト]]), (1, [[1バッチの画像リスト], [1バッチのラベルリスト]]) ・・・]

となります。このため、

for j, (x, t) in enumerate(train_loader):

上記のコードでは下記のデータが格納されて for 文で繰り返されます。

jイテレーションの数
x1 バッチの画像リスト
t1 バッチのラベルリスト

enumerate を使用せずに回す

イテレーションの中にはデータとラベルの 2つのリストが格納されているため、for 文で回すには変数を2つ用意します。

# エポック で回す
epoch = 2

for i in range(epoch):
    print('\n\nエポック数 : ', i + 1)
    print('---------------------------------------------------------')
    
    # ミニバッチ(x, t)を取り出す
    for x, t in train_loader:

        print('  バッチ数  : ', len(x))
        print('  画像    : ', x.shape)
        print('  ラベル   : ', t, '\n')
エポック数 :  1
---------------------------------------------------------
  バッチ数  :  14
  画像    :  torch.Size([14, 3, 128, 128])
  ラベル   :  tensor([3, 0, 8, 2, 6, 1, 9, 9, 4, 7, 6, 7, 7, 2]) 

  バッチ数  :  14
  画像    :  torch.Size([14, 3, 128, 128])
  ラベル   :  tensor([3, 6, 9, 2, 6, 0, 0, 5, 1, 9, 7, 5, 2, 5]) 

  バッチ数  :  14
  画像    :  torch.Size([14, 3, 128, 128])
  ラベル   :  tensor([5, 9, 0, 5, 8, 4, 4, 1, 7, 8, 6, 7, 4, 6]) 

  バッチ数  :  14
  画像    :  torch.Size([14, 3, 128, 128])
  ラベル   :  tensor([2, 1, 0, 0, 4, 5, 8, 9, 2, 6, 0, 5, 9, 8]) 



エポック数 :  2
---------------------------------------------------------
  バッチ数  :  14
  画像    :  torch.Size([14, 3, 128, 128])
  ラベル   :  tensor([6, 0, 7, 8, 4, 9, 6, 3, 6, 4, 4, 0, 7, 2]) 

  バッチ数  :  14
  画像    :  torch.Size([14, 3, 128, 128])
  ラベル   :  tensor([3, 0, 5, 0, 7, 3, 1, 9, 8, 4, 5, 1, 7, 2]) 

  バッチ数  :  14
  画像    :  torch.Size([14, 3, 128, 128])
  ラベル   :  tensor([6, 9, 7, 2, 4, 9, 5, 5, 6, 0, 5, 8, 5, 7]) 

  バッチ数  :  14
  画像    :  torch.Size([14, 3, 128, 128])
  ラベル   :  tensor([0, 2, 2, 9, 1, 8, 0, 5, 8, 9, 1, 4, 6, 6]) 

検証用に 1バッチのみを取り出す

上の方で既に行いましたが、文字列を統一させてもう一度書いておきます。

# ミニバッチ(x, t)を取り出す
x, t = iter(train_loader).next()

print('  バッチ数  : ', len(x))
print('  画像    : ', x.shape)
print('  ラベル   : ', t, '\n')

これで 1 バッチのみの画像データとラベルを取り出せます。

for 文では 1 バッチのみを取り出せない

下記の文ではエラーで止まります。

# ミニバッチ(x, t)を取り出す
for x, t in train_loader[0]:

    print('  バッチ数  : ', len(x))
    print('  画像    : ', x.shape)
    print('  ラベル   : ', t, '\n')
TypeError: 'DataLoader' object is not callable

ちなみに下記の文でもエラーで止まります。

# ミニバッチ(x, t)を取り出す
x, t = train_loader[0]

データローダの作成を汎用化させる

色々と設定が面倒なのでクラスにまとめて汎用的に使えるようにしています。

data_loader の名前で別ファイルにして読み込んで使用しています。

import data_loader
from torchvision import transforms

data_loader = data_loader.DataLoader()
data_loader.log                         = True
data_loader.batch_size                  = 20
data_loader.image_size                  = 256
data_loader.n_train_probability         = 50
data_loader.val_loader_status           = True
data_loader.test_loader_status          = True
data_loader.save_transformed_img_status = True

data_loader.pil_augmentation = [
    transforms.RandomRotation(degrees=90)
]

train_loader, val_loader, test_loader = data_loader.create_data_loader()
Compose(
    RandomRotation(degrees=[-90.0, 90.0], interpolation=nearest, expand=False, fill=0)
    Resize(size=(256, 256), interpolation=bilinear, max_size=None, antialias=None)
    ToTensor()
)

--------------------------------------------------------------------------


全体のデータ量 :  100

オーグメンテーション後の画像を data\transformed へ保存

訓練データ
 イテレーション :  2
 バッチサイズ  :  20
 画像情報      :  torch.Size([3, 256, 256])

検証データ
 イテレーション :  2
 バッチサイズ  :  20
 画像情報      :  torch.Size([3, 256, 256])

テストデータ
 イテレーション :  2
 バッチサイズ  :  20
 画像情報      :  torch.Size([3, 256, 256])

オーグメンテーション後の画像を保存

# オーグメンテーション後の画像データの保存
# ==================================================================================================================
def save_transformed_image(self, dataset, n=0):

    # 保存先のパス
    self.transformed_path = self.root_path + '\\' + self.transformed_name

    for img in dataset:

        if not os.path.isdir(self.transformed_path):
            os.makedirs(self.transformed_path)

        if not os.path.isdir(self.transformed_path + '\\' + str(img[1])):
            os.makedirs(self.transformed_path + '\\' + str(img[1]))

        pil_img = TF.to_pil_image(img[0])
        pil_img.save(self.transformed_path + '\\' + str(img[1]) + '\\transformed_' + str(n) + '.png')

        n += 1

オーグメンテーションの仕方で結果が変わることがよくあるんですが、
通常のコードでは実際にどういった画像に変換されているかがわかりません。

そこでオーグメンテーション後に画像を保存できるようにしています。
保存しておくことであとからオーグメンテーションについて検証することができます。

下記のパラメータで保存するかどうかを設定できるようにしています。

data_loader.save_transformed_img_status = False

-Machine Learning