文章目录
一、网络结构
VGG16网络的基本结构如下:
一个VGG_block的组成有如下特点:
- 带填充以保持分辨率的卷积层:指对输入特征图卷积操作时会带有填充,使得只改变通道数而不改变图像高、宽。
- 非线性激活函数ReLU:卷积操作后将特征图输入激活函数,提供使之具有非线性性。
- 最大池化层:使用最大池化函数,不改变图像通道数,但会缩小图像尺寸。
可归纳为:
卷积层(+relu激活函数)
卷积层(+relu激活函数)
最大池化层
卷积层(+relu激活函数)
卷积层(+relu激活函数)
最大池化层
卷积层(+relu激活函数)
卷积层(+relu激活函数)
卷积层(+relu激活函数)
最大池化层
卷积层(+relu激活函数)
卷积层(+relu激活函数)
卷积层(+relu激活函数)
最大池化层
卷积层(+relu激活函数)
卷积层(+relu激活函数)
卷积层(+relu激活函数)
最大池化层
全连接层(+relu激活函数)
全连接层(+relu激活函数)
全连接层
【第一层:卷积层】
前置知识:若输入图像大小为NxN,卷积核大小为FxF,若不填充而直接进行卷积操作,则输出图像大小为:
( N − F + 1 ) x ( N − F + 1 ) (N-F+1)x(N-F+1) (N−F+1)x(N−F+1)
而若在原始图像周围填充P个像素,此时图像大小为(N+2P)x(N+2P),则卷积后输出图像大小为:
( N + 2 P − F + 1 ) x ( N + 2 P − F + 1 ) (N+2P-F+1)x(N+2P-F+1) (N+2P−F+1)x(N+2P−F+1)
输入图像大小为 ( 224 , 224 , 3 ) (224,224,3) (224,224,3),使用了64个3x3大小的卷积核进行卷积,若不进行填充(padding),则输出图像大小应为 ( 222 , 222 , 64 ) (222,222,64) (222,222,64)。而图中给出输出图像大小为 ( 224 , 224 , 64 ) (224,224,64) (224,224,64),说明进行了填充操作,且padding=1。这就使得输出图像与原始图像大小一样,保证了图像大小的一致性。VGG16网络第一层代码:
py
self.conv1 = nn.Conv2d(3, 64, kernel_size=3, padding=1)
self.relu1 = nn.ReLU(inplace=True)#激活函数不会改变数据维度
【第二层:卷积层】
第二层中,输入图像大小为 ( 224 , 224 , 64 ) (224,224,64) (224,224,64),输出图像大小为 ( 224 , 224 , 64 ) (224,224,64) (224,224,64),同样需要填充padding=1。VGG网络第二层代码为:
py
self.conv2 = nn.Conv2d(64, 64, kernel_size=3, padding=1)
self.relu2 = nn.ReLU(inplace=True)
【池化层】
VGG网络的池化层采用最大池化操作,即,通过在每个池化窗口中选择像素大小最大值来减小特征图的尺寸。最大池化层通常用于减少特征图的空间维度,从而降低模型的计算量,同时保留重要的特征。VGG16池化核大小为2x2,若步长为1,即以1为间隔平移。则输出数据尺寸应为:224224 64->(224-2+1)(224-2+1)64也就是(223223 64)。而若步长为2,则刚好可使输出图像大小减半,即为 ( 112 , 112 , 64 ) (112,112,64) (112,112,64),代码:
py
self.max_pooling1 = nn.MaxPool2d(kernel_size=2, stride=2)
总结每一次操作后数据尺度的变化:
层 | 输入尺寸 | 输出尺寸 |
---|---|---|
卷积层1 | ( 224 , 224 , 3 ) (224,224,3) (224,224,3) | ( 224 , 224 , 64 ) (224,224,64) (224,224,64) |
卷积层2 | ( 224 , 224 , 64 ) (224,224,64) (224,224,64) | ( 224 , 224 , 64 ) (224,224,64) (224,224,64) |
池化层 | ( 224 , 224 , 64 ) (224,224,64) (224,224,64) | ( 112 , 112 , 64 ) (112,112,64) (112,112,64) |
卷积层3 | ( 112 , 112 , 64 ) (112,112,64) (112,112,64) | ( 112 , 112 , 128 ) (112,112,128) (112,112,128) |
卷积层4 | ( 112 , 112 , 128 ) (112,112,128) (112,112,128) | ( 112 , 112 , 128 ) (112,112,128) (112,112,128) |
池化层 | ( 112 , 112 , 128 ) (112,112,128) (112,112,128) | ( 56 , 56 , 128 ) (56,56,128) (56,56,128) |
卷积层5 | ( 56 , 56 , 128 ) (56,56,128) (56,56,128) | ( 56 , 56 , 256 ) (56,56,256) (56,56,256) |
卷积层6 | ( 56 , 56 , 256 ) (56,56,256) (56,56,256) | ( 56 , 56 , 256 ) (56,56,256) (56,56,256) |
卷积层7 | ( 56 , 56 , 256 ) (56,56,256) (56,56,256) | ( 56 , 56 , 256 ) (56,56,256) (56,56,256) |
池化层 | ( 56 , 56 , 256 ) (56,56,256) (56,56,256) | ( 28 , 28 , 256 ) (28,28,256) (28,28,256) |
卷积层8 | ( 28 , 28 , 256 ) (28,28,256) (28,28,256) | ( 28 , 28 , 512 ) (28,28,512) (28,28,512) |
卷积层9 | ( 28 , 28 , 256 ) (28,28,256) (28,28,256) | ( 28 , 28 , 512 ) (28,28,512) (28,28,512) |
卷积层10 | ( 28 , 28 , 256 ) (28,28,256) (28,28,256) | ( 28 , 28 , 512 ) (28,28,512) (28,28,512) |
池化层 | ( 28 , 28 , 512 ) (28,28,512) (28,28,512) | ( 14 , 14 , 512 ) (14,14,512) (14,14,512) |
卷积层11 | ( 14 , 14 , 512 ) (14,14,512) (14,14,512) | ( 14 , 14 , 512 ) (14,14,512) (14,14,512) |
卷积层12 | ( 14 , 14 , 512 ) (14,14,512) (14,14,512) | ( 14 , 14 , 512 ) (14,14,512) (14,14,512) |
卷积层13 | ( 14 , 14 , 512 ) (14,14,512) (14,14,512) | ( 14 , 14 , 512 ) (14,14,512) (14,14,512) |
池化层 | ( 14 , 14 , 512 ) (14,14,512) (14,14,512) | ( 7 , 7 , 512 ) (7,7,512) (7,7,512) |
全连接层1 | ( 7 , 7 , 512 ) (7,7,512) (7,7,512) | 4096 4096 4096 |
全连接层2 | 4096 4096 4096 | 4096 4096 4096 |
全连接层3 | 4096 4096 4096 | 1000 1000 1000 |
二、代码实现
代码实现:
py
import torch
import torch.nn as nn
import numpy as np
# 定义VGG16网络类
class VGG16(nn.Module):
def __init__(self):
super(VGG16, self).__init__()
# 卷积层部分
self.conv1 = nn.Conv2d(3, 64, kernel_size=3, padding=1)
self.relu1 = nn.ReLU(inplace=True)
self.conv2 = nn.Conv2d(64, 64, kernel_size=3, padding=1)
self.relu2 = nn.ReLU(inplace=True)
self.max_pooling1 = nn.MaxPool2d(kernel_size=2, stride=2)
self.conv3 = nn.Conv2d(64, 128, kernel_size=3, padding=1)
self.relu3 = nn.ReLU(inplace=True)
self.conv4 = nn.Conv2d(128, 128, kernel_size=3, padding=1)
self.relu4 = nn.ReLU(inplace=True)
self.max_pooling2 = nn.MaxPool2d(kernel_size=2, stride=2)
self.conv5 = nn.Conv2d(128, 256, kernel_size=3, padding=1)
self.relu5 = nn.ReLU(inplace=True)
self.conv6 = nn.Conv2d(256, 256, kernel_size=3, padding=1)
self.relu6 = nn.ReLU(inplace=True)
self.conv7 = nn.Conv2d(256, 256, kernel_size=3, padding=1)
self.relu7 = nn.ReLU(inplace=True)
self.max_pooling3 = nn.MaxPool2d(kernel_size=2, stride=2)
self.conv8 = nn.Conv2d(256, 512, kernel_size=3, padding=1)
self.relu8 = nn.ReLU(inplace=True)
self.conv9 = nn.Conv2d(512, 512, kernel_size=3, padding=1)
self.relu9 = nn.ReLU(inplace=True)
self.conv10 = nn.Conv2d(512, 512, kernel_size=3, padding=1)
self.relu10 = nn.ReLU(inplace=True)
self.max_pooling4 = nn.MaxPool2d(kernel_size=2, stride=2)
self.conv11 = nn.Conv2d(512, 512, kernel_size=3, padding=1)
self.relu11 = nn.ReLU(inplace=True)
self.conv12 = nn.Conv2d(512, 512, kernel_size=3, padding=1)
self.relu12 = nn.ReLU(inplace=True)
self.conv13 = nn.Conv2d(512, 512, kernel_size=3, padding=1)
self.relu13 = nn.ReLU(inplace=True)
self.max_pooling5 = nn.MaxPool2d(kernel_size=2, stride=2)
# 全连接层部分
self.fc1 = nn.Linear(512 * 7 * 7, 4096)
self.relu14 = nn.ReLU(inplace=True)
self.fc2 = nn.Linear(4096, 4096)
self.relu15 = nn.ReLU(inplace=True)
self.dropout = nn.Dropout()#正则化,防止过拟合
self.fc3 = nn.Linear(4096, 1000)
# 前向传播函数
def forward(self, x):
x = self.conv1(x)
x = self.relu1(x)
x = self.conv2(x)
x = self.relu2(x)
x = self.max_pooling1(x)
x = self.conv3(x)
x = self.relu3(x)
x = self.conv4(x)
x = self.relu4(x)
x = self.max_pooling2(x)
x = self.conv5(x)
x = self.relu5(x)
x = self.conv6(x)
x = self.relu6(x)
x = self.conv7(x)
x = self.relu7(x)
x = self.max_pooling3(x)
x = self.conv8(x)
x = self.relu8(x)
x = self.conv9(x)
x = self.relu9(x)
x = self.conv10(x)
x = self.relu10(x)
x = self.max_pooling4(x)
x = self.conv11(x)
x = self.relu11(x)
x = self.conv12(x)
x = self.relu12(x)
x = self.conv13(x)
x = self.relu13(x)
x = self.max_pooling5(x)
print(x.shape)
x = x.view(-1, 512*7*7)
print(x.shape)
x = self.fc1(x)
x = self.relu14(x)
x = self.fc2(x)
x = self.relu15(x)
x = self.fc3(x)
return x
- 卷积层:
kernel_size=3, padding=1
可使图片大小不会改变。 - 最大池化层:
kernel_size=2, stride=2
可使得图片宽高减半。 - 全连接层:设输入张量形状为 ( B , C , H , W ) (B,C,H,W) (B,C,H,W),其中 B B B表示批量大小、 C C C表示通道数、 H H H表示高度、 W W W表示宽度。执行
x = x.view(-1, 512*7*7)
可将 ( B , 7 , 7 , 512 ) (B,7,7,512) (B,7,7,512)的数据张量展平为 ( B , 7 ∗ 7 ∗ 512 ) (B,7*7*512) (B,7∗7∗512),即保持批次大小不变,将特征图数据展平为一维,之后再执行x = self.fc1(x)
,即将数据输入全连接层参与运算。
也可使用torch.nn.Sequential()
简化网络的写法,并且将类别数作为参数传入网络模型中:
py
import torch
import torch.nn as nn
import numpy as np
# 定义VGG16网络类
class VGG16(nn.Module):
def __init__(self, num_classes=1000):
super(VGG16, self).__init__()
# 卷积层部分
self.features = nn.Sequential(
nn.Conv2d(3, 64, kernel_size=3, padding=1),
nn.ReLU(inplace=True),
nn.Conv2d(64, 64, kernel_size=3, padding=1),
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=2, stride=2),
nn.Conv2d(64, 128, kernel_size=3, padding=1),
nn.ReLU(inplace=True),
nn.Conv2d(128, 128, kernel_size=3, padding=1),
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=2, stride=2),
nn.Conv2d(128, 256, kernel_size=3, padding=1),
nn.ReLU(inplace=True),
nn.Conv2d(256, 256, kernel_size=3, padding=1),
nn.ReLU(inplace=True),
nn.Conv2d(256, 256, kernel_size=3, padding=1),
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=2, stride=2),
nn.Conv2d(256, 512, kernel_size=3, padding=1),
nn.ReLU(inplace=True),
nn.Conv2d(512, 512, kernel_size=3, padding=1),
nn.ReLU(inplace=True),
nn.Conv2d(512, 512, kernel_size=3, padding=1),
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=2, stride=2),
nn.Conv2d(512, 512, kernel_size=3, padding=1),
nn.ReLU(inplace=True),
nn.Conv2d(512, 512, kernel_size=3, padding=1),
nn.ReLU(inplace=True),
nn.Conv2d(512, 512, kernel_size=3, padding=1),
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=2, stride=2),
)
# 全连接层部分
self.classifier = nn.Sequential(
nn.Linear(512 * 7 * 7, 4096),
nn.ReLU(inplace=True),
nn.Dropout(),
nn.Linear(4096, 4096),
nn.ReLU(inplace=True),
nn.Dropout(),
nn.Linear(4096, num_classes),
)
# 前向传播函数
def forward(self, x):
x = self.features(x)
x = torch.flatten(x, 1)
x = self.classifier(x)
return x
三、模型总结
VGG是为ImageNet分类挑战训练的,这是一个带有1000个类的对象识别问题,最后的全连接层(4096x1000)为每个输入图像输出一个长度为1000的向量,softmax层将这个长度为1000的向量转换为1000个类。
从网络结构中可看出,VGG均全部使用3×3大小、步长为1的小卷积核,3×3卷积核同时也是最小的能够表示上下左右中心的尺寸。3x3卷积核卷积过程如下:
假设输入图像尺寸为假输入为5×5,使用2次3×3卷积后最终得到1×1的特征图,这和直接使用一个5×5卷积核得到1×1的特征图是一样的。也就是说2次3×3卷积可以代替一次5×5卷积,并且,2次3×3卷积的参数更少(2×3×3=18<5×5=25),而且会经过两次激活函数进行非线性变换,学习能力会更好。同样的3次3×3卷积可以替代一次7×7的卷积等等。除此之外,步长为1可以不会丢失信息,网络深度增加可以提高网络性能。
在网络结构中还使用了Dropout
,这是一种提高深度学习泛化能力的方法,它将连接到网络中某一百分比节点的权重设置为0。VGG16在两个dropout层中将百分比设为0.5。