lw2333

lw2333

Keep calm and study.

第六章-卷積神經網絡

為什麼要有所謂卷積神經網絡#

初衷:圖像中本就擁有豐富的結構,而這些結構可以被人類和機器學習模型使用。通過使用這些結構,可以降低訓練的參數量,提高模型的準確性。

譬如在一張圖片裡做物體尋找的任務,一個比較合理的假設是物體位置具有空間不變性。卷積神經網絡正是將空間不變性(spatial invariance)的這一概念系統化,從而基於這個模型使用較少的參數來學習有用的表示。

Screenshot 2024-02-21 093549

可以歸納有以下兩個特點:

  • 平移不變性(translation invariance):不管檢測對象出現在圖像中的哪個位置,神經網絡的前面幾層應該對相同的圖像區域具有相似的反應,即為 “平移不變性”。
  • 局部性(locality):神經網絡的前面幾層應該只探索輸入圖像中的局部區域,而不過度在意圖像中相隔較遠區域的關係,這就是 “局部性” 原則。最終,可以聚合這些局部特徵,以在整個圖像級別進行預測。

數學表示#

H\mathbf{H} 是圖像矩陣 X\mathbf{X} 的隱藏表示。U\mathbf{U} 是偏置。W\mathbf{W} 是四階權重張量。

索引 (i,j)(i, j) 處表示在此位置的像素。於是全連接層中的隱藏表示可以推導如下:

[H]i,j=[U]i,j+kl[W]i,j,k,l[X]k,l=[U]i,j+ab[V]i,j,a,b[X]i+a,j+b.\begin{aligned} \left[\mathbf{H}\right]_{i, j} &= [\mathbf{U}]_{i, j} + \sum_k \sum_l[\mathsf{W}]_{i, j, k, l} [\mathbf{X}]_{k, l}\\ &= [\mathbf{U}]_{i, j} + \sum_a \sum_b [\mathsf{V}]_{i, j, a, b} [\mathbf{X}]_{i+a, j+b}.\end{aligned}

其中 W\mathsf{W}V\mathsf{V} 只不過是把求和 “中心” 移到了 (i,j)(i, j) 處而已。


平移不變性 意味著 U\mathsf{U}V\mathsf{V} 不依賴於 (i,j)(i, j) 的值。 這點很好理解,在不同的 (i,j)(i, j) 處,若 X\mathbf{X} 相同(物品一樣),那麼得到的隱藏表示也應該一樣。 故:

[H]i,j=u+ab[V]a,b[X]i+a,j+b.[\mathbf{H}]_{i, j} = u + \sum_a\sum_b [\mathbf{V}]_{a, b} [\mathbf{X}]_{i+a, j+b}.

局部性 意味著我們可以把在某個範圍之外的信息屏蔽,即在 [a,b][a,b] 之外的 V\mathbf{V} 設置為 0

[H]i,j=u+a=Δ1Δ1b=Δ2Δ2[V]a,b[X]i+a,j+b.[\mathbf{H}]_{i, j} = u + \sum_{a = -\Delta_1}^{\Delta_1} \sum_{b = -\Delta_2}^{\Delta_2} [\mathbf{V}]_{a, b} [\mathbf{X}]_{i+a, j+b}.

稱上式為一個卷積層。V\mathbf{V} 被稱為卷積核(convolution kernel)或者濾波器(filter)。


簡單地喵兩句卷積層。
卷積層所表達的運算其實是互相關運算(cross-correlation)
在卷積層中,輸入張量和核張量通過互相關運算產生輸出張量。
真正的卷積運算長這樣(但其實無所謂):

(fg)(x)=f(z)g(xz)dz.(fg)(i)=af(a)g(ia).(fg)(i,j)=abf(a,b)g(ia,jb).\begin{aligned} (f * g)(\mathbf{x}) &= \int f(\mathbf{z}) g(\mathbf{x}-\mathbf{z}) d\mathbf{z}.\\ (f * g)(i) &= \sum_a f(a) g(i-a).\\ (f * g)(i, j) &= \sum_a\sum_b f(a, b) g(i-a, j-b). \end{aligned}

Screenshot 2024-02-21 093524

實際上,圖像是一個由高度、寬度和顏色組成的三維張量,比如包含1024×1024×31024 \times 1024 \times 3個像素。
[X]i,j[X]i,j,k[V]a,b[V]a,b,c[\mathsf{X}]_{i, j}\to[\mathsf{X}]_{i, j, k}\quad [\mathbf{V}]_{a,b}\to [\mathsf{V}]_{a,b,c}

H\mathsf{H} 也採用三維張量。
換句話說,對於每一個空間位置,我們想要採用一組而不是一個隱藏表示。這樣一組隱藏表示可以想像成一些互相堆疊的二維網格。
因此,我們可以把隱藏表示想像為一系列具有二維張量的 通道(channel)。
這些通道有時也被稱為 特徵映射(feature maps),因為每個通道都向後續層提供一組空間化的學習特徵。
直觀上可以想像在靠近輸入的底層,一些通道專門識別邊緣,而一些通道專門識別紋理。

為了支持輸入 X\mathsf{X} 和隱藏表示 H\mathsf{H} 中的多個通道,我們可以在 V\mathsf{V} 中添加第四個坐標,即 [V]a,b,c,d[\mathsf{V}]_{a, b, c, d}。綜上所述,

[H]i,j,d=a=Δ1Δ1b=Δ2Δ2c[V]a,b,c,d[X]i+a,j+b,c,[\mathsf{H}]_{i,j,d} = \sum_{a = -\Delta_1}^{\Delta_1} \sum_{b = -\Delta_2}^{\Delta_2} \sum_c [\mathsf{V}]_{a, b, c, d} [\mathsf{X}]_{i+a, j+b, c},

其中隱藏表示 H\mathsf{H} 中的索引 dd 表示輸出通道,而隨後的輸出將繼續以三維張量 H\mathsf{H} 作為輸入進入下一個卷積層。
於是可以定義具有多個通道的卷積層,而其中 V\mathsf{V} 是該卷積層的權重。


(nhkh+1)×(nwkw+1).(n_h-k_h+1) \times (n_w-k_w+1).

代碼練習#

def corr2d(X, K):  
    """二維互相關運算

    Args:
        X (Tensor): 二維矩陣
        K (Tensor): Kernel

    Returns:
        Tensor: 二維互相關運算結果
    """
    h, w = K.shape
    Y = torch.zeros((X.shape[0] - h + 1, X.shape[1] - w + 1))
    for i in range(Y.shape[0]):
        for j in range(Y.shape[1]):
          # Hadamard product(Schur product, entrywise)
            Y[i, j] = (X[i:i + h, j:j + w] * K).sum() 
    return Y

二維卷積層

class Conv2D(nn.Module):
    def __init__(self, kernel_size):
        super().__init__()
        self.weight = nn.Parameter(torch.rand(kernel_size))
        self.bias = nn.Parameter(torch.zeros(1))

    def forward(self, x):
        return corr2d(x, self.weight) + self.bias

簡單的示例如下

import torch
import torch.nn as nn
import torch.optim as optim

conv2d = nn.Conv2d(1, 1, kernel_size=(1, 2), bias=False)
X = X.reshape((1, 1, 10, 12))
Y = Y.reshape((1, 1, 10, 11))

lr = 1e-1  

optimizer = optim.SGD([conv2d.weight], lr=lr)

for epoch in range(100):  
    Y_hat = conv2d(X)
    l = nn.MSELoss()(Y_hat, Y)

    optimizer.zero_grad()
    l.backward()
    optimizer.step()

    print(f"epoch {epoch+1}, loss {l.item():.3f}")

在卷積神經網絡中,對於某一層的任意元素
,其感受野(receptive field)是指在前向傳播期間可能影響
計算的所有元素(來自所有先前層)。

感受野可能大於輸入的實際大小。當一個特徵圖中的任意元素需要檢測更廣區域的輸入特徵時,我們可以構建一個更深的網絡。

填充和步幅#

卷積的輸出形狀取決於輸入形狀和卷積核的形狀。連續應用多次卷積之後得到的輸出遠小於輸入。這樣的副作用就是圖像邊缘的信息丟失。
可在邊界填充(冗餘)信息,來影響。
另外,對於分辨率過高的冗餘信息,可通過步幅來改進(一次走多步)。

Screenshot 2024-02-21 093454

這樣,填充之後輸出維度 (nhkh+1)×(nwkw+1).(n_h-k_h+1) \times (n_w-k_w+1). 變成
(nhkh+ph+1)×(nwkw+pw+1)(n_h-k_h+p_h+1)\times(n_w-k_w+p_w+1)。

卷積神經網絡中卷積核的高度和寬度通常為奇數,例如 1、3、5 或 7。 選擇奇數的好處是,保持空間維度的同時,我們可以在頂部和底部填充相同數量的行,在左側和右側填充相同數量的列。

常常設 ph=kh1,pw=kw1p_h=k_h-1, \,p_w=k_w-1,使輸入和輸出具有相同的高度和寬度。
這樣可以在構建網絡時更容易地預測每個圖層的輸出形狀。假設 khk_h 是奇數,我們將在高度的兩側填充 ph/2p_h/2 行。
如果 khk_h 是偶數,則一種可能性是在輸入頂部填充 ph/2\lceil p_h/2\rceil 行,在底部填充 ph/2\lfloor p_h/2\rfloor 行。同理,我們填充寬度的兩側。


步幅

每次滑動元素的數量稱為步幅(stride)。
通常,當垂直步幅為 shs_h、水平步幅為 sws_w 時,輸出形狀為
(nhkh+ph+sh)/sh×(nwkw+pw+sw)/sw.\lfloor(n_h-k_h+p_h+s_h)/s_h\rfloor \times \lfloor(n_w-k_w+p_w+s_w)/s_w\rfloor.

特別地,ph=kh1p_h=k_h-1pw=kw1p_w=k_w-1 時,輸出形狀為
(nh+sh1)/sh×(nw+sw1)/sw\lfloor(n_h+s_h-1)/s_h\rfloor \times \lfloor(n_w+s_w-1)/s_w\rfloor

更進一步,如果輸入的高度和寬度可以被垂直和水平步幅整除,則輸出形狀將為 (nh/sh)×(nw/sw)(n_h/s_h) \times (n_w/s_w)

代碼練習#

def comp_conv2d(conv2d, X):
    X = X.reshape((1, 1) + X.shape)
    Y = conv2d(X)
    return Y.reshape(Y.shape[2:])

conv2d = nn.Conv2d(1, 1, kernel_size=3, padding=1)
X = torch.rand(size=(8, 8))
comp_conv2d(conv2d, X).shape
conv2d = nn.Conv2d(1, 1, kernel_size=(5, 3), padding=(2, 1))
comp_conv2d(conv2d, X).shape

注意到 (parameter) padding: _size_2_t | strpadding 是高和寬的兩側分別填充的元素數量。


另外,nn 的 padding 默認是除後向上取整。

conv2d = nn.Conv2d(1, 1, kernel_size=(3, 5), padding=(0, 1))
comp_conv2d(conv2d, X).shape
conv2d = nn.Conv2d(1, 1, kernel_size=(3, 5), padding=(0, 1), stride=(3, 2))
comp_conv2d(conv2d, X).shape

多輸入多輸出通道#

例如,每個 RGB 輸入圖像具有 3×h×w3\times h\times w 的形狀。我們將這個大小為 3 的軸稱為 通道(channel)維度。

幾個通道一般就要構造幾個卷積核。得到一個 ci×kh×kwc_i\times k_h\times k_w 的卷積核,互相關之後再求和。

def corr2d_multi_in(X, K):
    """多通道二維卷積

    Args:
        X (Tensor): 輸入
        K (Tensor): 核

    Returns:
        Tensor: 結果
    """
    return sum(d2l.corr2d(x, k) for x, k in zip(X, K))
  
X = torch.tensor([[[0.0, 1.0, 2.0], [3.0, 4.0, 5.0], [6.0, 7.0, 8.0]],
               [[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]]])
K = torch.tensor([[[0.0, 1.0], [2.0, 3.0]], [[1.0, 2.0], [3.0, 4.0]]])

corr2d_multi_in(X, K)
載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。