笔记:VGG16 网络原理与 pytorch 网络实现

    VGG16 原始论文地址:https://arxiv.org/pdf/1409.1556.pdf

    在图像去雾、超分辨率、风格迁移等领域,感知损失被广泛使用。而感知损失采用较多的正是 VGG16 网路,后续虽然出现了残差网络 ResNet、密集连接网络 DenseNet、快速推理的 MobileNet 等,VGG16 网络(及其变种 VGG19 等)仍然被研究人员普遍使用。

    笔记将从以下 3 个方面对 VGG16 网络进行分析:

    (1)网络结构分析与训练细节

    (2)特点与贡献

    (3)pytorch 搭建

    在这里,我推荐给你们这个深度学习框架原理实战训练营,3 天就可以带你从零构建专家级神经网络框架,点击下方插件就可以免费报名哦 ↓ ↓ ↓

    1、网络结构分析与训练细节

    下面这张图给出了网络的输入输出以及特征图尺寸的变化情况(画的比较粗糙)。(1)左边输入图片的尺寸是 224x224x3,然后经过 Conv(卷积)和 ReLU(激活)得到 224x224x3 的特征图,这个过程中图片尺寸没有变化,仅仅是通道数从 3 增加到 64;(2)尺寸从 224 降为 112 是通过 maxpooling 实现的,选取 kernel_size=2 并且 stride=2 的 pooling 操作可以让长宽各降低一半;(3)网络的最后端是全连接层,全连接层最后输出 1000 维的目的是实现 1000 分类的效果:

    VGG16网络原理分析与pytorch实现 - 知乎 - 图1

    下面的表格来自 VGG16 论文,给出了 VGG11、VGG16 与 VGG19 不同的网络搭建方式:

    VGG16网络原理分析与pytorch实现 - 知乎 - 图2

    其中 ABCDE 表示不同深度的网络配置,convx-y 中 x 表示卷积核尺寸,y 表示特征通道数,比如 conv3-256 表示 3x3 的卷积核并且通道数为 256;同理 conv1-512 表示卷积核 3x3 通道数为 512。

    本篇笔记主要分析的是 VGG16 不用 1x1 卷积的版本,也就是上述表格中的 D。其特性包含下面几个方面:

    (1)包含 13 个卷积层,每个卷积层都不会改变前一层的特征图长和宽;通过 Conv 可以实现通道数的增加(表格中 maxpool 放置的位置容易误导读者,让人误以为池化层负责增加通道数)。假设输入图片尺寸为(batch_size, 3, 224, 224),如果希望这一层的输出特征图尺寸为 (batch_size, 64, 224, 224),需要使用 64 个尺寸为 3x3 并且 3 个通道的卷积核。

    (2)包含 5 个池化层,分别分布在 2 或者 3 次卷积以后。池化层的作用是降低特征图尺寸并且能提高网络抗干扰能力。假设上一层得到的特征图尺寸为(batch_size, 64, 224, 224),选取 Max Pooling 的核尺寸为 2x2,每次移动两个步长,那么得到的输出特征图尺寸为(batch_size, 64, 112, 112)。VGG16 选取的是 max pooling 仅仅是各种池化方式中的一种,类似的池化还有均值池化等。

    (3)包含 3 个全连接层,从现在普遍使用的 pytorch 与 tensorflow 框架上进行分析可知,卷积层的输入核输出应该是 4D 的张量(batch_size, channels, height, width),而全连接的输入核输出应该是 (batch_size, features_number)。因此,卷积层输出的特征图需要进行维度和尺寸变换操作,才能送入全连接层,各种框架提供的变换函数可能并不一致,比如 view 和 reshape 等。最后一个全连接层输出的特征数量为 1000,代表其功能为 1000 分类。

    VGG16 作者提供了网络的训练细节:batch_size 为 256,使用了 L2 惩罚,drop_out 比例设置为 0.5,学习率初始化为 0.01 并随着迭代进行动态更新。作者指出:“The initialisation of the network weights is important, since bad initialisation can stall learning due to the instability of gradient in deep nets.” 并采取了较为复杂的初始化策略,具体可看其论文。

    2、网络的特点与贡献

    从自己之前实现各种滤波算法与 DL 网络搭建的经验中(之前的文章)可以发现以下几点:

    (1)卷积核尺寸的不同影响最终效果(比如 3x3 还是 7x7 等);

    (2)边界是否进行填充(padding 操作)影响输出层尺寸,比如 224x224 的图片经过 3x3 的核进行卷积以后,如果不对边界进行补零,输出的尺寸会小于 224。

    (3)7x7 等更大的卷积核的计算量远大于 3x3 的小卷积核

    VGG16 网络的设计理念中就对上述问题进行了考虑,所以它提出了下面的解决方案:

    (1)采用尺寸较小的 3x3 卷积核(步长为 1),并证明了其有效性,通过 padding 对卷积结果填充,保证卷积后特征图尺寸和前层保持一致。

    (2)通过不断增加通道数达到更深的网络,(残差网络出来之前 VGG 已经非常深了),通过池化层降低特征图尺寸为前一层的一半。

    3、pytorch 搭建网络与功能测试

    由于其原始论文是用 caffe 上用 C++ 搭建的,而这种框架目前已经不是很流行了,所以自己参考一些 github 仓库重新实现了网络。为了让代码更加清晰,所以实现的时候需要和前面的表格对应上,根据 5 个 max pooling 层将网络分成 6 个 block。

    3.1 VGG 类的整体结构

    添加具体网络包含的操作之前应该首先设计整体的结构,比如类需要哪些初始化参数,类包含哪些具体的方法,每个方法负责完成哪些内容。

    (1)类初始化函数需要提供 num_classes,也就是自己想要进行多少分类;

    (2)init() 方法定义了 extract_feature 属性,这个属性负责卷积、激活和最大池化(也就是全连接)之前的操作,这个属性的操作来自 net 这个 list,其中 net 这个空列表通过不断 append 新的操作实现特征提取;classifier 属性的作用是提供最后的三个全连接操作;需要把全连接和前面的特征提取过程分离是因为全连接的输入需要 reshape,将会在 forward()方法采用手动 reshape;

    (3)forward()方法负责实现前向传播。

    1. class SE_VGG(nn.Module):
    2. def __init__(self, num_classes):
    3. super().__init__()
    4. self.num_classes = num_classes
    5. # define an empty for Conv_ReLU_MaxPool
    6. net = []
    7. net.append(...)
    8. ...
    9. # add net into class property
    10. self.extract_feature = nn.Sequential(*net)
    11. # define an empty container for Linear operations
    12. classifier = []
    13. classifier.append(...)
    14. ...
    15. # add classifier into class property
    16. self.classifier = nn.Sequential(*classifier)
    17. def forward(self, x):
    18. feature = self.extract_feature(x)
    19. feature = feature.view(x.size(0), -1)
    20. classify_result = self.classifier(feature)
    21. return classify_result

    3.2 VGG 具体实现

    完整的网络代码如下:

    1. import torch.nn as nn
    2. import torch
    3. class SE_VGG(nn.Module):
    4. def __init__(self, num_classes):
    5. super().__init__()
    6. self.num_classes = num_classes
    7. # define an empty for Conv_ReLU_MaxPool
    8. net = []
    9. # block 1
    10. net.append(nn.Conv2d(in_channels=3, out_channels=64, padding=1, kernel_size=3, stride=1))
    11. net.append(nn.ReLU())
    12. net.append(nn.Conv2d(in_channels=64, out_channels=64, padding=1, kernel_size=3, stride=1))
    13. net.append(nn.ReLU())
    14. net.append(nn.MaxPool2d(kernel_size=2, stride=2))
    15. # block 2
    16. net.append(nn.Conv2d(in_channels=64, out_channels=128, kernel_size=3, stride=1, padding=1))
    17. net.append(nn.ReLU())
    18. net.append(nn.Conv2d(in_channels=128, out_channels=128, kernel_size=3, stride=1, padding=1))
    19. net.append(nn.ReLU())
    20. net.append(nn.MaxPool2d(kernel_size=2, stride=2))
    21. # block 3
    22. net.append(nn.Conv2d(in_channels=128, out_channels=256, kernel_size=3, padding=1, stride=1))
    23. net.append(nn.ReLU())
    24. net.append(nn.Conv2d(in_channels=256, out_channels=256, kernel_size=3, padding=1, stride=1))
    25. net.append(nn.ReLU())
    26. net.append(nn.Conv2d(in_channels=256, out_channels=256, kernel_size=3, padding=1, stride=1))
    27. net.append(nn.ReLU())
    28. net.append(nn.MaxPool2d(kernel_size=2, stride=2))
    29. # block 4
    30. net.append(nn.Conv2d(in_channels=256, out_channels=512, kernel_size=3, padding=1, stride=1))
    31. net.append(nn.ReLU())
    32. net.append(nn.Conv2d(in_channels=512, out_channels=512, kernel_size=3, padding=1, stride=1))
    33. net.append(nn.ReLU())
    34. net.append(nn.Conv2d(in_channels=512, out_channels=512, kernel_size=3, padding=1, stride=1))
    35. net.append(nn.ReLU())
    36. net.append(nn.MaxPool2d(kernel_size=2, stride=2))
    37. # block 5
    38. net.append(nn.Conv2d(in_channels=512, out_channels=512, kernel_size=3, padding=1, stride=1))
    39. net.append(nn.ReLU())
    40. net.append(nn.Conv2d(in_channels=512, out_channels=512, kernel_size=3, padding=1, stride=1))
    41. net.append(nn.ReLU())
    42. net.append(nn.Conv2d(in_channels=512, out_channels=512, kernel_size=3, padding=1, stride=1))
    43. net.append(nn.ReLU())
    44. net.append(nn.MaxPool2d(kernel_size=2, stride=2))
    45. # add net into class property
    46. self.extract_feature = nn.Sequential(*net)
    47. # define an empty container for Linear operations
    48. classifier = []
    49. classifier.append(nn.Linear(in_features=512*7*7, out_features=4096))
    50. classifier.append(nn.ReLU())
    51. classifier.append(nn.Dropout(p=0.5))
    52. classifier.append(nn.Linear(in_features=4096, out_features=4096))
    53. classifier.append(nn.ReLU())
    54. classifier.append(nn.Dropout(p=0.5))
    55. classifier.append(nn.Linear(in_features=4096, out_features=self.num_classes))
    56. # add classifier into class property
    57. self.classifier = nn.Sequential(*classifier)
    58. def forward(self, x):
    59. feature = self.extract_feature(x)
    60. feature = feature.view(x.size(0), -1)
    61. classify_result = self.classifier(feature)
    62. return classify_result

    如果需要测试网络的输出是不是正常,可以运行下面的代码:

    1. if __name__ == "__main__":
    2. x = torch.rand(size=(8, 3, 224, 224))
    3. vgg = SE_VGG(num_classes=1000)
    4. out = vgg(x)
    5. print(out.size())

    以上就是本文的全部内容,如果你想在 AI 领域继续深入学习,光掌握这些理论知识还是不够的,所以我还是推荐这个 3 天深度学习框架原理实战训练营给你们,希望你们可以通过这个课程构建属于你自己的神经网络框架,点击下方链接 ↓ ↓ ↓ 就可以免费报名啦~
    https://zhuanlan.zhihu.com/p/87555358