LoginSignup
2
6

More than 3 years have passed since last update.

【pytorch-lightning入門】torchvision.transformsの使い方と自前datasetの自由な作り方♬

Last updated at Posted at 2021-01-17

いろいろなデータを使いたいということで、自前datasetの作り方をいろいろ試してみたので、まとめておきます。
denoising, coloring, ドメイン変換などをやるためには、必須な技術です。

今回は、二つの要素をまとめます。
一つは、torchvision.transformsの各種クラスの使い方と自前クラスの作り方、もう一つはそれらを利用した自前datasetの作り方です。
後半は、以下の参考がありますが、試行錯誤を随分したので、その結果を載せることとします。

【参考】
pyTorchのtransforms,Datasets,Dataloaderの説明と自作Datasetの作成と使用
PyTorchでDatasetの読み込みを実装してみた
TORCHVISION.TRANSFORMS

やったこと

・transformsの整理
・autoencoderに応用する
・自前datasetの作り方
  ①data-labelの場合
  ②data1-data2-labelのような場合

・transformsの整理

transformは以下のようにpytorch-lighitningのコンストラクタで出現(定義)していて、setupでデータ処理を簡単に定義し、Dataloaderで取得時にその処理を実行しています。
以下では、MNISTデータに対して、transforms.Normalize((0.1307,), (0.3081,))を実行しています。最初は、この数字は何というところからのまとめをしたいと思います。
※結論は、この数字は平均と標準偏差で、各画像数値をこの平均と標準偏差に入るように再規格化するという呪文でした
(画像なら暗すぎるものを明るくしたり、明るすぎるものを暗めにするなどの、輝度調整という処理になります)
実際、この数字がどこから来たかは今回は不明です。
全体の画像からそれぞれの平均の平均や標準偏差を求めて調整が筋ですが、そこまでやっている証拠は見つけられませんでした⇒以下の参考⑤を見ると、チャンネルごとの平均をそれぞれから引き、そのチャンネルの標準偏差で除しているようです。
しかし記述統計的にはそうすべきです。しかし、今回の興味の中心からはずれるので、パスすることにします。
※なお、研究テーマ的には、これを色々適応しつつ、精度への寄与をまじめに計測するのは面白いと思いますし、物体検出では重要なテーマ(必須で自分の対象にたいしてやるべき)だと思います

class LitAutoEncoder(pl.LightningModule):

    def __init__(self, data_dir='./'):
        super().__init__()
        self.data_dir = data_dir

        # Hardcode some dataset specific attributes
        self.num_classes = 10
        self.classes = ('0', '1', '2', '3', '4', '5', '6', '7', '8', '9')
        self.dims = (1, 28, 28)
        channels, width, height = self.dims
        self.transform=transforms.Compose([transforms.ToTensor(),
                              transforms.Normalize((0.1307,), (0.3081,))])

        self.encoder = nn.Sequential(nn.Linear(28 * 28, 128), nn.ReLU(), nn.Linear(128, 32))
        self.decoder = nn.Sequential(nn.Linear(32, 128), nn.ReLU(), nn.Linear(128, 28 * 28))

    def forward(self, x):
        # in lightning, forward defines the prediction/inference actions
        embedding = self.encoder(x)
        return embedding
...
    def setup(self, stage=None): #train, val, testデータ分割
        # Assign train/val datasets for use in dataloaders
        mnist_full =MNIST(self.data_dir, train=True, transform=self.transform)
        n_train = int(len(mnist_full)*0.8)
        n_val = len(mnist_full)-n_train
        self.mnist_train, self.mnist_val = torch.utils.data.random_split(mnist_full, [n_train, n_val])
        self.mnist_test = MNIST(self.data_dir, train=False, transform=self.transform)

    def train_dataloader(self):
        self.trainloader = DataLoader(self.mnist_train, shuffle=True, drop_last = True, batch_size=32, num_workers=0)
        # get some random training images
        return self.trainloader
...

そして、このtransformsは、上記の参考③にまとめられていました。
ここでは、全てを試していませんが、当面使いそうな以下の表の機能を動かしてみました。

機能 備考
rotate(x, angle) 角度に基いて回転する
to_grayscale(x) grayscaleに変換する
vflip(x) 上下フリップを行う
hflip(x) 左右フリップを行う
Resize(imageSize) 指定されたサイズにResizeを行う
Normalize(self.mean, self.std) 指定された平均と標準偏差で画像を規格化する
Compose() ()内の一連の変換をまとめて実行する
ToTensor() torchTensorに変換する
ToPILImage() PILImageに変換する

TORCHVISION.TRANSFORMSのクラス等
Compose(transforms)
CenterCrop(size)
ColorJitter(brightness=0, contrast=0, saturation=0, hue=0)
FiveCrop(size)
Grayscale(num_output_channels=1)
Pad(padding, fill=0, padding_mode='constant')
RandomAffine(degrees, translate=None, scale=None, shear=None, resample=0, fillcolor=0)
RandomApply(transforms, p=0.5)
RandomCrop(size, padding=None, pad_if_needed=False, fill=0, padding_mode='constant')
RandomGrayscale(p=0.1)
RandomHorizontalFlip(p=0.5)
RandomPerspective(distortion_scale=0.5, p=0.5, interpolation=2, fill=0)
RandomResizedCrop(size, scale=(0.08, 1.0), ratio=(0.75, 1.3333333333333333), interpolation=2)
RandomRotation(degrees, resample=False, expand=False, center=None, fill=None)
RandomSizedCrop(*args, **kwargs)
RandomVerticalFlip(p=0.5)
Resize(size, interpolation=2)
TenCrop(size, vertical_flip=False)
GaussianBlur(kernel_size, sigma=(0.1, 2.0))

Transforms on PIL Image only;
RandomChoice(transforms)
RandomOrder(transforms)

Transforms on torch.*Tensor only;
LinearTransformation(transformation_matrix, mean_vector)
Normalize(mean, std, inplace=False) output[channel] = (input[channel] - mean[channel]) / std[channel]
RandomErasing(p=0.5, scale=(0.02, 0.33), ratio=(0.3, 3.3), value=0, inplace=False)
ConvertImageDtype(dtype: torch.dtype)

Conversion Transforms;
ToPILImage(mode=None)
ToTensor

Generic Transforms;
Lambda(lambd)

Functional Transforms;
Example: you can apply a functional transform with the same parameters to multiple images like this:...
Example: you can use a functional transform to build transform classes with custom behavior:...
adjust_brightness(img: torch.Tensor, brightness_factor: float) → torch.Tensor
adjust_contrast(img: torch.Tensor, contrast_factor: float) → torch.Tensor
adjust_gamma(img: torch.Tensor, gamma: float, gain: float = 1) → torch.Tensor
adjust_hue(img: torch.Tensor, hue_factor: float) → torch.Tensor
adjust_saturation(img: torch.Tensor, saturation_factor: float) → torch.Tensor
...以下省略

コードは、おまけに掲載しておきます。
クラスの書き方は、以下の参考④を参考にしています。
また、各種のtransformの実行結果が参考⑤に掲載されています。
さらに、gaussian noizeの載せ方は、参考⑥を参考にしていますし、同じコードが参考⑦にも掲載されています。
自前のtransform関数をtransforms.Lambda(関数名)から使えることが、参考⑤に記載されていますが、今回は使用していません。
PIL.ImageOps.equalize(image, mask=None)なども使えそう

from PIL import ImageFilter
img = Image.open("sample.jpg")

def blur(img):
    """ガウシアンフィルタを適用する。
    """
    return img.filter(ImageFilter.BLUR)
transform = transforms.Lambda(blur)
img = transform(img)
img

【参考】
vision/docs/source/transforms.rst
Pytorch – torchvision で使える Transform まとめ
How to add noise to MNIST dataset when using pytorch
ということで、以下のような参考⑦のようなことがsample augmentationとして簡単に実行できます。
Pytorch Image Augmentation using Transforms.

・autoencoderに応用する

pytorch-lightningのコードは以下の通りです。
以下のコードでは、画像のリサイズは行っていませんが、Networkを変更すれば行えます。

autoencoderへの応用
class LitAutoEncoder(pl.LightningModule):

    def __init__(self, data_dir='./'):
        super().__init__()
        self.data_dir = data_dir

        # Hardcode some dataset specific attributes
        self.num_classes = 10
        self.classes = ('plane', 'car', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck')
        #self.classes = ('0', '1', '2', '3', '4', '5', '6', '7', '8', '9')
        self.dims = (3, 32, 32)
        self.mean = [0.5,0.5,0.5] #[0.485, 0.456, 0.406] #[0.5,0.5,0.5]
        self.std  = [0.25,0.25,0.25] #[0.229, 0.224, 0.225] #[0.5,0.5,0.5]
        self.imageSize = (32,32)
        self.p=0.5
        self.scale=(0.01, 0.05) #(0.02, 0.33)
        self.ratio=(0.3, 0.3) #(0.3, 3.3)
        self.value=0
        self.inplace=False
        #channels, width, height = self.dims
        self.transform = transforms.Compose([
            transforms.Resize(self.imageSize), # 画像のリサイズ
            transforms.ToTensor(),
            transforms.Normalize(self.mean, self.std),
            transforms.RandomErasing(p=self.p, scale=self.scale, ratio=self.ratio, value=self.value, inplace=self.inplace),
            MyAddGaussianNoise(0., 0.5)
        ])
        self.encoder = Encoder()
        self.decoder = Decoder()

    def forward(self, x):
        # in lightning, forward defines the prediction/inference actions
        embedding = self.encoder(x)
        return embedding

結果
どちらも、1epockでの出力だが、ノイズありの方が出力画像は改善している。

処理無し 上記のcomposeでtransforms適用後
ToTensor(), Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)) Resize(self.imageSize), ToTensor(), Normalize(self.mean, self.std), RandomErasing(...), MyAddGaussianNoise(0., 0.5)
入力original_images_cifar10_32_1.png 入力original_images_cifar10_32_3.png
出力original_autoencode_preds_cifar10_32_1_original.png 出力original_autoencode_preds_cifar10_32_1.png

・自前datasetの作り方

これは、上記では世の中で公開されているデータセットをダウンロードして利用する場合は、簡単に以下のコードで自前のDirに置いて、以下のようにそれを読み込む形でデータをtransformsすればよいが、自前データではファイルや画像を形式に合わせて読み込むところから実施する。
cifar10_full =CIFAR10(self.data_dir, train=True, transform=self.transform)

普通のdataset, Dataloaderの使い方コード
    def prepare_data(self):
        # download
        CIFAR10(self.data_dir, train=True, download=True)
        CIFAR10(self.data_dir, train=False, download=True)

    def setup(self, stage=None): #train, val, testデータ分割
        # Assign train/val datasets for use in dataloaders
        cifar10_full =CIFAR10(self.data_dir, train=True, transform=self.transform)
        n_train = int(len(cifar10_full)*0.8)
        n_val = len(cifar10_full)-n_train
        self.cifar10_train, self.cifar10_val = torch.utils.data.random_split(cifar10_full, [n_train, n_val])
        self.cifar10_test = CIFAR10(self.data_dir, train=False, transform=self.transform)

    def train_dataloader(self):
        self.trainloader = DataLoader(self.cifar10_train, shuffle=True, drop_last = True, batch_size=32, num_workers=0)
        # get some random training images
        return self.trainloader

    def val_dataloader(self):
        return DataLoader(self.cifar10_val, shuffle=False, batch_size=32, num_workers=0)

    def test_dataloader(self):
        self.testloader = DataLoader(self.cifar10_test, shuffle=False, batch_size=32, num_workers=0)
        return self.testloader

①data-labelの場合

まずは、参考②の通り基本が大切です。
前回のmediapipeの学習のところで、以下のようなdatasetを作成して利用しました。
以下では、csvファイルからデータを読み取り、座標に変換して、out_dataとその分類であるout_labelを提供するものでした。

前回のmediapipe_handsデータのためのdatasetコード
class HandsDataset(torch.utils.data.Dataset):
    def __init__(self, data_num, transform=None):
        self.transform = transform
        self.data_num = data_num
        self.data = []
        self.label = []
        df = pd.read_csv('./hands/sample_hands7.csv', sep=',')
        print(df.head(3)) #データの確認
        df = df.astype(int)
        x = []
        for j in range(self.data_num):
            x_ = []
            for i in range(0,21,1):
                x__ = [df['{}'.format(2*i)][j],df['{}'.format(2*i+1)][j]]
                x_.append(x__)
            x.append(x_)
        y = df['42'][:self.data_num]

        #以下のfloat() とlong()の指定は今回の肝です
        self.data = torch.from_numpy(np.array(x)).float()
        print(self.data)
        self.label = torch.from_numpy(np.array(y)).long()
        print(self.label)

    def __len__(self):
        return self.data_num

    def __getitem__(self, idx):
        out_data = self.data[idx]
        out_label =  self.label[idx]
        if self.transform:
            out_data = self.transform(out_data)
        return out_data, out_label

今回は、自前イメージデータを自前datasetとして提供する場合を示します。
結果は以下の通りです。

自前イメージデータのためのdatasetコード
class ImageDataset(torch.utils.data.Dataset):

    def __init__(self, data_num, transform=None):
        self.transform = transform
        self.data_num = data_num
        self.data = []
        self.label = []
        x = []
        y = []
        from_dir = './face/mayuyu/'
        sk = 0
        for path in glob.glob(os.path.join(from_dir, '*.jpg')):    
            image = Image.open(path)
            x.append(np.array(image)/255.)
            y.append(sk)
            sk += 1

        self.data = torch.from_numpy(np.array(x)).float()
        self.label = torch.from_numpy(np.array(y)).long()

    def __len__(self):
        return self.data_num

    def __getitem__(self, idx):
        out_data = self.data[idx]
        out_label =  self.label[idx]
        if self.transform:
            out_data = self.transform(out_data)
        return out_data, out_label

mean, std = [0.5,0.5,0.5], [0.25,0.25,0.25]
model = ImageDataset(10, transform = transforms.Normalize(mean, std))
for i in range(10):
    image =  model.data[i]
    print(model.label[i], image)
    plt.title('label_{}'.format(model.label[i]))
    plt.imshow(image)
    plt.pause(1)
    plt.close()

②data1-data2-labelのような場合

このコードでは、いわゆるcifar10のdatasetをダウンロードして、gray化し、元のカラー画像と同時にdatasetを提供するものである。このとき元のlabelは当然必要なシーンがあるので、それも同時に提供する。
基本は、上記の参考①でout_dataとしてgray画像、out_labelとしてカラー画像を出力するdatasetのコードを示しているが、ここに本来のlabelも同時に出力するものである。
また、コードは極力分かり易さを心がけた。
利用も今まで同様の以下のコードでデータ数、trans1, trans2に応じた処理されたデータ群に対して、batch数に応じたデータ数で使える。
つまり、実行結果のとおり、以下のコードの場合、32個のデータ生成をして、そこから4個ずつ取り出して使うこととなる。

dataset = ImageDataset(32,transform1 = trans1, transform2 = trans2)
testloader = DataLoader(dataset, batch_size=4, shuffle=True, num_workers=0)

Cifar10データを処理データと処理無しデータ,そしてlabel提供のためのdatasetコード
import numpy as np
import torch
import torchvision
from torch.utils.data import DataLoader, random_split
from torchvision import transforms
import cv2
import matplotlib.pyplot as plt
from torchvision.datasets import CIFAR10
from PIL import Image

class ImageDataset(torch.utils.data.Dataset):

    def __init__(self, data_num, transform1 = None, transform2 = None,train = True):

        self.transform1 = transform1
        self.transform2 = transform2
        self.ts = torchvision.transforms.ToPILImage()
        self.ts2 = transform=transforms.ToTensor()

        self.data_dir = './'
        self.data_num = data_num
        self.data = []
        self.label = []

        # download
        CIFAR10(self.data_dir, train=True, download=True)
        CIFAR10(self.data_dir, train=False, download=True)
        self.data =CIFAR10(self.data_dir, train=True, transform=self.ts2)

    def __len__(self):
        return self.data_num

    def __getitem__(self, idx):
        out_data = self.ts(self.data[idx][0])
        out_label =  np.array(self.data[idx][1])
        if self.transform1:
            out_data1 = self.transform1(out_data)
        if self.transform2:
            out_data2 = self.transform2(out_data)
        return out_data1, out_data2, out_label

trans1 = torchvision.transforms.ToTensor()
trans2 = torchvision.transforms.Compose([torchvision.transforms.Grayscale(), torchvision.transforms.ToTensor()])

dataset = ImageDataset(32,transform1 = trans1, transform2 = trans2)
testloader = DataLoader(dataset, batch_size=4,
                            shuffle=True, num_workers=0)

ts = torchvision.transforms.ToPILImage()

for out_data1, out_data2, out_label in testloader:
    print(len(out_label),out_label)
    for i in range(len(out_label)):
        image =  out_data1[i]
        image_gray = out_data2[i]
        im = ts(image)
        im_gray = ts(image_gray)
        #print(out_label[i])
        plt.imshow(np.array(im_gray),  cmap='gray')
        plt.title('{}'.format(out_label[i]))
        plt.pause(1)
        plt.clf()
        plt.imshow(np.array(im))
        plt.title('{}'.format(out_label[i]))
        plt.pause(1)
        plt.clf()
plt.close() 

実行結果は以下のとおり
>python dataset_cifar10_original.py
Files already downloaded and verified
Files already downloaded and verified
4 tensor([0, 3, 2, 6], dtype=torch.int32)
tensor(0, dtype=torch.int32)
tensor(3, dtype=torch.int32)
tensor(2, dtype=torch.int32)
tensor(6, dtype=torch.int32)
4 tensor([2, 2, 9, 5], dtype=torch.int32)
tensor(2, dtype=torch.int32)
tensor(2, dtype=torch.int32)
tensor(9, dtype=torch.int32)
tensor(5, dtype=torch.int32)
4 tensor([3, 6, 1, 7], dtype=torch.int32)
tensor(3, dtype=torch.int32)
tensor(6, dtype=torch.int32)
tensor(1, dtype=torch.int32)
tensor(7, dtype=torch.int32)
4 tensor([3, 9, 4, 9], dtype=torch.int32)
tensor(3, dtype=torch.int32)
tensor(9, dtype=torch.int32)
tensor(4, dtype=torch.int32)
tensor(9, dtype=torch.int32)
4 tensor([7, 8, 4, 4], dtype=torch.int32)
tensor(7, dtype=torch.int32)
tensor(8, dtype=torch.int32)
tensor(4, dtype=torch.int32)
tensor(4, dtype=torch.int32)
4 tensor([6, 7, 9, 0], dtype=torch.int32)
tensor(6, dtype=torch.int32)
tensor(7, dtype=torch.int32)
tensor(9, dtype=torch.int32)
tensor(0, dtype=torch.int32)
4 tensor([4, 1, 9, 2], dtype=torch.int32)
tensor(4, dtype=torch.int32)
tensor(1, dtype=torch.int32)
tensor(9, dtype=torch.int32)
tensor(2, dtype=torch.int32)
4 tensor([6, 9, 6, 3], dtype=torch.int32)
tensor(6, dtype=torch.int32)
tensor(9, dtype=torch.int32)
tensor(6, dtype=torch.int32)
tensor(3, dtype=torch.int32)

まとめ

・transformsで遊んでみた
・自前datasetを作って遊んでみた
・自前データを利用した自前datasetが作成できるようになった
・いろいろな処理を施して、その結果得られる各種datasetを同時に提供できるdataset及びそのDataloaderの利用の仕方を学んだ

・これを利用して、改めてdenoizing, coloring, 画像拡大、画像合成などの学習と利用アプリを作りたい

おまけ

import torchvision.transforms.functional as TF
import random
import matplotlib.pyplot as plt
import cv2
from PIL import Image
import numpy as np
import torch
import torchvision

class MyRotationTransform:MyRotationTransform
    """Rotate by one of the given angles."""

    def __init__(self, angles):
        self.angles = angles

    def __call__(self, x):
        angle = random.choice(self.angles)
        return TF.rotate(x, angle)

class MyGrayscaleTransform:
    """GrayScale by this class."""

    def __init__(self):
        pass

    def __call__(self, x):
        #return TF.rgb_to_grayscale(x)
        return TF.to_grayscale(x)

class MyVflipTransform:
    """Vertical flip by this class."""

    def __init__(self):
        pass

    def __call__(self, x):
        return TF.vflip(x)    

class MyHflipTransform:
    """Vertical flip by this class."""

    def __init__(self):
        pass

    def __call__(self, x):
        return TF.hflip(x)   

from torchvision import transforms    
class MyNormalizeTransform:
    """normalization by the image."""

    def __init__(self):
        self.imageSize = (512,512)
        self.mean = [0.485, 0.456, 0.406]
        self.std  = [0.229, 0.224, 0.225]

    def __call__(self, x):
        img = self.transform = transforms.Compose([
            transforms.Resize(self.imageSize), # 画像のリサイズ
            transforms.ToTensor(), # Tensor化
            transforms.Normalize(self.mean, self.std), # 標準化
        ])
        return img(x) 

class MyErasingTransform:
    """normalization by the image."""

    def __init__(self):
        self.imageSize = (512,512)
        self.p=0.5
        self.scale=(0.02, 0.33)
        self.ratio=(0.3, 3.3)
        self.value=0
        self.inplace=False

    def __call__(self, x):
        self.transform = transforms.Compose([
            transforms.Resize(self.imageSize), # 画像のリサイズ
            transforms.ToTensor(), # Tensor化
            transforms.RandomErasing(p=self.p, scale=self.scale, ratio=self.ratio, value=self.value, inplace=self.inplace)
        ])
        return self.transform(x)     

class MyAddGaussianNoise(object):
    def __init__(self, mean=0., std=0.1):
        self.std = std
        self.mean = mean

    def __call__(self, tensor):
        return tensor + torch.randn(tensor.size()) * self.std + self.mean

    def __repr__(self):
        return self.__class__.__name__ + '(mean={0}, std={1})'.format(self.mean, self.std)  

trans2 = torchvision.transforms.Compose([torchvision.transforms.Grayscale(), torchvision.transforms.ToTensor()])
ts = torchvision.transforms.ToPILImage()

trans3 = MyGrayscaleTransform()
trans4 = MyHflipTransform()
trans5 = MyNormalizeTransform()
trans6 = MyErasingTransform()
trans7 = transforms.Compose([
        transforms.ToTensor(),
        #transforms.Normalize((0.1307,), (0.3081,)),
        MyAddGaussianNoise(0., 0.1)
        ])

angle_list =[i for i in range(-10,10,1)] #[-30, -15, 0, 15, 30]
rotation_transform = MyRotationTransform(angles=angle_list)

x = Image.open('./face/mayuyu/2.jpg')
while 1:
    y = rotation_transform(x)
    #z = trans5(x)
    z = trans7(y)
    plt.imshow(ts(z))
    plt.pause(0.1)
    #z = trans3(x)
    #plt.imshow(z,  cmap='gray')
    #plt.pause(0.1)
    #plt.imshow(np.array(ts(trans2(y))),  cmap='gray')
    #plt.pause(0.1)
    plt.clf()
2
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
6