在这个部分,将介绍以下内容:
- 理解数据载入器(Dataloaders)的概念和Pytorch数据载入器API;
- 将图片数据集分成训练,验证和测试集;
- 创建Pytorch Dataloaders加载图片用于训练,验证和测试;
- 使用Pytorch API来定义变换(Transforms)进行数据集预处理,更有效的进行训练;
- 使用Pytorch API将所有图片转变成Pytorch Tensors;
- 使用图片的平均值和标准差来归一化数据集。
一、数据载入器(Data Loaders)
1. Pytorch Dataloaders充当Python生成器的对象。在进行训练和验证时,它们以块或批的形式提供数据。我们可以实例化Dataloader对象并将我们的数据集传递给它们。dataloader在内部存储数据集对象。
当应用请求下一批数据时,dataloader使用其存储的数据集作为Python迭代器来获取数据的下一个元素。然后它聚合一批数据并将其返回给应用。
下面是一个调用Dataloader构造函数的例子:
num_train = len(train_dataset)
indices = list(range(num_train))
train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=50,sampler=SubsetRandomSampler(indices),num_workers=0)
len(train_loader)
10000
- 这里,我们为批大小为50的训练数据集创建一个Dataloader对象;
- sampler参数指定了我们在构建批处理时要对数据进行采样的策略;
- 在torch.utils.data.sampler有不同的采样器。这个解释很简单。您可以通过在Pytorch文档https://pytorch.org/docs/stable/data.html#torch.utils.data.Sampler中了解它们;
- num_workers参数指定在加载数据时希望使用多少进程(或核心)。这在加载大型数据集时提供了并行性。默认值为0,表示加载主进程中的所有数据。
Dataloader以批的数量作为长度。我们创建了批大小为50的Dataloader,在训练数据集中有50000个图像,所以我们的Dataloader长度等于1000个批。
2. 分割数据
现在我们写一个函数将我们的数据分割成训练,验证和测试集,并创建它们相应的dataloaders
'''
这个函数将训练和测试数据集作为参数。
测试数据可以为None,在这种情况下,它将训练数据分割成三个数据集,训练,测试和验证集。
如果test_data不是none,它只是将训练集分割成训练和验证集,并从测试集创建一个单独的dataloader。
'''
def split_image_data(train_data,
test_data=None,
batch_size=20,
num_workers=0,
valid_size=0.2,
sampler=SubsetRandomSampler):
num_train = len(train_data)
'''
它使用Python range函数在训练集的长度上创建一个索引列表
'''
indices = list(range(num_train))
np.random.shuffle(indices)
'''
它根据给定的验证集大小(valid_size参数)分割索引列表,该参数的默认值为0.2(为验证而预留的训练数据的20%)
'''
split = int(np.floor(valid_size * num_train))
train_idx, valid_idx = indices[split:], indices[:split]
'''
它使用RandomSubsetSampler构造函数来打乱训练和验证集索引
'''
train_sampler = sampler(train_idx)
valid_sampler = sampler(valid_idx)
'''
如果给出一个单独的测试集,它只会从该测试集创建一个Dataloader。
如果没有给出测试集,则进一步将训练索引(这些索引是通过将原始的train_set分割为训练和验证索引获得的)分割为一组训练和测试索引。注意,测试索引的大小等于验证集。
这导致了一套新的索引和从训练集得到的采样的测试集。
'''
if test_data is not None:
test_loader = DataLoader(test_data, batch_size=batch_size,
num_workers=num_workers)
else:
train_idx, test_idx = train_idx[split:],train_idx[:split]
train_sampler = sampler(train_idx)
test_sampler = sampler(test_idx)
test_loader = torch.utils.data.DataLoader(train_data,
batch_size=batch_size,
sampler=test_sampler,
num_workers=num_workers)
train_loader = torch.utils.data.DataLoader(train_data,
batch_size=batch_size,
sampler=train_sampler,
num_workers=num_workers)
valid_loader = torch.utils.data.DataLoader(train_data,
batch_size=batch_size,
sampler=valid_sampler,
num_workers=num_workers)
return train_loader,valid_loader,test_loader
调用这个函数来获得我们的Dataloaders
trainloader,validloader,testloader = split_image_data(train_dataset,test_dataset,batch_size=50)
len(trainloader),len(testloader),len(validloader)
(800, 200, 200)
训练集800个批,验证集200个批,测试集200个批。
二、卷积神经网络(CNN)
虽然,我们假设你对CNNs有一个基本的了解,如果你想更新核心概念,下面是一些很棒的教程:
Convolutional Neural Networks CS231n Stanford
CNN Tutorial: AnalyticsVidhya
A Very Comprehensive Tutorial on ANN and CNN by Kaggle
1. 数据集预处理和转换
在我们继续定义我们的网络并开始训练之前,我们需要预处理我们的数据集。具体来说,我们需要执行以下步骤:
- 调整图片大小,获得匹配我们模型合适的大小;
- 实现一些基本的,最常用的数据增强;
- 将图片数据转化成Pytorch Tensors;
- 归一化图像数据。
2. 为什么我们需要调整图像大小?
我们的大多数迁移学习模型要求数据至少是224x224的大小。这些模型设计了大量的卷积层和池化层,最后是一个全连接(线性)层来生成分类输出。当输入图像到达最后一层时,由于定义卷积和池化的方式,它的大小已经大大减小。如果输入图像太小(在我们的示例中是32x32 CIFAR10图像),那么对于网络就太小,无法产生任何重要的输出。因此,这些模型某种程度上限制了我们输入图像需要大于等于224x224。
请注意,如果我们的图像已经是大于224x224,就像在ImageNet的情况下,或者如果我们使用自己的CNN架构,在通过卷积层传递图像时不会减少太多的图像大小,我们就不需要调整大小。
对于较大图像的数据集,我们的GPU或CPU内存可能成为一个约束因素。因此,我们将缩减规模与增加批大小相结合(直到达到批大小限制),以优化模型性能并平衡缩减规模的影响。
三、数据增强
数据增强是深度学习中常用的一种技术,我们一边对图像进行动态修改,一边对神经网络进行训练,将图像以不同的轴向和角度翻转或旋转。这通常会有更好的训练性能,因为当最小化损失函数时,网络可以看到同一图像的多个视图,并且有更好的机会识别它的类。
请注意,增强图像并没有添加到数据集中,它们只是在生成批处理时创建的,因此在训练期间看到的实际图像会增加,但不会看到数据集中的图像数量增加。长度和其他计算图像数量的函数仍然会给出相同的答案。我们使用以下两种常用的增强方法:
- RandomHorizontaFlip将一些图像沿垂直轴翻转,概率p默认为0.5,这意味着50%的图像将被翻转;
- RadomRotation在一个特定的角度(在我们下面的例子中是10)随机地以10度的角度旋转一些数据,p的概率为默认值0.5。
from torchvision import transforms
train_transform = transforms.Compose([transforms.Resize(224),
transforms.RandomHorizontalFlip(),
transforms.RandomRotation(10),
])
train_dataset = datasets.CIFAR10('Cifar10',download=False,transform=train_transform)
四、数据归一化
在数据归一化中,我们对图像中的像素值进行统计归一化。这会有更好的训练性能和更快的收敛。一种常用的归一化方法是将整个数据集的像素值的均值从每个像素中分离出来,然后除以整个数据集像素的标准差。
- 迁移学习最常用的方法是采用原始模型所训练的数据集的均值和std值。对于我们不想对原始模型的任何部分进行重新训练的情况,这可能是一个很好的策略;
- 如果我们的数据集很大,并且我们想要重新训练整个或部分原始模型,那么我们最好使用相关数据集的平均值和标准偏差进行规范化(在我们的例子中是CIFAR10)。但是,在大多数迁移学习教程中,都使用了ImageNet的平均值和std值。
下面,我给出两个计算数据集均值和标准差的函数: 首先,“calculate_img_stats_avg”是基于Dataloader,在从dataset对象检索数据时,计算每批数据的平均值和std值,最后取累计平均值和std值的平均值。这为我们提供了实际值的近似值,对于不能同时装入内存的大型数据集,使用它比较方便的。
第二个函数“calculate_img_stats_full”通过同时处理整个数据集来计算其实际平均值和std。这将给出更精确的值,尽管对于大型数据集,很可能会耗尽内存。本代码改编自Eli Stevens和Luca Antiga, Manning出版物的《Pytorch深度学习》一书。
你可以尝试在特定的数据集上运行第二个函数,如果遇到内存问题,则返回到第一个函数,以获得更好的近似值。然而,在CIFAR10中,许多人已经计算了数据集的平均值和std,这些值是众所周知的,比如ImageNet。我们在下面的代码中使用这些值。
from torchvision import transforms
transform = transforms.Compose([transforms.ToTensor()])
dataset = datasets.CIFAR10('Cifar10',download=False,transform=transform)
loader = torch.utils.data.DataLoader(dataset, batch_size=50,num_workers=0)
- 我们首先从完整数据创建一个数据集,然后使用dataloader将批大小为50的数据提供给循环。
- 注意,要使Dataloader工作,图像必须转换成一个张量,所以这是我们使用的唯一转换。
- 下面的函数是一个简单的实现,它计算每个批的平均值和std,并将它们加到它们的累积和中,最后除以批的总数,得到平均值
def calculate_img_stats_avg(loader):
mean = 0.
std = 0.
nb_samples = 0.
for imgs,_ in loader:
batch_samples = imgs.size(0)
imgs = imgs.view(batch_samples, imgs.size(1), -1)
mean += imgs.mean(2).sum(0)
std += imgs.std(2).sum(0)
nb_samples += batch_samples
mean /= nb_samples
std /= nb_samples
return mean,std
calculate_img_stats_avg(loader)
(tensor([0.4914, 0.4822, 0.4465]), tensor([0.2023, 0.1994, 0.2010]))
def calculate_img_stats_full(dataset):
imgs_ = torch.stack([img for img,_ in dataset],dim=3)
imgs_ = imgs_.view(3,-1)
imgs_mean = imgs_.mean(dim=1)
imgs_std = imgs_.std(dim=1)
return imgs_mean,imgs_std
calculate_img_stats_full(dataset)
(tensor([0.4915, 0.4823, 0.4468]), tensor([0.2470, 0.2435, 0.2616]))
cifar10_mean = [0.4915, 0.4823, 0.4468]
cifar10_std = [0.2470, 0.2435, 0.2616]
现在,我们可以再次从头开始创建数据集,应用所有的转换、增强和标准化,将它们分为训练和测试,并获得最终的dataloader。注意,我们还定义了批大小为50
batch_size = 50
'''
ToTensor()转化为numpy数组(我们读取所有的图像都是numpy数组).
Normalize()是另一个转换,根据每个通道的平均值和STD的传递值作为单独的列表或元组进行归一化
'''
train_transform = transforms.Compose([transforms.Resize((224,224)),
transforms.RandomHorizontalFlip(),
transforms.RandomRotation(10),
transforms.ToTensor(),
transforms.Normalize(cifar10_mean, cifar10_std)
])
test_transform = transforms.Compose([transforms.Resize((224,224)),
transforms.ToTensor(),
transforms.Normalize(cifar10_mean, cifar10_std)
])
train_data = datasets.CIFAR10('Cifar10', train=True,
download=False, transform=train_transform)
test_data = datasets.CIFAR10('Cifar10', train=False,
download=False, transform=test_transform)
trainloader,validloader,testloader = split_image_data(train_data,test_data,batch_size=batch_size)
len(trainloader),len(testloader),len(validloader)
(800, 200, 200)
数据增强(大多数)只应用于训练集
注意,我们通常不会将数据增强应用于测试集,因为我们希望测试数据尽可能地接近真实数据,否则就有可能高估模型的性能。例如,我们的模型可能错误地分类了测试图像,但是它的翻转和旋转版本是正确的。这虽然能增加总体的准确性,但容易误导结果。