一丶一般的神經網絡與CNN
CNN 中新增了 Convolution 層 和 Pooling 層。連接順序是“Convolution - ReLU -(Pooling)”(Pooling層有時會被省略),之前的“Affine - ReLU”連接被替換成了“Convolution -ReLU -(Pooling)”連接
- 全連接層存在的問題:
- 圖像通常是高、長、通道方向上的3維形狀但是,向全連接層輸入時,需要將3維數據拉平爲1維數據,輸入圖像就是1通道、高28像素、長28像素的(1, 28, 28)形狀,但卻被排成1列,以784個數據的形式輸入到最開始的Affine層無法利用與形狀相關的信息比如,空間上鄰近的像素爲相似的值
卷積層可以保持形狀不變。當輸入數據是圖像時,卷積層會以3維數據的形式接收輸入數據,並同樣以3維數據的形式輸出至下一層,因此,在CNN中,可以(有可能)正確理解圖像等具有形狀的數據
- 有一點需要注意
- 神經網絡的矩陣的乘積運算在幾何學領域被稱爲“仿射變換”。因此,這裏將進行仿射變換的處理實現爲“Affine層”,所以在Affine層的網絡中權重矩陣的大小爲(前一層神經元數量,後一次神經元數量),但使用卷積層的時候使用的是卷積運算,權重矩陣的大小不需要由前一層和當前神經元數量來決定(不過當然過濾器的通道數需要由前一層決定),是由你需要的濾波器的大小決定的,這也就是兩種初始化對網絡的區別
# 普通網絡
def __init__(self, input_size, hidden_size, output_size, weight_init_std = 0.01):
self.params['W1'] = weight_init_std * np.random.randn(input_size, hidden_size)
self.params['b1'] = np.zeros(hidden_size)
self.layers['Affine1'] = Affine(self.params['W1'], self.params['b1'])
# CNN
def __init__(self, input_dim=(1, 28, 28),
conv_param={'filter_num':30, 'filter_size':5, 'pad':0, 'stride':1},
hidden_size=100, output_size=10, weight_init_std=0.01):
self.params['W1'] = weight_init_std * np.random.randn(filter_num, input_dim[0], filter_size, filter_size)
self.params['b1'] = np.zeros(filter_num)
self.layers['Conv1'] = Convolution(self.params['W1'], self.params['b1'],conv_param['stride'], conv_param['pad'])
二丶卷積神經網絡實現
① 卷積層實現
這裏就不說卷積運算了:將各個位置上濾波器的元素和輸入的對應元素相乘,然後再求和
填充也不說了:用填充主要是爲了調整輸出的大小,比如,對大小爲(4, 4)的輸入數據應用(3, 3)的濾波器時,輸出大小變爲(2, 2),相當於輸出大小比輸入大小縮小了 2個元素。這在反覆進行多次卷積運算的深度網絡中會成爲問題。
如果老老實實地實現卷積運算,估計要重複好幾層的for語句。這樣的實現有點麻煩,而且,NumPy中存在使用for語句後處理變慢的缺點(NumPy中,訪問元素時最好不要用for語句)。這裏,我們不使用for語句,而是使用im2col這個便利的函數進行簡單的實現,im2col會把輸入數據展開以適合濾波器(權重),把包含批數量的4維數據轉換成了2維數據
- 比如下面 10個 通道 爲 3的7 × 7的數據 數據 然後濾波器的大小爲5 × 5,步幅爲1,填充爲0
x2 = np.random.rand(10, 3, 7, 7) # 10個數據
col2 = im2col(x2, 5, 5, stride=1, pad=0)
print(col2.shape)
>> (90,75)
上圖爲了便於觀察,將步幅設置得很大,以使濾波器的應用區
而在實際的卷積運算中,濾波器的應用區域幾乎都是重疊的。在濾波器的應用區域重疊的情況下,使用im2col展開後,展開後的元素個數會多於原方塊的元素個數。因此,使用im2col的實現存在比普通的實現消耗更多內存的缺點。但是,彙總成一個大的矩陣進行計算,對計算機的計算頗有益處。比如,在矩陣計算的庫(線性代數庫)等中,矩陣計算的實現已被高
度最優化,可以高速地進行大矩陣的乘法運算。因此,通過歸結到矩陣計算上,可以有效地利用線性代數庫
class Convolution:
def __init__(self, W, b, stride=1, pad=0):
self.W = W
self.b = b
self.stride = stride
self.pad = pad
# 中間數據(backward時使用)
self.x = None
self.col = None
self.col_W = None
# 權衡參數的梯度
self.dW = None
self.db = None
def forward(self, x):
FN, C, FH, FW = self.W.shape
N, C, H, W = x.shape
out_h = 1 + int((H + 2*self.pad - FH) / self.stride)
out_w = 1 + int((W + 2*self.pad - FW) / self.stride)
# 用im2col展開輸入數據
col = im2col(x, FH, FW, self.stride, self.pad)
# 用reshape將濾波器展開爲2維數組
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
- 卷積層的初始化方法將濾波器(權重)、偏置、步幅、填充作爲參數接收
- 濾波器是 (FN, C, FH, FW)的 4 維形狀。另外,FN、C、FH、FW分別是 Filter Number(濾波器數量)、Channel、Filter Height、Filter Width的縮寫,濾波器的這幾個參數是由權重W的shape獲取
- N, C, H, W 從輸入參數X的shape獲取
② 池化層實現
池化是縮小高、長方向上的空間的運算,除了Max池化之外,還有Average池化等
池化層的實現和卷積層相同,都使用im2col展開輸入數據,不過,池化
的情況下,在通道方向上是獨立的,這一點和卷積層不同。池化的應用區域按通道單獨展開
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, C).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
卷積運算
填充
數據應用(3, 3)的濾波器時,輸出大小變爲(2, 2),相當於輸出大小比輸入大小縮小了 2個元素。這在反覆進行多次卷積運算的深度網
絡中會成爲問題。爲什麼呢?因爲如果每次進行卷積運算都會縮小空間,那麼在使用填充主要是爲了調整輸出的大小。比如,對大小爲(4, 4)的輸入某個時刻輸出大小就有可能變爲 1,導致無法再應用卷積運算。
步幅
應用濾波器的位置間隔稱爲步幅(stride)