创新点:VGG块
1.模型结构
VGG块的组成规律是:连续使用数个相同的填充为1、窗口形状为 3x3的卷积层后接上一个步幅为 2、窗口形状为 2x2 的最大池化层。卷积层保持输入的高和宽不变,而池化层则对其减半。
VGG有两种结构,分别是VGG16(16指的是conv+fc的总层数是16,不包括max pool的层数!)和VGG19,两者并没有本质上的区别,只是网络深度不一样
VGG16:整个网络有5个vgg-block块和5个maxpool层逐个相连,然后进入FC层,直到最后1000路softmax输出
VGG都有相同的224×224×3的input+5个maxpool层+3层fc全连接层,区别在于中间的Vgg-block块的设计不同
- conv层: 输入为224×224×3,经过64个kernel size为3×3×3的filter,stride = 1,padding=same卷积后得到shape为224×224×64的block层(指由conv构成的vgg-block)。
- Max-pooling层: 输入为224×224×64,经过pool size=2,stride=2 减半池化后得到尺寸为112×112×64的池化层
- conv层: 输入尺寸为112×112×64,经128个3×3×64的filter卷积,得到112×112×128的block层。
- Max-pooling层:输入为112×112×128,经pool size = 2,stride = 2 减半池化后得到尺寸为56×56×128的池化层。
- conv层: 输入尺寸为56×56×128,经256个3×3×128的filter卷积,得到56×56×256的block层。
- Max-pooling层: 输入为56×56×256,经pool size = 2,stride = 2 减半池化后得到尺寸为28×28×256的池化层。
- conv层: 输入尺寸为28×28×256,经512个3×3×256的filter卷积,得到28×28×512的block层。
- Max-pooling层: 输入为28×28×512,经pool size = 2,stride = 2 减半池化后得到尺寸为14×14×512的池化层。
- conv层:输入尺寸为14×14×512,经512个3×3×512的filter卷积,得到14×14×512的block层。
- Max-pooling层: 输入为14×14×512,经pool size = 2,stride = 2 减半池化后得到尺寸为7×7×512的池化层。该层后面还隐藏了flatten操作,通过展平得到7×7×512=25088个参数后与之后的全连接层相连。
2.特点
1.vgg-block内的卷积层都是同结构的 意味着输入和输出的尺寸一样,且卷积层可以堆叠复用,通过统一的size为3×3的kernel size + stride1 + padding(same)实现。
2.maxpool层将前一层(vgg-block层)的特征缩减一半 使得尺寸缩减的很规整,从224-112-56-28-14-7。通过pool size2 + stride2实现
3.深度较深,参数量够大 较深的网络层数使得训练得到的模型分类效果优秀,但是较大的参数对训练和模型保存提出了更大的资源要求。
4.较小的filter size/kernel size **这里全局的kernel size都为3×3,相比以前的网络模型来说,尺寸足够小。
5.为了解决初始化(权重初始化)等问题,VGG采用的是一种Pre-training的方式,先训练浅层的的简单网络 VGG11,再复用 VGG11 的权重来初始化 VGG13,如此反复训练并初始化 VGG19,能够使训练时收敛的速度更快
3.改进:
- 相比AlexNet的一个改进是采用连续的几个3x3的卷积核代替AlexNet中的较大卷积核(11x11,7x7,5x5)使用了3个3x3卷积核来代替7x7卷积核,使用了2个3x3卷积核来代替5*5卷积核,这样做的主要目的是在保证具有相同感知野的条件下,提升了网络的深度,在一定程度上提升了神经网络的效果
- AlexNet相比,深度更深,参数更多(1.38亿),效果和可移植性更好。
- VGG的网络结构中(除一个特例外)都不包含AlexNet中提及的LRN(局部响应正则化),因为作者发现其并不能提高性能,反而会带来内存消耗和计算时间的增加
为什么使用2个3x3卷积核可以来代替5*5卷积核?
5x5卷积看做一个小的全连接网络在5x5区域滑动,可以先用一个3x3的卷积滤波器卷积,然后再用一个全连接层连接这个3x3卷积输出,这个全连接层也可以看做一个3x3卷积层。这样就可以用两个3x3卷积级联(叠加)起来代替一个 5x5卷积
为什么这里使用了filter size为3×3叠加卷积层的block?而不是直接用单层的7×7的卷积层?
1. 3个卷积层叠加,带来了3次relu的非线性校正,比使用一个relu的单层layer更具有识别力;
- 2. 降低了参数量,7×7×C×C 比3×3×C×C的参数量大81%。
4. 总结:
VGGNet通过在传统卷积神经网络模型(AlexNet)上的拓展,发现除了较为复杂的模型结构的设计外,深度对于提高模型准确率很重要
VGG和之前的AlexNet相比,深度更深,参数更多,效果和可移植性更好,且模型设计的简洁而规律,从而被广泛使用。
5.代码实现
import torch
from torch import nn, optim
import torchvision
import sys
from time import time
device = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')
print("using {} device.".format(device))
定义VGG块
'''定义能够指定卷积层的数量和输入输出通道数的VGG块'''
def vgg_block(num_convs, in_channels, out_channels):
blk = []
for i in range(num_convs):
if i==0:
blk.append(nn.Conv2d(in_channels, out_channels,kernel_size=3, padding=1))
else:
blk.append(nn.Conv2d(out_channels, out_channels,kernel_size=3, padding=1))
blk.append(nn.ReLU())
blk.append(nn.MaxPool2d(kernel_size=2, stride=2))
return nn.Sequential(*blk)
搭建VGG11模型
# 指定了每个VGG块里卷积层个数和输入输出通道数,前2块使用单卷积层,而后3块使用双卷积层
# 第一块的输入输出通道分别是1(因为下面要使用的Fashion-MNIST数据的通道数为1)和64
conv_arch = ((1, 1, 64), (1, 64, 128), (2, 128, 256),(2, 256, 512), (2, 512, 512))
fc_features = 512*7*7 #经过5个vgg_block,宽高会减半5次,变成224/2**5=224/32=7
fc_hidden_units = 4096
'''对 x 的形状转换 '''
class FlattenLayer(nn.Module):
def __init__(self):
super(FlattenLayer, self).__init__()
def forward(self, x):
return x.view(x.shape[0], -1)
def vgg11(conv_arch, fc_features, fc_hidden_units=4096):
net = nn.Sequential()
'''卷积层部分'''
for i, (num_convs, in_channels,out_channels) in enumerate(conv_arch):
net.add_module('vgg_block_' + str(i+1),vgg_block(num_convs, in_channels, out_channels))
'''全连接部分'''
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, 10)
))
return net
net = vgg11(conv_arch, fc_features, fc_hidden_units)
X = torch.rand(1, 1, 224, 224)
'''逐个遍历模型的子模块,对输入进行前向计算,并输出每个子模块的名称和输出形状'''
# named_children获取一级子模块及其名字
# named_modules会返回所有子模块,包括子模块的子模块
for name, blk in net.named_children():
X = blk(X)
print(name, 'output shape: ', X.shape)
测试
#构造一个通道数更小,或者说更窄的网络在Fashion-MNIST数据集上进行训练
ratio = 4
small_conv_arch = ((1, 1, 64//ratio),
(1, 64//ratio, 128//ratio),
(2, 128//ratio, 256//ratio),
(2, 256//ratio, 512//ratio),
(2, 512//ratio, 512//ratio))
net = vgg11(small_conv_arch, fc_features//ratio, fc_hidden_units//ratio)
print(net)
读取数据集
'''读取数据集'''
def load_data_fashion_mnist(batch_size, resize=None):
if sys.platform.startswith('win'):
num_workers = 0 # 0表示不用额外的进程来加速读取数据
else:
num_workers = 4
'''定义数据预处理的转换函数列表'''
trans = []
if resize: #判断是否需要进行图像尺寸调整(resize)
trans.append(torchvision.transforms.Resize(size=resize))
#将torchvision.transforms.Resize转换函数添加到转换函数列表trans中,并指定目标尺寸为resize
trans.append(torchvision.transforms.ToTensor())
# 将torchvision.transforms.ToTensor转换函数添加到转换函数列表trans中。这个函数用于将图像数据转换为张量,并且按照通道顺序排列(RGB)
transform = torchvision.transforms.Compose(trans)
#通过torchvision.transforms.Compose函数将转换函数列表trans组合成一个转换操作
mnist_train = torchvision.datasets.FashionMNIST(root='data/FashionMNIST',
train=True,
download=True,
transform=transform)
mnist_test = torchvision.datasets.FashionMNIST(root='data/FashionMNIST',
train=False,
download=True,
transform=transform)
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_train,
batch_size=batch_size,
shuffle=False,
num_workers=num_workers)
return train_iter, test_iter
'''确保计算使用的数据和模型同在内存或显存上'''
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()
for epoch in range(num_epochs):
# 在每一个迭代周期中,会使用训练集中所有样本一次(假设样本数能够被批量大小整除)
train_l_sum, train_acc_sum, n ,batch_count= 0.0, 0.0, 0,0
# 分别表示训练损失总和、训练准确度总和、样本数
start = time()
for X, y in train_iter:
X = X.to(device)
y = y.to(device)
'''前向传播'''
y_hat = net(X)
#由于变量 l 并不是一个标量,所以我们可以调用 .sum() 将其求和得到一个标量
'''计算损失'''
l = loss(y_hat, y).sum()
'''梯度清零'''
if optimizer is not None:
optimizer.zero_grad()
elif params is not None and params[0].grad is not None:
for param in params:
param.grad.data.zero_()
'''反向传播'''
l.backward() # 运行 l.backward() 得到该变量有关模型参数的梯度
if optimizer is None:
d2l.sgd(params, lr, batch_size)
else:
'''更新参数'''
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()-start))
训练模型
'''训练模型'''
batch_size = 256
train_iter, test_iter = load_data_fashion_mnist(batch_size, resize=224)
lr, num_epochs = 0.001, 5
optimizer = torch.optim.Adam(net.parameters(), lr=lr)
train_ch5(net, train_iter, test_iter, batch_size, optimizer,device, num_epochs)