Pytorch.nn.Conv2d詳解

Medlen發表於2020-11-22

首先看一下這個類的定義:

class Conv2d(_ConvNd):
    # 初始化函式,這裡主要了解有哪些引數傳進來就可以了
    def __init__(
        self,
        in_channels: int,
        out_channels: int,
        kernel_size: _size_2_t,
        stride: _size_2_t = 1,
        padding: _size_2_t = 0,
        dilation: _size_2_t = 1,
        groups: int = 1,
        bias: bool = True,
        padding_mode: str = 'zeros'  # TODO: refine this type
    ):
    #————————————————看到這就可以了————————————————————
        kernel_size = _pair(kernel_size)
        stride = _pair(stride)
        padding = _pair(padding)
        dilation = _pair(dilation)
        super(Conv2d, self).__init__(
            in_channels, out_channels, kernel_size, stride, padding, dilation,
            False, _pair(0), groups, bias, padding_mode)

    def _conv_forward(self, input, weight):
        if self.padding_mode != 'zeros':
            return F.conv2d(F.pad(input, self._reversed_padding_repeated_twice, mode=self.padding_mode),
                            weight, self.bias, self.stride,
                            _pair(0), self.dilation, self.groups)
        return F.conv2d(input, weight, self.bias, self.stride,
                        self.padding, self.dilation, self.groups)
	# 前向傳播計算
    def forward(self, input: Tensor) -> Tensor:
        return self._conv_forward(input, self.weight)

上面就是Cov2d這個類的原始碼實現,是不是感覺程式碼特別少?是不是突然對掌握它信心倍增!

從整體上來看:

Conv2d是一個類,它包含了做卷積運算所需要的引數(__init__函式),以及卷積操作(forward函式)。

再來看一下它的詳細引數:

一共九個引數,一般用前三個就可以處理一般的任務:

  1. in_channels :輸入通道數目
  2. out_channels :輸出通道數目
  3. kernel_size :卷積核大小,如果輸入是一個值,比如 3 3 3,那麼卷積核大小就是 3 × 3 3 \times 3 3×3 ,如果不想卷積核寬和高相等,還可以輸入tuple型別資料,比如: ( 3 , 5 ) (3, 5) (3,5)
  4. stride :步長大小,跟上面卷積核引數一樣,如果輸入是一個值,比如 2 2 2 ,步長就是 2 × 2 2 \times 2 2×2 ,還可以輸入元組 ( 2 , 1 ) (2, 1) (2,1) ,表示卷積核每次向右移動 1 1 1 個步長,向下移動 2 2 2 個步長。
  5. padding :填充,參數列示在周圍補0的情況。補0的方向為上、下、左、右四個方向。如果是輸入是單個值,比如1,就是在上下左右四個方向補一圈0。如果輸入是元組比如 (2,1) ,則表示在上方補兩行,左邊補兩列,下方補一行,右邊補一列。發現規律沒有。第一個值控制上、左兩個方向的填充,第二個值控制下、右兩個方向的填充。
  6. dilation :進行擴充套件卷積需要的引數。
  7. groups :進行分組卷積需要的引數。(有需要自行深入瞭解)
  8. bias :偏置,布林型別,預設為 True ,即增加一個學習的偏置項。
  9. padding_mode :填充的模式,預設是 zero ,還可以選擇 reflectreplicatecircular 。(有需要自行深入瞭解)

首先是關於輸入通道數 in_channels 和輸出通道數 out_channels 的解釋:

如果對通道是什麼還不太理解可以去看我的這篇部落格:如何理解卷積神經網路中的通道(channel)

如果卷積核輸入的是原始的圖片,那麼我們可以根據圖片型別決定圖片的輸入通道數,如果是RGB圖片, in_channels=3 ,如果是灰度影像,in_channels=1 。輸出通道數需要我們自己指定,具體指定多少需要根據經驗。

如果是第二層或者更多層卷積,當前層的輸入通道數就是上一層卷積的輸出通道數,當前層的輸出通道數需要自己指定。

關於卷積核的解釋:

這裡的卷積核引數只是指定卷積核的寬度和高度,對於多通道的卷積,程式碼內部會根據輸入通道數初始化對應的卷積核數目,即:如果輸入是3通道,那麼就會有三個我們指定寬高的卷積核做相應的卷積操作。

關於步長的解釋:

步長指的是卷積核在輸入矩陣的上每次移動幾步。移動方向由兩個,向右或者向下。分別由兩個值指定。

關於填充的解釋:

填充的主要目的是為了充分利用輸入圖片的邊緣資訊。

關於為什麼需要填充,可以參考這篇部落格:CNN基礎知識——卷積(Convolution)、填充(Padding)、步長(Stride)

關於擴張(dilation)引數的解釋:

本質上就是擴大卷積核,比如將 3 × 3 3 \times 3 3×3 的擴大為 7 × 7 7 \times 7 7×7 的,然後對擴大的部分用0進行填充,

目的是擴大感受野,捕獲多尺度上下文資訊。

具體怎麼擴張,詳細請參考:

【1】如何理解擴張卷積(dilated convolution)

【2】總結-空洞卷積(Dilated/Atrous Convolution)

關於分組(group)的解釋:

對於一般的卷積神經網路,假設通道數有100個,那麼我們需要100個卷積核對這100個通道分別做卷積操作,然後求和,最後得到1個特徵圖。如果我們需要得到30個特徵圖,就需要30*100=3000個卷積核。引數量非常大。

而對於分組卷積,依然假設通道數有100個,我們分成兩組,每組50個通道數,分別對每組單獨做卷積操作,每組需要50個卷積核,總的卷積核數目沒變,但是我們每組可以得到2個特徵圖。如果我們分成4組,每組25個通道數,最終可以得到4個特徵圖。

也就是說,我們在不增加引數量的情況下,得到了更多的特徵圖。這是分組的優勢之一,對於分組的通道,我們可以在不同的GPU上並行訓練,加速訓練過程。

更詳細的解釋參考:

卷積網路基礎知識—Group Convolution分組卷積

關於偏置項的解釋:

這個項是個布林型別值,如果為True,就會在訓練的時候加上偏置項,反之,不會。預設是True。

關於填充模式的解釋:

我們一般情況下對於填充的資料,預設是0。還可以選擇常數填充、映象填充、重複填充.。

常數填充就是指定一個常數值進行填充,如果為0,等價於零填充;

映象填充是指對影像邊緣進行映象對稱的填充;

重複填充指的是用影像邊緣的畫素值進行填充。

詳細參考:PyTorch中的padding(邊緣填充)操作

OK,到這裡就對所有的引數解釋完了。下面我們解釋一下,經過一次卷積操作之後,影像形狀如何進行變化。

卷積神經網路的輸入: N , C i n , H i n , W i n N,C_{in},H_{in},W_{in} N,Cin,Hin,Win

卷積神經網路的輸出: N , C o u t , H o u t , W o u t N,C_{out},H_{out},W_{out} N,Cout,Hout,Wout

其中, N N N 表示批處理的大小,即batch_size。 C i n C_{in} Cin C o u t C_{out} Cout 分別表示輸入和輸出通道數。 H i n H_{in} Hin W i n W_{in} Win 表示輸入圖片的高和寬,以畫素為單位。同理, H o u t H_{out} Hout W o u t W_{out} Wout 表示輸出圖片的高和寬。

OK,我們來看一下輸入輸出是怎麼變化的。

首先是batch_size,卷積神經網路並不會改變這個引數;然後是 C i n C_{in} Cin C o u t C_{out} Cout 這兩個引數是我們初始化的時候就指定的;實際上卷積神經網路改變的是圖片的尺寸,圖片尺寸變化公式為:
H o u t = ⌊ H i n + 2 × p a d d i n g ⌊ 0 ⌋ − d i l a t i o n ⌊ 0 ⌋ × ( k e r n e l _ s i z e ⌊ 0 ⌋ − 1 ) s t r i d e ⌊ 0 ⌋ + 1 ⌋ H_{out}=\lfloor \frac{H_{in} + 2 \times padding\lfloor 0 \rfloor - dilation \lfloor 0 \rfloor \times (kernel\_size\lfloor 0 \rfloor - 1)}{stride\lfloor 0 \rfloor} + 1 \rfloor Hout=stride0Hin+2×padding0dilation0×(kernel_size01)+1

W o u t = ⌊ W i n + 2 × p a d d i n g ⌊ 1 ⌋ − d i l a t i o n ⌊ 1 ⌋ × ( k e r n e l _ s i z e ⌊ 1 ⌋ − 1 ) s t r i d e ⌊ 1 ⌋ + 1 ⌋ W_{out}=\lfloor \frac{W_{in} + 2 \times padding\lfloor 1 \rfloor - dilation \lfloor 1 \rfloor \times (kernel\_size\lfloor 1 \rfloor - 1)}{stride\lfloor 1 \rfloor} + 1 \rfloor Wout=stride1Win+2×padding1dilation1×(kernel_size11)+1

然後我們用一段程式碼測試一下:

import torch
import torch.nn as nn
# in_channels=16, out_channels=33, kernel_size=(3, 5)
m = nn.Conv2d(16, 33, (3, 5), stride=(2, 1), padding=(4, 2), dilation=(3, 1))
print(m)
# 我們隨機構造輸入資料
# N=20, C_in=16, H_in=50, W_in=100
# 對應上面說的 (N, C, H, W)
input = torch.randn(20, 16, 50, 100)
output = m(input)
print(output.shape)

結果:

Conv2d(16, 33, kernel_size=(3, 5), stride=(2, 1), padding=(4, 2), dilation=(3, 1))
torch.Size([20, 33, 26, 100])

上面的torch.size表示輸出的形狀:

b a t c h _ s i z e = 20 batch\_size=20 batch_size=20

o u t _ c h a n n e l = 33 out\_channel=33 out_channel=33

H o u t = 26 H_{out}=26 Hout=26

W o u t = 100 W_{out}=100 Wout=100

我們帶入資料實際計算一下:
H o u t = ⌊ 50 + 2 × 4 − 3 × ( 3 − 1 ) − 1 2 + 1 ⌋ = 26 H_{out}= \lfloor \frac{50 + 2 \times 4 - 3 \times (3 - 1) - 1}{2} + 1\rfloor=26 Hout=250+2×43×(31)1+1=26

W o u t = ⌊ 100 + 2 × 2 − 1 × ( 5 − 1 ) − 1 1 + 1 ⌋ = 100 W_{out}=\lfloor \frac{100 + 2 \times 2 - 1 \times (5 - 1) - 1}{1} + 1 \rfloor = 100 Wout=1100+2×21×(51)1+1=100

最後我們有必要了解一下Conv2d的兩個變數:

~Conv2d.weight

其形狀為: ( o u t _ c h a n n e l s , i n _ c h a n n e l s g r o u p , k e r n e l _ s i z e [ 0 ] , k e r n e l _ s i z e [ 1 ] ) (out\_channels, \frac{in\_channels}{group},kernel\_size[0], kernel\_size[1]) (out_channels,groupin_channels,kernel_size[0],kernel_size[1])

資料從 U ( − k , k ) \mathcal{U}(-\sqrt{k}, \sqrt{k}) U(k ,k ) 中取樣,其中 k = g r o u p s C i n ∗ ∏ i = 0 1 k e r n e l _ s i z e [ i ] k=\frac{groups}{C_{in}*\prod_{i=0}^1kernel\_size[i]} k=Cini=01kernel_size[i]groups

~Conv2d.bias

其形狀為: ( o u t _ c h a n n e l s ) (out\_channels) (out_channels)

資料從 U ( − k , k ) \mathcal{U}(-\sqrt{k}, \sqrt{k}) U(k ,k ) 中取樣,其中 k = g r o u p s C i n ∗ ∏ i = 0 1 k e r n e l _ s i z e [ i ] k=\frac{groups}{C_{in}*\prod_{i=0}^1kernel\_size[i]} k=Cini=01kernel_size[i]groups

還是上面的程式碼,我們可以檢視這個卷積神經網路的權重和偏置的形狀:

import torch
import torch.nn as nn
# in_channels=16, out_channels=33, kernel_size=(3, 5)
m = nn.Conv2d(16, 33, (3, 5), stride=(2, 1), padding=(4, 2), dilation=(3, 1))
print(m)
# 我們隨機構造輸入資料
# N=20, C_in=16, H_in=50, W_in=100
# 對應上面說的 (N, C, H, W)
input = torch.randn(20, 16, 50, 100)
output = m(input)
print(m.weight.shape)
print(m.bias.shape)

輸出:

torch.Size([33, 16, 3, 5])
torch.Size([33])