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)。これは顔がこのようにアノテートされることを意味します :
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 を見てください。
以上