為什麼要有所謂卷積神經網絡#
初衷:圖像中本就擁有豐富的結構,而這些結構可以被人類和機器學習模型使用。通過使用這些結構,可以降低訓練的參數量,提高模型的準確性。
譬如在一張圖片裡做物體尋找的任務,一個比較合理的假設是物體位置具有空間不變性。卷積神經網絡正是將空間不變性(spatial invariance)的這一概念系統化,從而基於這個模型使用較少的參數來學習有用的表示。
可以歸納有以下兩個特點:
- 平移不變性(translation invariance):不管檢測對象出現在圖像中的哪個位置,神經網絡的前面幾層應該對相同的圖像區域具有相似的反應,即為 “平移不變性”。
- 局部性(locality):神經網絡的前面幾層應該只探索輸入圖像中的局部區域,而不過度在意圖像中相隔較遠區域的關係,這就是 “局部性” 原則。最終,可以聚合這些局部特徵,以在整個圖像級別進行預測。
數學表示#
是圖像矩陣 的隱藏表示。 是偏置。 是四階權重張量。
索引 處表示在此位置的像素。於是全連接層中的隱藏表示可以推導如下:
其中 到 只不過是把求和 “中心” 移到了 處而已。
平移不變性 意味著 和 不依賴於 的值。 這點很好理解,在不同的 處,若 相同(物品一樣),那麼得到的隱藏表示也應該一樣。 故:
局部性 意味著我們可以把在某個範圍之外的信息屏蔽,即在 之外的 設置為 0
稱上式為一個卷積層。 被稱為卷積核(convolution kernel)或者濾波器(filter)。
簡單地喵兩句卷積層。
卷積層所表達的運算其實是互相關運算(cross-correlation)
在卷積層中,輸入張量和核張量通過互相關運算產生輸出張量。
真正的卷積運算長這樣(但其實無所謂):
實際上,圖像是一個由高度、寬度和顏色組成的三維張量,比如包含個像素。
也採用三維張量。
換句話說,對於每一個空間位置,我們想要採用一組而不是一個隱藏表示。這樣一組隱藏表示可以想像成一些互相堆疊的二維網格。
因此,我們可以把隱藏表示想像為一系列具有二維張量的 通道(channel)。
這些通道有時也被稱為 特徵映射(feature maps),因為每個通道都向後續層提供一組空間化的學習特徵。
直觀上可以想像在靠近輸入的底層,一些通道專門識別邊緣,而一些通道專門識別紋理。
為了支持輸入 和隱藏表示 中的多個通道,我們可以在 中添加第四個坐標,即 。綜上所述,
其中隱藏表示 中的索引 表示輸出通道,而隨後的輸出將繼續以三維張量 作為輸入進入下一個卷積層。
於是可以定義具有多個通道的卷積層,而其中 是該卷積層的權重。
代碼練習#
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)是指在前向傳播期間可能影響
計算的所有元素(來自所有先前層)。
感受野可能大於輸入的實際大小。當一個特徵圖中的任意元素需要檢測更廣區域的輸入特徵時,我們可以構建一個更深的網絡。
填充和步幅#
卷積的輸出形狀取決於輸入形狀和卷積核的形狀。連續應用多次卷積之後得到的輸出遠小於輸入。這樣的副作用就是圖像邊缘的信息丟失。
可在邊界填充(冗餘)信息,來影響。
另外,對於分辨率過高的冗餘信息,可通過步幅來改進(一次走多步)。
這樣,填充之後輸出維度 變成
卷積神經網絡中卷積核的高度和寬度通常為奇數,例如 1、3、5 或 7。 選擇奇數的好處是,保持空間維度的同時,我們可以在頂部和底部填充相同數量的行,在左側和右側填充相同數量的列。
常常設 ,使輸入和輸出具有相同的高度和寬度。
這樣可以在構建網絡時更容易地預測每個圖層的輸出形狀。假設 是奇數,我們將在高度的兩側填充 行。
如果 是偶數,則一種可能性是在輸入頂部填充 行,在底部填充 行。同理,我們填充寬度的兩側。
步幅
每次滑動元素的數量稱為步幅(stride)。
通常,當垂直步幅為 、水平步幅為 時,輸出形狀為
特別地, , 時,輸出形狀為
更進一步,如果輸入的高度和寬度可以被垂直和水平步幅整除,則輸出形狀將為 。
代碼練習#
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 | str
,padding
是高和寬的兩側分別填充的元素數量。
另外,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 的軸稱為 通道(channel)維度。
幾個通道一般就要構造幾個卷積核。得到一個 的卷積核,互相關之後再求和。
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)