一、数据加载和处理教程
在解决机器学习问题时, 我们需要付出很多努力来准备数据, 为了使代码更具可读性, PyTorch提供了许多工具来使数据加载变得简单易行。在本教程中, 我们将要学习如何对 一个重要的数据集进行加载、预处理数据增强。
为了运行这个教程,请确认下列包已安装:
- scikit-image :用来读取图片和图像变换
- 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
# 忽略警告
import warnings
warnings.filterwarnings('ignore')
#开启交换模式
plt.ion()
我们将要处理的数据是人脸姿态。这就是对人脸的表示如下:
每张人脸图像上, 总共有68个不同的标注点被标记出来。
提示
打开http://t.cn/EaOQdfy 下载数据集, 这些图像在目录 'faces/'下. 这个数据集实际上是从imagenet数据集中选取标记为人脸的一些图片, 使用'dlib’s pose estimation 方法生成的.
### 下载图片数据
import os
import os.path
import errno
url ='https://download.pytorch.org/tutorial/faces.zip'
filename='faces.zip'
def download(root):
'''
下载数据人脸图像和标注点的压缩包。
使用zipfile包解压。
'''
root = os.path.expanduser(root)
import zipfile
#下载图片压缩包到指定路径
download_url(url,root,filename)
print("数据下载完毕!")
#获得当前路径
cwd = os.getcwd()
path = os.path.join(root, filename)
tar = zipfile.ZipFile(path, "r")
#解压文件
tar.extractall(root)
tar.close()
#切换到当前工作路径
os.chdir(cwd)
def download_url(url, root, filename):
from six.moves import urllib
root = os.path.expanduser(root)
fpath = os.path.join(root, filename)
try:
os.makedirs(root)
except OSError as e:
if e.errno == errno.EEXIST:
pass
else:
raise
# downloads file
if os.path.isfile(fpath) :
print('使用已下载文件: ' + fpath)
else:
try:
print('下载 ' + url + ' 到 ' + fpath)
urllib.request.urlretrieve(url, fpath)
except:
if url[:5] == 'https':
url = url.replace('https:', 'http:')
print('下载失败。尝试将https -> http'
' 下载 ' + url + ' 到 ' + fpath)
urllib.request.urlretrieve(url, fpath)
download('./')
数据集中的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表示标记点的个数.
- 读取标记点CSV文件,使用pandas的read_csv方法。
landmarks_frame = pd.read_csv('faces/face_landmarks.csv')
- 观察数据的结构,可以使用headinfo等方法,head()默认返回前5行,info()显示数据名称和数量。
landmarks_frame.info()
landmarks_frame.head()
由上表可知,每行表示一张图片和标记点的X、Y坐标,共有68个标记点。
- 已第4张图片为例。首先获得图片名称,其次获取所有标记点信息,首先转换成行向量(1×136),最后向量转换成N行2列的浮点型矩阵。
n = 3
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]))
我们写一个函数来显示一张图片和它的标记点, 然后用这个函数来显示一个样本。
def show_landmarks(image, landmarks):
"""显示带标记点的图片"""
plt.imshow(image)
plt.scatter(landmarks[:, 0], landmarks[:, 1], s=10, marker='.', c='r')
plt.pause(0.001) # 暂停一下, 使plots更新
plt.figure()
show_landmarks(io.imread(os.path.join('faces/', img_name)),
landmarks)
plt.show()
二、Dataset 类
orch.utils.data.Dataset 是一个表示数据集的抽象类。你自定义dataset类需继承Dataset并重写下列方法:
__ len __ 使用len(dataset)可以返回数据集的大小。
__ getitem __ 支持索引, 以便于使用 dataset[i] 可以 获取第i个样本。
让我们为landmars 数据集构建一个dataset类。我们将在__ init 中读取csv文件,在 getitem __ 中读取图像。这可以高效利用内存,因为所有图像不会立即存储在内存中,而是根据需要读取。
我们的数据集样本将是一个dict {'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()
#将矩阵转换成N行2列的矩阵。
landmarks = landmarks.astype('float').reshape(-1,2)
sample = {'image':image,'landmarks':landmarks}
if self.transform:
sample = self.transform(sample)
return sample
我们实例化这个数据集并迭代数据样本。我们将打印前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
三、Transforms
我们从上面可以发现一个问题是样本的尺寸(指宽和高)不相同。大部分的神经网络都期望一个固定尺寸的图像。所以,我们需要写一些预处理的代码。
让我们写三个 transform:
Rescale: 缩放图像
RandomCrop: 从图像中随机裁剪。这是数据增加。
ToTensor: 将numpy形式的图像数据转换成torch形式的图像数据(我们需要交换轴)。
我们将它们编写为可调用类而不是简单函数,这样每次调用时都不需要传递 transform 的参数。所以,我们只需要实现 call 方法,同时如果需要,也可实现 init 方法。我们可以像这样使用 transform:
tsfm = Transform(params)
transform_sample = tsfm(sample)
请注意以下这些 transforms 必须如何应用于图像和landmarks。
class Rescale(object):
'''
将样本图像缩放至给定尺寸。
Args:
output_size(tuple or int):期望的输出尺寸。如果是tuple,
则输出与output_size匹配。如果是int,则较小的图像边缘
与output_size匹配,保持纵横比相同。
'''
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 = h/w ,new_w = output_size
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:
# 直接以给定的数据为新的w、h
new_h , new_w = self.output_size
new_h , new_w = int(new_h) , int(new_w)
#使用skimage包中tansform类的resize方法缩放图像。
img = transform.resize(image,(new_h,new_w))
# 将landmarks的位置按照图像的缩放比较,就行缩放。
landmarks = landmarks * [new_w / w, new_h / h]
return {'image': img, 'landmarks': landmarks}
import numpy as np
class RandomCrop(object):
'''
随机修剪样本图像。
Args:
output_size(tuple or int):期望的输出尺寸。如果为int,
则进行方形裁剪。
'''
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']
# 交换颜色的轴
# numpy image: H x W x C
#轴对应编号:0,1,2
# torch image: C X H X W
#轴对应编号:2,0,1
'''
transpose()的操作对象是矩阵。
我们用一个例子来说明这个函数:
[[[0 1]
[2 3]]
[[4 5]
[6 7]]]
这是一个shape为(2,2,2)的矩阵,现在对它进行transpose操作。
首先我们对矩阵的维度进行编号,上述矩阵有三个维度,则编号分别为0,1,2,而transpose函数的参数输 入就是基于这个编号的,如果我们调用transpose(0,1,2),那么矩阵将不发生变化,如果我们不输入参 数,直接调用transpose(),其效果就是将矩阵进行转置,起作用等价与transpose(2,1,0)。
在举个例子,对上面那个矩阵调用transpose(0,2,1)
下面为结果
[[[0 2]
[1 3]]
[[4 6]
[5 7]]]
其实就是矩阵中每个元素按照一样的规则进行位置变换。
'''
image = image.transpose((2, 0, 1))
return {'image': torch.from_numpy(image),
'landmarks': torch.from_numpy(landmarks)}
Compose transforms
现在,我们已经在样本上使用了 transforms。让我们看看,我们想将图像的短边缩短至256在随机从图像中裁剪一个224的方形。我们想组成Rescale 和RandomCrop的联合transforms。torchvision.transforms.Compose是一个允许我们做这些的简单可调用类。
scale = Rescale(256)
crop = RandomCrop(100)
composed = transforms.Compose([Rescale(256),
RandomCrop(102)])
# 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()
四、迭代数据
Iterating through the dataset
让我们把这些组合在一起创建一个包含组合转换器(composed transforms)的数据集(dataset)。
- 总而言之, 每次取样数据集时:
- 即时从文件中读取图像
- 变换应用于读取的图像
- 由于其中一个变换是随机的,因此在采样时会增加数据
transformed_dataset = FaceLandmarksDataset(csv_file='faces/face_landmarks.csv',
root_dir='faces/',
transform =transforms.Compose([
Rescale(256),
RandomCrop(204),
ToTensor(),
]))
for i in range(len(transformed_dataset)):
sample = transformed_dataset[i]
print(i,sample['image'].size(),sample['landmarks'].size())
if i == 3:
break
然而,我们使用简单的for循环来迭代数据将会就丢失很多特性。事实上,我们错过了:
- 批处理数据
- 打乱(洗牌)数据
- 使用multiprocessing作业,并行加载数据。
torch.utils.data.DataLoader 是提供了所有特性的迭代器。下面使用的参数应该是清楚的。感兴趣的一个参数是collate_fn。您可以使用collate_fn指定需要批量处理样品的准确程度。但是,对于大多数用例,默认整理应该可以正常工作。
#定义数据加载器,数据需要打乱,每次取4个样本。
#num_workers,我暂时无法理解。
dataloader = DataLoader(transformed_dataset,batch_size=4,
shuffle=True,num_workers=4)
# 帮助类,展示一个批次的数据。
def show_landmarks_batch(sample_batched):
'''
显示一个批次的图像。
'''
images_batch , landmarks_batch = \
sample_batched['image'],sample_batched['landmarks']
batch_size = len(sample_batched)
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
后记:torchvision
在这个教程中, 我们学习了如何写和使用数据集, 图像变换和dataloder. torchvision 提供了常用的数据集和图像变换, 或许你甚至不必写自定义的类和变换. 在torchvision中一个最经常用的数据集是ImageFolder. 它要求数据按下面的形式存放:
/root/hymenoptera_data/ants/xxx.png
/root/hymenoptera_data/ants/xxy.jpeg
/root/hymenoptera_data/ants/xxz.png
.
.
.
/root/hymenoptera_data/bees/123.jpg
/root/hymenoptera_data/bees/nsdf3.png
/root/hymenoptera_data/bees/asd932_.png
‘ants’, ‘bees’ 等是图像的类标. 同样, PIL.Image 中出现的一般的图像变换像 RandomHorizontalFlip, Scale 也是可以使用的. 你可以像下面这样用这些函数来写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='./root/hymenoptera_data/train',
transform=data_transform)
dataset_loader = torch.utils.data.DataLoader(hymenoptera_dataset,
batch_size=4, shuffle=True,
num_workers=4)
⭐总结
自定义dataset。首先需要继承torch.utils.data.Dataset 类;其次要实现__ len (使用len(dataset)可以返回数据集的大小)和 getitem __ (支持索引, 以便于使用 dataset[i] 可以 获取第i个样本)方法。
自定义transform。首先要要继承object类;其次实现__ call 方法,需要时可实现 init __ 方法。