飞道的博客

pytorch搭建神经网络:从入门到精通,细节 make your network more perfect

187人阅读  评论(0)

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,这样使得它们对模型的影响具有同样的尺度。

数据扩增是对数据进行扩充的方法的总称。数据扩增可以增加训练集的样本,可以有效缓解模型过拟合的情况,也可以给模型带来的更强的泛化能力。即数据扩增的目的就是使得训练数据尽可能的接近测试数据,从而提高预测精度。另外数据扩增可以迫使网络学习到更鲁棒性的特征,从而使模型拥有更强的泛化能力。更多可参考数据增广的详细理解。代码如下:


  
  1. #引入必须的包
  2. import torch
  3. import torchvision
  4. import torchvision.transforms as transforms
  5. data_path= './dataset' #数据保存路径
  6. #数据的预处理操作
  7. training_transform=transforms.Compose([
  8. transforms.RandomCrop( 32, padding= 4), #数据增广
  9. transforms.RandomHorizontalFlip(), #数据增广
  10. transforms.ToTensor(),
  11. transforms.Normalize(mean=[ 0.4914, 0.4822, 0.4465],std=[ 0.2471, 0.2435, 0.2616]), #数据归一化
  12. ])
  13. validation_transforms=transforms.Compose([
  14. transforms.ToTensor(),
  15. transforms.Normalize(mean=[ 0.4914, 0.4822, 0.4465],std=[ 0.2471, 0.2435, 0.2616]),
  16. ])
  17. #训练集
  18. train_dataset = torchvision.datasets.CIFAR10(root=data_path, #数据下载和加载
  19. train= True,
  20. transform=training_transform,
  21. download= True) #若已下载改为False
  22. #测试集
  23. val_dataset = torchvision.datasets.CIFAR10(root=data_path,
  24. train= False,
  25. transform=validation_transforms,
  26. download= True)
  27. x,y= train_dataset[ 0]
  28. print(x.size(),y)
 

运行后输出输入图像的尺寸及其标签序号;

一些torch自带的数据集,如Imagenet, CIFAR10, MNIST都可以通过类似方式获取,此外,其他图像数据集还可以通过ImageFolder包引入,如下:


  
  1. train_dataset = torchvision.datasets.ImageFolder(traindir,
  2. transforms.Compose([
  3. transforms.RandomResizedCrop( 64),
  4. transforms.RandomHorizontalFlip(),
  5. transforms.ToTensor(),
  6. 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可以稳定提升模型的精度。


  
  1. from torch.optim.lr_scheduler import _LRScheduler
  2. class WarmUpLR(_LRScheduler):
  3. """warmup_training learning rate scheduler
  4. Args:
  5. optimizer: optimzier(e.g. SGD)
  6. total_iters: totoal_iters of warmup phase
  7. """
  8. def __init__(self, optimizer, total_iters, last_epoch=-1):
  9. self.total_iters = total_iters
  10. super().__init__(optimizer, last_epoch)
  11. def get_lr(self):
  12. """we will use the first m batches, and set the learning
  13. rate to base_lr * m / total_iters
  14. """
  15. 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),并且取消了下采样,如下:


  
  1. def Conv1(in_planes, places, stride=2):
  2. return nn.Sequential(
  3. nn.Conv2d(in_channels=in_planes, out_channels=places, kernel_size= 3, stride= 1, padding= 1, bias= False),
  4. nn.BatchNorm2d(places),
  5. nn.ReLU(inplace= True),
  6. # nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
  7. )

这是因为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,激活函数是一种非线性函数,它是衡量网络深度的标准,多层感知器(卷积层)的叠加若没有激活函数(非线性函数),那么它在神经网络的作用就相当于只有一层。类似与特征多项式一样,相同维度的多个多项式的叠加跟一个的作用是相同的。

这是我认为一些难以注意到的细节,其他网络上应有不少,更多可自行查阅。

模型的完整代码如下:


  
  1. import torch
  2. import torch.nn as nn
  3. import torch.nn.functional as F
  4. def Conv1(in_planes, places, stride=2):
  5. return nn.Sequential(
  6. nn.Conv2d(in_channels=in_planes, out_channels=places, kernel_size= 3, stride= 1, padding= 1, bias= False),
  7. nn.BatchNorm2d(places),
  8. nn.ReLU(inplace= True),
  9. # nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
  10. )
  11. class BasicBlock(nn.Module):
  12. expansion = 1
  13. def __init__(self, in_planes, planes, stride=1):
  14. super(BasicBlock, self).__init__()
  15. self.conv1 = nn.Conv2d(in_planes, planes, kernel_size= 3,
  16. stride=stride, padding= 1, bias= False)
  17. self.bn1 = nn.BatchNorm2d(planes)
  18. self.conv2 = nn.Conv2d(planes, planes, kernel_size= 3,
  19. stride= 1, padding= 1, bias= False)
  20. self.bn2 = nn.BatchNorm2d(planes)
  21. self.shortcut = nn.Sequential()
  22. # 经过处理后的x要与x的维度相同(尺寸和深度)
  23. # 如果不相同,需要添加卷积+BN来变换为同一维度
  24. if stride != 1 or in_planes != self.expansion * planes:
  25. self.shortcut = nn.Sequential(
  26. nn.Conv2d(in_planes, self.expansion * planes, kernel_size= 1, stride=stride, bias= False),
  27. nn.BatchNorm2d(self.expansion * planes)
  28. )
  29. def forward(self, x):
  30. # print(x.size())
  31. out = F.relu(self.bn1(self.conv1(x)))
  32. out = self.bn2(self.conv2(out))
  33. out += self.shortcut(x)
  34. out = F.relu(out)
  35. return out
  36. class ResNet(nn.Module):
  37. def __init__(self, block, num_blocks, num_classes=10):
  38. super(ResNet, self).__init__()
  39. self.in_planes = 64
  40. self.conv1 = Conv1(in_planes= 3, places= 64)
  41. self.layer1 = self._make_layer(block, 64, num_blocks[ 0], stride= 1)
  42. self.layer2 = self._make_layer(block, 128, num_blocks[ 1], stride= 2)
  43. self.layer3 = self._make_layer(block, 256, num_blocks[ 2], stride= 2)
  44. self.layer4 = self._make_layer(block, 512, num_blocks[ 3], stride= 2)
  45. self.avgpool = nn.AdaptiveAvgPool2d(( 1, 1))
  46. self.classifer = nn.Linear( 512*block.expansion, num_classes)
  47. def _make_layer(self, block, planes, num_blocks, stride):
  48. strides = [stride] + [ 1] * (num_blocks - 1)
  49. layers = []
  50. for stride in strides:
  51. layers.append(block(self.in_planes, planes, stride))
  52. self.in_planes = planes * block.expansion
  53. return nn.Sequential(*layers)
  54. def forward(self, x):
  55. x = self.conv1(x)
  56. x = self.layer1(x)
  57. x = self.layer2(x)
  58. x = self.layer3(x)
  59. x = self.layer4(x)
  60. x = self.avgpool(x)
  61. x = x.view(x.size()[ 0], -1) # 4, 2048
  62. x = self.classifer(x)
  63. return x
  64. def ResNet18(**kwargs):
  65. return ResNet(BasicBlock, [ 2, 2, 2, 2],**kwargs)
  66. net = ResNet18(num_classes= 10)
  67. y = net(torch.randn( 1, 3, 32, 32))
  68. print(y.size())
 

6. 训练和测试

(1)根据上述设置超参


  
  1. #超参数
  2. warm= 1
  3. epoch= 160
  4. batch_size= 128
  5. loss_function = nn.CrossEntropyLoss()
  6. optimizer = optim.SGD(net.parameters(), lr= 0.1, momentum= 0.9, weight_decay= 1e-4)
  7. train_scheduler = optim.lr_scheduler.MultiStepLR(optimizer, milestones=[ 80, 120], gamma= 0.1) #learning rate decay
  8. from torch.utils.data import DataLoader
  9. trainloader = DataLoader(train_dataset, batch_size, shuffle= True, num_workers= 5, pin_memory= True)
  10. valloader = DataLoader(val_dataset, batch_size, shuffle= False, num_workers= 5, pin_memory= True)
  11. iter_per_epoch = len(trainloader)
  12. warmup_scheduler = WarmUpLR(optimizer, iter_per_epoch * warm)

(2)准确率计算函数


  
  1. class AverageMeter(object):
  2. """Computes and stores the average and current value"""
  3. def __init__(self):
  4. self.reset()
  5. def reset(self):
  6. self.val = 0
  7. self.avg = 0
  8. self.sum = 0
  9. self.count = 0
  10. def update(self, val, n=1):
  11. self.val = val
  12. self.sum += val * n
  13. self.count += n
  14. self.avg = self.sum / self.count
  15. def accuracy(output, target, topk=(1,)):
  16. """Computes the precision@k for the specified values of k""" # [128, 10],128
  17. maxk = max(topk)
  18. batch_size = target.size( 0)
  19. _, pred = output.topk(maxk, 1, True, True) # [128, 5],indices
  20. pred = pred.t()
  21. correct = pred.eq(target.view( 1, -1).expand_as(pred)) # 5,128
  22. res = []
  23. for k in topk:
  24. correct_k = correct[:k].reshape( -1).float().sum( 0, keepdim= True)
  25. wrong_k = batch_size - correct_k
  26. res.append(wrong_k.mul_( 100.0 / batch_size))
  27. return res

(3)训练


  
  1. def train(trainloader, model, criterion, optimizer, epoch):
  2. losses = AverageMeter()
  3. top1 = AverageMeter()
  4. model.train()
  5. for i, (input, target) in enumerate(trainloader):
  6. # measure data loading time
  7. input, target = input.cuda(), target.cuda()
  8. # compute output
  9. output = model(input)
  10. loss = criterion(output, target)
  11. # measure accuracy and record loss
  12. prec = accuracy(output, target)[ 0]
  13. losses.update(loss.item(), input.size( 0))
  14. top1.update(prec.item(), input.size( 0))
  15. # compute gradient and do SGD step
  16. optimizer.zero_grad()
  17. loss.backward()
  18. optimizer.step()
  19. return top1.avg

(4)测试


  
  1. def validate(val_loader, model, criterion):
  2. losses = AverageMeter()
  3. top1 = AverageMeter()
  4. model.eval()
  5. with torch.no_grad():
  6. for i, (input, target) in enumerate(val_loader):
  7. input, target = input.cuda(), target.cuda()
  8. # compute output
  9. output = model(input)
  10. loss = criterion(output, target)
  11. # measure accuracy and record loss
  12. prec = accuracy(output, target)[ 0]
  13. losses.update(loss.item(), input.size( 0))
  14. top1.update(prec.item(), input.size( 0))
  15. print( ' * Prec {top1.avg:.3f}% '.format(top1=top1))
  16. return top1.avg

(5)最后


  
  1. best_prec = 0
  2. for e in range(epoch):
  3. train_scheduler.step( e)
  4. # train for one epoch
  5. train(trainloader, net, loss_function, optimizer, e)
  6. # evaluate on test set
  7. prec = validate(valloader, net, loss_function)
  8. # remember best precision and save checkpoint
  9. is_best = prec > best_prec
  10. best_prec = max(prec,best_prec)
  11. print(best_prec)

 

 


转载:https://blog.csdn.net/qq_39383591/article/details/116600740
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场