一、简介
VGGNet由牛津大学的视觉几何组(Visual Geometry Group)提出,获得了2014年ILSVRC竞赛的分类任务第二名和定位任务第一名。主要贡献在于证明了使用3x3小卷积核,增加网络深度可以有效提升模型性能,并且对于其他数据集也有很好的泛化性能。VGGNet探索了卷积神经网络的深度与其性m能之间的关系,成功地构筑了16~19层深的卷积神经网络,证明了增加网络的深度能够在一定程度上影响网络最终的性能,使错误率大幅下降,同时拓展性又很强,迁移到其它图片数据上的泛化性也非常好,到目前为止,VGG仍然被用来提取图像特征。
论文链接 :Very deep convolutional networks for large-scale image recognition
二、网络结构
VGGNet一共有6种不同的网络结构,分别是A、A-LRN、B、C、D,这6种网络结构相似,都是由5层卷积层、3层全连接层组成,其中区别在于每个卷积层的子层数量不同,从A至E依次增加(子层数量从1到4),总的网络深度从11层到19层(添加的层以粗体显示),表格中的卷积层参数表示为conv<感受野大小>-<通道数>,例如con3-128,表示使用3x3的卷积核,通道数为128。为了简洁起见,在表格中不显示ReLU激活功能。其中,网络结构D就是著名的VGG16,网络结构E就是著名的VGG19。
以VGG16为例进行分析,其网络结构图如下:
输入是大小为224*224的RGB图像,预处理(preprocession)时计算出三个通道的平均值,在每个像素上减去平均值(处理后迭代更少,更快收敛)。
图像经过一系列卷积层处理,在卷积层中使用了非常小的3x3卷积核,在有些卷积层里则使用了1x1的卷积核。
卷积层步长(stride)设置为1个像素,3x3卷积层的填充(padding)设置为1个像素。池化层采用max pooling,共有5层,在一部分卷积层后,max-pooling的窗口是2x2,步长设置为2。
卷积层之后是三个全连接层(fully-connected layers,FC)。前两个全连接层均有4096个通道,第三个全连接层有1000个通道,用来分类。所有网络的全连接层配置相同。
全连接层后是Softmax,用来分类。
所有隐藏层(每个conv层中间)都使用ReLU作为激活函数。VGGNet不使用局部响应标准化(LRN),这种标准化并不能在ILSVRC数据集上提升性能,却导致更多的内存消耗和计算时间(LRN:Local Response Normalization,局部响应归一化,用于增强网络的泛化能力)。
VGG16详细处理过程如下:
1、输入224x224x3的图片,经64个3x3的卷积核作两次卷积+ReLU,卷积后的尺寸变为224x224x64
2、作max pooling(最大化池化),池化单元尺寸为2x2(效果为图像尺寸减半),池化后的尺寸变为112x112x64
3、经128个3x3的卷积核作两次卷积+ReLU,尺寸变为112x112x128
4、作2x2的max pooling池化,尺寸变为56x56x128
5、经256个3x3的卷积核作三次卷积+ReLU,尺寸变为56x56x256
6、作2x2的max pooling池化,尺寸变为28x28x256
7、经512个3x3的卷积核作三次卷积+ReLU,尺寸变为28x28x512
8、作2x2的max pooling池化,尺寸变为14x14x512
9、经512个3x3的卷积核作三次卷积+ReLU,尺寸变为14x14x512
10、作2x2的max pooling池化,尺寸变为7x7x512
11、与两层1x1x4096,一层1x1x1000进行全连接+ReLU(共三层)
12、通过softmax输出1000个预测结果
简化结构图如下:
三、VGGNet网络特点
1、结构简洁
VGG结构由5层卷积层、3层全连接层、softmax输出层构成,层与层之间使用max-pooling(最大池化)分开,所有隐层的激活单元都采用ReLU函数。
2、小卷积核和多卷积子层
VGG使用多个较小卷积核(3x3)的卷积层代替一个卷积核较大的卷积层,一方面可以减少参数,另一方面相当于进行了更多的非线性映射,可以增加网络的拟合/表达能力。
小卷积核是VGG的一个重要特点,虽然VGG是在模仿AlexNet的网络结构,但没有采用AlexNet中比较大的卷积核尺寸(如7x7),而是通过降低卷积核的大小(3x3),增加卷积子层数来达到同样的性能(VGG:从1到4卷积子层,AlexNet:1子层)。
VGG的作者认为两个3x3的卷积堆叠获得的感受野大小,相当一个5x5的卷积;而3个3x3卷积的堆叠获取到的感受野相当于一个7x7的卷积。这样可以增加非线性映射,也能很好地减少参数(例如7x7的参数为49个,而3个3x3的参数为27)
3、小池化核
相比AlexNet的3x3的池化核,VGG全部采用2x2的池化核。
4、通道数多
VGG网络第一层的通道数为64,后面每层都进行了翻倍,最多到512个通道,通道数的增加,使得更多的信息可以被提取出来。
5、层数更深、特征图更宽
由于卷积核专注于扩大通道数、池化专注于缩小宽和高,使得模型架构上更深更宽的同时,控制了计算量的增加规模。
6、全连接转卷积(测试阶段)
这也是VGG的一个特点,在网络测试阶段将训练阶段的三个全连接替换为三个卷积,使得测试得到的全卷积网络因为没有全连接的限制,因而可以接收任意宽或高为的输入,这在测试阶段很重要。
如输入图像是224x224x3,若后面三个层都是全连接,那么在测试阶段就只能将测试的图像全部都要缩放大小到224x224x3,才能符合后面全连接层的输入数量要求,这样就不便于测试工作的开展。
而“全连接转卷积”,替换过程如下:
例如7x7x512的层要跟4096个神经元的层做全连接,则替换为对7x7x512的层作通道数为4096、卷积核为1x1的卷积。
四、存在的问题
1、虽然 VGGNet 减少了卷积层参数,但实际上其参数空间比 AlexNet 大,其中绝大多数的参数都是来自于第一个全连接层,耗费更多计算资源。在随后的 NIN 中发现将这些全连接层替换为全局平均池化,对于性能影响不大,同时显著降低了参数数量。
2、采用 Pre-trained 方法训练的 VGG model(主要是 D 和 E),相对其他的方法参数空间很大,所以训练一个 VGG 模型通常要花费更长的时间,所幸有公开的 Pre-trained model 让我们很方便的使用。
五、Pytorch实现
#encoding=utf-8
import time
import torch
import torch.nn as nn
import torchvision
import torch.optim as optim
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
# 构造一个将句子展开的层
class FlattenLayer(nn.Module):
def __init__(self):
super(FlattenLayer, self).__init__()
def forward(self, x):
return x.view(x.shape[0], -1)
# 构造一个VGG网络的块
def vgg_block(num_convs,in_channels,out_channels):
vgg_blocks = []
for i in range(num_convs):
if i == 0:
vgg_blocks.append(nn.Conv2d(in_channels,out_channels,kernel_size=3,padding=1))
else:
vgg_blocks.append(nn.Conv2d(out_channels,out_channels,kernel_size=3,padding=1))
vgg_blocks.append(nn.ReLU())
vgg_blocks.append(nn.MaxPool2d(kernel_size=2,stride=2))
# * 表示对List进行解码
return nn.Sequential(*vgg_blocks)
# 下面构造完整的VGG网络
# 整个VGG由5个块组成
# 第1,2个块 是单个的VGG,输入—输出通道(1,64) (64,128)
# 第3,4,5块是 两个VGG构成,输入—输出通道 (128 256) (256 512) (512 512)
conv_arch = ((1,1,64),(1,64,128),(2,128,256),(2,256,512),(2,512,512))
input_features = 512 * 7 * 7
hidden_features = 4096
def vgg(conv_arch,input_features,hidden_features=4096):
net = nn.Sequential()
for i,(num_convs,in_channels,out_channels) in enumerate(conv_arch):
net.add_module('vgg_block_'+str(i),vgg_block(num_convs,in_channels,out_channels))
net.add_module('fc',nn.Sequential(FlattenLayer(),
nn.Linear(input_features,hidden_features),
nn.ReLU(),
nn.Dropout(0.5),
nn.Linear(hidden_features,hidden_features),
nn.ReLU(),
nn.Dropout(0.5),
nn.Linear(hidden_features,10)))
return net
#定义数据加载
def load_data(batch_size,resize=None,root='~/Datasets/FashionMNIST'):
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)
train_iter = torch.utils.data.DataLoader(mnist_train, batch_size=batch_size, shuffle=True, num_workers=4)
test_iter = torch.utils.data.DataLoader(mnist_test, batch_size=batch_size, shuffle=False, num_workers=4)
return train_iter,test_iter
#定义准确率估计
def evaluate_accuracy(data_iter,net,device=None):
if device is None and isinstance(net,torch.nn.Module):
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()
acc_sum += (net(X.to(device)).argmax(dim=1) == y.to(device)).float().sum().cpu().item()
net.train()
else:
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(net,train_iter,test_iter,batch_size,optimizer,device, num_epochs):
net = net.to(device)
print("training on ",device)
loss = torch.nn.CrossEntropyLoss()
for epoch in range(num_epochs):
train_loss_sum,train_acc_sum,n,batch_count,start = 0.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)
loss_value = loss(y_hat,y)
optimizer.zero_grad()
loss_value.backward()
optimizer.step()
train_loss_sum += loss_value
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, loss_value / batch_count, train_acc_sum / n, test_acc, time.time() - start))
#实例化
net = vgg(conv_arch,input_features,hidden_features)
#获取数据集
batch_size = 1
train_iter,test_iter = load_data(batch_size,resize=224)
# 定义超参数
lr = 0.001
num_epoches = 5
optimizer = torch.optim.Adam(net.parameters(), lr=lr)
if __name__ == '__main__':
train(net, train_iter, test_iter, batch_size, optimizer, device, num_epoches)