MXNET:卷積神經網絡基礎


卷積神經網絡(convolutional neural network)。它是近年來深度學習能在計算機視覺中取得巨大成果的基石,它也逐漸在被其他諸如自然語言處理、推薦系統和語音識別等領域廣泛使用。

目前我關注的問題是:

  • 輸入數據的構建,尤其是多輸入、多輸出的情況。
  • finetune的實現,如何將已訓練網絡的部分層拿出來作為其他網絡的一部分。

二維卷積層

二維卷積:

實現如下:

def corr2d(X, K):
    h, w = K.shape
    Y = nd.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 = nd.array([[0, 1, 2], [3, 4, 5], [6, 7, 8]])
K = nd.array([[0, 1], [2, 3]])
corr2d(X, K)
# output 
[[ 19.  25.]
 [ 37.  43.]]
<NDArray 2x2 @cpu(0)>

二維卷積層就是將輸入和卷積核做相關運算,然后加上一個標量偏差來得到輸出。

class Conv2D(nn.Block):
    def __init__(self, kernel_size, **kwargs):
        super(Conv2D, self).__init__(**kwargs)
        self.weight = self.params.get('weight', shape=kernel_size)
        self.bias = self.params.get('bias', shape=(1,))

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

卷積運算的計算與二維相關運算類似,唯一的區別是反向的將核數組跟輸入做乘法,即 Y[0, 0] = (X[0:2, 0:2] * K[::-1, ::-1]).sum()。
但是因為在卷積層里 K 是學習而來的,所以不論是正向還是反向訪問都可以

通過數據學習核數組

雖然我們之前構造了 Conv2D 類,但由於 corr2d 使用了對單個元素賦值([i, j]=)的操作會導致無法自動求導,下面我們使用 Gluon 提供的 Conv2D 類來實現這個例子。

# 構造一個輸出通道是 1(將在后面小節介紹通道),核數組形狀是 (1,2) 的二維卷積層。
conv2d = nn.Conv2D(1, kernel_size=(1, 2))
conv2d.initialize()

# 二維卷積層使用 4 維輸入輸出,格式為(批量大小,通道數,高,寬),這里批量和通道均為 1。
X = X.reshape((1, 1, 6, 8))
Y = Y.reshape((1, 1, 6, 7))

for i in range(10):
    with autograd.record():
        Y_hat = conv2d(X)
        l = (Y_hat-Y) ** 2
        if i % 2 == 1:
            print('batch %d, loss %.3f' % (i, l.sum().asscalar()))
    l.backward()
    # 為了簡單起見這里忽略了偏差。
    conv2d.weight.data()[:] -= 3e-2 * conv2d.weight.grad()

填充和步幅

一般來說,假設輸入形狀是 \(n_h×n_w\),卷積核形狀是 \(k_h×k_w\),那么輸出形狀將會是

\[(n_h-k_h+1) \times (n_w-k_w+1). \]

所以卷積層的輸出形狀由輸入形狀和卷積核形狀決定。下面我們將介紹卷積層的兩個超參數,填充和步幅,它們可以在給定形狀的輸入和卷積核下來改變輸出形狀。

填充是指在輸入高和寬的兩端填充元素。如果在高兩側一共填充 \(p_h\) 行,在寬兩側一共填充 \(p_w\) 列,那么輸出形狀將會是

\[(n_h-k_h+p_h+1)\times(n_w-k_w+p_w+1), \]

通常我們會設置 \(p_h=k_h−1\)\(p_w=k_w−1\) 使得輸入和輸出有相同的高寬,這樣方便在構造網絡時容易推測每個層的輸出形狀。假設這里 \(k_h\) 是奇數,我們會在高的兩側分別填充 \(p_h/2\) 行。如果其是偶數,一種可能是上面填充 \(\lceil p_h/2\rceil\) 行,而下面填充 \(\lfloor p_h/2\rfloor\) 行。在寬上行為類似。

卷積神經網絡經常使用奇數高寬的卷積核,例如 1、3、5、和 7,所以填充在兩端上是對稱的。

# 注意這里是兩側分別填充 1,所以 p_w = p_h = 2。
conv2d = nn.Conv2D(1, kernel_size=3, padding=1)
conv2d.initialize()
X = nd.random.uniform(shape=(8, 8))
X = X.reshape((1, 1,) + X.shape)
Y = conv2d(X)
print Y.shape[2:]
# output
(8, 8)

當然我們可以使用非方形卷積核,使用對應的填充同樣可得相同高寬的輸出。

conv2d = nn.Conv2D(1, kernel_size=(5, 3), padding=(2, 1))

前面的例子中,在高和寬兩個方向上步幅均為 1。自然我們可以使用更大步幅。

一般來說,如果在高上使用步幅 \(s_h\),在寬上使用步幅 \(s_w\),那么輸出大小將是

\[\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. \]

如果我們設置\(p_h=k_h−1\)\(p_w=k_w−1\),那么輸出大小為\(\lfloor(n_h+s_h-1)/s_h\rfloor \times \lfloor(n_w+s_w-1)/s_w\rfloor\).更進一步,如果輸出高寬能分別被高寬上的步幅整除,那么輸出將是 \(n_h/s_h \times n_w/s_w\)。也就是說我們成倍的減小了輸入的高寬。

conv2d = nn.Conv2D(1, kernel_size=3, padding=1, strides=2)
conv2d = nn.Conv2D(1, kernel_size=(3, 5), padding=(0, 1), strides=(3, 4))
# output
(4, 4)
(2, 2)

通道

下圖展示了輸入通道是 2 的一個例子

輸入是\(c_i\)通道時,需要一個\(c_i \times k_h \times k_w\)的卷積核。在每個通道里對相應的輸入矩陣和核矩陣做相關計算,然后再將通道之間的結果相加得到最終結果。

上面是\(c_o=1\)的情況,如果是多通道輸出,那么卷積核的形狀變為:\(c_o \times c_i \times k_h \times k_w\).

1x1卷積層
因為使用了最小窗口,它失去了卷積層可以識別高寬維上相鄰元素構成的模式的功能,它的主要計算則是在通道維上。

在之后的模型里我們將會看到 1×1 卷積層是如何當做保持高寬維形狀的全連接層使用,其作用是通過調整網絡層之間的通道數來控制模型復雜度。

池化層

池化層提出可以緩解卷積層對位置的過度敏感性,也為了降低顯存。
同卷積層一樣,池化層也可以填充輸入高寬兩側的數據和調整窗口的移動步幅來改變輸出大小。

我們先構造一個 (1, 1, 4, 4) 形狀的輸入數據,前兩個維度分別是批量和通道。

X = nd.arange(16).reshape((1, 1, 4, 4))

MaxPool2D 類里默認步幅設置成跟池化窗大小一樣。下面使用 (3, 3) 窗口,默認獲得 (3, 3) 步幅。

pool2d = nn.MaxPool2D(3)
# 因為池化層沒有模型參數,所以不需要調用參數初始化函數。
pool2d(X)
# output
[[[[ 10.]]]]
<NDArray 1x1x1x1 @cpu(0)>

我們可以手動指定步幅和填充。

pool2d = nn.MaxPool2D(3, padding=1, strides=2)
pool2d = nn.MaxPool2D((2, 3), padding=(1, 2), strides=(2, 3))


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM