寫在前面
未經允許,不得轉載,謝謝~~
在深度學習的問題中處理數據都會占據比較大的時間,只有把數據處理好了才有可能對模型進行訓練、測試等后續工作。
PyTorch提供了很多用于讓數據加載變得更加方便的工具,接下來我們就來學習一下怎么樣處理那些PyTorch沒有提供直接接口的數據。
在學習這個之前,首先要保證電腦上已經安裝了下面這兩樣東西:
- scikit-image:用于圖像輸入輸出和轉換
- pandas:用于更好的處理csv數據
這篇文章內容還是比較多的,但認真看完應該就可以掌握各種數據集的處理了。
1. 頭文件導入
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
2. 數據集介紹及下載
2.1 數據集介紹
接下來我們要處理的數據集是關于臉部姿勢的,每張圖片都會被注釋成這樣,每張臉上都會有68各不同的標記點:
2.2 數據集下載與展示
-
戳這里下載需要教程中用到的臉部數據集,跟數據集一起的還有一個注釋文件
face_landmarks.csv
。
直接打開如下圖所示:
csv文件
即每張圖片都對應一個文件名和對應的N個臉部特征標記點。 在注釋文件中的是N個坐標點,每個坐標點由兩個橫縱坐標組成。所以先用
pandas
工具把注釋文件處理一下。
landmarks_frame = pd.read_csv('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]))
得到的結果為:
3.將圖像和對應的特征點標記出來展示。
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()
得到的結果為:
3. Dataset類介紹
3.1 原理介紹
torch.utils.data.Dataset
是一個PyTorch用來表示數據集的抽象類。我們用這個類來處理自己的數據集的時候必須繼承Dataset
,然后重寫下面的函數:
-
__len__
: 使得len(dataset)
返回數據集的大小; -
__getitem__
:使得支持dataset[i]
能夠返回第i個數據樣本這樣的下標操作。
3.2 創建臉部圖像數據集
- 在類的
__init__
函數中完成csv文件的讀取工作; - 在類的
__getitem__
函數中完成圖片的讀取工作。這樣是為了減小內存開銷,只要在需要用到的時候才將圖片讀入。 - 除此,數據集還會接收一個可以選擇的參數
transform
,用來對圖像做一些改變,具體的會在下面進行介紹。 - 最終返回的樣本數據是一個字典形式的,如下所示:
{‘image':image,'landmarks':landmarks}
那么現在我們就可以寫出類的定義:
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
3.3 實例化類
接下來我們對上面定義好的類做實例化,然后在數據樣本上進行迭代。我們會打印前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
結果如下所示:
4. Transforms
從上面顯示的圖片我們可以看到每張圖片的大小都不一樣,但往往我們在處理神經網絡的輸入圖像的時候都希望它們有一個相對固定的大小。因此,我們需要一些對圖像進行預處理的工作。
4.1 實現常用變換功能
我們試著寫一下這三個常用的變換功能:
-
Rescale
:重新調整圖像大小; -
RandomCrop
:隨機從圖像中截取一部分; -
ToTensor
:將numpy類型表示的圖像轉換成torch表示的圖像。
我們用類而不是函數來實現以上這三個功能,主要是考慮到如果用函數的話,每次都需要傳入參數,但是用類就可以省掉很多麻煩。我們只需要實現每個類的__call__
函數和__init__
函數。
下面是對這三個功能的實現:
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)}
4.2 組合以上變換功能
假設我們現在需要將圖像的較短邊調整到256,然后從中隨機截取224的正方形圖像。我們就可以調用torchvision.transforms.Compose
將以上的Rescale
和RandomCrop
兩個變換組合起來。
以下的代碼段展示了分開進行變換以及用Compose
組合進行變換的結果圖
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()
5. 合并dataset與transform、遍歷數據集
簡單回顧一下:
- 第3小節我們介紹了
dataset
類; - 第4小節我們我們介紹了怎么樣實現各個轉換函數,然后將其組合起來。
如果你還記得的話,我們在之前定義dataset
的時候是有一個transform
參數的,但我們在第4節中是先取了樣本數據,然后再進行變換操作,并沒有將其作為參數傳到dataset
中。所以我們現在要做的工作就是將所有的內容集成到一起。每次抽取一個樣本,都會有以下步驟:
- 從文件中讀取圖片;
- 將轉換應用于讀入的圖片;
- 由于做了隨機選取的操作,所以起到了數據增強的效果。
其實我們只要把Transform
的部分作為形參傳入dataset
就可以了,其他的都不變。
然后用for循環來依次獲得數據集樣本。
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
取到的四個數據樣本如下所示:
6. DataLoader類
以上我們已經實現了dataset
與transform
的合并,也實現了用for循環來獲取每一個樣本數據,好像事情就已經結束了。
但等等,真的結束了嗎?emmmm,我們好像還落了什么事情,是的沒錯:
- 按照
batch_size
獲得批量數據; - 打亂數據順序;
- 用多線程
multiprocessing
來加載數據;
torch.utils.data.DataLoader
這個類為我們解決了以上所有的問題,是不是很膩害~
只要按照要求設置DataLoader
的參數即可:
- 第一個參數傳入
transformed_dataset
,即已經用了transform
的Dataset
實例。 - 第二個參數傳入
batch_size
,表示每個batch包含多少個數據。 - 第三個參數傳入
shuffle
,布爾型變量,表示是否打亂。 - 第四個參數傳入
num_workers
表示使用幾個線程來加載數據。
如下所示即實現了DataLoader
函數的使用,及批樣本數據的展示。
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
這樣呢其實就完成了對數據集完整的處理了。
7. torchvision
torchvision
包提供了一些常用的數據集和轉換函數。使用torchvision
甚至不需要自己寫處理函數。
在torchvision
中最通用的數據集是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
這里的root
指代根目錄,ants
bees
指的是不同的類標簽,后面的是具體的圖片名稱。
當然它還提供了對PIL.Image
的常用操作,包括RandomHorizontalFlip
Scale
等等。
以下為用torchvision實現的超簡化版本的數據處理方法:
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)
整理總結
我們來整理一下整個實現思路哦~
主要分以下三種情況:
1 對于torchvision提供的數據集
- 這是最簡單的一種情況。
- 對于這一類數據集,就是PyTorch已經幫我們做好了所有的事情,連數據源都不需要自己下載。
- Imagenet,CIFAR10,MNIST等等PyTorch都提供了數據加載的功能,所以可以先看看你要用的數據集是不是這種情況。
- 具體的使用方法詳見之前的博客Pytorch入門學習(四)-training a classifier
2 對于特定結構的數據集
- 這種情況就是不在上述PyTorch提供數據庫之列,但是滿足下面的形式:
root/ants/xxx.png root/ants/xxy.jpeg root/ants/xxz.png . . . root/bees/123.jpg root/bees/nsdf3.png root/bees/asd932_.png
- 那么就可以通過
torchvision
中的通用數據集ImageFolder
來完成加載。 - 具體使用方法見上文。
3 對于最普通的數據集
- 最后一種情況是既不是自帶數據集,又不滿足
ImageFolder
,這種時候就自己進行處理。 - 首先,定義數據集的類
(myDataset)
,這個類要繼承dataset
這個抽象類,并實現__len__
以及__getitem__
這兩個函數,通常情況還包括初始函數__init__
. - 然后,實現用于特定圖像預處理的功能,并封裝成類。當然常用的一些變換可以在
torchvision
中找到。用torchvision.transforms.Compose
將它們進行組合成(transform)
-
transform
作為上面myDataset
類的參數傳入,并得到實例化myDataset
得到(transformed_dataset)
對象。 - 最后,將
transformed_dataset
作為torch.utils.data.DataLoader
類的形參,并根據需求設置自己是否需要打亂順序,批大小... - 具體見上文。