【神经网络】【VggNet】

1、引言

卷积神经网络是当前最热门的技术,我想深入地学习这门技术,从他的发展历史开始,了解神经网络算法的兴衰起伏;同时了解他在发展过程中的**里程碑式算法**,能更好的把握神经网络发展的未来趋势,了解神经网络的特征。
之前的LeNet为以后的神经网络模型打下了一个基础的框架,真正让神经网络模型在外界广泛引起关注的还是AlexNet,在AlexNet之后也出现了相应对他的改进,或多或少会有一些效果。但是ZFNet是在AlexNet上的改进,他的论文对神经网络的各个层级的作用,做了十分详细的阐述,为如何优化模型,怎样“有理有据”地调节参数指出了一个方向,这是他最大的一个贡献。VggNet也是在2014年的ImageNet的定位赛和分类赛上获得了第一名和第二名,他在原来卷积神经网络结构的基础上,大大增加了网络的深度,最后取得了不错的成绩。
VggNet论文原文下载地址
VggNet论文中文翻译

2、VGGNet提出背景

VGG Net由牛津大学的视觉几何组(Visual Geometry Group)和 Google DeepMind公司的研究员一起研发的的深度卷积神经网络,在 ILSVRC 2014 上取得了第二名的成绩,将 Top-5错误率降到7.3%。它主要的贡献是展示出网络的深度(depth)是算法优良性能的关键部分。目前使用比较多的网络结构主要有ResNet(152-1000层),GooleNet(22层),VGGNet(19层),大多数模型都是基于这几个模型上改进,采用新的优化算法,多模型融合等。到目前为止,VGG Net 依然经常被用来提取图像特征。
注:
ImageNet是一个在2009年创建的图像数据集,从2010年开始到2017年举办了七届的ImageNet 挑战赛——ImageNet Large Scale Visual Recognition ChallengeI (LSVRC),在这个挑战赛上诞生了AlexNet、ZFNet、OverFeat、VGG、Inception、ResNet、WideResNet、FractalNet、DenseNet、ResNeXt、DPN、SENet 等经典模型。

3、VGGNet的模型详解

在这里插入图片描述
VGG 的结构与 AlexNet 类似,区别是深度更深,但形式上更加简单。VGG由5层卷积层、3层全连接层、1层softmax输出层构成,层与层之间使用maxpool(最大化池)分开,所有隐藏层的激活单元都采用ReLU函数。作者在原论文中,根据卷积层不同的子层数量,设计了A、A-LRN、B、C、D、E这6种网络结构。
在这里插入图片描述

VGG16总共包含16个子层,第1层卷积层由2个conv3-64组成,第2层卷积层由2个conv3-128组成,第3层卷积层由3个conv3-256组成,第4层卷积层由3个conv3-512组成,第5层卷积层由3个conv3-512组成,然后是2个FC4096,1个FC1000。总共16层,这也就是VGG16名字的由来。

3.1 输入层

VGG输入图片的尺寸是224x224x3。

3.2 第1层卷积层

在这里插入图片描述

第1层卷积层由2个conv3-64组成。
该层的处理流程是:卷积–>ReLU–> 卷积–>ReLU–>池化
卷积:输入是224x224x3,使用64个3x3x3的卷积核进行卷积,padding=1,stride=1,根据公式:
(input_size + 2 x padding - kernel_size) / stride + 1=(224+2 x 1-3)/1+1=224
得到输出是224x224x64。
ReLU:将卷积层输出的FeatureMap输入到ReLU函数中。
卷积:输入是224x224x64,使用64个3x3x64的卷积核进行卷积,padding=1,stride=1,根据公式:
(input_size + 2 x padding - kernel_size) / stride + 1=(224+2 x 1-3)/1+1=224
得到输出是224 x 224 x 64。
ReLU:将卷积层输出的FeatureMap输入到ReLU函数中。
池化:使用2 x 2,stride=2的池化单元进行最大池化操作(max pooling)。根据公式:
(224+2 x 0-2)/2+1=112
每组得到的输出为112 x 112 x 64。

3.3 第2层卷积层

在这里插入图片描述

第2层卷积层由2个conv3-128组成。
该层的处理流程是:卷积–>ReLU–> 卷积–>ReLU–>池化
卷积:输入是112x112x64,使用128个3x3x64的卷积核进行卷积,padding=1,stride=1,根据公式:
(input_size + 2 x padding - kernel_size) / stride + 1=(112+2 x 1-3)/1+1=112
得到输出是112x112x128。
ReLU:将卷积层输出的FeatureMap输入到ReLU函数中。
卷积:输入是112x112x128,使用128个3x3x128的卷积核进行卷积,padding=1,stride=1,根据公式:
(input_size + 2 x padding - kernel_size) / stride + 1=(112+2 x 1-3)/1+1=112
得到输出是112x112x128。
ReLU:将卷积层输出的FeatureMap输入到ReLU函数中。
池化:使用2 x 2,stride=2的池化单元进行最大池化操作(max pooling)。根据公式:
(112+2*0-2)/2+1=56
每组得到的输出为56 x 56 x 128。

3.4 第3层卷积层

在这里插入图片描述

第3层卷积层由3个conv3-256组成。
该层的处理流程是:卷积–>ReLU–> 卷积–>ReLU–>池化
卷积:输入是56x56x128,使用256个3x3x128的卷积核进行卷积,padding=1,stride=1,根据公式:
(input_size + 2 x padding - kernel_size) / stride + 1=(56+2 x 1-3)/1+1=56
得到输出是56x56x256。
ReLU:将卷积层输出的FeatureMap输入到ReLU函数中。
卷积:输入是56 x 56 x 256,使用256个3 x 3 x 256的卷积核进行卷积,padding=1,stride=1,根据公式:
(input_size + 2 x padding - kernel_size) / stride + 1=(56+2 x 1-3)/1+1=56
得到输出是56x56x256。
ReLU:将卷积层输出的FeatureMap输入到ReLU函数中。
池化:使用2x2,stride=2的池化单元进行最大池化操作(max pooling)。根据公式:
(56+2 x 0-2)/2+1=28
每组得到的输出为28 x 28 x 256。

3.5 第4层卷积层

在这里插入图片描述

第4层卷积层由3个conv3-512组成。
该层的处理流程是:卷积–>ReLU–> 卷积–>ReLU–>池化
**卷积:**输入是28x28x256,使用512个3x3x256的卷积核进行卷积,padding=1,stride=1,根据公式:
(input_size + 2 x padding - kernel_size) / stride + 1=(28+2 x 1-3)/1+1=28
得到输出是28x28x512。
**ReLU:**将卷积层输出的FeatureMap输入到ReLU函数中。
卷积:输入是28x28x512,使用512个3x3x512的卷积核进行卷积,padding=1,stride=1,根据公式:
(input_size + 2 x padding - kernel_size) / stride + 1=(28+2 x 1-3)/1+1=28
得到输出是28x28x512。
ReLU:将卷积层输出的FeatureMap输入到ReLU函数中。
池化:使用2x2,stride=2的池化单元进行最大池化操作(max pooling)。根据公式:
(28+2 x 0-2)/2+1=14
每组得到的输出为14x14x512。

3.6 第5层卷积层

在这里插入图片描述

第5层卷积层由3个conv3-512组成。
该层的处理流程是:卷积–>ReLU–> 卷积–>ReLU–>池化
卷积:输入是14x14x512,使用512个3x3x512的卷积核进行卷积,padding=1,stride=1,根据公式:
(input_size + 2 x padding - kernel_size) / stride + 1=(14+2x1-3)/1+1=14
得到输出是14x14x512。
ReLU:将卷积层输出的FeatureMap输入到ReLU函数中。
卷积:输入是14x14x512,使用512个3x3x512的卷积核进行卷积,padding=1,stride=1,根据公式:
(input_size + 2 x padding - kernel_size) / stride + 1=(14+2x1-3)/1+1=14
得到输出是14x14x512。
ReLU:将卷积层输出的FeatureMap输入到ReLU函数中。
池化:使用2x2,stride=2的池化单元进行最大池化操作(max pooling)。
根据公式:(14+2x0-2)/2+1=7
每组得到的输出为7x7x512。

3.7 第1层全连接层

在这里插入图片描述

第1层全连接层FC4096由4096个神经元组成。
该层的处理流程是:FC–>ReLU–>Dropout
**FC:**输入是7x7x512的FeatureMap,展开为77512的一维向量,即77512个神经元,输出为4096个神经元。
**ReLU:**这4096个神经元的运算结果通过ReLU激活函数中。
Dropout:随机的断开全连接层某些神经元的连接,通过不激活某些神经元的方式防止过拟合。

3.8 第2层全连接层

在这里插入图片描述

第2层全连接层FC4096由4096个神经元组成。
该层的处理流程是:FC–>ReLU–>Dropout
**FC:**输入是4096个神经元,输出为4096个神经元。
ReLU:这4096个神经元的运算结果通过ReLU激活函数中。
**Dropout:**随机的断开全连接层某些神经元的连接,通过不激活某些神经元的方式防止过拟合。

3.9 第3层全连接层

在这里插入图片描述

第3层全连接层FC1000由1000个神经元组成,对应ImageNet数据集的1000个类别。
该层的处理流程是:FC。
FC:输入是4096个神经元,输出为1000个神经元。

3.10 softmax层

在这里插入图片描述

该层的流程为:Softmax
Softmax:这1000个神经元的运算结果通过Softmax函数中,输出1000个类别对应的预测概率值。

4、VGGNet的创新之处

4.1结构简单

VGGNet的结构十分简洁,由5个卷积层、3个全连接层和1个softmax层构成,层与层之间使用最大池化连接,隐藏层之间使用的激活函数全都是ReLU。并且网络的参数也是整齐划一的,赏心悦目。

4.2使用小卷积核

VGGNet使用含有多个小型的3×3卷积核的卷积层来代替卷积核较大的卷积层。2个3×3的卷积核堆叠的感受野相当于一个5×5的卷积核的感受野,而3个3×3的卷积核堆叠的感受野则相当于一个7×7的卷积核的感受野。因此,采用多个小型卷积核,既能减少参数的数量,又能增强网络的非线性映射从而提升网络的表达能力。
为什么可以增加网络的非线性?我们知道激活函数的作用就是给神经网络增加非线性因素,使其可以拟合任意的函数,每个卷积操作后都会通过ReLU激活,ReLU函数就是一个非线性函数。下图展示了为什么使用2个3x3的卷积核可以代替5×5卷积核。
在这里插入图片描述

总结一下,使用多个3×3卷积堆叠的作用有两个:一是在不影响感受野的前提下减少了参数;二是增加了网络的非线性。

4.3使用小滤波器

与AlexNet相比,VGGNet在池化层全部采用的是2×2的小滤波器。

4.4通道数更多,特征度更宽

VGGNet的第一层有64个通道,后面的每一层都对通道进行了翻倍,最多达到了512个通道。由于每个通道都代表着一个feature map,这样就使更多的信息可以被提取出来。

4.5将全连接层转换为卷积层

这个特征是体现在VGGNet的测试阶段。在进行网络测试时,将训练阶段的3个全连接层替换为3个卷积层,使测试得到的网络没有全连接的限制,能够接收任意宽和高的输入。如果后面3个层都是全连接层,那么在测试阶段就只能将测试的图像全部缩放到固定尺寸,这样就不便于多尺度测试工作的开展

在这里插入图片描述
为什么这样替换之后就可以处理任意尺寸的输入图像了呢?因为1×1卷积一个很重要的作用就是调整通道数。如果下一层输入的特征图需要控制通道数为N,那么设置N个1×1卷积核就可以完成通道数的调整。比如最后需要1000个神经元用于分出1000个类别,那就在最后一层的前面使用1000个1×1的卷积核,这样的到的结果就是(1, 1, 1000)正好可以匹配。
在这里插入图片描述

5、VGGNet的代码实现

from torchvision import transforms
from torch.utils.data import Dataset, DataLoader
from PIL import Image
import numpy as np


def Myloader(path):
    return Image.open(path).convert('RGB')


# get a list of paths and labels.
def init_process(path, lens):
    data = []
    name = find_label(path)
    for i in range(lens[0], lens[1]):
        data.append([path % i, name])

    return data


class MyDataset(Dataset):
    def __init__(self, data, transform, loader):
        self.data = data
        self.transform = transform
        self.loader = loader

    def __getitem__(self, item):
        img, label = self.data[item]
        img = self.loader(img)
        img = self.transform(img)
        return img, label

    def __len__(self):
        return len(self.data)


def find_label(str):
    """
    Find image tags based on file paths.

    :param str: file path
    :return: image label
    """
    first, last = 0, 0
    for i in range(len(str) - 1, -1, -1):
        if str[i] == '%' and str[i - 1] == '.':
            last = i - 1
        if (str[i] == 'c' or str[i] == 'd') and str[i - 1] == '/':
            first = i
            break

    name = str[first:last]
    if name == 'dog':
        return 1
    else:
        return 0


def load_data():
    print('data processing...')
    transform = transforms.Compose([
        transforms.RandomHorizontalFlip(p=0.3),
        transforms.RandomVerticalFlip(p=0.3),
        transforms.Resize((256, 256)),
        transforms.ToTensor(),
        transforms.Normalize(mean=(0.5, 0.5, 0.5), std=(0.5, 0.5, 0.5))  # normalization
    ])
    path1 = 'data/training_data/cats/cat.%d.jpg'
    data1 = init_process(path1, [0, 500])
    path2 = 'data/training_data/dogs/dog.%d.jpg'
    data2 = init_process(path2, [0, 500])
    path3 = 'data/testing_data/cats/cat.%d.jpg'
    data3 = init_process(path3, [1000, 1200])
    path4 = 'data/testing_data/dogs/dog.%d.jpg'
    data4 = init_process(path4, [1000, 1200])
    data = data1 + data2 + data3 + data4   # 1400
    # shuffle
    np.random.shuffle(data)
    # train, val, test = 900 + 200 + 300
    train_data, val_data, test_data = data[:900], data[900:1100], data[1100:]
    train_data = MyDataset(train_data, transform=transform, loader=Myloader)
    Dtr = DataLoader(dataset=train_data, batch_size=50, shuffle=True, num_workers=0)
    val_data = MyDataset(val_data, transform=transform, loader=Myloader)
    Val = DataLoader(dataset=val_data, batch_size=50, shuffle=True, num_workers=0)
    test_data = MyDataset(test_data, transform=transform, loader=Myloader)
    Dte = DataLoader(dataset=test_data, batch_size=50, shuffle=True, num_workers=0)

    return Dtr, Val, Dte

import copy
import os
import random

import numpy as np
import torch
import torch.nn as nn
from torch import optim
from torch.autograd import Variable
from tqdm import tqdm

from data_process import load_data
import torch.nn.functional as F

from torchvision.models import vgg16
model_vgg = vgg16(pretrained=True)
model_vgg.classifier._modules['6'] = nn.Sequential(nn.Linear(4096, 2))
print(model_vgg)

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

def setup_seed(seed):
    os.environ['PYTHONHASHSEED'] = str(seed)
    torch.manual_seed(seed)
    torch.cuda.manual_seed_all(seed)
    np.random.seed(seed)
    random.seed(seed)
    torch.backends.cudnn.deterministic = True

setup_seed(20)


def get_val_loss(model, Val):
    model.eval()
    criterion = nn.CrossEntropyLoss().to(device)
    val_loss = []
    for (data, target) in Val:
        data, target = Variable(data).to(device), Variable(target.long()).to(device)
        output = model(data)
        loss = criterion(output, target)
        val_loss.append(loss.cpu().item())

    return np.mean(val_loss)


def train(model_vgg):
    path = "model/vggnet.pkl"
    lr = 0.0001
    Dtr, Val, Dte = load_data()
    model = model_vgg.to(device)
    print('train...')
    epoch_num = 5
    best_model = model
    min_epochs = 5
    min_val_loss = 5
    optimizer = optim.Adam(model.parameters(), lr=lr)
    criterion = nn.CrossEntropyLoss().to(device)
    # criterion = nn.BCELoss().to(device)
    for epoch in tqdm(range(epoch_num), ascii=True):
        train_loss = []
        for batch_idx, (data, target) in enumerate(Dtr, 0):
            data, target = Variable(data).to(device), Variable(target.long()).to(device)
            # target = target.view(target.shape[0], -1)
            optimizer.zero_grad()
            output = model(data)
            loss = criterion(output, target)
            loss.backward()
            optimizer.step()
            train_loss.append(loss.cpu().item())
        # validation
        val_loss = get_val_loss(model, Val)
        model.train()
        if epoch + 1 > min_epochs and val_loss < min_val_loss:
            min_val_loss = val_loss
            best_model = copy.deepcopy(model)

        tqdm.write('Epoch {:03d} train_loss {:.5f} val_loss {:.5f}'.format(epoch, np.mean(train_loss), val_loss))

    torch.save(best_model.state_dict(), path)
    return best_model


def test(model_vgg):
    Dtr, Val, Dte = load_data()
    path = "model/vggnet.pkl"
    model = model_vgg
    #device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
    model.load_state_dict(torch.load(path),False)
    model = model.to(device)

    model.eval()
    total = 0
    current = 0
    for (data, target) in Dte:
        data, target = data.to(device), target.to(device)
        outputs = model(data)
        predicted = torch.max(outputs.data, 1)[1].data
        total += target.size(0)
        current += (predicted == target).sum()

    print('Accuracy:%d%%' % (100 * current / total))


if __name__ == '__main__':
    train(model_vgg)
    test(model_vgg)

6、总结

书山有路勤为径,学海无涯苦作舟。

7、参考文章

7.1 手撕 CNN 经典网络之 VGGNet(理论篇)
7.2 PyTorch搭建预训练AlexNet、DenseNet、ResNet、VGG实现猫狗图片分类

  • 4
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值