NN入門,手把手教你用Numpy手撕NN(3)
這是一篇包含極少數學的CNN入門文章
上篇文章中簡單介紹了NN的反向傳播,並利用反向傳播實現了一個簡單的NN,在這篇文章中將介紹一下CNN。
CNN
CV(計算機視覺)作為AI的一大研究方向,越來越多的人選擇了這個方向,其中使用的深度學習的方法基本以卷積神經網絡(CNN)為基礎。因此,這篇文章將介紹CNN的實現。
CNN與我們之前介紹的NN的相比,出現了卷積層(Convolution層)和池化層(Pooling層)。其網絡架構大致如下圖所示
卷積層
與全連接神經網絡的對比
為什么在有存在全連接神經網絡的情況下還會出現卷積神經網絡呢?
這就得來看看全連接神經網絡存在的問題了,全連接神經網絡中存在的問題是數據的形狀被“忽略了”。比如,輸入的數據是圖像時,圖像通常是高、長、通道方向上的3為形狀。
但是,全連接層輸入時,需要將3維數據拉平為1維數據,因此,我們可能會丟失圖像數據中存在有的空間信息(如空間上臨近像素為相似的值、RGB的各個通道之間的關聯性、相距較遠像素之間的關聯性等),這些信息都會被全連接層丟失。
而卷積層可以保持形狀不變,將輸入的數據以相同的維度輸出,因此,可能可以正確理解圖像等具有形狀的數據。
卷積運算
卷積層進行的處理就是卷積運算,運算方式如下圖所示
一般在計算中也會加上偏置
填充
在進行卷積層的處理之前,有時要向輸入數據的周圍填入固定的數據(如0),稱為填充(padding)。如下圖所示
向輸入數據的周圍填入0,將大小為(4, 4)的輸入數據變成了(6, 6)的形狀。
為什么要進行填充操作?
在對大小為(4, 4)的輸入數據使用(3, 3)的濾波器時,輸出的大小會變成(2, 2),如果反復進行多次卷積運算,在某個時刻輸出大小就有可能變成1,導致無法再應用卷積運算。為了避免這種情況,就要使用填充,使得卷積運算可以在保持空間大小不變的情況下將數據傳給下一層。
步幅
應用濾波器的位置間隔稱為步幅(stride)。在上面的例子中,步幅都為1,如果將步幅設為2,則如下圖所示
可以發現,增大步幅后,輸出大小會變小,增大填充后,輸出大小會變大。
假設輸入大小為(H, W),濾波器大小為(FH, FW),輸出大小為(OH, OW),填充為P,步幅為S,則輸出大小可以表示為
三維數據卷積運算
上面卷積運算的例子都是二維的數據,但是,一般來說我們的圖像數據都是三維的,除了高、寬之外,還需要處理通道方向的數據。如下圖所示
其計算方式為每個通道處的數據與對應通道的卷積核相乘,最后將各個通道得到的結果相加,從而得到輸出。
這里需要注意的是,一般情況下,卷積核的通道數需要與輸入數據的通道數相同。(有時會使用1x1卷積核來對通道數進行降/升維操作)。可參考這篇文章
上面給出的例子輸出的結果還是一個通道的,如果我們想要輸出的結果在通道上也有多個輸出,該怎么做呢?如下圖所示
即使用多個卷積核
池化層
池化是縮小高、長方向上的空間的運算。比如下圖所示的最大池化
除了上圖所示的Max池化之外,還有Average池化。一般來說,池化的窗口大小會和步幅設定成相同的值。
im2col
從前面的例子來看,會發現,如果完全按照計算過程來寫代碼的話,要用上好幾層for循環,這樣的話不僅寫起來麻煩,估計在運行的時候計算速度也很慢。這里將介紹im2col的方法。
im2col將輸入數據展開以適合卷積核,如下圖所示
對3維的輸入數據應用im2col之后,數據轉化維2維矩陣。
使用im2col展開輸入數據后,之后就只需將卷積層的卷積核縱向展開為1列,並計算2個矩陣的乘積即可。這和全連接層的Affine層進行的處理基本相同。
代碼實現
講了這么多,這里將給出代碼實現
import numpy as np
def im2col(input_data, filter_h, filter_w, stride=1, pad=0):
"""
input_data : 由(數據量,通道,高,長)的4維數組構成的輸入數據
filter_h : 濾波器的高
filter_w : 濾波器的長
stride : 步幅
pad : 填充
"""
N, C, H, W = input_data.shape
out_h = (H + 2*pad - filter_h)//stride + 1
out_w = (W + 2*pad - filter_w)//stride + 1
img = np.pad(input_data, [(0,0), (0,0), (pad, pad), (pad, pad)], 'constant')
col = np.zeros((N, C, filter_h, filter_w, out_h, out_w))
for y in range(filter_h):
y_max = y + stride*out_h
for x in range(filter_w):
x_max = x + stride*out_w
col[:, :, y, x, :, :] = img[:, :, y:y_max:stride, x:x_max:stride]
col = col.transpose(0, 4, 5, 1, 2, 3).reshape(N*out_h*out_w, -1)
return col
def col2im(col, input_shape, filter_h, filter_w, stride=1, pad=0):
N, C, H, W = input_shape
out_h = (H + 2*pad - filter_h)//stride + 1
out_w = (W + 2*pad - filter_w)//stride + 1
col = col.reshape(N, out_h, out_w, C, filter_h, filter_w).transpose(0, 3, 4, 5, 1, 2)
img = np.zeros((N, C, H + 2*pad + stride - 1, W + 2*pad + stride - 1))
for y in range(filter_h):
y_max = y + stride*out_h
for x in range(filter_w):
x_max = x + stride*out_w
img[:, :, y:y_max:stride, x:x_max:stride] += col[:, :, y, x, :, :]
return img[:, :, pad:H + pad, pad:W + pad]
class Convolution:
def __init__(self, W, b, stride=1, pad=0):
self.W = W
self.b = b
self.stride = stride
self.pad = pad
self.x = None
self.col = None
self.col_W = None
self.dW = None
self.db = None
def forward(self, x):
# [N, C, H, W]
FN, C, FH, FW = self.W.shape
N, C, H, W = x.shape
out_h = int(1 + (H + 2 * self.pad - FH) / self.stride)
out_w = int(1 + (W + 2 * self.pad - FW) / self.stride)
col = im2col(x, FH, FW, self.stride, self.pad)
col_W = self.W.reshape(FN, -1).T # 濾波器展開
out = np.dot(col, col_W) + self.b
out = out.reshape(N, out_h, out_w, -1).transpose(0, 3, 1, 2)
self.x = x
self.col = col
self.col_W = col_W
return out
def backward(self, dout):
FN, C, FH, FW = self.W.shape
dout = dout.transpose(0, 2, 3, 1).reshape(-1, FN)
self.db = np.sum(dout, axis=0)
self.dW = np.dot(self.col.T, dout)
self.dW = self.dW.transpose(1, 0).reshape(FN, C, FH, FW)
dcol = np.dot(dout, self.col_W.T)
dx = col2im(dcol, self.x.shape, FH, FW, self.stride, self.pad)
return dx
class Pooling:
def __init__(self, pool_h, pool_w, stride=1, pad=0):
self.pool_h = pool_h
self.pool_w = pool_w
self.stride = stride
self.pad = pad
self.x = None
self.arg_max = None
def forward(self, x):
N, C, H, W = x.shape
out_h = int(1 + (H - self.pool_h) / self.stride)
out_w = int(1 + (W - self.pool_w) / self.stride)
col = im2col(x, self.pool_h, self.pool_w, self.stride, self.pad)
col = col.reshape(-1, self.pool_h*self.pool_w)
arg_max = np.argmax(col, axis=1)
out = np.max(col, axis=1)
out = out.reshape(N, out_h, out_w, -1).transpose(0, 3, 1, 2)
self.x = x
self.arg_max = arg_max
return out
def backward(self, dout):
dout = dout.transpose(0, 2, 3, 1)
pool_size = self.pool_h * self.pool_w
dmax = np.zeros((dout.size, pool_size))
dmax[np.arange(self.arg_max.size), self.arg_max.flatten()] = dout.flatten()
dmax = dmax.reshape(dout.shape + (pool_size,))
dcol = dmax.reshape(dmax.shape[0] * dmax.shape[1] * dmax.shape[2], -1)
dx = col2im(dcol, self.x.shape, self.pool_h, self.pool_w, self.stride, self.pad)
return dx
小節
這篇文章斷斷續續地寫了好久,中間還順便在學tensorflow 2.0 ,還是框架用的舒服 orz。。。這幾天還是決定把這篇文章寫完,坑挖了還是得填,numpy手撕NN系列也算是暫時完成了,RNN后面再考慮。。。這之后准備再補補一些學過的算法的總結以及前段時間看的一些論文的總結。
本文首發於我的知乎