PyTorch 1.0 : Getting Started : データロードと処理

PyTorch 1.0 : Getting Started : データロードと処理 (翻訳/解説)
翻訳 : (株)クラスキャット セールスインフォメーション
作成日時 : 12/09/2018 (1.0.0.dev20181207)

* 本ページは、PyTorch 1.0 Tutorials の DATA LOADING AND PROCESSING TUTORIAL を翻訳した上で適宜、補足説明したものです:

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

 

データロードと処理

任意の機械学習問題を解く際の多大な努力はデータの準備にあります。PyTorch はデータ・ローディングを簡単にしてそして願わくば、貴方のコードの可読性をより高めるための多くのツールを提供します。このチュートリアルでは、自明ではないデータセットからデータをどのようにロードして前処理/増強するかを見ていきます。

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

  • scikit-image: 画像 io と変換のために
  • pandas: より容易な csv 解析のために
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

私達が扱うデータセットは顔姿勢です(= facial pose)。これは顔がこのようにアノテートされることを意味します :

全体として、各顔のために 68 の異なる目印ポイントがアノテートされています。

NOTE: データセットは ここ からダウンロードしてください、画像は ‘data/faces/’ という名前のディレクトリ内にあります。このデータセットは実際には ‘face’ としてタグ付けされた imagenet からの少々の画像上で優れた dlib’s pose estimation を適用することにより生成されました。

データセットはこのように見えるアノテーションを持つ 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('data/faces/face_landmarks.csv')

n = 65
img_name = landmarks_frame.iloc[n, 0]
landmarks = landmarks_frame.iloc[n, 1:].as_matrix()
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]))
Image name: person-7.jpg
Landmarks shape: (68, 2)
First 4 Landmarks: [[32. 65.]
 [33. 76.]
 [34. 86.]
 [34. 97.]]

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

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('data/faces/', img_name)),
               landmarks)
plt.show()

 

Dataset クラス

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

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

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

dataset のサンプルは辞書 {‘image’: image, ‘landmarks’: landmarks} です。dataset はオプションの引数 tranform を取り任意の必要な処理がサンプルに適用されます。次のセクションで 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):
        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:].as_matrix()
        landmarks = landmarks.astype('float').reshape(-1, 2)
        sample = {'image': image, 'landmarks': landmarks}

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

        return sample

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

face_dataset = FaceLandmarksDataset(csv_file='data/faces/face_landmarks.csv',
                                    root_dir='data/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

0 (324, 215, 3) (68, 2)
1 (500, 333, 3) (68, 2)
2 (250, 258, 3) (68, 2)
3 (434, 290, 3) (68, 2)

 

Transform

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

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

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

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

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

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

 

transform を構成する

さて、サンプル上で transform を適用します。

画像の短い側を 256 に再スケールしてそしてランダムにそれからサイズ 224 の矩形をクロップすることを望むと仮定します。i.e., Rescale と RandomCrop transform を組み合わせることを望みます。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()

 

dataset を通して iterate する

構成された transform で dataset を作成するためにこの総てを一緒にしましょう。要約すれば、このデータがサンプリングされるたびに :

  • 画像は on the fly にファイルから読まれます。
  • Transform は読まれた画像に適用されます。
  • transform の一つがランダムなので、data はサンプリング上で増強されます。

作成された dataset に渡って前のように for i in range ループで iterate できます。

transformed_dataset = FaceLandmarksDataset(csv_file='data/faces/face_landmarks.csv',
                                           root_dir='data/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
0 torch.Size([3, 224, 224]) torch.Size([68, 2])
1 torch.Size([3, 224, 224]) torch.Size([68, 2])
2 torch.Size([3, 224, 224]) torch.Size([68, 2])
3 torch.Size([3, 224, 224]) torch.Size([68, 2])

けれども、データに渡り iterate するために単純な 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

0 torch.Size([4, 3, 224, 224]) torch.Size([4, 68, 2])
1 torch.Size([4, 3, 224, 224]) torch.Size([4, 68, 2])
2 torch.Size([4, 3, 224, 224]) torch.Size([4, 68, 2])
3 torch.Size([4, 3, 224, 224]) torch.Size([4, 68, 2])

 

この後: torchvision

このチュートリアルでは、dataset、transform そして dataloader をどのように書いて使用するかを見てきました。torchvision パッケージは幾つかの一般的な dataset と transform を提供します。貴方はカスタムクラスを書くことさえ必要ないかもしれません。torchvision で利用可能なより一般的な dataset の一つは ImageFolder です。それは画像が次の方法で体系化されていることを仮定しています :

root/ants/xxx.png
root/ants/xxy.jpeg
root/ants/xxz.png
.
.
.
root/bees/123.jpg
root/bees/nsdf3.png
root/bees/asd932_.png

ここで ‘ants’, ‘bees’ etc. はクラス・ラベルです。同様に RandomHorizontalFlip, Scale のように PIL.Image 上で動作する一般的な変換もまた利用可能です。このように dataloader を書くためにこれらを使用できます :

import torch
from torchvision import transforms, datasets

data_transform = transforms.Compose([
        transforms.RandomSizedCrop(224),
        transforms.RandomHorizontalFlip(),
        transforms.ToTensor(),
        transforms.Normalize(mean=[0.485, 0.456, 0.406],
                             std=[0.229, 0.224, 0.225])
    ])
hymenoptera_dataset = datasets.ImageFolder(root='hymenoptera_data/train',
                                           transform=data_transform)
dataset_loader = torch.utils.data.DataLoader(hymenoptera_dataset,
                                             batch_size=4, shuffle=True,
                                             num_workers=4)

訓練コードを持つサンプルについては、Transfer Learning Tutorial を見てください。

 
以上