官方教程:https://pytorch.org/tutorials/beginner/data_loading_tutorial.html
解决任何机器学习问题都需要花费大量精力来准备数据。
PyTorch提供了许多工具来简化数据加载过程,并使代码更具可读性。
在本教程中,我们将看到如何从有价值的数据集中加载和预处理/增强数据。
要运行本教程,请确保已安装以下软件包:
scikit-image
:用于图像io和变换 scikit-image 0.14.0以及torch 1.1.0pandas
:为了更轻松地解析csv以下是bug的解决,跳过:准备工作
先检查一下有没有包,在尝试import的过程中,出现了下面的错误:
-
>>> from skimage import io, transform Traceback (most recent call last): File "<stdin>", line 1, in <module> File "D:\Anaconda3\lib\site-packages\skimage\io\__init__.py", line 11, in <mod ule> from ._io import * File "D:\Anaconda3\lib\site-packages\skimage\io\_io.py", line 5, in <module> from ..color import rgb2gray File "D:\Anaconda3\lib\site-packages\skimage\color\__init__.py", line 1, in <m odule> from .colorconv import (convert_colorspace, File "D:\Anaconda3\lib\site-packages\skimage\color\colorconv.py", line 60, in <module> from ..util import dtype, dtype_limits File "D:\Anaconda3\lib\site-packages\skimage\util\__init__.py", line 8, in <mo dule> from .arraycrop import crop File "D:\Anaconda3\lib\site-packages\skimage\util\arraycrop.py", line 8, in <m odule> from numpy.lib.arraypad import _validate_lengths ImportError: cannot import name '_validate_lengths' from 'numpy.lib.arraypad' (D :\Anaconda3\lib\site-packages\numpy\lib\arraypad.py)
解决方案:https://blog.csdn.net/qq_22764813/article/details/86534227
对numpy降级:pip install numpy==1.15.0
正常import,然而torch这边又不行了,可能是降级太狠了,查看一下低于1.16.0(1.16.0,skimage不可以)的版本
那我该1.15.4试试:torch不可以!
尝试升级skimage,选择版本0.14.3,numpy为1.17.0,torch1.1.0通过,不过torchvision又发生了一些问题。
(base) C:\Users\PC>pip install torchvision==0.3.0
Requirement already satisfied: torchvision==0.3.0 in d:\anaconda3\lib\site-packages (0.3.0)
Requirement already satisfied: torch>=1.1.0 in d:\anaconda3\lib\site-packages (from torchvision==0.3.0) (1.1.0)
Requirement already satisfied: pillow>=4.1.1 in d:\anaconda3\lib\site-packages (from torchvision==0.3.0) (5.4.1)
Requirement already satisfied: six in d:\anaconda3\lib\site-packages (from torchvision==0.3.0) (1.12.0)
Requirement already satisfied: numpy in d:\anaconda3\lib\site-packages (from torchvision==0.3.0) (1.17.0)
requirement都满足了我也很疑惑。。要不torchvision降一下版本
(base) C:\Users\PC>pip install --no-deps torchvision==0.2.2
搞定!终于可以愉快地运行了。
准备工作
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
导入包,我们使用的面部姿势数据集,每个面孔都标注了68个不同的界标点。
从官方教程下载数据集, 图像位于名为“ data / faces /”的目录中(这里的路径是可以更改的,不过在后续的代码中要注意匹配)。该数据集来源于imagenet,选取其中一些被标记为“面部”的图像。
数据集带有一个带注释的face_landmarks.csv文件。
让我们快速阅读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]))
(base) C:\Users>cd /d "d:\demo" 更改一下当前的工作路径
(base) d:\demo>python dataload.py 执行demo
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()
为了方便观察,我增加了
import time
time.sleep(5)
数据集类Dataset
torch.utils.data.Dataset
是表示数据集的抽象类。
自定义数据集应继承Dataset
并覆盖以下方法:
__len__ 以便
len(dataset)
返回数据集的大小。__getitem__
支持索引,以便dataset[i]
可以用来获取第i个样本
为面部轮廓数据集创建一个数据集类:
在__init__
读取csv,将在 __getitem__
读取图像。(所有图像不会立即存储在内存中,而是根据需要读取,因此可以提高内存效率。)
数据集样本将是dict 。
{'image': image, 'landmarks': landmarks}
数据集将使用可选参数transform
,以便可以对样本进行任何必需的处理。我们将在下一节中看到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):
if torch.is_tensor(idx):
idx = idx.tolist()
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:]
landmarks = np.array([landmarks])
landmarks = landmarks.astype('float').reshape(-1, 2)
sample = {'image': image, 'landmarks': landmarks}
if self.transform:
sample = self.transform(sample)
return sample
实例化该类并遍历数据样本。我们将打印前4个样本的大小并显示其地标(landmark)。
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) # 4是人为规定显示的数量
plt.tight_layout()
ax.set_title('Sample #{}'.format(i))
ax.axis('off')
show_landmarks(**sample)
if i == 3: # i的判断值为4-1,坐标从0开始
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)
转换Transforms
数据集存在样本的大小不同的问题。
大多数神经网络期望图像的大小固定。因此,需要编写一些前置代码。让我们创建三个转换:
Rescale
:缩放图像RandomCrop
:从图像中随机裁剪。这是数据扩充。ToTensor
:将numpy图片转换为torch图片(我们需要交换轴)。
我们将编写为可调用的类,而不是简单的函数,这样就不必每次调用转换时都传递其参数。为此,我们只需要实现__call__
方法,如果需要的话,可以实现__init__
方法。然后我们可以使用这样的转换:
tsfm = Transform(params)
transformed_sample = tsfm(sample)
以下说明如何将这些变换同时应用于图像和地标
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)}
撰写变换
将转换应用于样本。
假设我们要将图像的较短边重新缩放为256,然后从中随机裁剪一个尺寸为224的正方形。即,我们要用到 Rescale
和RandomCrop
变换。 torchvision.transforms.Compose
是一个简单的可调用类,它使我们可以执行此操作。
scale = Rescale(256)
crop = RandomCrop(128) # 随机裁剪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()
遍历数据集
将所有放在一起,创建具有组合转换的数据集。总而言之,每次采样此数据集时:
- 从文件中即时读取图像
- 转换应用于读取的图像
- 由于其中一种转换是随机的,因此在采样时会增加数据
我们可以像以前一样通过 for i in range
循环遍历创建的数据集。
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])
但是,通过使用简单的for
循环迭代数据,我们失去了很多功能。特别是:
- 批量处理数据
- 整理数据
- 使用
multiprocessing
并行工作加载数据。
torch.utils.data.DataLoader
是提供以上所有功能的迭代器。下面使用的方法及参数应当明确。
collate_fn是一个比较重要的
参数。可以使用collate_fn
指定要精确批处理样品的数量。但是,在大多数情况下,默认排序规则可以正常工作。
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_border_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 + (i + 1) * grid_border_size,
landmarks_batch[i, :, 1].numpy() + grid_border_size,
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])
注意报错:
RuntimeError:
An attempt has been made to start a new process before the
current process has finished its bootstrapping phase.
This probably means that you are not using fork to start your
child processes and you have forgotten to use the proper idiom
in the main module:
if __name__ == '__main__':
freeze_support()
...
The "freeze_support()" line can be omitted if the program
is not going to be frozen to produce an executable.
原因:在windows中, 创建进程由于没有folk创建进程, 所以是通过重载自身模块来创建的, 如果代码放在if __name__ == "__main__"外部, 就会进行无限递归模块(参考:https://www.cnblogs.com/hpcm/p/9779966.html)
多进程需要在main函数中运行,
解决方法1:
加main函数,在main中调用,不过main之外的代码会随着每个进程的创建而重新执行。
(我之前显示图片的代码执行了四遍,差点死机,微笑)
if __name__=='__main__':
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
解决方法2:
去掉num_workers参数,或者num_workers改为0,单进程加载
dataloader = DataLoader(transformed_dataset, batch_size=4,
shuffle=True)#, num_workers=0)
后记: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
其中“蚂蚁”,“蜜蜂”等是类标签。同样,也可以使用类似 RandomHorizontalFlip、Scale的
操作PIL.Image
进行通用转换。
可以使用以下代码编写数据加载器:
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)
以上代码是在根目录下寻找子文件夹中的图片,如蜜蜂示例root=”root“,上文提到的人脸示例就是”data“(查找子文件夹faces下的图片)
支持的扩展名: .jpg,.jpeg,.png,.ppm,.bmp,.pgm,.tif,.tiff,webp