深度学习入门教程二:VGG网络的搭建与训练

在前面,我们已经完成Conda的安装、Pytorch环境的搭建,现在就来搭建VGG这个经典的CNN卷积神经网络。

论文简读

还是先来看一下VGG的原论文,VGG这篇论文引用量达到了11万,是一篇传神之作,VGG搭建了第一个具有真正足够“深”的深度学习模型,在上一篇Alexnet中,深度可能也就5层卷积神经网络,但是VGG达到11层,甚至最深达到19层,这个深度是在当时非常深的,而且也需要足够强大的GPU和庞大的数据集才能进行训练。

 先简单地阅读论文的内容

1、摘要

这篇文章的主要贡献在于使用具有非常小(3×3)卷积滤波器的架构,对逐渐增加深度的网络进行全面评估。研究结果表明,在将深度推至16至19个权重层时,相较于先前的最先进配置,取得了显著的改进。

2、引言

先是回顾前人在这个领域所做的工作,然后提出本文的工作和贡献:探讨了ConvNet架构设计的另一个重要方面——深度。为此,我们固定了架构的其他参数,并通过增加更多卷积层的方式稳步增加网络的深度,这得益于在所有层中使用非常小的(3×3)卷积滤波器。结果,我们提出了显著更准确的ConvNet架构,不仅在ILSVRC分类和定位任务上达到了最先进的准确性,而且在应用于其他图像识别数据集时,即使作为相对简单流程的一部分(例如,由线性SVM分类的深度特征而无需微调),它们也能取得出色的性能。我们已经发布了我们性能最佳的两个模型,以促进进一步的研究。

3、总结

这项工作中,作者评估了用于大规模图像分类的非常深的卷积网络(多达19个权重层)。结果表明,对于分类准确性,表示深度是有益的,并且可以使用传统的ConvNet架构(LeCun等人,1989年;Krizhevsky等人,2012年)实现在ImageNet挑战数据集上的最先进性能,尽管深度显著增加。在附录中,我们还展示了我们的模型对各种任务和数据集的良好泛化性能,与围绕较浅的图像表示构建的更复杂的识别流程相匹敌或胜过。实验的结果再次证实了视觉表示中深度的重要性。

4、模型的结构

网络的输入是一个固定大小的224 × 224 RGB图像。作者唯一进行的预处理是从每个像素中减去在训练集上计算的平均RGB值。图像通过一系列卷积(conv.)层,其中我们使用具有非常小的感受野的滤波器:3 × 3(这是捕获左/右、上/下、中心概念的最小大小)。在其中一种配置中,我们还使用了1 × 1卷积滤波器,可以看作是输入通道的线性变换(后跟非线性)。

卷积步幅固定为1像素;卷积层输入的空间填充设计为在卷积后保留空间分辨率,即对于3 × 3卷积层,填充为1像素。空间池化由五个最大池化层执行,这些层跟随一些卷积层(并非所有卷积层都跟随最大池化)。最大池化在2 × 2像素窗口上执行,步幅为2。

一系列卷积层(在不同架构中具有不同深度)后跟三个全连接(FC)层:前两个每个有4096个通道,第三个执行1000路ILSVRC分类(就是作者使用的ILSVRC数据集有1000个类别,如果要使用其他的类别数,可以在后面添加线性层,详情可以阅读模型代码),因此包含1000个通道(每个类别一个通道)。

最后一层是softmax层。所有隐藏层都配备有修正线性单元(ReLU)非线性函数。 我们注意到,除了一个网络之外,我们的所有网络都不包含局部响应规范化(LRN)。作者认为这种规范化并不会提高ILSVRC数据集上的性能,但会导致内存消耗和计算时间增加。在适用的情况下,LRN层的参数与中的参数相同。

那么VGG的几种模型可以用下表表示:依次分别为VGG11,VGG11-LRN,VGG13,VGG16,VGG19,这几个模型都不通过卷积层进行下采样,也就是里面的卷积层都会保持输入尺寸的大小,都是通过最大池化进行下采样。

作者这模型对比中提到,VGG网络没有在第一个卷积层中使用相对较大的感受野(例如在(Krizhevsky等人,2012年)中使用11×11的步幅4,或在(Zeiler&Fergus,2013年;Sermanet等人,2014年)中使用7×7的步幅2),而是在整个网络中使用了非常小的3×3感受野,这些感受野在每个像素处与输入进行卷积(步幅为1)。作者提出,两个3×3卷积层的堆叠(之间没有空间池化)具有有效感受野为5×5;三个这样的层具有7×7的有效感受野。那么,例如使用三个3×3卷积层而不是单个7×7层,模型得到了什么好处呢?首先,VGG包含了三个非线性矫正层而不是一个,这使决策函数更有区分度。其次,减少了参数的数量:假设三层3×3卷积堆叠的输入和输出都具有C个通道,则该堆叠由3 × 3 (22个权重层)和小的卷积滤波器(除了3 × 3外,它们还使用1 × 1和5 × 5卷积)。然而,先前的网络拓扑结构比VGG的更复杂,并且为了减少计算量,第一层的特征图的空间分辨率在前几层中被更激进地减小。

在这里作者也提出本论文一个非常重要的创新点——就是使用多个小核卷积代替一个大核卷积,既能直接减少计算量,又能提升特征的提取效率。在当时GPU运算效率不高的情况下,VGG这个观点的提出,也是跨时代的作品,直接加速深度学习领域的发展。

5、模型的训练

训练是通过使用小批量梯度下降来优化多项式 logistic 回归目标完成的,其中动量为0.9,Batch size大小设置为256。通过权重衰减(L2惩罚乘数设置为5×10^-4)和dropout正则化对前两个全连接层进行正则化(dropout比率设置为0.5)。学习速率最初设置为10^-2,然后在验证集准确性不再提高时减小10倍。

网络权重的初始化很重要,初始化位置不好可能会导致深度网络中梯度不稳定而停滞学习。作者为了解决这个问题,我们首先使用随机初始化对配置A进行训练,这个配置足够浅以进行随机初始化的训练。然后,当训练更深层次的体系结构时,我们使用net A的前四个卷积层和最后三个全连接层来初始化这些层(中间层被随机初始化)。我们没有减小预初始化层的学习率,允许它们在学习过程中发生变化。对于随机初始化(如果适用),我们从均值为零、方差为10^-2的正态分布中采样权重。偏置以零初始化。(在初始化这一步,由于VGG是一个比较大的模型,直接训练的话需要足够大的数据集和比较长的时间,而且效果也不一定会好,一般是使用预训练权重进行训练,能缩短训练时间而且一般使用预训练模型训练效果往往会比较好,因此使用预训练模型的话,就没必要对整个网络权重进行初始化,只需要对某个层进行初始化就OK了)

下表是VGG的几种模型在ILSVRC数据集上的表现,从上往下分别是VGG11,VGG11-LRN,VGG13,VGG16,VGG19

下图是VGG模型在不同尺度的训练和测试的表现

 下表是各个不同模型在ILSVRC数据集上的做分类任务的表现,可以发现VGG这个足够“深”的模型表现出的性能是最强大的。

VGG模型复现

VGG网络是第一个搭建了在当时非常深的神经网络,因为特征提取非常强,在其他任务中许多网络会采用VGG网络作为主干网络用于特征提取。

VGG的网络架构如下:

如图所示,一张大小224x224,通道为3的图片,经过五次卷积和最大池化下采样,生成特征图,然后再展平,再通过线性层和softmax层输出分类的1000个类别。

详细模型代码如下:

import torch
import torch.nn as nn
import torch.nn.functional as F

class VGG(nn.Module):
    def __init__(self, layer: object, use_BN: bool,num_classes: int=1000,) -> object:
        # 接受的layer为object类型,返回的数据也为object类型
        super(VGG, self).__init__()
        self.use_BN = use_BN
        self.layer_par = layer
        self.in_channels = 3  # 默认输入为RGB图像
        self.max_pool = nn.MaxPool2d(kernel_size=2, stride=2)
        self.conv3_64 = self.make_layer(64, layer[0])    # output size [BS,64,112,112]
        self.conv3_128 = self.make_layer(128, layer[1])  # output size [BS,128,56,56]
        self.conv3_256 = self.make_layer(256, layer[2])  # output size [BS,256,28,28]
        self.conv3_512_l = self.make_layer(512, layer[3])# output size [BS,512,14,14]
        self.conv3_512_s = self.make_layer(512, layer[4])# output size [BS,512,7,7]
        # 前面为特征提取,生成8*8的特征图,下面为分类头
        self.fc_head = nn.Sequential(
            nn.Flatten(),
            nn.Linear(512*7*7,4096),
            nn.ReLU(True),
            nn.Dropout(p=0.5),
            nn.Linear(4096, 4096),
            nn.ReLU(True),
            nn.Dropout(p=0.5),
            nn.Linear(4096,num_classes),
        )


    def forward(self, x):

        output = self.conv3_64(x)
        output = self.max_pool(output)

        output = self.conv3_128(output)
        output = self.max_pool(output)

        output = self.conv3_256(output)
        output = self.max_pool(output)

        output = self.conv3_512_l(output)
        output = self.max_pool(output)

        output = self.conv3_512_s(output)
        output = self.max_pool(output)

        output = self.fc_head(output)
        return output

    def make_layer(self, channels, param):
        layers = []
        for i in range(param):
            if self.use_BN:
                layers.append(nn.Conv2d(self.in_channels, channels, 3, stride=1, padding=1, bias=False))  # 使用BN不需要b
                layers.append(nn.BatchNorm2d(channels))
                layers.append(nn.ReLU(inplace=False))
                self.in_channels = channels  # 下一层输入通道数为上一层输出通道数
            else:
                layers.append(nn.Conv2d(self.in_channels, channels, 3, stride=1, padding=1))
                layers.append(nn.ReLU(inplace=False))
                self.in_channels = channels  # 下一层输入通道数为上一层输出通道数
        return nn.Sequential(*layers)

    def load_dict_from_pre(self, path):
        # 加载训练模型权重(重要!必须!)
        vgg_state_dict = torch.load(path)
        vgg_name = []
        for name, par in vgg_state_dict.items():
            vgg_name.append(name)
        own_state_dict = self.state_dict()
        for i, [name, param] in enumerate(own_state_dict.items(), 0):
            param.copy_(vgg_state_dict[vgg_name[i]])
            # print('Successfully copied parameters: ', name)

    def _initialize_weights(self):
        for m in self.modules():
            if isinstance(m, nn.Conv2d):
                nn.init.normal_(m.weight, 0, 0.01)
                if m.bias is not None:
                    m.bias.data.zero_()


def make_VGG11(path, use_BN: bool,num_classes):
    net = VGG([1, 1, 2, 2, 2], use_BN)
    net.load_dict_from_pre(path)
    return net


def make_VGG13(path, use_BN: bool,num_classes):
    net = VGG([2, 2, 2, 2, 2], use_BN)
    net.load_dict_from_pre(path)
    return net


def make_VGG16(path, use_BN: bool,num_classes):
    net = VGG([2, 2, 3, 3, 3], use_BN)
    net.load_dict_from_pre(path)# 这一行是使用预训练权重,不使用预训练权重可以注释
    return net


def make_VGG19(path, use_BN: bool,num_classes):
    net = VGG([2, 2, 4, 4, 4], use_BN)
    net.load_dict_from_pre(path)
    return net

在模型训练,我采用的是CIFAR10数据集,使用VGG16模型,并且使用了VGG16的预训练模型

预训练模型的下载连接如下:

需要说明白但是,这几个模型的预训练都是使用标准的VGG模型,也就是下表红框这几个模型

    vgg11: https://download.pytorch.org/models/vgg11-bbd30ac9.pth
    vgg13: https://download.pytorch.org/models/vgg13-c768596a.pth
    vgg16: https://download.pytorch.org/models/vgg16-397923af.pth
    vgg19: https://download.pytorch.org/models/vgg19-dcbb9e9d.pth

借用Alexnet的训练代码详细内容如代码如下:

import numpy as np
import torch
import torchvision
import argparse
from torch import nn
from torch.utils.data import DataLoader
from torch.utils.tensorboard import SummaryWriter
from torchvision import transforms
from torchvision.transforms import InterpolationMode
from alexnet import AlexNet
from VGG_net import make_VGG16
import ssl

# 全局取消证书验证
ssl._create_default_https_context = ssl._create_unverified_context

device= torch.device('cuda' if torch.cuda.is_available() else 'cpu')#
print(device)

def train(args):
    # 超参数设置,方便管理
    num_epochs = args.max_epoch
    batch_size = args.batch_size
    learning_rate = args.lr
    image_size = args.image_size
    momentum = args.momentum
    # 设置数据集的格式
    transform = transforms.Compose([transforms.Resize((image_size, image_size),
                                    interpolation=InterpolationMode.BICUBIC),
                                    transforms.ToTensor(),
                                    transforms.Normalize(mean=[0.4914, 0.4822, 0.4465], std=[0.247, 0.2435, 0.2616])
                                    ])
    # 数据加载
    # 如果没有这个数据集的话会自动下载
    train_data = torchvision.datasets.CIFAR10(root="dataset",download=True,transform=transform,train=True)
    test_data = torchvision.datasets.CIFAR10(root="dataset",download=True,transform=transform,train=False)

    train_dataloader = DataLoader(train_data, batch_size=batch_size)
    test_dataloader = DataLoader(test_data, batch_size=batch_size)
    print('Dataload is Ready')
    # 添加tensorboard路径
    writer = SummaryWriter(log_dir=args.SummerWriter_log)
    # 模型加载
    model = make_VGG16(num_classes=10,path=r'./vgg16.pth',use_BN=False).to(device)# 注意,这里是False,也就是不使用BN正则化,论文中的VGG16的是没有BN正则化的
    # model = AlexNet(num_classes=10).to(device)
    # 参数量估计
    total = sum([param.nelement() for param in model.parameters()])
    print("Number of parameters: %.2fM" % (total / 1e6))
    # Loss and optimizer
    # 选择交叉熵作为损失函数
    criterion = nn.CrossEntropyLoss()
    # 选择SGD为优化器
    optimizer = torch.optim.SGD(model.parameters(), lr=learning_rate, momentum=momentum)
    total_train_step = 0#记录训练次数
    total_test_step=0#记录测试次数


    # 开始训练
    for epoch in range(num_epochs):
        print("---------------第{}轮训练开始-------------".format(epoch + 1))
        for i, (images, labels) in enumerate(train_dataloader):
            images = images.to(device)
            labels = labels.to(device)

            # Forward pass
            outputs = model(images)
            loss = criterion(outputs, labels)

            # Backward and optimize
            optimizer.zero_grad()
            loss.backward()
            optimizer.step()

            total_train_step = total_train_step + 1

            if (i + 1) % args.print_map_epoch == 0:# 100次显示一次loss
                print("Epoch [{}/{}], Step [{}] Loss: {:.4f}"
                          .format(epoch + 1, num_epochs, total_train_step, loss.item()))
            writer.add_scalar("train_loss", loss.item(), total_train_step)

        # Test the model
        model.eval()
        total_test_loss = 0
        total_accuary = 0  # 正确率
        with torch.no_grad():
            correct = 0
            total = 0
            for images, labels in test_dataloader:
                images = images.to(device)
                labels = labels.to(device)
                outputs = model(images)
                loss = criterion(outputs, labels)
                _, predicted = torch.max(outputs.data, 1)
                total += labels.size(0)
                correct += (predicted == labels).sum().item()
                total_test_loss += loss
                total_accuary += correct
            print('Accuracy of the model on the test images: {} %'.format(100 * correct / total))
            writer.add_scalar("test_loss",total_test_loss,total_test_step)
            writer.add_scalar("test_accuary", correct / total, total_test_step)
            total_test_step += 1
    # Save the model checkpoint
    torch.save(model, 'weights.pth')




if __name__ == "__main__":
    parser = argparse.ArgumentParser()

    '''------------------------------------------调  节  部  分------------------------------------------------------'''
    parser.add_argument('--max_epoch', type=int, default=30, help='total epoch')
    parser.add_argument('--device_num', type=str, default='cpu', help='select GPU or cpu')
    parser.add_argument('--image_size', type=int, default=224, help='if crop img, img will be resized to the size')
    parser.add_argument('--batch_size', type=int, default=8, help='batch size, recommended 16')
    parser.add_argument('--lr', type=float, default=0.0001, help='learning rate')
    parser.add_argument('--momentum', type=float, default=0.90, help='choice a float in 0-1')
    parser.add_argument('--print_map_epoch', type=int, default=10, help='')
    parser.add_argument('--SummerWriter_log', type=str, default='VGGnet', help='')

    args = parser.parse_args()

    train(args)






训练结果

训练结果如下:

 由于使用了VGG16的预训练模型,在训练接近5000次的时候损失基本接近平稳,由于我手上这台电脑显卡比较老,训练速度慢,就没有完整地训练完模型,如果大家有兴趣的话可以继续训练,看看最终的训练效果。

不过有一点的就是,VGG是大模型,CIFAR10是小模型,有可能会出现过拟合,也就是训练集出现loss为0,测试集100%的情况,这时候可以选择其他更大的数据集,比如CIFAR100,或者一步到位选择COCO数据集,因为本文作为入门介绍,就不进行更进一步的实验。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值