1. 前言
入坑深度学习已快一年,写过不少代码,也复现过不少论文,因为代码这块一直都是自己摸索着前行,走了不少弯路,甚至现在,也偶尔会发现一些自己一直忽略的细节。我们写一份代码,复现一篇论文,都希望它是完全正确的。但是从博客或文章中参考到的网络结构都是比较粗糙的,即使你能从某些地方获取到源码,也很难对某些细节做出说明。特别地,深度学习发展到成熟的今天,你的一行代码,一个符号都可能导致另一个不一致或更差的结果。
因此,本文以神经网络实现的一些细节为出发点,以常用分类数据集cifar10的分类为任务,手把手带你编写一份更完美的深度学习代码。
2. 任务分析
CIFAR10是kaggle计算机视觉竞赛的一个图像分类项目。该数据集共有60000张32*32彩色图像,一共分为"plane", "car", "bird","cat", "deer", "dog", "frog","horse","ship", "truck" 10类,每类6000张图。有50000张用于训练,构成了5个训练批,每一批10000张图;10000张用于测试,单独构成一批。可点击直接下载。
3. 数据集下载与预处理
一些经典的数据集,如Imagenet, CIFAR10, MNIST都可以通过torchvision来获取,并且torchvision还提供了transforms类可以用来正规化处理数据。
(1)数据集
数据集可分为训练集、验证集和训练集,训练集用于训练,验证集用于验证训练期间的模型,测试集用于测试最终模型的表现。这是基本的理解。验证集可用来设计一些交叉验证方法,在数据量较少的情况下能够提高模型的鲁棒性,通常任务分为训练集和测试集即可。
(2)数据预处理。
常用数据预处理方法可概述为2类,数据标准化处理和数据增广。
最常用的数据标准化处理就是数据的归一化,原数据可能数据量很大,维数很,计算机处理起来时间复杂度很高,预处理可以降低数据维度。同时,把数据都规范到0到1,这样使得它们对模型的影响具有同样的尺度。
数据扩增是对数据进行扩充的方法的总称。数据扩增可以增加训练集的样本,可以有效缓解模型过拟合的情况,也可以给模型带来的更强的泛化能力。即数据扩增的目的就是使得训练数据尽可能的接近测试数据,从而提高预测精度。另外数据扩增可以迫使网络学习到更鲁棒性的特征,从而使模型拥有更强的泛化能力。更多可参考数据增广的详细理解。代码如下:
-
#引入必须的包
-
import torch
-
import torchvision
-
import torchvision.transforms
as transforms
-
-
data_path=
'./dataset'
#数据保存路径
-
-
#数据的预处理操作
-
training_transform=transforms.Compose([
-
transforms.RandomCrop(
32, padding=
4),
#数据增广
-
transforms.RandomHorizontalFlip(),
#数据增广
-
transforms.ToTensor(),
-
transforms.Normalize(mean=[
0.4914,
0.4822,
0.4465],std=[
0.2471,
0.2435,
0.2616]),
#数据归一化
-
])
-
validation_transforms=transforms.Compose([
-
transforms.ToTensor(),
-
transforms.Normalize(mean=[
0.4914,
0.4822,
0.4465],std=[
0.2471,
0.2435,
0.2616]),
-
])
-
-
#训练集
-
train_dataset = torchvision.datasets.CIFAR10(root=data_path,
#数据下载和加载
-
train=
True,
-
transform=training_transform,
-
download=
True)
#若已下载改为False
-
#测试集
-
val_dataset = torchvision.datasets.CIFAR10(root=data_path,
-
train=
False,
-
transform=validation_transforms,
-
download=
True)
-
x,y= train_dataset[
0]
-
print(x.size(),y)
运行后输出输入图像的尺寸及其标签序号;
一些torch自带的数据集,如Imagenet, CIFAR10, MNIST都可以通过类似方式获取,此外,其他图像数据集还可以通过ImageFolder包引入,如下:
-
train_dataset = torchvision.datasets.ImageFolder(traindir,
-
transforms.Compose([
-
transforms.RandomResizedCrop(
64),
-
transforms.RandomHorizontalFlip(),
-
transforms.ToTensor(),
-
normalize ]))
只需将下载好的数据集放到traindir文件夹下,注意:ImageFolder默认traindir文件夹下的每个子文件夹代表一个类,所以,若使用该方式,你还需将每个类的图像移动到一个单独的子文夹杂下。
4. 超参数设置
在机器学习的上下文中,超参数是在开始学习过程之前设置值的参数,而不是通过训练得到的参数数据。通常情况下,需要对超参数进行优化,给学习机选择一组最优超参数,以提高学习的性能和效果。比如学习率、神经网络的深度、隐藏层层数、优化策略、训练次数、训练批次大小等。目前超参数往往只能人工基于经验来设置,以及采用暴力枚举的方式来尝试以寻求最优的超参数。但经验也是有规律可言的。
(1)优化器选择
优化器的目的是为了让损失函数尽可能的小,从而找到合适的参数来完成某项任务。最常用的优化器有SGD、RMSProp、Adam、AdaDelt等,推荐使用带momentum的SGD优化器,这会带来更高的收敛精度,带momentum的SGD优化器有两个劣势,其一是收敛速度慢,其二是初始学习率的设置需要依靠大量的经验,然而如果初始学习率设置得当并且迭代轮数充足,该优化器也会在众多的优化器中脱颖而出,使得其在验证集上获得更高的准确率。一些自适应学习率的优化器如Adam、RMSProp等,收敛速度往往比较快,但是最终的收敛精度会稍差一些。
optimizer = optim.SGD(net.parameters(), lr=0.1, momentum=0.9, weight_decay=1e-4)
(2)学习率
学习率是通过损失函数的梯度调整网络权重的超参数的速度。学习率越低,损失函数的变化速度就越慢。虽然使用低学习率可以确保不会错过任何局部极小值,但也意味着将花费更长的时间来进行收敛,还容易引起局部拟合。
当使用平方和误差作为成本函数时,随着数据量的增多,学习率应该被设置为相应更小的值。adam一般初始化为0.001,sgd 0.1,随着batchsize增大,学习率一般也要增大根号n倍。
(3)学习率下降策略
在整个训练过程中,我们不能使用同样的学习率来更新权重,否则无法到达最优点,所以需要在训练过程中调整学习率的大小。在训练初始阶段,由于权重处于随机初始化的状态,损失函数相对容易进行梯度下降,所以可以设置一个较大的学习率。在训练后期,由于权重参数已经接近最优值,较大的学习率无法进一步寻找最优值,所以需要设置一个较小的学习率。
比较几种下降策略:piecewise_decay(阶梯式下降学习率)、polynomial_decay(多项式下降)、exponential_decay(指数下降),cosine_decay(余弦下降)。
最常用的是阶梯式下降策略,如我们在cifar10训练resnet18网络使用的是分别在[60,120,160]epoch时调整学习率,通过手动设置调整学习率的阶段可以根据经验得出一个更好的效果,更重要的是方便模型的对比。使用相同的阶梯式下降策略是最能评估两个模型好坏的学习策略;
余弦下降策略无需调整超参数,鲁棒性也比较高,所以成为现在提高模型精度首选的学习率下降方式。两种策略对比如下:
(4)权重衰减:weight_decay
过拟合是机器学习中常见的一个名词,简单理解即为模型在训练数据上表现很好,但在测试数据上表现较差,在卷积神经网络中,同样存在过拟合的问题,为了避免过拟合,很多正则方式被提出,其中,weight_decay是其中一个广泛使用的避免过拟合的方式。Weight_decay等价于在最终的损失函数后添加L2正则化,L2正则化使得网络的权重倾向于选择更小的值,最终整个网络中的参数值更趋向于0,模型的泛化性能相应提高。在训练ImageNet的任务中,大多数的网络将该参数值设置为1e-4,在一些小的网络如MobileNet系列网络中,为了避免网络欠拟合,该值设置为1e-5~4e-5之间。简单来说,数据集越大越复杂,模型越简单,相应调小;数据集小、模型越复杂,相应调大。
train_scheduler = optim.lr_scheduler.MultiStepLR(optimizer, milestones=[80,120], gamma=0.1)
(5)warmup策略
如果使用较大的batch_size训练神经网络时,我们建议您使用warmup策略。Warmup策略顾名思义就是让学习率先预热一下,在训练初期我们不直接使用最大的学习率,而是用一个逐渐增大的学习率去训练网络,当学习率增大到最高点时,再使用学习率下降策略中提到的学习率下降方式衰减学习率的值。实验表明,在batch_size较大时,warmup可以稳定提升模型的精度。
-
from torch.optim.lr_scheduler
import _LRScheduler
-
class WarmUpLR(_LRScheduler):
-
"""warmup_training learning rate scheduler
-
Args:
-
optimizer: optimzier(e.g. SGD)
-
total_iters: totoal_iters of warmup phase
-
"""
-
def __init__(self, optimizer, total_iters, last_epoch=-1):
-
-
self.total_iters = total_iters
-
super().__init__(optimizer, last_epoch)
-
-
def get_lr(self):
-
"""we will use the first m batches, and set the learning
-
rate to base_lr * m / total_iters
-
"""
-
return [base_lr * self.last_epoch / (self.total_iters +
1e-8)
for base_lr
in self.base_lrs]
我们在此将warmup中的epoch设置为1,即先在1epoch内将学习率从0增加到初始值,再去做相应的学习率衰减。
5. 网络结构
网络结构这里就不编了,我直接拿resnet模型来修改讲解。
值得注意的是,为了适应cifar10数据集,我们将resnet模型的第一层卷积改小了卷积核(7*7到3*3),并且取消了下采样,如下:
-
def Conv1(in_planes, places, stride=2):
-
return nn.Sequential(
-
nn.Conv2d(in_channels=in_planes, out_channels=places, kernel_size=
3, stride=
1, padding=
1, bias=
False),
-
nn.BatchNorm2d(places),
-
nn.ReLU(inplace=
True),
-
# nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
-
)
这是因为cifar10数据集图像分辨率只有32*32,若按源resnet代码,会在第一层卷积进行2次下采样,变成8*8,造成大量的像素特征丢失,对结果的影响是巨大的。
(1)卷积操作
卷积操作是神经网络的最重要组成成分,有1d、2d、3d、空洞卷积等。主要参数包括in_channels:输入通道, out_channels:输出通道, kernel_size:卷积核尺寸, stride:步长, padding:填充, bias:偏置,其中:
输入通道:一般根据上层输入设置,如输入RGB图像的第一层卷积in_channels=3;
输出通道:也可称为输出神经元个数、卷积核个数、输出特征数量等,用来表示要学习的输出特征的数量,根据网络而定,越大能够学习到更多的特征,但也会导致更多的参数和存储占用,更容易过拟合;
卷积核尺寸:一般设置为1*1,3*3,5*5,7*7等单数,因为填充padding=(kernel_size-1)/2。越大的尺寸表示更大的感受野,能够学习到更多的特征,但不能盲目增大,这带来的提升往往不如代价。因此一般使用3*3卷积,1*1通常用于升降维度,以及给网络增加非线性。常用的组合为[3*3,3*3],[1*1,3*3,1*1]。
步长:默认为1,需要下采样时设为2,特征图尺寸相应会减半;
偏置:一般后面接BN层设置为False,因为bias不能与bn共存;其他可设为True。
(2)正则化
BatchNorm是神经网络所必须的,也是神经网络做得越深越宽的基石,通常只需设置输出通道即可,momentum默认为0.1,即每次都从上一次的BN层继承90%的参数。
(3)激活函数
常用的特征层激活函数relu,输出层激活函数softmax、sigmoid,激活函数是一种非线性函数,它是衡量网络深度的标准,多层感知器(卷积层)的叠加若没有激活函数(非线性函数),那么它在神经网络的作用就相当于只有一层。类似与特征多项式一样,相同维度的多个多项式的叠加跟一个的作用是相同的。
这是我认为一些难以注意到的细节,其他网络上应有不少,更多可自行查阅。
模型的完整代码如下:
-
import torch
-
import torch.nn
as nn
-
import torch.nn.functional
as F
-
-
-
def Conv1(in_planes, places, stride=2):
-
return nn.Sequential(
-
nn.Conv2d(in_channels=in_planes, out_channels=places, kernel_size=
3, stride=
1, padding=
1, bias=
False),
-
nn.BatchNorm2d(places),
-
nn.ReLU(inplace=
True),
-
# nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
-
)
-
-
class BasicBlock(nn.Module):
-
expansion =
1
-
-
def __init__(self, in_planes, planes, stride=1):
-
super(BasicBlock, self).__init__()
-
self.conv1 = nn.Conv2d(in_planes, planes, kernel_size=
3,
-
stride=stride, padding=
1, bias=
False)
-
self.bn1 = nn.BatchNorm2d(planes)
-
self.conv2 = nn.Conv2d(planes, planes, kernel_size=
3,
-
stride=
1, padding=
1, bias=
False)
-
self.bn2 = nn.BatchNorm2d(planes)
-
self.shortcut = nn.Sequential()
-
# 经过处理后的x要与x的维度相同(尺寸和深度)
-
# 如果不相同,需要添加卷积+BN来变换为同一维度
-
if stride !=
1
or in_planes != self.expansion * planes:
-
self.shortcut = nn.Sequential(
-
nn.Conv2d(in_planes, self.expansion * planes, kernel_size=
1, stride=stride, bias=
False),
-
nn.BatchNorm2d(self.expansion * planes)
-
)
-
def forward(self, x):
-
# print(x.size())
-
out = F.relu(self.bn1(self.conv1(x)))
-
out = self.bn2(self.conv2(out))
-
-
out += self.shortcut(x)
-
out = F.relu(out)
-
return out
-
-
class ResNet(nn.Module):
-
def __init__(self, block, num_blocks, num_classes=10):
-
super(ResNet, self).__init__()
-
self.in_planes =
64
-
self.conv1 = Conv1(in_planes=
3, places=
64)
-
-
self.layer1 = self._make_layer(block,
64, num_blocks[
0], stride=
1)
-
self.layer2 = self._make_layer(block,
128, num_blocks[
1], stride=
2)
-
self.layer3 = self._make_layer(block,
256, num_blocks[
2], stride=
2)
-
self.layer4 = self._make_layer(block,
512, num_blocks[
3], stride=
2)
-
-
self.avgpool = nn.AdaptiveAvgPool2d((
1,
1))
-
self.classifer = nn.Linear(
512*block.expansion, num_classes)
-
-
def _make_layer(self, block, planes, num_blocks, stride):
-
strides = [stride] + [
1] * (num_blocks -
1)
-
layers = []
-
for stride
in strides:
-
layers.append(block(self.in_planes, planes, stride))
-
self.in_planes = planes * block.expansion
-
return nn.Sequential(*layers)
-
-
def forward(self, x):
-
x = self.conv1(x)
-
-
x = self.layer1(x)
-
x = self.layer2(x)
-
x = self.layer3(x)
-
x = self.layer4(x)
-
-
x = self.avgpool(x)
-
x = x.view(x.size()[
0],
-1)
# 4, 2048
-
x = self.classifer(x)
-
return x
-
-
def ResNet18(**kwargs):
-
return ResNet(BasicBlock, [
2,
2,
2,
2],**kwargs)
-
-
net = ResNet18(num_classes=
10)
-
y = net(torch.randn(
1,
3,
32,
32))
-
print(y.size())
6. 训练和测试
(1)根据上述设置超参
-
#超参数
-
warm=
1
-
epoch=
160
-
batch_size=
128
-
loss_function = nn.CrossEntropyLoss()
-
optimizer = optim.SGD(net.parameters(), lr=
0.1, momentum=
0.9, weight_decay=
1e-4)
-
train_scheduler = optim.lr_scheduler.MultiStepLR(optimizer, milestones=[
80,
120], gamma=
0.1)
#learning rate decay
-
-
from torch.utils.data
import DataLoader
-
trainloader = DataLoader(train_dataset, batch_size, shuffle=
True, num_workers=
5, pin_memory=
True)
-
valloader = DataLoader(val_dataset, batch_size, shuffle=
False, num_workers=
5, pin_memory=
True)
-
iter_per_epoch = len(trainloader)
-
warmup_scheduler = WarmUpLR(optimizer, iter_per_epoch * warm)
(2)准确率计算函数
-
class AverageMeter(object):
-
"""Computes and stores the average and current value"""
-
def __init__(self):
-
self.reset()
-
-
def reset(self):
-
self.val =
0
-
self.avg =
0
-
self.sum =
0
-
self.count =
0
-
-
def update(self, val, n=1):
-
self.val = val
-
self.sum += val * n
-
self.count += n
-
self.avg = self.sum / self.count
-
def accuracy(output, target, topk=(1,)):
-
"""Computes the precision@k for the specified values of k"""
# [128, 10],128
-
maxk = max(topk)
-
batch_size = target.size(
0)
-
-
_, pred = output.topk(maxk,
1,
True,
True)
# [128, 5],indices
-
-
pred = pred.t()
-
correct = pred.eq(target.view(
1,
-1).expand_as(pred))
# 5,128
-
-
res = []
-
for k
in topk:
-
correct_k = correct[:k].reshape(
-1).float().sum(
0, keepdim=
True)
-
wrong_k = batch_size - correct_k
-
res.append(wrong_k.mul_(
100.0 / batch_size))
-
-
return res
(3)训练
-
def train(trainloader, model, criterion, optimizer, epoch):
-
-
losses = AverageMeter()
-
top1 = AverageMeter()
-
model.train()
-
for i, (input, target)
in enumerate(trainloader):
-
# measure data loading time
-
-
input, target = input.cuda(), target.cuda()
-
-
# compute output
-
output = model(input)
-
loss = criterion(output, target)
-
-
# measure accuracy and record loss
-
prec = accuracy(output, target)[
0]
-
losses.update(loss.item(), input.size(
0))
-
top1.update(prec.item(), input.size(
0))
-
-
# compute gradient and do SGD step
-
optimizer.zero_grad()
-
loss.backward()
-
optimizer.step()
-
return top1.avg
(4)测试
-
def validate(val_loader, model, criterion):
-
-
losses = AverageMeter()
-
top1 = AverageMeter()
-
model.eval()
-
with torch.no_grad():
-
for i, (input, target)
in enumerate(val_loader):
-
input, target = input.cuda(), target.cuda()
-
-
# compute output
-
output = model(input)
-
loss = criterion(output, target)
-
-
# measure accuracy and record loss
-
prec = accuracy(output, target)[
0]
-
losses.update(loss.item(), input.size(
0))
-
top1.update(prec.item(), input.size(
0))
-
-
print(
' * Prec {top1.avg:.3f}% '.format(top1=top1))
-
-
return top1.avg
(5)最后
-
best_prec =
0
-
for e
in range(epoch):
-
train_scheduler.step( e)
-
-
# train for one epoch
-
train(trainloader, net, loss_function, optimizer, e)
-
-
# evaluate on test set
-
prec = validate(valloader, net, loss_function)
-
-
# remember best precision and save checkpoint
-
is_best = prec > best_prec
-
best_prec = max(prec,best_prec)
-
print(best_prec)
转载:https://blog.csdn.net/qq_39383591/article/details/116600740