PyTorch 1.5 レシピ : データカスタマイゼーション : カスタム PyTorch データローダを開発する

PyTorch 1.5 レシピ : データカスタマイゼーション : カスタム PyTorch データローダを開発する (翻訳/解説)
翻訳 : (株)クラスキャット セールスインフォメーション
作成日時 : 05/14/2020 (1.5.0)

* 本ページは、PyTorch 1.5 Recipes の以下のページを翻訳した上で適宜、補足説明したものです:

* サンプルコードの動作確認はしておりますが、必要な場合には適宜、追加改変しています。
* ご自由にリンクを張って頂いてかまいませんが、sales-info@classcat.com までご一報いただけると嬉しいです。

 

無料セミナー開催中 クラスキャット主催 人工知能 & ビジネス Web セミナー

人工知能とビジネスをテーマにウェビナー (WEB セミナー) を定期的に開催しています。スケジュールは弊社 公式 Web サイト でご確認頂けます。
  • お住まいの地域に関係なく Web ブラウザからご参加頂けます。事前登録 が必要ですのでご注意ください。
  • Windows PC のブラウザからご参加が可能です。スマートデバイスもご利用可能です。

お問合せ : 本件に関するお問い合わせ先は下記までお願いいたします。

株式会社クラスキャット セールス・マーケティング本部 セールス・インフォメーション
E-Mail:sales-info@classcat.com ; WebSite: https://www.classcat.com/
Facebook: https://www.facebook.com/ClassCatJP/

 

基本 : カスタム PyTorch データローダを開発する

機械学習アルゴリズムを開発するために適用される努力の本質的な総量はデータ準備に関係します。PyTorch はデータロードを容易にして願わくばコードをより可読にするための多くのツールを提供します。このレシピでは、どのように以下を行なうかを学習します :

  1. PyTorch データセット API を活用してカスタム・データセットを作成する ;
  2. 構成可能な callable カスタム変換を作成する ; そして
  3. これらのコンポーネントをまとめてカスタム・データローダを作成する。

このチュートリアルを実行するためには、以下のパッケージがインストールされていることを確実にすることに注意してください :

  • scikit-image: 画像 io と変換のため
  • pandas: 容易な csv 解析のため

帰属のポイントとして、このレシピは、後で Joe Spisak により編集された、Sasank Chilamkurthy からのオリジナルのチュートリアルに基づきます。

 

セットアップ

最初にこのレシピのために必要なライブラリの総てをインポートしましょう。

from __future__ import print_function, division
import os
import torch
import pandas as pd
from skimage import io, transform
import numpy as np
import matplotlib.pyplot as plt
from torch.utils.data import Dataset, DataLoader
from torchvision import transforms, utils

# Ignore warnings
import warnings
warnings.filterwarnings("ignore")

plt.ion()   # interactive mode

 

パート 1: データセット

扱っていくデータセットは顔のポーズのそれです。全体として、各顔のために 68 の異なる目印点がアノテートされています。

次のステップとして、ここ からデータセットをダウンロードしてください、その結果画像は ‘data/faces/’ という名前のディレクトリにあります。

Note: データセットは実際には ‘face’ タグを含む imagenet データセットからの画像上で dlib のポーズ推定 を適用することにより生成されました。

!wget https://download.pytorch.org/tutorial/faces.zip
!mkdir data/faces/
import zipfile
with zipfile.ZipFile("faces.zip","r") as zip_ref:
zip_ref.extractall("/data/faces/")
%cd /data/faces/

データセットはこのように見えるアノテーションを持つ csv ファイルを伴っています :

image_name,part_0_x,part_0_y,part_1_x,part_1_y,part_2_x, ... ,part_67_x,part_67_y
0805personali01.jpg,27,83,27,98, ... 84,134
1084239450_e76e00b7e7.jpg,70,236,71,257, ... ,128,312

CSV を素早く読んで (N, 2) 配列のアノテーションを得ましょう、ここで N は目印の数です。

landmarks_frame = pd.read_csv('faces/face_landmarks.csv')

n = 65
img_name = landmarks_frame.iloc[n, 0]
landmarks = landmarks_frame.iloc[n, 1:]
landmarks = np.asarray(landmarks)
landmarks = landmarks.astype('float').reshape(-1, 2)

print('Image name: {}'.format(img_name))
print('Landmarks shape: {}'.format(landmarks.shape))
print('First 4 Landmarks: {}'.format(landmarks[:4]))

 

1.1 画像を表示するため単純なヘルパー関数を書く

次に画像、その目印を表示するための単純なヘルパー関数を書いてそしてサンプルを表示するためにそれを使用しましょう。

def show_landmarks(image, landmarks):
    """Show image with landmarks"""
    plt.imshow(image)
    plt.scatter(landmarks[:, 0], landmarks[:, 1], s=10, marker='.', c='r')
    plt.pause(0.001)  # pause a bit so that plots are updated

plt.figure()
show_landmarks(io.imread(os.path.join('faces/', img_name)),
               landmarks)
plt.show()

 

1.2 dataset クラスを作成する

今は PyTorch dataset クラスについて話しましょう。

torch.utils.data.Dataset はデータセットを表す抽象クラスです。貴方のカスタム・データセットは Dataset を継承して以下のメソッドを override するべきです :

  • __len__ len(dataset) がデータセットのサイズを返すため。
  • __getitem__ dataset[i] が $i$ th サンプルを得るために利用できるようなインデキシングをサポートするため。

顔目印データセットのために dataset クラスを作成しましょう。__init__ で csv を読みますが画像の読み込みは __getitem__ に任せます。これはメモリ効率のためです、何故ならば総ての画像は一度にメモリにストアされないからです、しかし必要に応じて読まれます。

ここでデータセットのサンプルを dict {‘image’: image, ‘landmarks’: landmarks} の形式で示します。私達のデータセットはオプションの引数 transform を取ります、任意の必要な処理がサンプル上に適用できるようにです。他のレシピで transform の有用性を見ます。

class FaceLandmarksDataset(Dataset):
    """Face Landmarks dataset."""

    def __init__(self, csv_file, root_dir, transform=None):
        """
        Args:
            csv_file (string): Path to the csv file with annotations.
            root_dir (string): Directory with all the images.
            transform (callable, optional): Optional transform to be applied
                on a sample.
        """
        self.landmarks_frame = pd.read_csv(csv_file)
        self.root_dir = root_dir
        self.transform = transform

    def __len__(self):
        return len(self.landmarks_frame)

    def __getitem__(self, idx):
        if torch.is_tensor(idx):
            idx = idx.tolist()

        img_name = os.path.join(self.root_dir,
                                self.landmarks_frame.iloc[idx, 0])
        image = io.imread(img_name)
        landmarks = self.landmarks_frame.iloc[idx, 1:]
        landmarks = np.array([landmarks])
        landmarks = landmarks.astype('float').reshape(-1, 2)
        sample = {'image': image, 'landmarks': landmarks}

        if self.transform:
            sample = self.transform(sample)

        return sample

 

1.3 データサンプルを通して iterate する

次にこのクラスをインスタンス化してデータサンプルを通して iterate しましょう。最初の 4 サンプルのサイズをプリントしてそれらの目印を示します。

face_dataset = FaceLandmarksDataset(csv_file='faces/face_landmarks.csv',
                                    root_dir='faces/')

fig = plt.figure()

for i in range(len(face_dataset)):
    sample = face_dataset[i]

    print(i, sample['image'].shape, sample['landmarks'].shape)

    ax = plt.subplot(1, 4, i + 1)
    plt.tight_layout()
    ax.set_title('Sample #{}'.format(i))
    ax.axis('off')
    show_landmarks(**sample)

    if i == 3:
        plt.show()
        break

 

パート 2 : データ変換

作業するデータセットを持ちそしてある程度のカスタマイゼーションを行なった今、カスタム変換の作成に移行できます。コンピュータビジョンでは、これらはアルゴリズムを一般化して精度を改善する助けとなる結果になります。訓練時に使用される変換のスーツは典型的にはデータ増強として参照されて現代的なモデル開発のための一般的な実践です。

データセットを処理する際に共通な一つの問題はサンプルは総てが同じサイズではないかもしれないことです。殆どのニューラルネットワークは固定サイズの画像を想定します。従って、何某かの前処理コードを書く必要があります。3 つの変換を作成しましょう :

  • Rescale: 画像をスケールするため
  • RandomCrop: 画像からランダムにクロップします。これはデータ増強です。
  • ToTensor: numpy 画像を torch 画像に変換するため (軸を swap する必要があります)。

それらを単純な関数の代わりに callable クラスとして書きます、その結果、変換のパラメータはそれが呼び出されるたびに渡される必要はありません。このためには、__call__ メソッドと必要であれば、__init__ メソッドを実装する必要があるだけです。それからこのように変換を使用できます :

tsfm = Transform(params)
transformed_sample = tsfm(sample)

これらの変換が画像と目印の両者の上でどのように適用されなければならないかを下で観察してください。

 

2.1 callable クラスを作成する

各変換のための callable クラスを作成することから始めましょう。

class Rescale(object):
    """Rescale the image in a sample to a given size.

    Args:
        output_size (tuple or int): Desired output size. If tuple, output is
            matched to output_size. If int, smaller of image edges is matched
            to output_size keeping aspect ratio the same.
    """

    def __init__(self, output_size):
        assert isinstance(output_size, (int, tuple))
        self.output_size = output_size

    def __call__(self, sample):
        image, landmarks = sample['image'], sample['landmarks']

        h, w = image.shape[:2]
        if isinstance(self.output_size, int):
            if h > w:
                new_h, new_w = self.output_size * h / w, self.output_size
            else:
                new_h, new_w = self.output_size, self.output_size * w / h
        else:
            new_h, new_w = self.output_size

        new_h, new_w = int(new_h), int(new_w)

        img = transform.resize(image, (new_h, new_w))

        # h and w are swapped for landmarks because for images,
        # x and y axes are axis 1 and 0 respectively
        landmarks = landmarks * [new_w / w, new_h / h]

        return {'image': img, 'landmarks': landmarks}


class RandomCrop(object):
    """Crop randomly the image in a sample.

    Args:
        output_size (tuple or int): Desired output size. If int, square crop
            is made.
    """

    def __init__(self, output_size):
        assert isinstance(output_size, (int, tuple))
        if isinstance(output_size, int):
            self.output_size = (output_size, output_size)
        else:
            assert len(output_size) == 2
            self.output_size = output_size

    def __call__(self, sample):
        image, landmarks = sample['image'], sample['landmarks']

        h, w = image.shape[:2]
        new_h, new_w = self.output_size

        top = np.random.randint(0, h - new_h)
        left = np.random.randint(0, w - new_w)

        image = image[top: top + new_h,
                      left: left + new_w]

        landmarks = landmarks - [left, top]

        return {'image': image, 'landmarks': landmarks}


class ToTensor(object):
    """Convert ndarrays in sample to Tensors."""

    def __call__(self, sample):
        image, landmarks = sample['image'], sample['landmarks']

        # swap color axis because
        # numpy image: H x W x C
        # torch image: C X H X W
        image = image.transpose((2, 0, 1))
        return {'image': torch.from_numpy(image),
                'landmarks': torch.from_numpy(landmarks)}

 

2.2 変換を構成してサンプルに適用する

次にこれらの変換を構成してサンプルに適用しましょう。

画像の短い側を 256 にリスケールしてからそれからサイズ 224 の正方形をランダムにクロップすることを望むとします i.e. Rescale と RandomCrop 変換を構成することを望みます。torchvision.transforms.Compose はこれを行なうことを可能にする単純な callable クラスです。

scale = Rescale(256)
crop = RandomCrop(128)
composed = transforms.Compose([Rescale(256),
                               RandomCrop(224)])

# Apply each of the above transforms on sample.
fig = plt.figure()
sample = face_dataset[65]
for i, tsfrm in enumerate([scale, crop, composed]):
    transformed_sample = tsfrm(sample)

    ax = plt.subplot(1, 3, i + 1)
    plt.tight_layout()
    ax.set_title(type(tsfrm).__name__)
    show_landmarks(**transformed_sample)

plt.show()

 

2.3 データセットを通して反復する

次にデータセットを通して反復します。

構成された変換を持つデータセットを作成するためにこの全てを一つにまとめましょう。要約すれば、このデータセットがサンプリングされるたびに :

  • 画像が on the fly にファイルから読まれます。
  • 変換が読まれた画像に適用されます。
  • 変換の一つがランダムですから、データはサンプリング時に増強されます。

作成されたデータセットに渡り “for i in range” ループで前のように反復できます。

transformed_dataset = FaceLandmarksDataset(csv_file='faces/face_landmarks.csv',
                                           root_dir='faces/',
                                           transform=transforms.Compose([
                                               Rescale(256),
                                               RandomCrop(224),
                                               ToTensor()
                                           ]))

for i in range(len(transformed_dataset)):
    sample = transformed_dataset[i]

    print(i, sample['image'].size(), sample['landmarks'].size())

    if i == 3:
        break

 

パート 3: データローダ

データセットに直接作用することにより、データに渡り反復するために単純な for ループを使用することで多くの機能を見逃しています。特に以下について失っています :

  • データをバッチ処理する
  • データをシャッフルする
  • マルチ処理ワーカーを使用してデータを並列にロードする

torch.utils.data.DataLoader はこれら総ての機能を提供する iterator です。下で使用されるパラメータは明らかであるはずです。関心のある一つのパラメータは collate_fn です。サンプルが collate_fn を使用してどのように正確にバッチ処理される必要があるかを指定できます。けれども、デフォルトの collate は殆どのユースケースに対して素晴らしく動作するはずです。

dataloader = DataLoader(transformed_dataset, batch_size=4,
                        shuffle=True, num_workers=4)


# Helper function to show a batch
def show_landmarks_batch(sample_batched):
    """Show image with landmarks for a batch of samples."""
    images_batch, landmarks_batch = \
            sample_batched['image'], sample_batched['landmarks']
    batch_size = len(images_batch)
    im_size = images_batch.size(2)

    grid = utils.make_grid(images_batch)
    plt.imshow(grid.numpy().transpose((1, 2, 0)))

    for i in range(batch_size):
        plt.scatter(landmarks_batch[i, :, 0].numpy() + i * im_size,
                    landmarks_batch[i, :, 1].numpy(),
                    s=10, marker='.', c='r')

        plt.title('Batch from dataloader')

for i_batch, sample_batched in enumerate(dataloader):
    print(i_batch, sample_batched['image'].size(),
          sample_batched['landmarks'].size())

    # observe 4th batch and stop.
    if i_batch == 3:
        plt.figure()
        show_landmarks_batch(sample_batched)
        plt.axis('off')
        plt.ioff()
        plt.show()
        break

カスタム・データローダを PyTorch でどのように作成するかを学習した今、docs に深くダイブして貴方のワークフローを更にカスタマイズすることを勧めます。torch.utils.data docs において ここ で更に学習できます。

 
以上