【譯】pytorch教程之載入數據與預處理
寫在前面:本文僅是自己的學習筆記,還有待完善的地方,如有不當,還請指正。【侵刪】
解決機器學習問題花費的很多心思都是在準備數據上。為了讓你的代碼可讀性更高,Pytorch提供了很多讓載入數據更簡單的工具。在這個教程中,我們將看到如何對不一般的數據進行載入和預處理/數據增強。
為了能夠運行教程中的例子,請確保你已經安裝了下面的包:
- scikit-image:需要io和transforms
- pandas:操作csv文件更方便
(譯者注:先導入我們接下來所需要的庫)
from __future__ import print_function, divisionnimport osnimport torchnimport pandas as pdnfrom skimage import io, transformnimport numpy as npnimport matplotlib.pyplot as pltnfrom torch.utils.data import Dataset, DataLoadernfrom torchvision import transforms, utilsnn# 忽略warningsnimport warningsnwarnings.filterwarnings("ignore")nnplt.ion() # 開啟交互模式n
我們接下來將要處理的是面部姿勢數據集。這意味著一張臉做了這樣的標註:
每張臉的圖像總共有68個不同的標記點。
Note:
為了讓圖片以『faces/』的文件夾命名,請從這裡下載數據集。這份數據是從ImageNet中選取一些標記為『face』的圖片,使用 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_yn0805personali01.jpg,27,83,27,98, ... 84,134n1084239450_e76e00b7e7.jpg,70,236,71,257, ... ,128,312n
讓我們快速的讀取CSV文件,以(N,2)的數組形式獲得標記點,其中N表示標記點的個數。
landmarks_frame = pd.read_csv(faces/face_landmarks.csv)nnn = 65nimg_name = landmarks_frame.ix[n, 0]nlandmarks = landmarks_frame.ix[n, 1:].as_matrix().astype(float)nlandmarks = landmarks.reshape(-1, 2)nnprint(Image name: {}.format(img_name))nprint(Landmarks shape: {}.format(landmarks.shape))nprint(First 4 Landmarks: {}.format(landmarks[:4]))n
輸出:
Image name: person-7.jpgnLandmarks shape: (68, 2)nFirst 4 Landmarks: [[ 32. 65.]n [ 33. 76.]n [ 34. 86.]n [ 34. 97.]]n
我們先寫一個可以顯示一張圖片和它的標記點的函數,然後可以用它來顯示一個樣本。
def show_landmarks(image, landmarks):n """顯示帶標記點的圖片"""n plt.imshow(image)n plt.scatter(landmarks[:, 0], landmarks[:, 1], s=10, marker=., c=r)n plt.pause(0.001) nnplt.figure()nshow_landmarks(io.imread(os.path.join(faces/, img_name)),n landmarks)nplt.show()n
Dataset類
torch.utils.data.Dataset是一個表示數據集的抽象類。自己的數據集需要繼承Dataset這個類,然後再重寫下面的方法:
- __len__ 使len(dataset)返回數據集的大小
- __getitem__ 支持地址索引,使得dataset[i]可以獲取第i個樣本
我們創建一個人臉標記點數據的類。把讀取csv文件的工作放在了__init__中,而將讀取圖片的任務放在了__getitem__里。這樣做可以充分利用內存,因為圖片不需要一下子全部讀取內存,而是按需讀取(譯者注:讀取一批)。
我們的數據集是以{image: image, landmarks: landmarks}的字典形式存儲的。數據集的類有一個可選的參數transform,這樣就可以對數據做特定的預處理操作。在下一節中我們會看到transfrom的作用。
class FaceLandmarksDataset(Dataset):n """人臉標記點數據"""nn def __init__(self, csv_file, root_dir, transform=None):n """n 參數:n csv_file (字元串): 標記點csv文件的路徑。n root_dir (字元串): 圖片的字典。n transform (callable, 可選): 可選的transform操作。n """n self.landmarks_frame = pd.read_csv(csv_file)n self.root_dir = root_dirn self.transform = transformnn def __len__(self):n return len(self.landmarks_frame)nn def __getitem__(self, idx):n img_name = os.path.join(self.root_dir, self.landmarks_frame.ix[idx, 0])n image = io.imread(img_name)n landmarks = self.landmarks_frame.ix[idx, 1:].as_matrix().astype(float)n landmarks = landmarks.reshape(-1, 2)n sample = {image: image, landmarks: landmarks}nn if self.transform:n sample = self.transform(sample)nn return samplen
讓我們實例化這個類,並且遍歷所有的數據樣本。我們將列印前4個樣本的形狀,顯示他們的標記點。
face_dataset = FaceLandmarksDataset(csv_file=faces/face_landmarks.csv,n root_dir=faces/)nnfig = plt.figure()nnfor i in range(len(face_dataset)):n sample = face_dataset[i]nn print(i, sample[image].shape, sample[landmarks].shape)nn ax = plt.subplot(1, 4, i + 1)n plt.tight_layout()n ax.set_title(Sample #{}.format(i))n ax.axis(off)n show_landmarks(**sample) # 1 nn if i == 3:n plt.show()n breakn
(譯者注:# 1 **表示接收字典參數)
輸出:
0 (324, 215, 3) (68, 2)n1 (500, 333, 3) (68, 2)n2 (250, 258, 3) (68, 2)n3 (434, 290, 3) (68, 2)n
Transforms
從上面的輸出中我們可以發現樣本的尺寸大小不一致。而大部分的神經網路都要求圖片以固定的尺寸輸入網路。因此,我們需要寫個預處理函數。我們創建三個transforms:
- Rescale:修改圖片尺寸
- RandomCrop:隨機裁剪圖片。數據增強方法
- ToTensor:將圖片從numpy格式轉為torch(我們需要交換維度)
我們把它們寫成可以調用的類而不是簡單的函數,這樣每次只需要傳遞需要的參數就可以調用transforms函數了。這樣的話,我們就需要完成__call__方法以及__init__,如果需要這個的話。之後我們就可以像這樣使用transforms了:
tsfm = Transform(params)ntransformed_sample = tsfm(sample)n
仔細觀察下面的函數是如何同時對圖片和標記點做transforms的。
class Rescale(object):n """將樣本中圖片修改為規定的尺寸.nn 參數:n output_size (init 或者 tuple): 要求的輸出尺寸. 如果是tuple, 輸出和output_size匹配。n如果是int, 圖片的短邊是output_size,長邊按比例縮放。n """nn def __init__(self, output_size):n assert isinstance(output_size, (int, tuple))n self.output_size = output_sizenn def __call__(self, sample):n image, landmarks = sample[image], sample[landmarks]nn h, w = image.shape[:2]n if isinstance(self.output_size, int):n if h > w:n new_h, new_w = self.output_size * h / w, self.output_sizen else:n new_h, new_w = self.output_size, self.output_size * w / hn else:n new_h, new_w = self.output_sizenn new_h, new_w = int(new_h), int(new_w)nn img = transform.resize(image, (new_h, new_w))nn # 對landmarks來說h和w需要交換位置,因為對圖片來說,x和y分別是第1維和第0維n landmarks = landmarks * [new_w / w, new_h / h]nn return {image: img, landmarks: landmarks}nnnclass RandomCrop(object):n """隨機裁剪圖片.nn 參數:n output_size (tuple or int): 期望的輸出尺寸. 如果是int, 做正方形裁剪.n """nn def __init__(self, output_size):n assert isinstance(output_size, (int, tuple))n if isinstance(output_size, int):n self.output_size = (output_size, output_size)n else:n assert len(output_size) == 2n self.output_size = output_sizenn def __call__(self, sample):n image, landmarks = sample[image], sample[landmarks]nn h, w = image.shape[:2]n new_h, new_w = self.output_sizenn top = np.random.randint(0, h - new_h)n left = np.random.randint(0, w - new_w)nn image = image[top: top + new_h,n left: left + new_w]nn landmarks = landmarks - [left, top]nn return {image: image, landmarks: landmarks}nnnclass ToTensor(object):n """將ndarrays轉化為Tensors."""nn def __call__(self, sample):n image, landmarks = sample[image], sample[landmarks]nn # 交換顏色通道,因為n # numpy圖片: H x W x Cn # torch圖片: C X H X Wn image = image.transpose((2, 0, 1))n return {image: torch.from_numpy(image),n landmarks: torch.from_numpy(landmarks)}n
Compose transforms
現在我們就將transform運用在一個樣本上。
我們說我們想將圖片的短邊變為256,之後隨機裁剪一個邊長為224的正方形。這樣的話,我們就需要組合Rescale和RandomCrop了。
scale = Rescale(256)ncrop = RandomCrop(128)ncomposed = transforms.Compose([Rescale(256),n RandomCrop(224)])nn# 對樣本進行上面的每一個操作.nfig = plt.figure()nsample = face_dataset[65]nfor i, tsfrm in enumerate([scale, crop, composed]):n transformed_sample = tsfrm(sample)nn ax = plt.subplot(1, 3, i + 1)n plt.tight_layout()n ax.set_title(type(tsfrm).__name__)n show_landmarks(**transformed_sample)nnplt.show()n
(譯者注:左圖是scale操作,中圖是crop操作,右圖是composed操作)
迭代數據
我們將所有的放在一起來生成composed transforms之後的數據。總之,每次迭代的數據:
- 從文件中讀取一幅圖像
- 在讀取圖片時進行transforms操作
- 由於其中一個transforms是隨機的,所以迭代的數據樣本進行了增強
我們可以像之前一樣使用for i in range循環從創建的數據中進行迭代。
transformed_dataset = FaceLandmarksDataset(csv_file=faces/face_landmarks.csv,n root_dir=faces/,n transform=transforms.Compose([n Rescale(256),n RandomCrop(224),n ToTensor()n ]))nnfor i in range(len(transformed_dataset)):n sample = transformed_dataset[i]nn print(i, sample[image].size(), sample[landmarks].size())nn if i == 3:n breakn
輸出:
0 torch.Size([3, 224, 224]) torch.Size([68, 2])n1 torch.Size([3, 224, 224]) torch.Size([68, 2])n2 torch.Size([3, 224, 224]) torch.Size([68, 2])n3 torch.Size([3, 224, 224]) torch.Size([68, 2])n
然而,通過使用一個簡單的for循環來迭代數據我們可能損失很多信息。尤其是我們丟失了這些操作:
- 按批讀取數據
- 打亂數據順序
- 使用多進程(multiprocessing)並行載入數據
torch.utils.data.DataLoader是一個提供上面所有信息的迭代器。下面使用的參數應該很清晰。其中一個蠻有趣的參數是collate_fn。你可以使用collate_fn來指定如何讀取一批的額樣本。然而,默認的collate在大部分的情況下都表現得很好。
dataloader = DataLoader(transformed_dataset, batch_size=4,n shuffle=True, num_workers=4)nnn# 顯示一批的數據ndef show_landmarks_batch(sample_batched):n """顯示一批樣本的圖片和標記點."""n images_batch, landmarks_batch = n sample_batched[image], sample_batched[landmarks]n batch_size = len(images_batch)n im_size = images_batch.size(2)nn grid = utils.make_grid(images_batch)n plt.imshow(grid.numpy().transpose((1, 2, 0)))nn for i in range(batch_size):n plt.scatter(landmarks_batch[i, :, 0].numpy() + i * im_size,n landmarks_batch[i, :, 1].numpy(),n s=10, marker=., c=r)nn plt.title(Batch from dataloader)nnfor i_batch, sample_batched in enumerate(dataloader):n print(i_batch, sample_batched[image].size(),n sample_batched[landmarks].size())nn # 觀察到第4批的時候就停止.n if i_batch == 3:n plt.figure()n show_landmarks_batch(sample_batched)n plt.axis(off)n plt.ioff() # 1n plt.show()n breakn
(譯者注:#1 顯示前關掉交互模式)
輸出結果:
0 torch.Size([4, 3, 224, 224]) torch.Size([4, 68, 2])n1 torch.Size([4, 3, 224, 224]) torch.Size([4, 68, 2])n2 torch.Size([4, 3, 224, 224]) torch.Size([4, 68, 2])n3 torch.Size([4, 3, 224, 224]) torch.Size([4, 68, 2])n
後記:torchvision
這這個教程里,我們已經學會如何寫並且使用datasets, transforms 和 dataloader。torchvision提供了一些常用的數據集和transforms。你甚至就不必寫自定義的類。在torchvision中一個最經常用的數據集是ImageFolder。它要求數據按下面的形式存放:
root/ants/xxx.pngnroot/ants/xxy.jpegnroot/ants/xxz.pngn.n.n.nroot/bees/123.jpgnroot/bees/nsdf3.pngnroot/bees/asd932_.pngn
其中,『ants』, 『bees』等是類別。對PIL.Image進行的通用的transforms操作如RandomHorizontalFlip, Scale也可以隨時使用。你可以像這樣使用這樣函數來寫dataloader:
import torchnfrom torchvision import transforms, datasetsnndata_transform = transforms.Compose([n transforms.RandomSizedCrop(224),n transforms.RandomHorizontalFlip(),n transforms.ToTensor(),n transforms.Normalize(mean=[0.485, 0.456, 0.406],n std=[0.229, 0.224, 0.225])n ])nhymenoptera_dataset = datasets.ImageFolder(root=hymenoptera_data/train,n transform=data_transform)ndataset_loader = torch.utils.data.DataLoader(hymenoptera_dataset,n batch_size=4, shuffle=True,n num_workers=4)n
關於訓練代碼的例子,請看【譯】pytorch遷移學習(原文: Transfer Learning tutorial)
怎能忘本,原文在這裡。
推薦閱讀:
※Datalab來了:Google Cloud NEXT 17
※人類首次!機器人宇航員將乘俄聯邦號飛船登月
※天天說人工智慧,人工智慧到底能做些什麼?
TAG:PyTorch | 深度学习DeepLearning | 机器学习 |