論文為VERY DEEP CONVOLUTIONAL NETWORKS FOR LARGE-SCALE IMAGE RECOGNITION,主要討論了在大規模圖片識別中,卷積神經網絡的深度對准確率的影響。本篇論文提出的vgg網絡在2014年的ImageNet比賽中分別在定位和分類中獲得了第一和第二的成績。
改進創新點
VGGNet對2012年的AlexNet模型主要提出了兩種改進思路:
- 小卷積核(kernal size = 3x3)+小步幅(stride = 1)
- 多尺度:使用多尺度圖片來訓練和測試
卷積神經網絡設置
架構
結構特點:
- 整體結構上包含5組卷積層,卷積層不改變輸出圖片的尺寸。
- 每層包含有1-4個卷積操作,分別在論文中對應不同的VGG結構。卷積核大小為3x3,步幅為1。
- 每組卷積層后跟1個最大池化層,池化核大小為2x2,步幅為2,因此池化層會使得圖片尺寸縮小為原來一半。論文中共設計了5組池化層,因此圖片的尺寸會變為原來的1/32。
- 最后跟3層全連接層
對比試驗
論文中針對網絡深度、卷積核尺寸、LRN操作方面做了對比試驗,設計了6個VGG結構。如下圖所示。
為何使用3x3卷積核
- 2個3x3卷積層的感受野 = 1個5x5卷積層的感受野。同理,3個3x3卷積層感受野 = 1個7x7卷積層感受野。這樣加深了網絡,同時由於激活函數的加入,增加了網絡的非線性。
- 參數量減少。這里作者舉了一個例子,假設一個含有三層3x3卷積層堆疊的輸入和輸出都包含C個通道的網絡,權重數量為3(32C2)=27C2; 而一個7x7的卷積層,需要72C2=49C2個權重參數,相對增加了81%。
為何使用1x1卷積核
- 增加非線性,同時不影響感受野
- 調整維度輸出
訓練
- 初始化
batch size為256,學習率初始化為0.01,用包含動量的小批量梯度下降。
權重隨機初始化,從0均值和0.01方差的正態分布中取值。偏差初始化為0。 - 調整圖片尺寸
網絡輸入的圖片尺寸為224x224,因此必須調整圖片的尺寸。選取訓練圖像最小邊為S,若S=224,則不需要裁剪;若S>>224,裁剪圖像就會取圖像的一小部分。這樣選擇的圖片可以選取S>224的圖片,作為多尺寸輸入,只需要裁剪成224x224規格的圖片即可。下面的測試將會分別固定尺寸測試和多尺寸測試。
測試
測試主要針對上面的6鍾結構,然后加入了多尺寸輸入訓練以及測試。
測試的結果:
- 加入了LRN,沒有效果
- 從11層到19層,隨着層數的增大,錯誤率降低
- 結構C(包含3個1x1卷積層)比網絡B性能好,這意味着添加非線性層的確有用,但是使用卷積獲取空間上下文信息更有用(D比C好)
- 當深度達到19層時,錯誤率達到飽和
- 加入多尺寸訓練后,網絡抵抗尺寸波動的性能增強
結論
本文評估了深度卷積網絡(到19層)在大規模圖片分類中的應用。
結果表明,深度有益於提高分類的正確率,通過在傳統的卷積網絡框架中使用更深的層能夠在ImageNet數據集上取得優異的結果。
NOTE:
- 每個圖片或者特征圖的維數看作4維:樣本數 x 通道數c x 高h x 寬w
- 卷積層看作5維:樣本數 x 輸出通道數cout x 輸入通道數cin x 高h x 寬w
- 全連接層看作2個維度:樣本數 x (輸出通道數cout * 高h * 寬w)
Pytorch實現VGGNet
import torch
import time
from torch import nn, optim
import torchvision
import sys
#定義VGG各種不同的結構和最后的全連接層結構
cfg = {
'VGG11': [64, 'M', 128, 'M', 256,'M', 512, 'M', 512,'M'],
'VGG13': [64, 64, 'M', 128, 128, 'M', 256, 256, 'M', 512, 512, 'M', 512, 512, 'M'],
'VGG16': [64, 64, 'M', 128, 128, 'M', 256, 256, 256, 'M', 512, 512, 512, 'M', 512, 512, 512, 'M'],
'VGG19': [64, 64, 'M', 128, 128, 'M', 256, 256, 256, 256, 'M', 512, 512, 512, 512, 'M', 512, 512, 512, 512, 'M'],
'FC': [512*7*7, 4096, 10]
}
#將數據展開成二維數據,用在全連接層之前和卷積層之后
class FlattenLayer(torch.nn.Module):
def __init__(self):
super(FlattenLayer, self).__init__()
def forward(self, x): # x shape: (batch, *, *, ...)
return x.view(x.shape[0], -1)
class VGG(nn.Module):
# nn.Module是一個特殊的nn模塊,加載nn.Module,這是為了繼承父類
def __init__(self, vgg_name):
super(VGG, self).__init__()
# super 加載父類中的__init__()函數
self.VGG_layer = self.vgg_block(cfg[vgg_name])
self.FC_layer = self.fc_block(cfg['FC'])
#前向傳播算法
def forward(self, x):
out_vgg = self.VGG_layer(x)
out = out_vgg.view(out_vgg.size(0), -1)
# 這一步將out拉成out.size(0)的一維向量
out = self.FC_layer(out_vgg)
return out
#VGG模塊
def vgg_block(self, cfg_vgg):
layers = []
in_channels = 1
for out_channels in cfg_vgg:
if out_channels == 'M':
layers.append(nn.MaxPool2d(kernel_size=2, stride=2))
else:
layers.append(nn.Conv2d(in_channels, out_channels, kernel_size=3,padding=1, bias=False))
layers.append(nn.BatchNorm2d(out_channels))
layers.append(nn.ReLU(inplace=True))
in_channels = out_channels
return nn.Sequential(*layers)
#全連接模塊
def fc_block(self, cfg_fc):
fc_net = nn.Sequential()
fc_features, fc_hidden_units, fc_output_units = cfg_fc[0:]
fc_net.add_module("fc", nn.Sequential(
FlattenLayer(),
nn.Linear(fc_features, fc_hidden_units),
nn.ReLU(),
nn.Dropout(0.5),
nn.Linear(fc_hidden_units, fc_hidden_units),
nn.ReLU(),
nn.Dropout(0.5),
nn.Linear(fc_hidden_units, fc_output_units)
))
return fc_net
#加載MNIST數據,返回訓練數據集和測試數據集
def load_data_fashion_mnist(batch_size, resize=None, root='~/chnn/Datasets/FashionMNIST'):
"""Download the fashion mnist dataset and then load into memory."""
trans = []
if resize:
trans.append(torchvision.transforms.Resize(size=resize))
trans.append(torchvision.transforms.ToTensor())
transform = torchvision.transforms.Compose(trans)
mnist_train = torchvision.datasets.FashionMNIST(root=root, train=True, download=True, transform=transform)
mnist_test = torchvision.datasets.FashionMNIST(root=root, train=False, download=True, transform=transform)
if sys.platform.startswith('win'):
num_workers = 0 # 0表示不用額外的進程來加速讀取數據
else:
num_workers = 4
train_iter = torch.utils.data.DataLoader(mnist_train, batch_size=batch_size, shuffle=True, num_workers=num_workers)
test_iter = torch.utils.data.DataLoader(mnist_test, batch_size=batch_size, shuffle=False, num_workers=num_workers)
return train_iter, test_iter
#測試准確率
def evaluate_accuracy(data_iter, net, device=None):
if device is None and isinstance(net, torch.nn.Module):
# 如果沒指定device就使用net的device
device = list(net.parameters())[0].device
acc_sum, n = 0.0, 0
with torch.no_grad():
for X, y in data_iter:
if isinstance(net, torch.nn.Module):
net.eval() # 評估模式, 這會關閉dropout
acc_sum += (net(X.to(device)).argmax(dim=1) == y.to(device)).float().sum().cpu().item()
net.train() # 改回訓練模式
else: # 自定義的模型, 3.13節之后不會用到, 不考慮GPU
if('is_training' in net.__code__.co_varnames): # 如果有is_training這個參數
# 將is_training設置成False
acc_sum += (net(X, is_training=False).argmax(dim=1) == y).float().sum().item()
else:
acc_sum += (net(X).argmax(dim=1) == y).float().sum().item()
n += y.shape[0]
return acc_sum / n
#模型訓練,定義損失函數、優化函數
def train_ch5(net, train_iter, test_iter, batch_size, optimizer, device, num_epochs):
net = net.to(device)
print("training on ", device)
loss = torch.nn.CrossEntropyLoss()
batch_count = 0
for epoch in range(num_epochs):
train_l_sum, train_acc_sum, n, start = 0.0, 0.0, 0, time.time()
for X, y in train_iter:
X = X.to(device)
y = y.to(device)
y_hat = net(X)
l = loss(y_hat, y)
optimizer.zero_grad()
l.backward()
optimizer.step()
train_l_sum += l.cpu().item()
train_acc_sum += (y_hat.argmax(dim=1) == y).sum().cpu().item()
n += y.shape[0]
batch_count += 1
test_acc = evaluate_accuracy(test_iter, net)
print('epoch %d, loss %.4f, train acc %.3f, test acc %.3f, time %.1f sec'
% (epoch + 1, train_l_sum / batch_count, train_acc_sum / n, test_acc, time.time() - start))
def main():
net = VGG('VGG16')
print(net)
#一個batch_size為64張圖片,進行梯度下降更新參數
batch_size = 64
#使用cuda來訓練
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
#加載MNIST數據集,返回訓練集和測試集
train_iter, test_iter = load_data_fashion_mnist(batch_size, resize=224)
lr, num_epochs = 0.001, 5
#使用Adam優化算法替代傳統的SGD,能夠自適應學習率
optimizer = torch.optim.Adam(net.parameters(), lr=lr)
#訓練--迭代更新參數
train_ch5(net, train_iter, test_iter, batch_size, optimizer, device, num_epochs)
main()
NOTE:
程序中使用MNIST數據集,pytorch打印的網絡結構為:
訓練結果為:
因為使用原文結構參數量太大,造成顯存爆滿,於是將結構中的通道數變為1/8。訓練結果中,迭代了5次后,訓練集精確度提高,但測試集精度結果不是很理想。
已經將代碼上傳到GitHub:https://github.com/chnngege/vgg-pytorch