4.6 经典Model代码分析

torchvision中提供了一些经典的卷积神经网络模型实现,本小节将挑选部分进行分析,学习torchvision是如何构建复杂的网络模型,学习它们的代码风格、代码规范。

AlexNet

出自:ImageNet Classification with Deep Convolutional Neural Networks

模型结构图如下图所示:整体可分为前半部分的特征提取与后半部分的分类。

0

代码分析:

D:\Anaconda_data\envs\pytorch_1.10_gpu\Lib\site-packages\torchvision\models\alexnet.py

代码中定义了一个AlexNet类与一个alexnet函数,这样的封装形式贯穿整个torchvision的模型定义。

AlexNet类是nn.Module,其中定义了AlexNet模型的具体结构,而alexnet函数则是对Alexnet类的包装,并且实现加载预训练参数的功能,即以下代码:

    model = AlexNet(**kwargs)
    if pretrained:
        state_dict = load_state_dict_from_url(model_urls["alexnet"], progress=progress)
        model.load_state_dict(state_dict)
    return model

从此也知道,torchvision中定义模型所采用的预训练模型均是通过指定的url下载,并存储于本地磁盘供下一次使用。

由于“网络问题”,通常建议大家通过代码中给出的url自行下载权重文件,然后在自己的代码中使用load_state_dict方法加载预训练参数。

分析了alexnet.py整体结构,下面回到AlexNet类本身,看看具体模型如何写的。

class AlexNet(nn.Module):
    def __init__(self, num_classes: int = 1000) -> None:
    def forward(self, x: torch.Tensor) -> torch.Tensor:

AlexNet采用熟悉的方式定义了两个函数,熟悉4.1小结中的知识点的话,这里不比多说。

forward函数第48行代码值得注意,二维特征图要输入到Linear层,通常通过flatten函数对特征图进行变换。

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

总结

采用函数形式封装模型类,额外提供预训练权重加载功能;

Linear层之前可通过torch.flatten将数据变为一维向量;

VGG

VGG出自:Very Deep Convolutional Networks For Large-Scale Image Recognition

其共有4种深度,分别是11, 13, 16, 19层,用得比较多的VGG16、19。VGG的代码就比AlexNet的代码复杂了,因为它涉及8个具体的网络模型定义,因此不能再使用面向过程的方式进行编写,需要将共性的部分抽象出来,这一份代码值得新手仔细、认真学习

首先是大体了解VGG整体结构,网络结构示意图如下图所示:

0

VGG最大特点是2个3x3、3个3x3卷积层的堆叠,并且堆叠总共分5次,最后接入三个FC层。从此可知,核心是如何将特征提取部分进行抽象,请大家带着这个问题观察代码:D:\Anaconda_data\envs\pytorch_1.10_gpu\Lib\site-packages\torchvision\models\vgg.py

vgg.py中定义了VGG类、_vgg函数、make_layers函数、cfgs字典,以及一系列具体网络模型封装的函数,如vgg11,vgg13, vgg16等。

看过alexnet.py,这里能猜出VGG类是一个nn.module。

_vgg函数:vgg函数接收具体的网络参数,以此决定返回哪一个vgg模型;

vggxxx:定义了具体VGG所需要的参数,并调用_vgg函数得到具体模型;

make_layers函数:创建可抽象出来、共性的网络层函数,即网络结构图中的5次堆叠部分

cfgs字典:配置各具体vgg模型所需要的参数,主要在make_layers中使用。

下面以vgg16为例,观察vgg.py是如何实现它的。

看到153行代码:

def vgg16(pretrained: bool = False, progress: bool = True, **kwargs: Any) -> VGG:
    return _vgg("vgg16", "D", False, pretrained, progress, **kwargs)

可知道,vgg16是对_vgg的封装,并且固定了两个参数"vgg16" 和 "D"。

跳到94行代码:

def _vgg(arch: str, cfg: str, batch_norm: bool, pretrained: bool, progress: bool, **kwargs: Any) -> VGG:
    if pretrained:
        kwargs["init_weights"] = False
    model = VGG(make_layers(cfgs[cfg], batch_norm=batch_norm), **kwargs)
    if pretrained:
        state_dict = load_state_dict_from_url(model_urls[arch], progress=progress)
        model.load_state_dict(state_dict)
    return model

可知道_vgg调用了VGG类得到最终的模型,并且给VGG传入了make_layers函数创建的网络层;

通过这行代码可知道,需要进入make_layers去观察如何创建网络层的。进入make_layers前,需要知道cfgs[cfg]当前传入的是:

'D': [64, 64, 'M', 128, 128, 'M', 256, 256, 256, 'M', 512, 512, 512, 'M', 512, 512, 512, 'M'],

跳到69行代码:

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)

从这里可知道是对cfg中进行for循环,不断的构建网络层,并且添加到list中,最后组装成一个Sequential的形式。这里的代码逻辑就是网络结构图中的抽象,把四种模型的共性地方抽象出来,然后通过不同的配置参数可生成vgg11, vgg13, vgg16, vgg19。这里的代码值得学习

弄清楚make_layers是生成前面一系列卷积层的堆叠Sequential之后,继续进入VGG类观察。

跳到25行代码,看一个Module,可以先看forward函数,再看forward中的属性是怎么来的。

    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

可以发现它的forward十分简洁,因为vgg模型就是以简洁出名的,像一个糖葫芦一样串起来即可。接着去看看self.features是什么,怎么来的,这个需要到init函数中寻找。

跳到34行代码:self.features = features

由此可知道,VGG特征提取部分的网络层均是通过make_layers函数定义的那个Sequential。

接着36行代码的classifier就没啥好说的。

接着的第45行代码出现了新内容,权重初始化。调用了_initialize_weights函数对VGG模型进行权重初始化。众所周知,良好的权重初始化对模型训练是至关重要的,早期对于权重初始化有许多的研究,比较著名的有Xavier方法、MSRA(Kaiming)方法。

预告:具体的权重初始化方法将在下一小节详细介绍。

下面观察如何编写函数对VGG进行权重初始化:跳转55行

def _initialize_weights(self) -> None:
    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)

此函数的逻辑遍历所有Module,并判断Module类型,根据不同的Module类型设置不同的初始化方法,如卷积层则用kaiming方法设置weight,bias全部设置为0;BN层的weight设置为1,bias设置为0;全连接层的weight用正态分布进行随机初始化,bias设置为0。

到这里一个具体的VGG模型定义就讲完了,下面总结一下它们的调用关系与逻辑。

vgg16() --> _vgg() --> make_layers --> VGG:最核心在于如何构建一个模块(函数也好、类也好)可以接收不同的参数(cfgs)就能生成对应VGG的特征提取部分的网络层(一个大的Sequential)。

GoogLeNet

GoogLeNet-V1 出自 Going deeper with convolutions,后续也有V2,V3,V4,这里不进行介绍。

V1的提出最大的特点在于提出Inception模块,它是一个多分支的特征提取模块,如下图所示:

0

网络结构如下图所示:

0

代码并不复杂,但其中的Inception模块的编写,是之前没有遇到的,可以借鉴学习。

观察D:\Anaconda_data\envs\pytorch_1.10_gpu\Lib\site-packages\torchvision\models\googlenet.py

可以看到熟悉的定义了具体的Module——GoogLeNet类,模型的封装调用函数——googlenet,以及从GoogLeNet模型抽象出来的、反复需要使用的模块——Inception、InceptionAux、BasicConv2d。

这里面的代码并不复杂,这里不逐行分析,只把GoogLeNet类的逻辑关系理一理。

首先,将反复使用的模块抽象成一个类,这样在使用的时候只需要一行代码即可定义好,如BasicConv2d:包含了卷积层+BN层;

Inception:包含四个分支的处理并合并最终特征图;

InceptionAux:辅助分类层输出。

然后在init函数中像搭积木一样,把需要用到的模块逐一定义

最后在forward函数中调用定义好的网络层即可。

总结:

反复使用的模块抽象为一个Moudle类,并作为参数进行调用。好处在于当想修改这些基础元素模块的时候,仅需要重新写一个Module类替换即可,并不需要改动GoogLeNet类当中的任何代码(在resnet中会有体现)。要想理解好这一点,请仔细体会这几行代码

blocks = [BasicConv2d, Inception, InceptionAux]
conv_block = blocks[0]
inception_block = blocks[1]
inception_aux_block = blocks[2]

Resnet

ResNet出自何恺明的《Deep Residual Learning for Image Recognition》,是目前工业界应用最广泛的卷积神经网络。

网络结构如下图所示,有ResNet-18, 34, 50, 101, 152,使用较多的为ResNet-50。其结构特点也是模块的堆叠,如表格中看到的x2, x3,x4, x6表示的是一个模块堆叠2次、3次、4次、6次。

0

在resnet模型中,最大的特点在于采用了残差结构的模块,如下图所示:

0

这里有两种形式,一种是BasicBlock,另外一种是resnet50/101/152用的Bottleneck。

下面就来看看D:\Anaconda_data\envs\pytorch_1.10_gpu\Lib\site-packages\torchvision\models\resnet.py

是如何实现这一系列复杂的resnet模型。

提示:pycharm中按住Ctrl+Shift+ "-" ,可以把代码块收起来,可以快速浏览resnet.py下的主要内容,可以发现,还是熟悉的结构,分别有

  • ResNet类
  • _resnet函数
  • resnet18\34\50...一系列具体模型函数
  • 抽象出来的基础模块:BasicBlock、Bottleneck、conv1x1和conv3x3。

这其中最为特色的是BasicBlock和Bottleneck,分别对应论文图5中的两个结构,它们将在不同的模型中使用。

下面就看看BasicBlockBottleneck到底是如何使用的。

跳到144行代码:class ResNet(nn.Module),观察init函数里是如何使用block的。

跳到第178行代码:self.layer1 = self._make_layer(block, 64, layers[0]),在make_layer函数中使用了block进行网络层的构建。这点与VGG中的make_layers类似。

跳到205行代码:

    def _make_layer(
        self,
        block: Type[Union[BasicBlock, Bottleneck]],
        planes: int,
        blocks: int,
        stride: int = 1,
        dilate: bool = False,
    ) -> nn.Sequential:
        norm_layer = self._norm_layer
        downsample = None
        previous_dilation = self.dilation
        if dilate:
            self.dilation *= stride
            stride = 1
        if stride != 1 or self.inplanes != planes * block.expansion:
            downsample = nn.Sequential(
                conv1x1(self.inplanes, planes * block.expansion, stride),
                norm_layer(planes * block.expansion),
            )

        layers = []
        layers.append(
            block(
                self.inplanes, planes, stride, downsample, self.groups, self.base_width, previous_dilation, norm_layer
            )
        )
        self.inplanes = planes * block.expansion
        for _ in range(1, blocks):
            layers.append(
                block(
                    self.inplanes,
                    planes,
                    groups=self.groups,
                    base_width=self.base_width,
                    dilation=self.dilation,
                    norm_layer=norm_layer,
                )
            )

        return nn.Sequential(*layers)

在此函数中使用block(是一个基础模块的类,是BasicBlock或Bottleneck)定义网络层,然后堆叠起来,最后使用Sequential进行包装,构成一个整体。

回到init函数可知道,178-184行代码所构建的模块对应了网络结构的四个部分,对应关系如下图所示:

        self.layer1 = self._make_layer(block, 64, layers[0])
        self.layer2 = self._make_layer(block, 128, layers[1], stride=2, dilate=replace_stride_with_dilation[0])
        self.layer3 = self._make_layer(block, 256, layers[2], stride=2, dilate=replace_stride_with_dilation[1])
        self.layer4 = self._make_layer(block, 512, layers[3], stride=2, dilate=replace_stride_with_dilation[2])

0

在这里,可以发现resnet18和34用的是BasicBlock, resnet50/101/152用的是Bottleneck

def resnet18(pretrained: bool = False, progress: bool = True, **kwargs: Any) -> ResNet:
    return _resnet("resnet18", BasicBlock, [2, 2, 2, 2], pretrained, progress, **kwargs)

def resnet50(pretrained: bool = False, progress: bool = True, **kwargs: Any) -> ResNet:
    return _resnet("resnet50", Bottleneck, [3, 4, 6, 3], pretrained, progress, **kwargs)

BasicBlock和Bottleneck的使用与googlenet中的blocks呼应上了,请大家仔细对比。

resnet总结

resnet的搭建是将block抽象出来提供接口,由用户自行传入,并且设定堆叠次数,如resnet18就是BasicBlock, [2, 2, 2, 2], resnet50就是 Bottleneck, [3, 4, 6, 3],处处体现了面向对象的编程思维,值得学习。

总结

本小节从简单的AlexNet到复杂的ResNet进行了代码分析,剖析了pytorch的代码结构,编写逻辑以及思想,其中面向对象的思维值得认真学习借鉴。

VGG中的make_layers():通过参数配置形式搭建一个大的Sequential;

GoogLeNet的BasicConv2d, Inception, InceptionAux、ResNet的BasicBlock、Bottleneck、conv1x1、conv3x3都是抽象的基础模块。

本小节在多出看到了权重初始化方法,好的权重初始化是模型训练的第一步,下一小节将介绍pytorch提供的系列权重初始化方法及其应用。

Copyright © TingsongYu 2021 all right reserved,powered by Gitbook文件修订时间: 2024年04月26日21:48:10

results matching ""

    No results matching ""