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