参考资料:
- 《PyTorch深度学习》(人民邮电出版社)第5章 深度学习之计算机视觉
- PyTorch官方文档
- 廖星宇著《深度学习入门之Pytorch》第4章 卷积神经网络
- 其他参考的网络资料在文中以超链接的方式给出
池化层和ReLU层的关系
一般来说都是先非线性的激活函数,然后再加pooling层。
但是对于ReLU和max pooling来说,顺序是可以交换的。
关于卷积神经网络的一些理论知识,可以看这篇文章:CNN(卷积神经网络)详解
卷积神经网络有三个非常重要的思想,这些思想也是为什么CNN能够真正起作用的原因。
卷积神经网络和全连接神经网络是相似的,都是由一些神经元构成,这些神经元有需要学习的参数,通过网络输入最后输出结构,并通过损失函数来优化网络中的参数。
然而,如果采用全连接神经网络去处理图片,当处理比较大的彩色图片时i,神经网络的参数增加的特别快,效率特别低。
而卷积神经网络的处理过程,不同于一般的全连接神经网络,卷积神经网络的层结构是不同的(如下图)。
卷积神经网络是一个3D容量的神经元,神经元是以三个维度来排列的:宽度、高度和深度。卷积神经网络中的主要层结构有三个:卷积层、池化层和全连接层,通过堆叠这些层结构形成一个完整的卷积神经网络。卷积神经网络将原始图片转化成最后的类别得分,其中一些层包含参数,一些层没有包含参数,比如卷积层和全连接层拥有参数,而激活层和池化层不含参数。这些参数通过梯度下降法来更新,最后使模型尽可能正确地识别出图片类别。
推荐阅读:
如何理解卷积神经网络(CNN)中的卷积和池化?
卷积层是卷积神经网络的核心,大多数计算都是在卷积层中进行的。
概述
卷积神经网络的参数是由一些可学习的滤波器集合构成,每个滤波器在空间上(宽度和高度)都比较小,但是深度和输入数据的深度保持一致。在前向传播的时候,让每个滤波器都在输入数据的宽度和高度上滑动(卷积),然后计算整个滤波器和输入数据任意一处的内积。
滤波器可以视为二维数字矩阵。卷积操作可以看成以下四个步骤:
在卷积层中还有一个重要的概念——感受野(receptive field)。与神经元连接的空间大小叫做神经元的感受野,它的大小是人为设置的一个超参数。多层卷积操作之后的感受野计算可以看这篇文章:感受野。
在滑动滤波器的时候,需要设置步长限制,步长就是滤波器一次移动的像素格的个数。输出图片的尺寸可以由公式 (W-F+2P)/S+1 来计算。其中W表示输入数据的大小,F表示卷积层中神经元的感受野尺寸,S表示步长,P表示边界填充0的数量。步长的设置不能使上述公式计算的结果为非整数。
举个例子来说明,图片像素中的数字表示像素格的亮度(步长=1):
卷积有助于我们找到特定的局部图像特征(如边缘),用在后面的网络中。比如上面这个滤波器(索伯滤波器,Sobel filter)就可以对图片进行如下所示的处理,这个滤波器的作用就是输出图像中更亮的像素表示原始图像中存在的边缘。
我们可以看到,上面4X4的图片通过3X3的滤波器,就变成了2X2的图片。为了解决这个问题,可以再图片的像素矩阵周围填充0像素:
最后,总结一下卷积层的一些性质:
(1)输入数据体的尺寸是W1×H1×D1。
(2)有4个超参数:滤波器数量K,滤波器空间尺寸F,滑动步长S,零填充的数量P。
(3)输出数据体的尺寸为W2×H2×D2,其中W2=(W1-F+2P)/S+1,H2=(H1-F+2P)/S+1,D2=K。
(4)由于参数共享,每个滤波器包含的权重数目为F×F×D1,卷积层一共有F×F×D1×K个权重和K个偏置。
(5)在输出体数据中,第d个深度切片(空间尺寸是W2×H2),用第d个滤波器和输入数据进行有效卷积运算的结果,再加上第d个偏置。
对于卷积神经网络的一些超参数,常见的设置是F=3,S=1,P=1。
通常会在卷积层之间周期性插入一个池化层,其作用是逐渐降低数据体的空间尺寸,这样就能够减少网络中参数的数量,减少计算资源耗费,同时也能有效地控制过拟合。
池化一般通过简单的最大值、最小值或平均值操作完成。以下是池大小为2的最大池层的示例。除了最大值池化外,还有一些其他的池化函数,比如平均池化,或者L2范数池化。在实际中证明,在卷积层之间引入最大池化的效果是最好的,而平均池化一般放在卷积神经网络的最后一层。
最常用的池化层形式是尺寸为2×2的窗口,华东步长为2,对图像进行最大池化层采样,可以将其中75%的激活信息都丢掉,选择其中最大的保留下来。这样做的目的是希望能够更加激活里面的数值大的特征,去除一些噪声信息。
池化层和卷积层有一些类似的性质:
(1)输入数据体的尺寸是W1×H1×D1。
(2)有2个需要设置的超参数:空间大小F和滑动步长S。池化层中很少引入零填充,即P=0。
(3)输出数据体的尺寸为W2×H2×D2,其中W2=(W1-F)/S+1,H2=(H1-F)/S+1,D2=D1。
(4)对输入进行固定函数的计算,没有参数引入。
在实际中,有两种方式:
一种是F=3,S=2,这种池化有重叠;
另外一种更常用,F=2,S=2。
一般来说,应该谨慎使用比较大的池化窗口,以免对网络有破坏性。
全连接层和之前介绍的全连接神经网络是一样的,每个神经元与前一层所有的神经元全部连接。
一般经过了一系列的卷积层和池化层之后,提取出图片的特征图,将特征图中的所有神经元变成全连接层的样子,再经过几个隐藏层,最后输出结果。
在这个过程中,为了防止过拟合,会引入Droopout。有研究表明,在进入全连接层之前,使用全局平均池化能够有效地降低过拟合。
卷积神经网络通常由上面介绍的三种层结构构成。
最常见的形式就是将一些卷积层和ReLU层放在一起(有时候会在ReLU层前面加上批标准化层),随后紧跟着池化层,再不断重复,直到图像在空间上被缩小到一个足够小的尺寸,然后将特征图展开,连接基层全连接层,最后输出结果。
这里说明几个要点:
小滤波器的有效性
一般而言,几个小滤波器卷积层的组合比一个大滤波器卷积层要好。比如说,三个滤波器为3×3的卷积层(模型1)的感受野为7,一个滤波器为7×7的卷积层(模型2)感受野也为7,但是前者的效果要更好一些。
首先,多个卷积层与非线性激活层交替的结构,比单一卷积层的结构更能提取出深层的特征;
其次,假设输入数据体的深度是C,模型1的组合仅包含3×(3×3×C×C)=27×C2 的参数,模型2包含 7×7×C×C=49×C2 的参数。
不过,对于模型1来说,当反向传播更新参数的时候,中间的卷积层可能会占用更多的内存。
网络的尺寸
关于卷积神经网络的尺寸设计,有一些经验可以参考:
(1)输入层:一般而言,输入层的大小应该能够被2整除很多次,常用的数字包括32,64,96和124。
(2)卷积层:卷积层应该尽可能使用小尺寸的滤波器,比如3×3或者5×5,如果要使用更大的滤波器尺寸(如7×7),通常会用在第一个面对原始图像的卷积层上。滑动步长取1。需要对输入数据进行零填充,这样可以有效地保证卷积层不会改变输入数据体的空间尺寸。
(3)池化层:常用的设置使用2×2的感受野做最大值池化,滑动步长取2.一般而言池化层的感受野大小很少超过3,因为这样会使得池化过程过于激烈,造成信息的丢失,进而导致算法的性能变差。
(4)零填充:零填充可以让卷积层的输入和输出在空间上的维度保持一致,此外,如果不适用零填充,数据体的尺寸就会略微减少,在不断进行卷积的过程中,图像的边缘信息就会过快地损失掉。
整体架构如下:
import torch.nn as nn
import torch.nn.functional as F
class SimpleCNN(nn.Module):
def __init__(self):
super(SimpleCNN, self).__init__()
layer1 = nn.Sequential()
layer1.add_module('conv1', nn.Conv2d(3, 32, 3, 1, padding = 1))
layer1.add_module('relu1', nn.ReLU(True))
layer1.add_module('pool1', nn.MaxPool2d(2,2))
self.conv1 = layer1
layer2 = nn.Sequential()
layer2.add_module('conv2', nn.Conv2d(32, 64, 3, 1, padding = 1))
layer2.add_module('relu2', nn.ReLU(True))
layer2.add_module('pool2', nn.MaxPool2d(2,2))
self.conv2 = layer2
layer3 = nn.Sequential()
layer3.add_module('conv3', nn.Conv2d(64, 128, 3, 1, padding = 1))
layer3.add_module('relu3', nn.ReLU(True))
layer3.add_module('pool3', nn.MaxPool2d(2,2))
self.conv3 = layer3
layer4 = nn.Sequential()
layer4.add_module('fc1', nn.Linear(2048, 512))
layer4.add_module('fc_relu1', nn.ReLU(True))
layer4.add_module('fc2', nn.Linear(512, 64))
layer4.add_module('fc_relu2', nn.ReLU(True))
layer4.add_module('fc3', nn.Linear(64, 10))
self.fc = layer4
def forward(self, x):
conv1 = self.conv1(x)
conv2 = self.conv2(conv1)
conv3 = self.conv3(conv2)
fc_input = conv3.view(conv3.size(0), -1)
fc_out = self.fc(fc_input)
return fc_out
torch.nn.Conv2d(in_channels, out_channels, kernel_size, stride=1, padding=0, dilation=1, groups=1, bias=True, padding_mode='zeros')
nn.Conv2d()是PyTorch中的卷积模块,常用的参数有5个——
in_channels:输入数据体的深度。
out_channels:输出数据体的深度。
kernel_size:滤波器(卷积核)的大小。表示相同高宽的滤波器 kernel_size=3;表示不同高宽的滤波器 kernel_size=(3, 2)。
stride:步长,默认为1。
padding:是否对周围进行零填充。默认为0,表示不填充。如果设置padding=1,则表示对四周进行1个像素点的零填充(padding_mode=‘zeros’)。
dilation:卷积对于输入数据体的空间间隔,默认为1。
groups:表示输出数据体深度上和输入数据体深度上的联系,默认为1,也就是所有的输出和输入都是相关联的;如果groups=2,则表示输入的深度被分割成22份,输出的深度也被分割成2份,他们之间分别对应起来。所以要求输出和输入都必须要能被groups整除。
bias:默认为True,表示使用偏置。
torch.nn.MaxPool2d(kernel_size, stride=None, padding=0, dilation=1, return_indices=False, ceil_mode=False)
kernel_size、stride、padding、dilation的参数含义和卷积层的一样。
其他参数(一般情况下下面这两个参数都不会设置):
return_indices:表示是否返回最大值所处的下标。默认为False。
ceil_mode:表示使用一些方格代替层结构,默认为False。
PyTorch也提供了其他的池化层,在官方文档里面可以找到。
推荐阅读:torch.nn与torch.nn.functional之间的区别和联系
view()函数的功能是把原先tensor中的数据按照行优先的顺序排成一个一维的数据(这里应该是因为要求地址是连续存储的),然后按照参数组合成其他维度的tensor。
推荐阅读:Pytorch-view函数
经典卷积神经网络 | 描述 | 参考资料 |
---|---|---|
LeNet | 卷积神经网络的开山之作 | 论文:Gradient-Based Learning Applied to Document Recognition;博客文章:经典CNN之:LeNet介绍 |
AlexNet | ImageNet 竞赛史上第二次基于卷积神经网络的模型得到冠军,从此掀起了深度学习在计算机视觉上的革命 | 百度百科:AlexNet |
VGGNet | VGGNet介绍 | |
GoogLeNet / Inception N et | 2014年ImageNet竞赛冠军 | |
ResNet | 2015年ImageNet竞赛冠军 | 经典分类CNN模型系列其四:Resnet |
导入相应的包
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torchvision import datasets,transforms
from torch.autograd import Variable
import matplotlib.pyplot as plt
建立CNN模型架构
class Net(nn.Module):
def __init__(self):
super().__init__()
self.conv1 = nn.Sequential(nn.Conv2d(1, 10, kernel_size = 5),
nn.ReLU(True),
nn.MaxPool2d(2, 2))
self.conv2 = nn.Sequential(nn.Conv2d(10, 20, kernel_size = 5),
nn.Dropout2d(),
nn.ReLU(True),
nn.MaxPool2d(2, 2))
self.fc1 = nn.Sequential(nn.Linear(320, 50),
nn.ReLU(True),
nn.Dropout2d())
self.fc2 = nn.Sequential(nn.Linear(50, 10),
nn.LogSoftmax(dim = 1))
def forward(self, x):
x = self.conv1(x)
x = self.conv2(x)
x = x.view(-1, 320)
x = self.fc1(x)
x = self.fc2(x)
return x
上面建立的卷积神经网络含有2个卷积层,2个最大池化层,使用ReLU激活函数增加非线性,最后使用全连接层输出分类得分。
数据预处理
#数据预处理
transformation = transforms.Compose([transforms.ToTensor(),
transforms.Normalize((0.1307,), (0.3081,))])
train_dataset = datasets.MNIST('data/',train=True, transform=transformation, download=True)
test_dataset = datasets.MNIST('data/',train=False, transform=transformation, download=True)
train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=32, shuffle=True)
test_loader = torch.utils.data.DataLoader(test_dataset, batch_size=32, shuffle=True)
is_cuda=False
if torch.cuda.is_available():
is_cuda = True
model = Net()
if is_cuda:
model.cuda()
设置优化器
optimizer = optim.SGD(model.parameters(),lr=0.01)
训练模型
def fit(epoch, model, data_loader, phase='training', volatile=False):
if phase == 'training':
model.train()
if phase == 'validation':
model.eval()
volatile=True
running_loss = 0.0
running_correct = 0
for batch_idx , (data,target) in enumerate(data_loader):
if is_cuda:
data,target = data.cuda(),target.cuda()
data , target = Variable(data,volatile),Variable(target)
if phase == 'training':
optimizer.zero_grad()
output = model(data)
loss = F.nll_loss(output,target)
running_loss += F.nll_loss(output,target,reduction = 'sum').item()
preds = output.data.max(dim=1,keepdim=True)[1]
running_correct += preds.eq(target.data.view_as(preds)).cpu().sum()
if phase == 'training':
loss.backward()
optimizer.step()
loss = running_loss/len(data_loader.dataset)
accuracy = 100. * running_correct/len(data_loader.dataset)
print(f'{phase} loss is {loss:{5}.{2}} and {phase} accuracy is {running_correct}/{len(data_loader.dataset)}{accuracy:{10}.{4}}')
return loss,accuracy
train_losses , train_accuracy = [],[]
val_losses , val_accuracy = [],[]
num_epoches = 20
for epoch in range(num_epoches):
print('-'*10)
print('epoch {}/{}'.format(epoch+1, num_epoches))
epoch_loss, epoch_accuracy = fit(epoch, model, train_loader, phase='training')
val_epoch_loss , val_epoch_accuracy = fit(epoch, model, test_loader, phase='validation')
train_losses.append(epoch_loss)
train_accuracy.append(epoch_accuracy)
val_losses.append(val_epoch_loss)
val_accuracy.append(val_epoch_accuracy)
模型训练结果:
分别对前面创建的三个模型(简单易懂的深度学习(二):多层全连接神经网络与MNIST手写数字分类)进行训练20轮,得到的准确率如下:
模型 | 准确率(%)(epoch=20) |
---|---|
简单全连接(SimpleNet) | 58.57 |
改进网络——增加激活函数(Acctivation_Net) | 61.78 |
再改进一下网络——添加批标准化(Batch_Net ) | 62.50 |
卷积神经网络(CNN) | 98.62 |
导入相应的包
import matplotlib.pyplot as plt
from torchvision import transforms
import torch
from torch.autograd import Variable
import torch.nn as nn
import torch.nn.functional as F
from torch import optim
from torchvision.datasets import ImageFolder
建立CNN模型架构
class Net(nn.Module):
def __init__(self):
super().__init__()
self.conv1 = nn.Sequential(nn.Conv2d(3, 10, kernel_size = 5),
nn.ReLU(),
nn.MaxPool2d(2, 2))
self.conv2 = nn.Sequential(nn.Conv2d(10, 20, kernel_size = 5),
nn.Dropout2d(),
nn.ReLU(),
nn.MaxPool2d(2, 2))
self.fc1 = nn.Sequential(nn.Linear(56180, 500),
nn.ReLU(),
nn.Dropout2d())
self.fc2 = nn.Sequential(nn.Linear(500, 50),
nn.ReLU(),
nn.Dropout2d())
self.fc3 = nn.Sequential(nn.Linear(50, 2),
nn.LogSoftmax(dim = 1))
def forward(self, x):
x = self.conv1(x)
x = self.conv2(x)
x = x.view(x.size(0), -1)
x = self.fc1(x)
x = self.fc2(x)
x = self.fc3(x)
return x
数据预处理
#数据预处理
simple_transform = transforms.Compose([transforms.Resize((224,224)),
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
])
train = ImageFolder('cat_and_dog/train/training/',simple_transform)
valid = ImageFolder('cat_and_dog/train/validation/',simple_transform)
batch_size = 32
train_data_loader = torch.utils.data.DataLoader(train, batch_size=batch_size, num_workers=0, shuffle=True)
valid_data_loader = torch.utils.data.DataLoader(valid, batch_size=batch_size, num_workers=0, shuffle=True)
is_cuda=False
if torch.cuda.is_available():
is_cuda = True
设置优化器
model = Net()
if is_cuda:
model.cuda()
optimizer = optim.SGD(model.parameters(),lr=0.01)
训练模型
训练模型部分的代码和案例应用一种的一样。
最后可以绘制训练损失、验证损失、训练准确率、验证准确率的图,便于分析模型性能。
plt.figure()
plt.plot(range(1,len(train_losses)+1),train_losses,'bo',label = 'training loss')
plt.plot(range(1,len(val_losses)+1),val_losses,'r',label = 'validation loss')
plt.legend()
plt.figure()
plt.plot(range(1,len(train_accuracy)+1),train_accuracy,'ro',label = 'train accuracy')
plt.plot(range(1,len(val_accuracy)+1),val_accuracy,'b',label = 'val accuracy')
plt.legend()
从案例二中可以看到,虽然每次迭代训练集的损失都在减少,但验证集的损失却没有很大的改善;在训练过程中,准确率也在增加,但在78%左右时几乎饱和。(如下图)
显而易见,这个模型并没有很好的泛化能力。我们可以采用迁移学习这项技术,帮助我们训练更准确的模型,以及加快训练的速度。
关于迁移学习的相关概念,可以看这篇文章:迁移学习概述(Transfer Learning)
简单来说,就是在类似的数据集上使用训练好的算法,无须从头开始训练。
因此,我们就需要知道怎样利用PyTorch去提出已经训练好的模型的某些层、权重以及修改他们的参数。
给定一个模型,如果只想提取模型中的某一层或者某几层,可以采用nn.Module里面提供的一些属性来解决。
children()
例子,提取上面构建好的网络的前两层:
model = SimpleCNN()
new_model = nn.Sequential(*list(model.children())[:2])
print(new_model)
'''
out:
Sequential(
(0): Sequential(
(conv1): Conv2d(3, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(relu1): ReLU(inplace=True)
(pool1): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
)
(1): Sequential(
(conv2): Conv2d(32, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(relu2): ReLU(inplace=True)
(pool2): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
)
)
'''
name_modules()
如果想提取出模型中的所有卷积层:
conv_model = nn.Sequential()
model = SimpleCNN()
for layer in model.named_modules():
if isinstance(layer[1], nn.Conv2d):
conv_model.add_module(layer[0].replace('.', ' '), layer[1])
print(conv_model)
'''
out:
Sequential(
(conv1 conv1): Conv2d(3, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(conv2 conv2): Conv2d(32, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(conv3 conv3): Conv2d(64, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
)
'''
named_parameters() 给出网络层的名字和参数的迭代器
parameters() 给出网络的全部参数的迭代器
model = SimpleCNN()
for param in model.named_parameters():
print(param[0])
'''
out:
conv1.conv1.weight
conv1.conv1.bias
conv2.conv2.weight
conv2.conv2.bias
conv3.conv3.weight
conv3.conv3.bias
fc.fc1.weight
fc.fc1.bias
fc.fc2.weight
fc.fc2.bias
fc.fc3.weight
fc.fc3.bias
'''
对于权重的初始化,只需要取出其中的data属性,对它进行所需要的处理即可:
model = SimpleCNN()
for m in model.modules():
if isinstance(m, nn.Conv2d):
nn.init.normal(m.weight.data)
nn.init.xavier_normal(m.weight.data)
nn.init.kaiming_normal(m.weight.data)
m.bias.data.fill_(0)
elif isinstance(m, nn.Linear):
m.weight.data.normal_()
PyTorch在torchvision库中提供了一组已经训练好的模型,这些模型可以通过设置其参数pretrained=True,来下载为ImageNet分类问题调整好的权重。
from torchvision import models
vgg = models.vgg16(pretrained = True)
我们把VGG16模型打印出来,可以看到:
VGG(
(features): Sequential(
(0): Conv2d(3, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(1): ReLU(inplace=True)
(2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(3): ReLU(inplace=True)
(4): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
(5): Conv2d(64, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(6): ReLU(inplace=True)
(7): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(8): ReLU(inplace=True)
(9): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
(10): Conv2d(128, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(11): ReLU(inplace=True)
(12): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(13): ReLU(inplace=True)
(14): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(15): ReLU(inplace=True)
(16): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
(17): Conv2d(256, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(18): ReLU(inplace=True)
(19): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(20): ReLU(inplace=True)
(21): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(22): ReLU(inplace=True)
(23): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
(24): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(25): ReLU(inplace=True)
(26): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(27): ReLU(inplace=True)
(28): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(29): ReLU(inplace=True)
(30): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
)
(avgpool): AdaptiveAvgPool2d(output_size=(7, 7))
(classifier): Sequential(
(0): Linear(in_features=25088, out_features=4096, bias=True)
(1): ReLU(inplace=True)
(2): Dropout(p=0.5, inplace=False)
(3): Linear(in_features=4096, out_features=4096, bias=True)
(4): ReLU(inplace=True)
(5): Dropout(p=0.5, inplace=False)
(6): Linear(in_features=4096, out_features=1000, bias=True)
)
)
冻结层
VGG16模型包含了两个序列模型:features和classifiers。features主要用于识别许多重要的特征,为了减轻我们电脑的负担,我们直接使用已经预训练好的VGG16模型的参数,在后续猫狗图片分类中,我们不训练这部分的参数(即不更新)。
那么,就需要将features序列模型的所有层冻结:
# 冻结features 模型的所有参数
for param in vgg.features.parameters():
param.requires_grad = False
改变VGG模型的输出特征
我们可以看到VGG模型的classifier序列模型的最后一层的输出维度是1000,因为VGG16模型被训练为针对1000个类别的分类,但是我们的案例中只需要输出两个分类——猫(0)和狗(1)。因此,我们需要对VGG模型的最后一层的输出进行修改:
# 修改最后一层的输出
vgg.classifier[-1] = nn.Linear(in_features=4096, out_features=2, bias=True)
我们再次打印VGG16模型架构,可以发现已经对VGG16模型进行了修改。
……
(classifier): Sequential(
(0): Linear(in_features=25088, out_features=4096, bias=True)
(1): ReLU(inplace=True)
(2): Dropout(p=0.5, inplace=False)
(3): Linear(in_features=4096, out_features=4096, bias=True)
(4): ReLU(inplace=True)
(5): Dropout(p=0.5, inplace=False)
(6): Linear(in_features=4096, out_features=2, bias=True)
由于我们只需要训练VGG16模型的classifier序列模型,因此我们只需要将classifier.parameters()传入优化器:
# 构建优化器和损失函数
optimizer = optim.SGD(vgg.classifier.parameters(), lr = 0.0001, momentum = 0.5)
cost = torch.nn.CrossEntropyLoss()
def fit(epoch, model, data_loader, phase='training', volatile=False):
if phase == 'training':
model.train()
if phase == 'validation':
model.eval()
volatile=True
running_loss = 0.0
running_correct = 0
for batch_idx , (data,target) in enumerate(data_loader):
if is_cuda:
data,target = data.cuda(),target.cuda()
data, target = Variable(data,volatile),Variable(target)
if phase == 'training':
optimizer.zero_grad()
output = model(data)
preds = output.data.max(dim=1,keepdim=True)[1]
loss = cost(output,target)
running_loss += loss.item()
running_correct += preds.eq(target.data.view_as(preds)).cpu().sum()
if phase == 'training':
loss.backward()
optimizer.step()
loss = running_loss/len(data_loader.dataset)
accuracy = 100. * running_correct/len(data_loader.dataset)
print(f'{phase} loss is {loss:{5}.{2}} and {phase} accuracy is {running_correct}/{len(data_loader.dataset)}{accuracy:{10}.{4}}')
return loss,accuracy
train_losses , train_accuracy = [],[]
val_losses , val_accuracy = [],[]
num_epoches = 1
for epoch in range(num_epoches):
print('-'*10)
print('epoch {}/{}'.format(epoch+1, num_epoches))
epoch_loss, epoch_accuracy = fit(epoch, vgg, train_data_loader, phase='training')
val_epoch_loss , val_epoch_accuracy = fit(epoch, vgg, valid_data_loader, phase='validation')
train_losses.append(epoch_loss)
train_accuracy.append(epoch_accuracy)
val_losses.append(val_epoch_loss)
val_accuracy.append(val_epoch_accuracy)
训练模型的代码和案例二的基本一致,做了一点小小的改动。
由于训练速度实在是太慢了,而且我的电脑的GPU内存不够,我只选了50张猫的图片和50张狗的图片作为数据集进行训练。
模型 | 准确率(%)(epoch=20) |
---|---|
CNN(案例二) | 78 |
迁移学习(VGG16微调模型) | 95 |
可以看到,使用预训练好的权重,模型的准确率有了很大的提升。
我们可以应用一些技巧,例如数据增强和使用不同的dropout值来改进模型的泛化能力。
将dropout值从0.5改成0.2
# 修改dropout值
for layer in vgg.classifier.children():
if (type(layer) == nn.Dropout):
layer.p = 0.2
Dropout()中的参数p的含义是“一个神经元被归零(dropout)的概率”,其默认为0.5。将p=0.5改成p=0.2,降低了神经元被dropout的概率,模型的复杂度增加,参与训练的参数增加,模型的准确率也会改善。
torch.nn.Dropout(p=0.5, inplace=False)
数据增强
改进模型泛化能力的另一个技巧是添加更多的数据或者进行数据增强。例如,可以随机地水平翻转图像或以小角度旋转图像来进行数据增强。torchvision包里面为数据增强提供了很多工具:
transforms.RandomHorizontalFlip()
transforms.RandomRotation(0, 2)
还有一些其他的工具,可以参考官方的文档。
在案例应用三里面,我们采用了迁移学习对猫狗图片进行了分类,训练的结果有了很大的改善。但是,案例三采用的训练框架训练时间非常慢,原因在于即使我们已经冻结了VGG16中的features序列模型的参数,但是在训练期间,都要计算卷积特征,
为了提高模型的训练速度,我们可以只计算一次这些卷积特征,并保存下来,并仅训练线性层(classifier序列模型)
vgg = models.vgg16(pretrained = True)
vgg = vgg.cuda()
features = vgg.features
def preconvfeat(dataset,model):
conv_features = []
labels_list = []
for data in dataset:
inputs,labels = data
if is_cuda:
inputs , labels = inputs.cuda(),labels.cuda()
inputs , labels = Variable(inputs),Variable(labels)
output = model(inputs)
conv_features.extend(output.data.cpu().numpy())
labels_list.extend(labels.data.cpu().numpy())
conv_features = np.concatenate([[feat] for feat in conv_features])
return (conv_features,labels_list)
conv_feat_train,labels_train = preconvfeat(train_data_loader,features)
conv_feat_val,labels_val = preconvfeat(valid_data_loader,features)
在获得了train和validation集的卷积特征后,可以创建我们自己的数据加载类,建立新的数据集。
class My_dataset(Dataset):
def __init__(self,feat,labels):
self.conv_feat = feat
self.labels = labels
def __len__(self):
return len(self.conv_feat)
def __getitem__(self,idx):
return self.conv_feat[idx],self.labels[idx]
train_feat_dataset = My_dataset(conv_feat_train,labels_train)
val_feat_dataset = My_dataset(conv_feat_val,labels_val)
batch_size = 5
train_feat_loader = DataLoader(train_feat_dataset,batch_size=batch_size,shuffle=True)
val_feat_loader = DataLoader(val_feat_dataset,batch_size=batch_size,shuffle=True)
def data_gen(conv_feat,labels,batch_size=64,shuffle=True):
labels = np.array(labels)
if shuffle:
index = np.random.permutation(len(conv_feat))
conv_feat = conv_feat[index]
labels = labels[index]
for idx in range(0,len(conv_feat),batch_size):
yield(conv_feat[idx:idx+batch_size],labels[idx:idx+batch_size])
train_batches = data_gen(conv_feat_train,labels_train)
val_batches = data_gen(conv_feat_val,labels_val)