以下內容為結合李沐老師的課程和教材補充的學習筆記,以及對課后練習的一些思考,自留回顧,也供同學之人交流參考。
本節課程地址:卷積層_嗶哩嗶哩_bilibili?代碼_嗶哩嗶哩_bilibili
本節教材地址:6.2. 圖像卷積 — 動手學深度學習 2.0.0 documentation (d2l.ai)
本節開源代碼:...>d2l-zh>pytorch>chapter_multilayer-perceptrons>conv-layer.ipynb
圖像卷積
上節我們解析了卷積層的原理,現在我們看看它的實際應用。由于卷積神經網絡的設計是用于探索圖像數據,本節我們將以圖像為例。
互相關運算
嚴格來說,卷積層是個錯誤的叫法,因為它所表達的運算其實是互相關運算(cross-correlation),而不是卷積運算。 根據 6.1節 中的描述,在卷積層中,輸入張量和核張量通過(互相關運算)產生輸出張量。
首先,我們暫時忽略通道(第三維)這一情況,看看如何處理二維圖像數據和隱藏表示。在 下圖 中,輸入是高度為3、寬度為3的二維張量(即形狀為3 × 3)。卷積核的高度和寬度都是2,而卷積核窗口(或卷積窗口)的形狀由內核的高度和寬度決定(即2 × 2)。
在二維互相關運算中,卷積窗口從輸入張量的左上角開始,從左到右、從上到下滑動。 當卷積窗口滑動到新一個位置時,包含在該窗口中的部分張量與卷積核張量進行按元素相乘,得到的張量再求和得到一個單一的標量值,由此我們得出了這一位置的輸出張量值。 在如上例子中,輸出張量的四個元素由二維互相關運算得到,這個輸出高度為2、寬度為2,如下所示:
0×0+1×1+3×2+4×3=19,1×0+2×1+4×2+5×3=25,3×0+4×1+6×2+7×3=37,4×0+5×1+7×2+8×3=43.
注意,輸出大小略小于輸入大小。這是因為卷積核的寬度和高度大于1, 而卷積核只與圖像中每個大小完全適合的位置進行互相關運算。 所以,輸出大小等于輸入大小??減去卷積核大小?
?,即:
這是因為我們需要足夠的空間在圖像上“移動”卷積核。稍后,我們將看到如何通過在圖像邊界周圍填充零來保證有足夠的空間移動卷積核,從而保持輸出大小不變。 接下來,我們在corr2d
函數中實現如上過程,該函數接受輸入張量X
和卷積核張量K
,并返回輸出張量Y
。
二維卷積層:
- 輸入?
- 核?
- 偏差?
- 輸出 c
?和?
?是可學習的參數
import torch
from torch import nn
from d2l import torch as d2l
def corr2d(X, K): #@save"""計算二維互相關運算"""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]):Y[i, j] = (X[i:i + h, j:j + w] * K).sum()return Y
通過 上圖 的輸入張量X
和卷積核張量K
,我們來[驗證上述二維互相關運算的輸出]。
X = torch.tensor([[0.0, 1.0, 2.0], [3.0, 4.0, 5.0], [6.0, 7.0, 8.0]])
K = torch.tensor([[0.0, 1.0], [2.0, 3.0]])
corr2d(X, K)
輸出結果:
tensor([[19., 25.],
[37., 43.]])
卷積層
卷積層對輸入和卷積核權重進行互相關運算,并在添加標量偏置之后產生輸出。 所以,卷積層中的兩個被訓練的參數是卷積核權重和標量偏置。 就像我們之前隨機初始化全連接層一樣,在訓練基于卷積層的模型時,我們也隨機初始化卷積核權重。
基于上面定義的corr2d
函數[實現二維卷積層]。在__init__
構造函數中,將weight
和bias
聲明為兩個模型參數。前向傳播函數調用corr2d
函數并添加偏置。
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
高度和寬度分別為??和?
?的卷積核可以被稱為?
?卷積或?
?卷積核。 我們也將帶有?
?卷積核的卷積層稱為?
?卷積層。
圖像中目標的邊緣檢測
如下是[卷積層的一個簡單應用:]通過找到像素變化的位置,來(檢測圖像中不同顏色的邊緣)。 首先,我們構造一個 6 × 8像素的黑白圖像。中間四列為黑色(0),其余像素為白色(1)。
X = torch.ones((6, 8))
X[:, 2:6] = 0
X
輸出結果:
tensor([[1., 1., 0., 0., 0., 0., 1., 1.],
[1., 1., 0., 0., 0., 0., 1., 1.],
[1., 1., 0., 0., 0., 0., 1., 1.],
[1., 1., 0., 0., 0., 0., 1., 1.],
[1., 1., 0., 0., 0., 0., 1., 1.],
[1., 1., 0., 0., 0., 0., 1., 1.]])
接下來,我們構造一個高度為1、寬度為2的卷積核K
。當進行互相關運算時,如果水平相鄰的兩元素相同,則輸出為零,否則輸出為非零。
K = torch.tensor([[1.0, -1.0]])
現在,我們對參數X
(輸入)和K
(卷積核)執行互相關運算。 如下所示,[輸出Y
中的1代表從白色到黑色的邊緣,-1代表從黑色到白色的邊緣],其他情況的輸出為0。
Y = corr2d(X, K)
Y
輸出結果:
tensor([[ 0., 1., 0., 0., 0., -1., 0.],
[ 0., 1., 0., 0., 0., -1., 0.],
[ 0., 1., 0., 0., 0., -1., 0.],
[ 0., 1., 0., 0., 0., -1., 0.],
[ 0., 1., 0., 0., 0., -1., 0.],
[ 0., 1., 0., 0., 0., -1., 0.]])
現在我們將輸入的二維圖像轉置,再進行如上的互相關運算。 其輸出如下,之前檢測到的垂直邊緣消失了。 不出所料,這個[卷積核K
只可以檢測垂直邊緣],無法檢測水平邊緣。
corr2d(X.t(), K)
輸出結果:
tensor([[0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0.]])
補充:
學習卷積核
如果我們只需尋找黑白邊緣,那么以上[1, -1]
的邊緣檢測器足以。然而,當有了更復雜數值的卷積核,或者連續的卷積層時,我們不可能手動設計濾波器。那么我們是否可以[學習由X
生成Y
的卷積核]呢?
現在讓我們看看是否可以通過僅查看“輸入-輸出”對來學習由X
生成Y
的卷積核。 我們先構造一個卷積層,并將其卷積核初始化為隨機張量。接下來,在每次迭代中,我們比較Y
與卷積層輸出的平方誤差,然后計算梯度來更新卷積核。為了簡單起見,我們在此使用內置的二維卷積層,并忽略偏置。
# 構造一個二維卷積層,它具有1個輸出通道和形狀為(1,2)的卷積核
conv2d = nn.Conv2d(1,1, kernel_size=(1, 2), bias=False)# 這個二維卷積層使用四維輸入和輸出格式(批量大小、通道、高度、寬度),
# 其中批量大小和通道數都為1
X = X.reshape((1, 1, 6, 8))
Y = Y.reshape((1, 1, 6, 7))
lr = 3e-2 # 學習率for i in range(10):Y_hat = conv2d(X)l = (Y_hat - Y) ** 2conv2d.zero_grad()l.sum().backward()# 迭代卷積核conv2d.weight.data[:] -= lr * conv2d.weight.gradif (i + 1) % 2 == 0:print(f'epoch {i+1}, loss {l.sum():.3f}')
輸出結果:
epoch 2, loss 4.114
epoch 4, loss 0.798
epoch 6, loss 0.178
epoch 8, loss 0.048
epoch 10, loss 0.016
在10 次迭代之后,誤差已經降到足夠低。現在我們來看看我們[所學的卷積核的權重張量]。
conv2d.weight.data.reshape((1, 2))
輸出結果:
tensor([[ 1.0016, -0.9785]])
細心的讀者一定會發現,我們學習到的卷積核權重非常接近我們之前定義的卷積核K
。
互相關和卷積
回想一下我們在 6.1節 中觀察到的互相關和卷積運算之間的對應關系。 為了得到正式的卷積運算輸出,我們需要執行 (6.1.6)中定義的嚴格卷積運算,而不是互相關運算。 幸運的是,它們差別不大,我們只需水平和垂直翻轉二維卷積核張量,然后對輸入張量執行互相關運算。
值得注意的是,由于卷積核是從數據中學習到的,因此無論這些層執行嚴格的卷積運算還是互相關運算,卷積層的輸出都不會受到影響。 為了說明這一點,假設卷積層執行互相關運算并學習 上圖 中的卷積核,該卷積核在這里由矩陣??表示。 假設其他條件不變,當這個層執行嚴格的卷積時,學習的卷積核?
?在水平和垂直翻轉之后將與?
?相同。 也就是說,當卷積層對 上圖 中的輸入和?
?執行嚴格卷積運算時,將得到與互相關運算 上圖中相同的輸出。
為了與深度學習文獻中的標準術語保持一致,我們將繼續把“互相關運算”稱為卷積運算,盡管嚴格地說,它們略有不同。 此外,對于卷積核張量上的權重,我們稱其為元素。
補充:
- 二維互相關
- 二維卷積
- 由于對稱性,在實際使用中沒有區別
一維和三維互相關:
- 一維:文本、語言、時序序列(音頻)
- 三維:視頻、醫學圖像、氣象地圖
特征映射和感受野
如在 6.1.4.1節 中所述, 上圖 中輸出的卷積層有時被稱為特征映射(feature map),因為它可以被視為一個輸入映射到下一層的空間維度的轉換器。 在卷積神經網絡中,對于某一層的任意元素??,其感受野(receptive field)是指在前向傳播期間可能影響?
?計算的所有元素(來自所有先前層)。
請注意,感受野可能大于輸入的實際大小。讓我們用 上圖 為例來解釋感受野: 給定 2×2 卷積核,陰影輸出元素值19的感受野是輸入陰影部分的四個元素。 假設之前輸出為??,其大小為 2×2,現在我們在其后附加一個卷積層,該卷積層以?
?為輸入,輸出單個元素?
?。 在這種情況下,
?上的?
?的感受野包括?
?的所有四個元素,而輸入的感受野包括最初所有九個輸入元素。 因此,當一個特征圖中的任意元素需要檢測更廣區域的輸入特征時,我們可以構建一個更深的網絡。
小結
- 二維卷積層的核心計算是二維互相關運算。最簡單的形式是,對二維輸入數據和卷積核執行互相關操作,然后添加一個偏置。
- 我們可以設計一個卷積核來檢測圖像的邊緣。
- 我們可以從數據中學習卷積核的參數。
- 學習卷積核時,無論用嚴格卷積運算或互相關運算,卷積層的輸出不會受太大影響。
- 當需要檢測輸入特征中更廣區域時,我們可以構建一個更深的卷積網絡。
練習
1. 構建一個具有對角線邊緣的圖像X
。
1)如果將本節中舉例的卷積核K
應用于X
,會發生什么情況?
2)如果轉置X
會發生什么?
3)如果轉置K
會發生什么?
解:
代碼如下:
X = torch.ones((5, 5))
for i in range(X.shape[0]):for j in range(X.shape[1]):if i == j:X[i,j] = 0
X
輸出結果:
tensor([[0., 1., 1., 1., 1.],
[1., 0., 1., 1., 1.],
[1., 1., 0., 1., 1.],
[1., 1., 1., 0., 1.],
[1., 1., 1., 1., 0.]])
K = torch.tensor([[1.0, -1.0]])
corr2d(X, K)
# 輸出形狀為(5,4),檢測對角線邊緣,對角線上為-1,下為1
輸出結果:
tensor([[-1., 0., 0., 0.],
[ 1., -1., 0., 0.],
[ 0., 1., -1., 0.],
[ 0., 0., 1., -1.],
[ 0., 0., 0., 1.]])
corr2d(X.t(), K)
# X轉置后結果不變
輸出結果:
tensor([[-1., 0., 0., 0.],
[ 1., -1., 0., 0.],
[ 0., 1., -1., 0.],
[ 0., 0., 1., -1.],
[ 0., 0., 0., 1.]])
corr2d(X, K.t())
# 輸出形狀變成(4,5),檢測對角線邊緣,對角線上為1,下為-1
輸出結果:
tensor([[-1., 1., 0., 0., 0.],
[ 0., -1., 1., 0., 0.],
[ 0., 0., -1., 1., 0.],
[ 0., 0., 0., -1., 1.]])
2. 在我們創建的Conv2D
自動求導時,有什么錯誤消息?
解:
報錯:維度不匹配。因為輸入和輸出格式是四維的,包括批量大小、通道、高和寬,但自定義的Conv2D僅包含二維信息,即高和寬。
將輸入和輸出的批量大小和通道去掉,或者創建卷積層時增加批量大小和通道,均可解決該問題。
conv2d = Conv2D((1,2))
Y_hat = conv2d(X)
l = (Y_hat - Y) ** 2
conv2d.zero_grad()
l.sum().backward()
conv2d.weight.data[:] -= lr * conv2d.weight.grad
輸出結果:
---------------------------------------------------------------------------
RuntimeError Traceback (most recent call last)
Cell In[24], line 3
1 conv2d = Conv2D((1,2))
2 Y_hat = conv2d(X)
----> 3 l = (Y_hat - Y) ** 2
4 conv2d.zero_grad()
5 l.sum().backward()
RuntimeError: The size of tensor a (4) must match the size of tensor b (7) at non-singleton dimension 3
3. 如何通過改變輸入張量和卷積核張量,將互相關運算表示為矩陣乘法?
解:
核心思想是,將輸入張量中每個與卷積核做互相關運算的元素展成一行,
比如將本節舉例的X轉變為:?[0134124534674578]
再與reshape成列向量的卷積核K做矩陣乘法:?[0134124534674578][0123]=[19253743]
再將結果reshape成輸出Y的尺寸即可。
def corr2d_matmul(X, K): h, w = K.shape# 將輸入X中每次與卷積核做互相關運算的元素展平成一行# X_reshaped的每一行元素均為與卷積核做互相關的元素X_reshaped = torch.zeros((X.shape[0] - h + 1)*(X.shape[1] - w + 1), h*w)k = 0for i in range(X.shape[0] - h + 1):for j in range(X.shape[1] - w + 1):X_reshaped[k,:] = X[i:i + h, j:j + w].reshape(1,-1)k += 1# 將卷積核K轉為列向量,并與X_reshaped做矩陣乘法,再reshape成輸出尺寸即可Y = X_reshaped@K.view(-1)return Y.reshape(X.shape[0] - h + 1, X.shape[1] - w + 1)
X = torch.tensor([[0.0, 1.0, 2.0], [3.0, 4.0, 5.0], [6.0, 7.0, 8.0]])
K = torch.tensor([[0.0, 1.0], [2.0, 3.0]])
Y = corr2d_matmul(X, K)
Y
輸出結果:
tensor([[19., 25.],
[37., 43.]])
4.手工設計一些卷積核。
1)二階導數的核的形式是什么?
2)積分的核的形式是什么?
3)得到?𝑑?次導數的最小核的大小是多少?
解:
1)
- 對于一階導數:
對應的一階導數卷積核為?
擴展至二維,對應的一階導數水平卷積核為??或者?
對應的一階導數垂直卷積核為?或者?
- 進一步地,對于二階導數:?
對應的二階導數卷積核為?
擴展至二維,?
對應的二階導數水平卷積核為??或者?
二階導數垂直卷積核為??或者?
二階導數疊加卷積核為?
2)
- 一維積分核--平均濾波器:?
?其中,?
?是?
?中的元素數量。
- 二維積分核: 積分核可以是平均濾波器,也稱為均值濾波器,其形式如下:
二維平均濾波器 (也稱為盒式濾波器或均質濾波器):
?其中,?
?和?
?分別是?
?的高度和寬度。
高斯積分核: 在空間域具有高斯形狀,用于實現平滑效果,如下:
其中,??是高斯分布的標準差,?
?是濾波器中每個元素的坐標。
雙邊濾波器核:結合了空間域的高斯權重和強度域的高斯權重,用于在保持邊緣的同時實現平滑,如下:?
其中,??是歸一化因子,確保核的總權重為 1,?
?是空間域的標準差,?
?是強度域的標準差,?𝐼(𝑥,𝑦)?是圖像在點?
?的強度,?
?是中心像素的強度。
3)結合第一小問的解答,
- 對于一階導數,最小核的大小是3(一個3點中心差分近似)。
- 對于二階導數,在一維情況下,最小核的大小是3(一個3點中心差分近似)。在二維情況下,一個簡單的近似可能是一個3×3的核。
- 對于更高階的導數,核的大小至少應與導數的階數d相匹配,但這通常不足以提供良好的近似。實際使用的核會更大,以確保足夠的平滑性和減少誤差。