close

4.6 經典Model代碼分析

torchvision中提供了一些經典的卷積神經網路模型實現,本小節將挑選部分進行分析,學習torchvision是如何構建複雜的網路模型,學習它們的代碼風格、代碼規範。

AlexNet

出自:ImageNet Classification with Deep Convolutional Neural Networks

模型結構圖如下圖所示:整體可分為前半部分的特徵提取與後半部分的分類。

​ <<AI人工智慧 PyTorch自學>> 4.6 經典Mod

代碼分析:

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

Copy

從此也知道,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:

Copy

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

Copy

總結

採用函數形式封裝模型類,額外提供預訓練權重載入功能;

Linear層之前可通過torch.flatten將數據變為一維向量;

VGG

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

其共有4種深度,分別是11 13 16 19層,用得比較多的VGG1619VGG的代碼就比AlexNet的代碼複雜了,因為它涉及8個具體的網路模型定義,因此不能再使用面向過程的方式進行編寫,需要將共性的部分抽象出來,這一份代碼值得新手仔細、認真學習

首先是大體瞭解VGG整體結構,網路結構示意圖如下圖所示:

​ <<AI人工智慧 PyTorch自學>> 4.6 經典Mod

VGG最大特點是23x333x3卷積層的堆疊,並且堆疊總共分5次,最後接入三個FC層。從此可知,核心是如何將特徵提取部分進行抽象,請大家帶著這個問題觀察代碼:D:\Anaconda_data\envs\pytorch_1.10_gpu\Lib\site-packages\torchvision\models\vgg.py

vgg.py中定義了VGG類、_vgg函數、make_layers函數、cfgs字典,以及一系列具體網路模型封裝的函數,如vgg11vgg13 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)

Copy

可知道,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

Copy

可知道_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)

Copy

從這裡可知道是對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

Copy

可以發現它的forward十分簡潔,因為vgg模型就是以簡潔出名的,像一個糖葫蘆一樣串起來即可。接著去看看self.features是什麼,怎麼來的,這個需要到init函數中尋找。

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

由此可知道,VGG特徵提取部分的網路層均是通過make_layers函式定義的那個Sequential

接著36行代碼的classifier就沒啥好說的。

接著的第45行代碼出現了新內容,權重初始化。調用了_initialize_weights函數對VGG模型進行權重初始化。眾所周知,良好的權重初始化對模型訓練是至關重要的,早期對於權重初始化有許多的研究,比較著名的有Xavier方法、MSRAKaiming)方法。

預告:具體的權重初始化方法將在下一小節詳細介紹。

下面觀察如何編寫函數對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)

Copy

此函數的邏輯遍歷所有Module,並判斷Module類型,根據不同的Module類型設置不同的初始化方法,如卷積層則用kaiming方法設置weightbias全部設置為0BN層的weight設置為1bias設置為0;全連接層的weight用正態分佈進行隨機初始化,bias設置為0

到這裡一個具體的VGG模型定義就講完了,下面總結一下它們的調用關係與邏輯。

vgg16() --> _vgg() --> make_layers --> VGG:最核心在於如何構建一個模組(函數也好、類也好)可以接收不同的參數(cfgs)就能生成對應VGG的特徵提取部分的網路層(一個大的Sequential)。

GoogLeNet

GoogLeNet-V1 出自 Going deeper with convolutions,後續也有V2V3V4,這裡不進行介紹。

V1的提出最大的特點在於提出Inception模組,它是一個多分支的特徵提取模組,如下圖所示:

​ <<AI人工智慧 PyTorch自學>> 4.6 經典Mod

網路結構如下圖所示:

​ <<AI人工智慧 PyTorch自學>> 4.6 經典Mod

代碼並不複雜,但其中的Inception模組的編寫,是之前沒有遇到的,可以借鑒學習。

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

可以看到熟悉的定義了具體的Module——GoogLeNet類,模型的封裝調用函數——googlenet,以及從GoogLeNet模型抽象出來的、反復需要使用的模組——InceptionInceptionAuxBasicConv2d

這裡面的代碼並不複雜,這裡不逐行分析,只把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]

Copy

Resnet

ResNet出自何愷明的《Deep Residual Learning for Image Recognition》,是目前工業界應用最廣泛的卷積神經網路。

網路結構如下圖所示,有ResNet-18 34 50 101 152,使用較多的為ResNet-50。其結構特點也是模組的堆疊,如表格中看到的x2 x3x4, x6表示的是一個模組堆疊2次、3次、4次、6次。

​ <<AI人工智慧 PyTorch自學>> 4.6 經典Mod

resnet模型中,最大的特點在於採用了殘差結構的模組,如下圖所示:

​ <<AI人工智慧 PyTorch自學>> 4.6 經典Mod

這裡有兩種形式,一種是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...一系列具體模型函數
  • 抽象出來的基礎模組:BasicBlockBottleneckconv1x1conv3x3

這其中最為特色的是BasicBlockBottleneck,分別對應論文圖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)

Copy

在此函數中使用block(是一個基礎模組的類,是BasicBlockBottleneck)定義網路層,然後堆疊起來,最後使用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])

Copy

​ <<AI人工智慧 PyTorch自學>> 4.6 經典Mod

在這裡,可以發現resnet1834用的是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)

Copy

BasicBlockBottleneck的使用與googlenet中的blocks呼應上了,請大家仔細對比。

resnet總結

resnet的搭建是將block抽象出來提供介面,由使用者自行傳入,並且設定堆疊次數,如resnet18就是BasicBlock, [2, 2, 2, 2] resnet50就是 Bottleneck, [3, 4, 6, 3],處處體現了物件導向的程式設計思維,值得學習。

總結

本小節從簡單的AlexNet到複雜的ResNet進行了代碼分析,剖析了pytorch的代碼結構,編寫邏輯以及思想,其中物件導向的思維值得認真學習借鑒。

VGG中的make_layers():通過參數配置形式搭建一個大的Sequential

GoogLeNetBasicConv2d, Inception, InceptionAuxResNetBasicBlockBottleneckconv1x1conv3x3都是抽象的基礎模組。

本小節在多出看到了權重初始化方法,好的權重初始化是模型訓練的第一步,下一小節將介紹pytorch提供的系列權重初始化方法及其應用。

 

arrow
arrow
    全站熱搜
    創作者介紹
    創作者 HCHUNGW 的頭像
    HCHUNGW

    HCHUNGW的部落格

    HCHUNGW 發表在 痞客邦 留言(0) 人氣()