经典卷积神经网络-VGGNet

经典卷积神经网络-VGGNet

一、背景介绍

VGG是Oxford的V isual G eometry G roup的组提出的。该网络是在ILSVRC 2014上的相关工作,主要工作是证明了增加网络的深度能够在一定程度上影响网络最终的性能。VGG有两种结构,分别是VGG16和VGG19,两者并没有本质上的区别,只是网络深度不一样。

二、VGG-16网络结构

其中VGG系列具体的网络结构如下表所示:

如图所示,这是论文中所有VGG网络的详细信息,D列对应的为VGG-16网络。16指的是在这个网络中包含16个卷积层和全连接层(不算池化层和Softmax)。

  • VGG-16的卷积层没有那么多的超参数,在整个网络模型中,所有卷积核的大小都是 3 × 3的,并且padding为same,stride为1。所有池化层的池化核大小都是 2 × 2 的,并且步长为2。在几次卷积之后紧跟着池化,整个网络结构很规整。

  • 总共包含约1.38亿个参数,但其结构并不复杂,结构很规整,都是几个卷积层后面跟着可以压缩图像大小的池化层,同时,卷积层的卷积核数量的变化也存在一定的规律,都是池化之后图像高度宽度减半,但在下一个卷积层中通道数翻倍,这正是这种简单网络结构的一个规则。

  • VGG16相比AlexNet的一个改进是采用连续的几个3x3的卷积核代替AlexNet中的较大卷积核(11x11,7x7,5x5)。对于给定的感受野(与输出有关的输入图片的局部大小),采用堆积的小卷积核是优于采用大的卷积核,因为多层非线性层可以增加网络深度来保证学习更复杂的模式,而且代价还比较小(参数更少)。在VGG中,使用了3个3x3卷积核来代替7x7卷积核,使用了2个3x3卷积核来代替5×5卷积核,这样做的主要目的是在保证具有相同感受野的条件下,提升了网络的深度,在一定程度上提升了神经网络的效果。

  • 它的主要缺点就是需要训练的特征数量非常大。有些文章介绍了VGG-19,但通过研究发现VGG-19和VGG-16的性能表现几乎不分高下,所以很多人还是使用VGG-16,这也说明了单纯的增加网络深度,其性能不会有太大的提升。

  • 论文中还介绍了权重初始化方法,即预训练低层模型参数为深层模型参数初始化赋值。原文:网络权重初始化是非常重要的,坏的初始化会使得深度网络的梯度的不稳定导致无法学习。为了解决这个问题,我们首先在网络A中使用随机初始化进行训练。然后到训练更深的结构时,我们将第一层卷积层和最后三层全连接层的参数用网络A中的参数初始化(中间层的参数随机初始化)。

  • 论文中揭示了,随着网络深度的增加,图像的高度和宽度都以一定规律不断缩小,每次池化之后刚好缩小一半,而通道数量在不断增加,而且刚好也是在每组卷积操作后增加一倍。也就是说,图像缩小和通道增加的比例是有规律的,从这个角度看,这篇论文很吸引人。

三、VGG-16的Pytorch实现

我们可以根据:https://dgschwend.github.io/netscope/#/preset/vgg-16,来搭建VGG-16。

后面要将VGG-16Net应用到CIFAR10数据集上,所以对网络做了一些修改,具体代码如下:

python 复制代码
from torch import nn


class Vgg16_Net(nn.Module):
    def __init__(self):
        super(Vgg16_Net, self).__init__()

        self.layer1 = nn.Sequential(
            # input_size = (3, 32, 32)
            nn.Conv2d(in_channels=3, out_channels=64, kernel_size=3, stride=1, padding=1),
            nn.BatchNorm2d(64),
            nn.ReLU(inplace=True),

            # input_size = (64, 32, 32)
            nn.Conv2d(in_channels=64, out_channels=64, kernel_size=3, stride=1, padding=1),
            nn.BatchNorm2d(64),
            nn.ReLU(inplace=True),

            # input_size = (64, 32, 32)
            nn.MaxPool2d(kernel_size=2, stride=2)
        )

        self.layer2 = nn.Sequential(
            # input_size = (64, 16, 16)
            nn.Conv2d(in_channels=64, out_channels=128, kernel_size=3, stride=1, padding=1),
            nn.BatchNorm2d(128),
            nn.ReLU(inplace=True),

            # input_size = (128, 16, 16)
            nn.Conv2d(in_channels=128, out_channels=128, kernel_size=3, stride=1, padding=1),
            nn.BatchNorm2d(128),
            nn.ReLU(inplace=True),

            # input_size = (128, 16, 16)
            nn.MaxPool2d(2, 2)
        )

        self.layer3 = nn.Sequential(
            # input_size = (128, 8, 8)
            nn.Conv2d(in_channels=128, out_channels=256, kernel_size=3, stride=1, padding=1),
            nn.BatchNorm2d(256),
            nn.ReLU(inplace=True),

            # input_size = (256, 8, 8)
            nn.Conv2d(in_channels=256, out_channels=256, kernel_size=3, stride=1, padding=1),
            nn.BatchNorm2d(256),
            nn.ReLU(inplace=True),

            # input_size = (256, 8, 8)
            nn.Conv2d(in_channels=256, out_channels=256, kernel_size=3, stride=1, padding=1),
            nn.BatchNorm2d(256),
            nn.ReLU(inplace=True),

            # input_size = (256, 8, 8)
            nn.MaxPool2d(2, 2)
        )

        self.layer4 = nn.Sequential(
            # input_size = (256, 4, 4)
            nn.Conv2d(in_channels=256, out_channels=512, kernel_size=3, stride=1, padding=1),
            nn.BatchNorm2d(512),
            nn.ReLU(inplace=True),

            # input_size = (512, 4, 4)
            nn.Conv2d(in_channels=512, out_channels=512, kernel_size=3, stride=1, padding=1),
            nn.BatchNorm2d(512),
            nn.ReLU(inplace=True),

            # input_size = (512, 4, 4)
            nn.Conv2d(in_channels=512, out_channels=512, kernel_size=3, stride=1, padding=1),
            nn.BatchNorm2d(512),
            nn.ReLU(inplace=True),

            # input_size = (512, 4, 4)
            nn.MaxPool2d(2, 2)
        )

        self.layer5 = nn.Sequential(
            # input_size = (512, 2, 2)
            nn.Conv2d(in_channels=512, out_channels=512, kernel_size=3, stride=1, padding=1),
            nn.BatchNorm2d(512),
            nn.ReLU(inplace=True),

            # input_size = (512, 2, 2)
            nn.Conv2d(in_channels=512, out_channels=512, kernel_size=3, stride=1, padding=1),
            nn.BatchNorm2d(512),
            nn.ReLU(inplace=True),

            # input_size = (512, 2, 2)
            nn.Conv2d(in_channels=512, out_channels=512, kernel_size=3, stride=1, padding=1),
            nn.BatchNorm2d(512),
            nn.ReLU(inplace=True),

            # input_size = (512, 2, 2)
            nn.MaxPool2d(2, 2)
            # output_size = (512, 1, 1)
        )

        self.conv = nn.Sequential(
            self.layer1,
            self.layer2,
            self.layer3,
            self.layer4,
            self.layer5
        )

        self.fc = nn.Sequential(
            # input_size = 512
            nn.Linear(512, 512),
            nn.ReLU(inplace=True),
            nn.Dropout(0.5),

            nn.Linear(512, 256),
            nn.ReLU(inplace=True),
            nn.Dropout(0.5),

            nn.Linear(256, 10)
        )

    def forward(self, x):
        x = self.conv(x)
        # -1表示自动计算行数
        # -1也可以改成x.size(0) 表示batch_size的大小
        x = x.view(-1, 512 * 1 * 1)
        x = self.fc(x)
        return x

四、案例:CIFAR-10分类问题

python 复制代码
import time
import torch
import torchvision
from model import *
from torch.utils.data import DataLoader
import torchvision.transforms as transforms
from matplotlib import pyplot as plt

# 加载数据集 拿到dataloader
def load_dataset(batch_size):
    train_data = torchvision.datasets.CIFAR10("../dataset/CIFAR10", train=True, download=True, transform=transforms.ToTensor())
    test_data = torchvision.datasets.CIFAR10("../dataset/CIFAR10", train=False, download=True, transform=transforms.ToTensor())
    train_dataloader = DataLoader(train_data, batch_size=batch_size, shuffle=True, num_workers=2)
    test_dataloader = DataLoader(test_data, batch_size=batch_size, shuffle=False, num_workers=2)
    return train_dataloader, test_dataloader


# 模型训练
def train(model, train_dataloader, criterion, optimizer, epochs, device, num_print, lr_scheduler=None, test_dataloader=None):
    # 记录train和test的acc方便绘制学习曲线
    record_train = list()
    record_test = list()

    # 开始训练
    model.train()
    for epoch in range(epochs):
        print("========== epoch: [{}/{}] ==========".format(epoch + 1, epochs))
        # total记录样本数 correct记录正确预测样本数
        total, correct, train_loss = 0, 0, 0
        start = time.time()

        # 结合enumerate函数和迭代器的unpacking 可以在获取数据的同时获取该批次数据对应的索引
        for i, (image, target) in enumerate(train_dataloader):
            image, target = image.to(device), target.to(device)
            output = model(image)
            loss = criterion(output, target)

            optimizer.zero_grad()
            loss.backward()
            optimizer.step()

            train_loss += loss.item()
            total += target.size(0)
            correct += (output.argmax(dim=1) == target).sum().item()
            train_acc = 100.0 * correct / total

            if (i + 1) % num_print == 0:
                print("step: [{}/{}], train_loss: {:.3f} | train_acc: {:6.3f}% | lr: {:.6f}".format(i + 1,
                                 len(train_dataloader), train_loss / (i + 1), train_acc, get_cur_lr(optimizer)))

        # 更新当前优化器的学习率
        if lr_scheduler is not None:
            lr_scheduler.step()

        print("--- cost time: {:.4f}s ---".format(time.time() - start))

        if test_dataloader is not None:
            record_test.append(test(model, test_dataloader, criterion, device))
        record_train.append(train_acc)

        # 保存当前模型
        torch.save(model.state_dict(), "train_model/VGG-16Net_{}.pth".format(epoch + 1))

    return record_train, record_test


# 模型测试
def test(model, test_dataloader, criterion, device):
    # total记录样本数 correct记录正确预测样本数
    total, correct = 0, 0

    # 开始测试
    model.eval()
    with torch.no_grad():
        print("*************** test ***************")
        for X, y in test_dataloader:
            X, y = X.to(device), y.to(device)

            output = model(X)
            loss = criterion(output, y)

            total += y.size(0)
            correct += (output.argmax(dim=1) == y).sum().item()

    test_acc = 100.0 * correct / total

    print("test_loss: {:.3f} | test_acc: {:6.3f}%".format(loss.item(), test_acc))
    print("************************************\n")

    # 记得重新调用model.train()
    model.train()

    return test_acc

# 获取当前的学习率 这里直接返回了第一个参数分组的学习率
def get_cur_lr(optimizer):
    for param_group in optimizer.param_groups:
        return param_group['lr']

# 绘制学习曲线
def learning_curve(record_train, record_test=None):
    # 设置 Matplotlib 图形样式
    # ggplot2 是一个用于数据可视化的流行 R 语言包,以其优雅和灵活的语法而闻名
    plt.style.use("ggplot")

    plt.plot(range(1, len(record_train) + 1), record_train, label="train acc")
    if record_test is not None:
        plt.plot(range(1, len(record_test) + 1), record_test, label="test acc")

    plt.legend(loc=4)
    plt.title("learning curve")
    plt.xticks(range(0, len(record_train) + 1, 5))
    plt.yticks(range(0, 101, 5))
    plt.xlabel("epoch")
    plt.ylabel("accuracy")

    plt.show()

# 定义超参数
BATCH_SIZE = 128
NUM_EPOCHS = 20
NUM_CLASSES = 10
LEARNING_RATE = 0.02
MOMENTUM = 0.9
WEIGHT_DECAY = 0.0005
NUM_PRINT = 100
DEVICE = "cuda" if torch.cuda.is_available() else "cpu"


def main():
    model = Vgg16_Net()
    model = model.to(DEVICE)

    # 加载数据
    train_dataloader, test_dataloader = load_dataset(BATCH_SIZE)

    # 定义损失函数
    criterion = nn.CrossEntropyLoss()

    # 定义优化器
    optimizer = torch.optim.SGD(
        model.parameters(),
        lr=LEARNING_RATE,
        momentum=MOMENTUM,
        weight_decay=WEIGHT_DECAY,
        nesterov=True
    )
    # 定义学习率调度器
    lr_scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=7, gamma=0.1)

    # 进行训练 返回训练集正确率和测试集正确率
    record_train, record_test = train(model, train_dataloader, criterion, optimizer, NUM_EPOCHS, DEVICE, NUM_PRINT, lr_scheduler, test_dataloader)

    # 绘制学习曲线
    learning_curve(record_train, record_test)

if __name__ == '__main__':
    main()

查看训练结果可以发现,测试集正确率基本保持在87.3%左右,训练集正确率接近100%:

学习曲线如下:

参考链接:

相关推荐
tianyunlinger2 分钟前
rope编码代码分享
pytorch·python·深度学习
如生命般费解的谜团33 分钟前
LLM学习笔记(7)Scaled Dot-product Attention
人工智能·笔记·学习·语言模型·json
FreeIPCC3 小时前
电话机器人是什么?
大数据·人工智能·语言模型·机器人·开源·信息与通信
醉酒柴柴3 小时前
【代码pycharm】动手学深度学习v2-08 线性回归 + 基础优化算法
深度学习·算法·pycharm
字节数据平台3 小时前
火山引擎数据飞轮探索零售企业大促新场景:下放营销活动权限
大数据·人工智能
啊啊啊六子3 小时前
windows下安装wsl的ubuntu,同时配置深度学习环境
windows·深度学习·ubuntu
努力学习的啊张4 小时前
消息称三星正与 OpenAI 洽谈,有望令 Galaxy AI 整合ChatGPT,三星都要和chatgpt合作了,你会使用chatgpt了吗?
人工智能·chatgpt
Together_CZ4 小时前
GPT-4 Technical Report——GPT-4技术报告
人工智能·gpt-4
huaqianzkh5 小时前
人工智能大趋势下软件开发的未来
人工智能
years_GG5 小时前
【Git多人开发与协作之团队的环境搭建】
spring boot·深度学习·vue·github·团队开发·个人开发