目錄
-
- 1. im2col
- 2.卷積層
- 3.池化層
講前注意:
了解卷積層和池化層的代碼主要有兩個稍困難的點,一個是關于transpose軸變換與reshape的使用,一個是反向傳播的思想。關于反向傳播的思想大家可以看這。Affine層和RuLe層的實作思想會用在卷積層和池化層的實作上。
本文不講卷積層和池化層的原理,是代碼實踐,請了解原理後看代碼。代碼中有巨量注釋,不多闡述
注意前向傳播和反向傳播代碼在相反處理資料,軸變換不改變數組的元素,隻改變元素的結構,隻要對應索引值不變,那麼元素是不會變的。大家可以先把im2col當作黑盒,看完卷積層和池化層的實作之後再看會很清晰。大家看代碼一個要注意每個矩陣的transpose和shape。
1. im2col
im2col是講多元資料放在二維矩陣中友善計算。
def im2col(input_data, filter_h, filter_w, stride=1, pad=0):
"""
Parameters
----------
input_data : 由(資料量, 通道, 高, 長)的4維數組構成的輸入資料
filter_h : 濾波器的高
filter_w : 濾波器的長
stride : 步幅
pad : 填充
Returns
-------
col : 2維數組
"""
N, C, H, W = input_data.shape#N 是資料個數 C 是通道數 H 是高 W 是長
out_h = (H + 2*pad - filter_h)//stride + 1 #計算輸出資料 即輸出特征圖 out_h 輸出特征圖高
out_w = (W + 2*pad - filter_w)//stride + 1#計算輸入資料 即輸入特征特 out_w 輸出特征圖長
# // 整除
img = np.pad(input_data, [(0,0), (0,0), (pad, pad), (pad, pad)], 'constant')
#np.pad 填充數組input_data
#constant_values沒有指派,預設填充0
#第一次元(shape[0])前面填充0個後面填充0個
#第二次元同理(0,0),第三第四次元同理(pad,pad)
#資料量和通道預設(0,0), 咩咩咩咩咩咩咩咩,,長和高根據pad參數填充
col = np.zeros((N, C, filter_h, filter_w, out_h, out_w))
# col 6維數組
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]
# 一次操作取了N批C個通道上
# col 的第三維y,第四維x指派 img第三次元y導y_max隔stride是步長,隔stride取一位,第四次元同理
# 資料批量和通道數不變的
# filter_h也會簡寫為FH或fh
col = col.transpose(0, 4, 5, 1, 2, 3).reshape(N*out_h*out_w, -1)
# transpose軸變換 軸變換的目的是改變索引順序友善reshape成想要的模樣
# filter_h也會簡寫為FH或fh
# col.shape = (N*out_h*out_w,C*FH*FW)
return col
這裡有一張圖,可以友善你了解im2col的循環到底做了什麼
![](https://img.laitimes.com/img/_0nNw4CM6IyYiwiM6ICdiwiI2EzX4xSZz91ZsAzNfRHLGZkRGZkRfJ3bs92YsAjMfVmepNHLtIjN1w2TyVTQClGVF5UMR9Fd4VGdsATNfd3bkFGazxSUhxGatJGbwhFT1Y0Mk9VZwlHdssmch1mclRXY39CXldWYtlWPzNXZj9mcw1ycz9WL49zZuBnLxYmN2MWY4YDOwQTY4QTNiRmNkRjN2kTO5YjYkJzN1MzLc52YucWbp5GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.png)
2.卷積層
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 濾波器長 C 通道數(和濾波器通道數一樣才能進行通道方向的卷積)
FN, C, FH, FW = self.W.shape
N, C, H, W = x.shape
out_h = int((H + 2 * self.pad - FH) / self.stride + 1)
out_w = int((W + 2 * self.pad - FW) / self.stride + 1)
#im2col四維變二維,實際上把N放在了一次元,把C放在了二次元
#im2col作為黑盒,詳細見im2col講解
#實際上im2col是把元素相乘變為矩陣相乘
col = im2col(x, FH, FW, self.stride, self.pad)
#col.shape =(N*out_h*out_w,FH*FW*C)
col_W = self.W.reshape(FN,-1).T
#col_W.shape=(FN,FH*FW*C) 轉置後(FH*FW*C,FN)
#其實col_W.shape=(FN,C*FH*FW),這裡提醒大家reshape是按索引順序來的是以不能直接col_W=reshape(-1,FN)
#不過值相同是以我沒有按順序寫
#矩陣乘法的規定了 濾波器通道數和輸入資料通道數必須相同,否則無法相乘
out = np.dot(col,col_W) + self.b
#out.shape = (N*out_h*out_w,FN)
#out的一個元素每個通道上 一個濾波器與輸入資料對應元素乘積和 的和
#行就是輸出資料的一個通道上每批資料的每個元素,列就是輸出資料的每個通道上的一批資料的一個元素 (一批指批處理)
out = out.reshape(N, out_h, out_w, -1).transpose(0,3,1,2)
#二維變成思維,然後再進行軸變換,把通道索引變到輸出資料長索引和輸出資料寬索引前面
#其實就是變成了下一層輸入資料的模樣,要知道本層輸入資料也是(批,通道,高,長)
#此時 out.shape = (N,FN,out_h,out_w)
#本層濾波器的個數就是下一層輸入資料的通道數
self.x = x
self.col = col
self.col_W = col_W
return out
def backward(self, dout):
#根據誤差反向傳播思想,詳細見誤差反向傳播章節
#與Affine層不同的此處會注釋一下
FN, C, FH, FW = self.W.shape
# 與forward相反 把通道索引變在最後
dout = dout.transpose(0,2,3,1).reshape(-1, FN)
#transpose後 dout.shape=(批,out_h,out_w,通道)
#reshape後dout.shape=(批*out_h*out_w,濾波器個數(輸出通道數))
self.db = np.sum(dout, axis=0)
#在forward中加了N*out_h*out_w個b,反向傳播就要加這些個dout
self.dW = np.dot(self.col.T, dout)
#反向傳播矩陣求導操作 dW.shape = (FH*FW*C,FN) 這四個參數熟悉吧,就是濾波器的參數也就是W該有的參數
self.dW = self.dW.transpose(1, 0).reshape(FN, C, FH, FW)
#dw換成标準形式 也就是w的輸入形式
dcol = np.dot(dout, self.col_W.T)
#反向傳播矩陣求導操作 dcol.shape=(批*out_h*out_w,C*FH*FW)
dx = col2im(dcol, self.x.shape, FH, FW, self.stride, self.pad)
#反向操作im2col
return dx
3.池化層
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)
#一行即是一個池化的應用區域内的值
# col.shape=(N*C*out_h*out_w,pool_h*pool_w)
arg_max = np.argmax(col, axis=1)
#最大值索引數組
out = np.max(col,axis=1)
#每行隻留下最大值 max池化
out = out.reshape(N,out_h,out_w,C).transpose(0,3,1,2)
#重新reshape為四維後做軸變換調整為輸入資料标準形式
self.x = x
self.arg_max = arg_max
return out
def backward(self, dout):
dout = dout.transpose(0, 2, 3, 1)
#forward的反向操作 此時dout.shape=(批,out_h,out_w,通道數)
pool_size = self.pool_h * self.pool_w
#pool_size 為im2col展開的列數
dmax = np.zeros((dout.size, pool_size))
#池化層輸出是所有im2col展開行的最大值,元素的個數即為原展開行的行數
#dout.size = arg_max.size
dmax[np.arange(self.arg_max.size), self.arg_max.flatten()] = dout.flatten()
#在最大值索引處賦予最大值
dmax = dmax.reshape(dout.shape + (pool_size,))
#dout.shape+pool_size是在dout的次元上又加了一個次元pool_size
#dmax.shape = (批,out_h,out_w,C,pool_size)
dcol = dmax.reshape(dmax.shape[0] * dmax.shape[1] * dmax.shape[2], -1)
#dcol.shape = (N*out_h*out_w,C*pool_size)而pool_size = pool_h*pool_w 正式im2col的輸出
dx = col2im(dcol, self.x.shape, self.pool_h, self.pool_w, self.stride, self.pad)
return dx