【研究生学习】Pytorch的官方代码搭建VGG模型

本篇博客是【深度学习个人笔记】Pytorch基本知识博客的扩展,采用里面介绍的Pytorch中的常用模块来搭建经典的VGG网络,使用的是Pytorch的官方代码,通过官方代码来体会如何用Pytorch搭建神经网络,并且学习如何保存神经网络的参数,并在GPU上进行训练
部分参考资料如下:
【Pytorch学习】-- 使用pytorch自带模型 -- VGG16
PyTorch中模型的parameters()方法浅析
使用pytorch搭建VGG网络 学习笔记
VGG网络的Pytorch官方实现过程解读
Pytorch(六)(模型参数的遍历) —— model.parameters() & model.named_parameters() & model.state_dict()
pytorch_神经网络模型搭建系列(1):自定义神经网络模型
PyTorch中的model.modules(), model.children(), model.named_children(), model.parameters(), model.nam...
深入浅出Pytorch函数——torch.nn.init.normal_

VGG基本介绍

VGG是非常经典的神经网络模型,其中比较常用的结构是VGG-16和VGG-19,16和19代表神经网络的层数,其具体网络结构如下表所示:

网络结构 VGG-16 VGG-19
模块1 conv3-64
conv3-64
conv3-64
conv3-64
下采样 maxpool maxpool
模块2 conv3-128
conv3-128
conv3-128
conv3-128
下采样 maxpool maxpool
模块3 conv3-256
conv3-256
conv3-256
conv3-256
conv3-256
conv3-256
conv3-256
下采样 maxpool maxpool
模块4 conv3-512
conv3-512
conv3-512
conv3-512
conv3-512
conv3-512
conv3-512
下采样 maxpool maxpool
模块5 conv3-512
conv3-512
conv3-512
conv3-512
conv3-512
conv3-512
conv3-512
下采样 maxpool maxpool
全连接层 FC-4096 FC-4096
全连接层 FC-4096 FC-4096
全连接层 FC-1000 FC-1000
softmax分类输出层 softmax softmax

上表中卷积层的参数表示为conv<卷积核大小>-<卷积核个数>,采用ReLU激活函数
可见VGG-16中包含了16个隐藏层(13个卷积层和3个全连接层),VGG-19中包含了19个隐藏层(16个卷积层和3个全连接层)
 
 
可以用可视化的形式来理解VGG的网络结构,以VGG-16为例:
VGG-16的可视化形式
上图中黑色边框代表卷积层和ReLU激活函数。红色边框代表最大池化,黄色边框代表softmax,蓝色边框代表全连接层和ReLU激活函数

Pytorch搭建VGG-16和VGG-19

在Pytorch中调用实现好的VGG-16和VGG-19

Pytorch已经实现了很多经典模型,VGG就是其中之一,同时现在我们使用VGG网络通常是采用预训练模型。在Pytorch中直接调用VGG可以用torchvision.models,并可以自己选择是否使用预训练的模型:

import torchvision
vgg16 = torchvision.models.vgg16(pretrained = False)
vgg16_pretrained = torchvision.models.vgg16(pretrained = True)
vgg19 = torchvision.models.vgg19(pretrained = False)
vgg19_pretrained = torchvision.models.vgg19(pretrained = True)

可以直接通过print查看网络结构和网络参数(将下面代码中的net替换为上面的模型变量即可):

print(net)
print(net.parameters())
print(net.named_parameters())

对于直接打印模型变量,输出结果如下(以上面的vgg16为例):

VGG(
  (features): Sequential(
    (0): Conv2d(3, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (1): ReLU(inplace=True)
    (2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (3): ReLU(inplace=True)
    (4): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
    (5): Conv2d(64, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (6): ReLU(inplace=True)
    (7): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (8): ReLU(inplace=True)
    (9): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
    (10): Conv2d(128, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (11): ReLU(inplace=True)
    (12): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (13): ReLU(inplace=True)
    (14): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (15): ReLU(inplace=True)
    (16): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
    (17): Conv2d(256, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (18): ReLU(inplace=True)
    (19): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (20): ReLU(inplace=True)
    (21): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (22): ReLU(inplace=True)
    (23): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
    (24): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (25): ReLU(inplace=True)
    (26): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (27): ReLU(inplace=True)
    (28): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (29): ReLU(inplace=True)
    (30): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
  )
  (avgpool): AdaptiveAvgPool2d(output_size=(7, 7))
  (classifier): Sequential(
    (0): Linear(in_features=25088, out_features=4096, bias=True)
    (1): ReLU(inplace=True)
    (2): Dropout(p=0.5, inplace=False)
    (3): Linear(in_features=4096, out_features=4096, bias=True)
    (4): ReLU(inplace=True)
    (5): Dropout(p=0.5, inplace=False)
    (6): Linear(in_features=4096, out_features=1000, bias=True)
  )
)

可见与之前表中的VGG-16的网络结构一致
 
而对于直接打印模型变量,输出结果如下(以上面的vgg16为例):

<generator object Module.named_parameters at 0x00000183F6A9EF90>
<generator object Module.parameters at 0x00000183F6A9EF90>

可见.parameters()方法和.named_parameters()方法会返回一个生成器(迭代器),可以直接将其转换为列表并打印出来(这样子做的结果就是一堆tensor),也可以用enumerate,只不过对于.named_parameters()方法而言返回的是len为2的tuple,第一个元素是name,第二个元素是name对应的值,如下所示:

for _,param in enumerate(net.named_parameters()):
    print(param[0])
    print(param[1])
    print('----------------')

for _,param in enumerate(net.parameters()):
    print(param)
    print('----------------')

Pytorch官方实现VGG的过程

如何参考Pytorch官方实现VGG的过程

在上一小节中,调用VGG模型使用了torchvision.models,实际上说的更具体些,是models里的vgg.py,具体在PC上的路径如下(其中的torch是自己在anaconda创建的环境名,需要根据实际情况修改):

Anaconda3\envs\torch\lib\site-packages\torchvision\models\vgg.py

下面就看一下Pytorch官方实现VGG-16和VGG-19的过程,学习一下Pytorch的用法,顺便学习一些里面用到的Python知识,值得一提的是,这里使用的是Python版本是3.9,Pytorch版本是1.12,CUDA版本是11.6,可以通过如下的代码获取:

print("Python version:", sys.version)
print("Pytorch version:", torch.__version__)
print("CUDA version:", torch.version.cuda)

在我的电脑上的运行结果如下:

Python version: 3.9.13 (main, Oct 13 2022, 21:23:06) [MSC v.1916 64 bit (AMD64)]
Pytorch version: 1.12.0
CUDA version: 11.6

vgg.py

  • 在vgg.py中首先定义了一个列表,很显然列表里的内容对应着Pytorch实现的与VGG相关的不同模型种类,具体这些模型有什么区别可以等后面看到具体的类或函数再来深究(这里只关注和VGG-16和VGG-19相关的模型):
__all__ = [
    "VGG",
    "VGG11_Weights",
    "VGG11_BN_Weights",
    "VGG13_Weights",
    "VGG13_BN_Weights",
    "VGG16_Weights",
    "VGG16_BN_Weights",
    "VGG19_Weights",
    "VGG19_BN_Weights",
    "vgg11",
    "vgg11_bn",
    "vgg13",
    "vgg13_bn",
    "vgg16",
    "vgg16_bn",
    "vgg19",
    "vgg19_bn",
]

 

  • 接下来在vgg.py中定义了VGG通用网络结构,这是非常标准的定义自己的神经网络模型的方法,通过继承nn.Module类并写好forward方法:
class VGG(nn.Module):
    def __init__(
        self, features: nn.Module, num_classes: int = 1000, init_weights: bool = True, dropout: float = 0.5
    ) -> None:
        super().__init__()
        _log_api_usage_once(self)
        self.features = features
        self.avgpool = nn.AdaptiveAvgPool2d((7, 7))
        self.classifier = nn.Sequential(
            nn.Linear(512 * 7 * 7, 4096),
            nn.ReLU(True),
            nn.Dropout(p=dropout),
            nn.Linear(4096, 4096),
            nn.ReLU(True),
            nn.Dropout(p=dropout),
            nn.Linear(4096, num_classes),
        )
        if init_weights:
            for m in self.modules():
                if isinstance(m, nn.Conv2d):
                    nn.init.kaiming_normal_(m.weight, mode="fan_out", nonlinearity="relu")
                    if m.bias is not None:
                        nn.init.constant_(m.bias, 0)
                elif isinstance(m, nn.BatchNorm2d):
                    nn.init.constant_(m.weight, 1)
                    nn.init.constant_(m.bias, 0)
                elif isinstance(m, nn.Linear):
                    nn.init.normal_(m.weight, 0, 0.01)
                    nn.init.constant_(m.bias, 0)

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        x = self.features(x)
        x = self.avgpool(x)
        x = torch.flatten(x, 1)
        x = self.classifier(x)
        return x

可见Pytorch官方在forward方法中将VGG通用网络结构分为了三个部分:featuresavgpoolclassifier,其实这对应的是上面的VGG网络结构表中的模块5的后面的部分,即通过前面的卷积神经网络(不同的VGG类型,比如VGG-16和VGG-19对应的卷积神经网络结构不同)提取特征,通过自适应平均池化(nn.AdaptiveAvgPool2d)将特征图池化到固定尺寸7×7大小,然后将特征图展平为1维向量(torch.flatten(x, 1)),最后通过分类器,即三层全连接层输出对应类别数量维度的向量(这里默认是1000维,即在类的初始化中输入的num_classes: int = 1000)

上面的代码除了定义VGG通用网络结构外,还有关于权重初始化的部分,即在类的初始化方法中if init_weights:后面的内容,这里的判断条件init_weights默认为True(init_weights: bool = True),而self.modules()会返回一个迭代器,会迭代遍历模型的所有子层(即nn.Module的子类,这里VGG、features、avgpool、classifier、池化、Linear等都是nn.Module的子类);isinstance()用于判断一个对象是否是一个已知的类型,同时会认为子类是一种父类类型,考虑继承关系。

根据上面的分析,这里的if判断语句分为了三个部分:nn.Conv2d、nn.BatchNorm2d和nn.Linear,分别对这三部分进行初始化,初始化函数有三种,其功能分别如下:

(1)nn.init.kaiming_normal_:何恺明大神提出的参数初始化(论文:Delving deep into rectifiers: Surpassing human-level performance on ImageNet classification)
(2)nn.init.constant_(tensor, val):用val的值填充整个张量
(3)nn.init.normal_(tensor, mean=0.0, std=1.0):从给定均值和标准差的正态分布中生成值,填充输入的张量

介绍了Pytorch搭建神经网络的相关知识,下面就介绍一下代码中有关Python的技巧:

(1)在初始化方法和forward方法传入的参数中,每一个都有一个冒号,比如num_classes: int = 1000,这些参数的冒号其实是参数的类型建议符,目的是告诉使用程序的人希望传入的实参的类型,类型建议符并非强制规定和检查,即使传入的实际参数与建议参数不符,也不会报错
(2)同时初始化方法和forward方法后面都跟着一个箭头,比如-> torch.Tensor,这是说明该方法返回的值是什么类型

 

  • 接下来在vgg.py中定义了函数make_layers和_vgg以及字典cfgs和_COMMON_META,这些都是构成后面具体网络结构,比如VGG-16和VGG-19的基础:
def make_layers(cfg: List[Union[str, int]], batch_norm: bool = False) -> nn.Sequential:
    layers: List[nn.Module] = []
    in_channels = 3
    for v in cfg:
        if v == "M":
            layers += [nn.MaxPool2d(kernel_size=2, stride=2)]
        else:
            v = cast(int, v)
            conv2d = nn.Conv2d(in_channels, v, kernel_size=3, padding=1)
            if batch_norm:
                layers += [conv2d, nn.BatchNorm2d(v), nn.ReLU(inplace=True)]
            else:
                layers += [conv2d, nn.ReLU(inplace=True)]
            in_channels = v
    return nn.Sequential(*layers)


cfgs: Dict[str, List[Union[str, int]]] = {
    "A": [64, "M", 128, "M", 256, 256, "M", 512, 512, "M", 512, 512, "M"],
    "B": [64, 64, "M", 128, 128, "M", 256, 256, "M", 512, 512, "M", 512, 512, "M"],
    "D": [64, 64, "M", 128, 128, "M", 256, 256, 256, "M", 512, 512, 512, "M", 512, 512, 512, "M"],
    "E": [64, 64, "M", 128, 128, "M", 256, 256, 256, 256, "M", 512, 512, 512, 512, "M", 512, 512, 512, 512, "M"],
}


def _vgg(cfg: str, batch_norm: bool, weights: Optional[WeightsEnum], progress: bool, **kwargs: Any) -> VGG:
    if weights is not None:
        kwargs["init_weights"] = False
        if weights.meta["categories"] is not None:
            _ovewrite_named_param(kwargs, "num_classes", len(weights.meta["categories"]))
    model = VGG(make_layers(cfgs[cfg], batch_norm=batch_norm), **kwargs)
    if weights is not None:
        model.load_state_dict(weights.get_state_dict(progress=progress))
    return model


_COMMON_META = {
    "min_size": (32, 32),
    "categories": _IMAGENET_CATEGORIES,
    "recipe": "https://github.com/pytorch/vision/tree/main/references/classification#alexnet-and-vgg",
    "_docs": """These weights were trained from scratch by using a simplified training recipe.""",
}

可见make_layers函数是根据配置表返回模型层列表;而_vgg函数实现了整个网络模型的生成以及预训练权重的导入。由于上面的代码涉及到一些新的数据类型,这里先对这些进行讲解,即List、Union、Dict,这些数据类型来自typing模块

最后也介绍一下这一段代码使用的Python技巧:

  • 在make_layers函数的返回值中,其返回中传入的参数带有*,即nn.Sequential(*layers),事实上,很多传入的参数还带有两个星号
posted @ 2023-10-03 15:18  Destiny_zxx  阅读(288)  评论(0编辑  收藏  举报