CNN02:Pytorch实现VGG16的CIFAR10分类
CNN02:Pytorch实现VGG16的CIFAR10分类
1、VGG16的网络结构和原理
VGG
的具体网络结构和原理参考博客:
https://www.cnblogs.com/guoyaohua/p/8534077.html
该博客不只讲了VGG
还讲了其他卷积神经网络的网络结构,比较详细,容易理解。
2、基于Pytorch的VGG的CIFAR10分类Python代码实现
(1)整体代码:
import torch
import torch.nn as nn
from torch import optim
from torch.autograd import Variable
from torch.utils.data import DataLoader
from torchvision import transforms
from torchvision import datasets
from tqdm import tqdm
'''定义超参数'''
batch_size = 256 # 批的大小
learning_rate = 1e-2 # 学习率
num_epoches = 10 # 遍历训练集的次数
'''
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 ]),
])
'''
'''下载训练集 CIFAR-10 10分类训练集'''
train_dataset = datasets.CIFAR10('./data', train=True, transform=transforms.ToTensor(), download=True)
train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True)
test_dataset = datasets.CIFAR10('./data', train=False, transform=transforms.ToTensor(), download=True)
test_loader = DataLoader(test_dataset, batch_size=32, shuffle=False)
'''定义网络模型'''
class VGG16(nn.Module):
def __init__(self, num_classes=10):
super(VGG16, self).__init__()
self.features = nn.Sequential(
#1
nn.Conv2d(3,64,kernel_size=3,padding=1),
nn.BatchNorm2d(64),
nn.ReLU(True),
#2
nn.Conv2d(64,64,kernel_size=3,padding=1),
nn.BatchNorm2d(64),
nn.ReLU(True),
nn.MaxPool2d(kernel_size=2,stride=2),
#3
nn.Conv2d(64,128,kernel_size=3,padding=1),
nn.BatchNorm2d(128),
nn.ReLU(True),
#4
nn.Conv2d(128,128,kernel_size=3,padding=1),
nn.BatchNorm2d(128),
nn.ReLU(True),
nn.MaxPool2d(kernel_size=2,stride=2),
#5
nn.Conv2d(128,256,kernel_size=3,padding=1),
nn.BatchNorm2d(256),
nn.ReLU(True),
#6
nn.Conv2d(256,256,kernel_size=3,padding=1),
nn.BatchNorm2d(256),
nn.ReLU(True),
#7
nn.Conv2d(256,256,kernel_size=3,padding=1),
nn.BatchNorm2d(256),
nn.ReLU(True),
nn.MaxPool2d(kernel_size=2,stride=2),
#8
nn.Conv2d(256,512,kernel_size=3,padding=1),
nn.BatchNorm2d(512),
nn.ReLU(True),
#9
nn.Conv2d(512,512,kernel_size=3,padding=1),
nn.BatchNorm2d(512),
nn.ReLU(True),
#10
nn.Conv2d(512,512,kernel_size=3,padding=1),
nn.BatchNorm2d(512),
nn.ReLU(True),
nn.MaxPool2d(kernel_size=2,stride=2),
#11
nn.Conv2d(512,512,kernel_size=3,padding=1),
nn.BatchNorm2d(512),
nn.ReLU(True),
#12
nn.Conv2d(512,512,kernel_size=3,padding=1),
nn.BatchNorm2d(512),
nn.ReLU(True),
#13
nn.Conv2d(512,512,kernel_size=3,padding=1),
nn.BatchNorm2d(512),
nn.ReLU(True),
nn.MaxPool2d(kernel_size=2,stride=2),
nn.AvgPool2d(kernel_size=1,stride=1),
)
self.classifier = nn.Sequential(
#14
nn.Linear(512,4096),
nn.ReLU(True),
nn.Dropout(),
#15
nn.Linear(4096, 4096),
nn.ReLU(True),
nn.Dropout(),
#16
nn.Linear(4096,num_classes),
)
#self.classifier = nn.Linear(512, 10)
def forward(self, x):
out = self.features(x)
# print(out.shape)
out = out.view(out.size(0), -1)
# print(out.shape)
out = self.classifier(out)
# print(out.shape)
return out
'''创建model实例对象,并检测是否支持使用GPU'''
model = VGG16()
use_gpu = torch.cuda.is_available() # 判断是否有GPU加速
if use_gpu:
model = model.cuda()
'''定义loss和optimizer'''
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(model.parameters(), lr=learning_rate)
'''训练模型'''
for epoch in range(num_epoches):
print('*' * 25, 'epoch {}'.format(epoch + 1), '*' * 25) # .format为输出格式,formet括号里的即为左边花括号的输出
running_loss = 0.0
running_acc = 0.0
for i, data in tqdm(enumerate(train_loader, 1)):
img, label = data
# cuda
if use_gpu:
img = img.cuda()
label = label.cuda()
img = Variable(img)
label = Variable(label)
# 向前传播
out = model(img)
loss = criterion(out, label)
running_loss += loss.item() * label.size(0)
_, pred = torch.max(out, 1) # 预测最大值所在的位置标签
num_correct = (pred == label).sum()
accuracy = (pred == label).float().mean()
running_acc += num_correct.item()
# 向后传播
optimizer.zero_grad()
loss.backward()
optimizer.step()
print('Finish {} epoch, Loss: {:.6f}, Acc: {:.6f}'.format(
epoch + 1, running_loss / (len(train_dataset)), running_acc / (len(train_dataset))))
model.eval() # 模型评估
eval_loss = 0
eval_acc = 0
for data in test_loader: # 测试模型
img, label = data
if use_gpu:
img = Variable(img, volatile=True).cuda()
label = Variable(label, volatile=True).cuda()
else:
img = Variable(img, volatile=True)
label = Variable(label, volatile=True)
out = model(img)
loss = criterion(out, label)
eval_loss += loss.item() * label.size(0)
_, pred = torch.max(out, 1)
num_correct = (pred == label).sum()
eval_acc += num_correct.item()
print('Test Loss: {:.6f}, Acc: {:.6f}'.format(eval_loss / (len(
test_dataset)), eval_acc / (len(test_dataset))))
print()
# 保存模型
torch.save(model.state_dict(), './cnn.pth')
(2)代码详解:
该代码与我的上一篇博客关于LeNet
的代码结构和训练测试部分相同,具体可参看上一篇博客:Pytorch实现LeNet的手写数字识别 ,且上一篇博客详细介绍了pytorch
在神经网络的搭建、数据加载等方面的模块应用,因此本篇博客只介绍VGG
不同的地方:数据加载部分和网络定义部分。
1). 数据加载部分
本次是训练CIFAR10
数据集,Pytorch
的torchvision.datasets
包含CIFAR10
数据集,参照上一篇博客,故只需将数据加载改为CIFAR10
即可,其余不变。
代码:train_dataset = datasets.CIFAR10()
2). 网络定义部分
代码:
'''定义网络模型'''
class VGG16(nn.Module):
def __init__(self, num_classes=10):
super(VGG16, self).__init__()
self.features = nn.Sequential(
#1
nn.Conv2d(3,64,kernel_size=3,padding=1),
nn.BatchNorm2d(64),
nn.ReLU(True),
#2
nn.Conv2d(64,64,kernel_size=3,padding=1),
nn.BatchNorm2d(64),
nn.ReLU(True),
nn.MaxPool2d(kernel_size=2,stride=2),
#3
nn.Conv2d(64,128,kernel_size=3,padding=1),
nn.BatchNorm2d(128),
nn.ReLU(True),
#4
nn.Conv2d(128,128,kernel_size=3,padding=1),
nn.BatchNorm2d(128),
nn.ReLU(True),
nn.MaxPool2d(kernel_size=2,stride=2),
#5
nn.Conv2d(128,256,kernel_size=3,padding=1),
nn.BatchNorm2d(256),
nn.ReLU(True),
#6
nn.Conv2d(256,256,kernel_size=3,padding=1),
nn.BatchNorm2d(256),
nn.ReLU(True),
#7
nn.Conv2d(256,256,kernel_size=3,padding=1),
nn.BatchNorm2d(256),
nn.ReLU(True),
nn.MaxPool2d(kernel_size=2,stride=2),
#8
nn.Conv2d(256,512,kernel_size=3,padding=1),
nn.BatchNorm2d(512),
nn.ReLU(True),
#9
nn.Conv2d(512,512,kernel_size=3,padding=1),
nn.BatchNorm2d(512),
nn.ReLU(True),
#10
nn.Conv2d(512,512,kernel_size=3,padding=1),
nn.BatchNorm2d(512),
nn.ReLU(True),
nn.MaxPool2d(kernel_size=2,stride=2),
#11
nn.Conv2d(512,512,kernel_size=3,padding=1),
nn.BatchNorm2d(512),
nn.ReLU(True),
#12
nn.Conv2d(512,512,kernel_size=3,padding=1),
nn.BatchNorm2d(512),
nn.ReLU(True),
#13
nn.Conv2d(512,512,kernel_size=3,padding=1),
nn.BatchNorm2d(512),
nn.ReLU(True),
nn.MaxPool2d(kernel_size=2,stride=2),
nn.AvgPool2d(kernel_size=1,stride=1),
)
self.classifier = nn.Sequential(
#14
nn.Linear(512,4096),
nn.ReLU(True),
nn.Dropout(),
#15
nn.Linear(4096, 4096),
nn.ReLU(True),
nn.Dropout(),
#16
nn.Linear(4096,num_classes),
)
#self.classifier = nn.Linear(512, 10)
def forward(self, x):
out = self.features(x)
# print(out.shape)
out = out.view(out.size(0), -1)
# print(out.shape)
out = self.classifier(out)
# print(out.shape)
return out
VGG
的卷积核大小是3x3
,步长strd
是1
,而卷积不改变图像的大小,故填充padding
的大小是1
。池化用的是最大池化MaxPool
,大小是2x2
,故每卷积池化一次,图像大小要缩小一半。VGG
z中有5个池化层,故最后一个池化层的输出,即第一个全连接层的输入大小为原始图像大小的1/32
。
本代码中,在卷积后面加了一行代码:
nn.BatchNorm2d(out_channel)
批归一化操作,用于防止梯度消失或梯度爆炸,参数为卷积后输出的通道数。
全连接部分加入了一行代码:
nn.Dropout()
使用Dropout
防止过拟合。
模型训练部分和其他部分基本和上一篇博客相同,故不再分析。有一个不同的地方就是加了tqdm
的用法,该用法可以显示/打印出循环运行的进度。
3、关于LeNet
和VGG
的一些总结
1). 网络结构
LeNet
和VGG
这些传统的卷积神经网络的结构一般都是卷积层+全连接层,而卷积层则一般包括卷积(nn.conv2d
)、激活(nn.ReLU(True)
)和池化(一般为最大池化nn.MaxPool2d(ksize,stride)
),在卷积之后也可以加入批归一化(nn.BatchNorm2d(out_channel)
)。全连接一般有两-三层,第一层的输入为卷积层最终的输出,大小为卷积层最终输出的数据拉伸为一维向量的大小。
2). 代码结构
代码结构基本相同,基本分为以下几部分:
- 导入各种包
- 定义超参数
- 下载数据集
- 定义网络模型
- 定义损失函数和优化方式
- 训练模型
1). 初始化loss和accuracy
2). 前向传播
3). 反向传播
4). 测试模型
5). 打印每个epoch的loss和acc - 保存模型
不同的地方就是网络模型的定义部分,以及定义损失函数和优化方式的定义也有可能不同。对于不同的网络,其结构必然不同,需要重新定义,但其实也是大同小异。
3). 遇到的问题和解决
从LeNet
到VGG
,一直以来进入了一个误区,一直以为数据图像的大小要匹配/适应网络的输入大小。在LeNet
中,网络输入大小为32x32
,而MNIST
数据集中的图像大小为28x28
,当时认为要使两者的大小匹配,将padding
设置为2
即解决了这个问题。然而,当用VGG
训练CIFAR10
数据集时,网络输入大小为224x224
,而数据大小是32x32
,这两者该怎么匹配呢?试过将32
用padding
的方法填充到224x224
,但是运行之后显示内存不足(笑哭.jpg)。也百度到将数据图像resize
成224x224
。这个问题一直困扰了好久,看着代码里没有改动数据尺寸和网络的尺寸,不知道是怎么解决的这个匹配/适应的问题。最后一步步调试才发现在第一个全连接处报错,全连接的输入尺寸和设定的尺寸不一致,再回过头去一步步推数据的尺寸变化,发现原来的VGG
网络输入是224x224
的,由于卷积层不改变图像的大小,只有池化层才使图像大小缩小一半,所以经过5
层卷积池化之后,图像大小缩小为原来的1/32
。卷积层的最终输出是7x7x512=25088
,所以全连接层的输入设为25088
。当输入图像大小为32x32
时,经过5
层卷积之后,图像大小缩小为1x1x512
,全连接的输入大小就变为了512
,所以不匹配的地方在这里,而不是网络的输入处。所以输入的训练图像的大小不必要与网络原始的输入大小一致,只需要计算经过卷积池化后最终的输出(也即全连接层的输入),然后改以下全连接的输入即可。
参考
1、https://www.cnblogs.com/guoyaohua/p/8534077.html
2、https://blog.csdn.net/qq_36556893/article/details/86608963
3、https://github.com/Lornatang/pytorch/tree/master/research/MNIST/mnist
4、https://www.jianshu.com/p/86530a0a3935
5、https://blog.csdn.net/qq_39938666/article/details/84992336