Pytorch入門中 —— 搭建網路模型

WINLSR發表於2021-12-16

本節內容參照小土堆的pytorch入門視訊教程,主要通過查詢文件的方式講解如何搭建卷積神經網路。學習時要學會查詢文件,這樣會比直接搜尋良莠不齊的部落格更快、更可靠。講解的內容主要是pytorch核心包中TORCH.NN中的內容(nnNeural Netwark的縮寫)。

image-20211212100053540

通常,我們定義的神經網路模型會繼承torch.nn.Module類,該類為我們定義好了神經網路骨架。

image-20211212142221857

卷積層

對於影像處理來說,我們通常使用二維卷積,即使用torch.nn.Conv2d類:

image-20211212145717587

建立該類時,我們通常只需要傳入以下幾個引數,其他不常用引數入門時可以不做了解,使用預設值即可,以後需要時再查詢文件:

in_channels (int):輸入資料的通道數,圖片通常為3
out_channels (int):輸出資料的通道數,也是卷積核的個數
kernel_size (int or tuple):卷積核大小,傳入int表示正方形,傳入tuple代表高和寬
stride (int or tuple, optional):卷積操作的步長,傳入int代表橫向和縱向步長相同,預設為1
padding (int, tuple or str, optional):填充厚度,傳入int代表上下左右四個邊填充厚度相同,預設為0,即不填充
padding_mode (string, optional):填充模式,預設為'zeros',即0填充

卷積操作後輸出的張量的高和寬計算公式如下:

image-20211212153137648

其中inputoutput中的N代表BatchSizeC代表通道數,他們不影響HW的計算。在保持dilation為預設值1的情況下,計算公式可簡化為如下:

\[H_{out} = \left\lfloor\frac{H_{in} + 2 \times \text{padding}[0] -\text{kernel\_size}[0]}{\text{stride}[0]} + 1\right\rfloor \]

\[W_{out} = \left\lfloor\frac{W_{in} + 2 \times \text{padding}[1] - \text{kernel\_size}[1]}{\text{stride}[1]} + 1\right\rfloor \]

池化層

常用的二維最大池化定義在torch.nn.MaxPool2d類中:

image-20211212160443655

建立該類時,我們通常只需要傳入以下幾個引數,其他不常用引數入門時可以不做了解,使用預設值即可,以後需要時再查詢文件:

kernel_size:池化操作時的視窗大小
stride:池化操作時的步長,預設為kernel_size
padding:每個邊的填充厚度(0填充)

池化操作後輸出的張量的高和寬計算公式與卷積操作後的計算公式相同。

非線性啟用

常見的ReLU啟用定義在torch.nn.ReLU類中:

image-20211212163925272

引數inplace代表是否將ouput直接修改在input中。

線性層

線性層的定義在torch.nn.Linear類中:

image-20211212180532344

建立線性層使用的引數如下:

in_features:輸入特徵大小
out_features:輸出特徵大小
bias:是否新增偏置,預設為True

模型搭建示例

下圖是一個CIFAR10資料集上的分類模型,下面將根據圖片進行模型程式碼的編寫。

Structure-of-CIFAR10-quick-model

1.由於CIFAR10資料集中圖片為3*32*32,所以圖中模型的輸入為3通道,高寬都為32的張量。

2.使用 5*5的卷積核進行卷積操作,得到通道數為32,高和寬為32的張量。因此我們可以推出該卷積層的引數如下:

in_channels = 3
out_channels = 32
kernel_size = 5
stride = 1
padding = 2

注:將 Hin = 32,Hout  = 32 以及kernal_size[0] = 5三個引數帶入:

\[H_{out} = \left\lfloor\frac{H_{in} + 2 \times \text{padding}[0] -\text{kernel\_size}[0]}{\text{stride}[0]} + 1\right\rfloor \]

有:

\[32 = \left\lfloor\frac{32 + 2 \times \text{padding}[0] -\text{5}}{\text{stride}[0]} + 1\right\rfloor \]

發現stride[0] = 1padding[0] = 2可以使得等式成立。同理可以得到stride[1] = 1padding[1] = 2

3.使用2*2的核進行最大池化操作,得到通道數為32,高和寬為16的張量。可以推出該池化層的引數如下:

kernel_size = 2
stride = 2
padding = 0

注:stridepadding推導方式與2中相同。

4.使用 5*5的卷積核進行卷積操作,得到通道數為32,高和寬為16的張量。因此我們可以推出該卷積層的引數如下:

in_channels = 32
out_channels = 32
kernel_size = 5
stride = 1
padding = 2

5.使用2*2的核進行最大池化操作,得到通道數為32,高和寬為8的張量。可以推出該池化層的引數如下:

kernel_size = 2
stride = 2
padding = 0

6.使用 5*5的卷積核進行卷積操作,得到通道數為64,高和寬為8的張量。因此我們可以推出該卷積層的引數如下:

in_channels = 32
out_channels = 64
kernel_size = 5
stride = 1
padding = 2

7.使用2*2的核進行最大池化操作,得到通道數為64,高和寬為4的張量。可以推出該池化層的引數如下:

kernel_size = 2
stride = 2
padding = 0

8.將64*4*4的張量進行展平操作得到長為1024的向量。

9.將長為1024的向量進行線性變換得到長為64的向量(隱藏層),可以推出該線性層的引數如下:

in_features:1024
out_features:64

10.將長為64的向量進行線性變換得到長為10的向量,可以推出該線性層的引數如下:

in_features:64
out_features:10

因此,模型程式碼如下:

from torch import nn

class MyModel(nn.Module):
    def __init__(self):
        super(MyModel, self).__init__()
        self.conv1 = nn.Conv2d(3, 32, 5, padding=2)
        self.max_pool1 = nn.MaxPool2d(2)
        self.conv2 = nn.Conv2d(32, 32, 5, padding=2)
        self.max_pool2 = nn.MaxPool2d(2)
        self.conv3 = nn.Conv2d(32, 64, 5, padding=2)
        self.max_pool3 = nn.MaxPool2d(2)
        self.flatten = nn.Flatten()
        self.linear1 = nn.Linear(1024, 64)
        self.linear2 = nn.Linear(64, 10)

    # 必須覆蓋該方法,該方法會在例項像函式一樣呼叫時被呼叫,後面會有示例
    def forward(self, x):
        x = self.conv1(x)
        x = self.max_pool1(x)
        x = self.conv2(x)
        x = self.max_pool2(x)
        x = self.conv3(x)
        x = self.max_pool3(x)
        x = self.flatten(x)
        x = self.linear1(x)
        x = self.linear2(x)
        return x

sequential

使用torch.nn.sequential可以簡化模型的搭建程式碼,他是一個順序存放Module的容器。當sequential執行時,會按照Module在建構函式中的先後順序依次呼叫,前面Module的輸出會作為後面Module的輸入。

使用sequential,上一節的程式碼可以簡化為:

from torch import nn

class MyModel(nn.Module):
    def __init__(self):
        super.__init__(MyModel, self)
        self.model = nn.Sequential(
            nn.Conv2d(3, 32, 5, padding=2),
            nn.MaxPool2d(2),
            nn.Conv2d(32, 32, 5, padding=2),
            nn.MaxPool2d(2),
            nn.Conv2d(32, 64, 5, padding=2),
            nn.MaxPool2d(2),
            nn.Flatten(),
            nn.Linear(1024, 64),
            nn.Linear(64, 10)
        )

    def forward(self, x):
        x = self.model(x)
        return x

損失函式、反向傳播以及優化器

上面兩節我們已經將CIFAR10的分類模型搭建好,但還需要進行訓練後才能用來預測分類。訓練模型時,會用損失函式來衡量模型的好壞,並利用反向傳播來求梯度,然後利用優化器對模型引數進行梯度下降,多次迴圈往復以訓練出最優的模型。

模型訓練程式碼如下:

import torch
from torch.optim import SGD
import torchvision
from torch.utils.data import DataLoader
from cifar10_model import MyModel
from torch import nn
from torch.utils import tensorboard


def train():
    # 獲取 cifar10 資料集
    root = "./dataset"
    transform = torchvision.transforms.Compose([
        torchvision.transforms.ToTensor()
    ])
    train_cifar10 = torchvision.datasets.CIFAR10(root=root, train=True,
                                                 transform=transform,
                                                 download=True)

    # 建立dataloader
    train_dataloader = DataLoader(dataset=train_cifar10, batch_size=64,
                                  shuffle=True,
                                  num_workers=16)

    # 建立模型
    model = MyModel()
    # 建立交叉熵損失函式
    loss = nn.CrossEntropyLoss()
    # 建立優化器,傳入需要更新的引數,以及學習率
    optim = SGD(model.parameters(), lr=0.01)

    # 建立 SummaryWriter
    writer = tensorboard.SummaryWriter("logs")
    # 寫入模型圖,隨機生成一個輸入
    writer.add_graph(model, torch.randn(64, 3, 32, 32))

    for epoch in range(20):
        loss_temp = 0.0

        for batch_num, batch_data in enumerate(train_dataloader):
            images, targets = batch_data
            # 像呼叫方法一樣呼叫例項
            outputs = model(images)
            loss_res = loss(outputs, targets)
            loss_temp = loss_res
            # 清空前一次計算的梯度
            optim.zero_grad()
            # 反向傳播求梯度
            loss_res.backward()
            # 更新引數
            optim.step()
        # 記錄每個epoch之後的loss
        writer.add_scalar("Loss/train", loss_temp, epoch)

    writer.close()


if __name__ == "__main__":
    train()

模型圖如下:

png

損失函式隨訓練週期的下降情況如下:

image-20211213191734504

相關文章