主要內容來自DeepLearning.AI的卷積神經網絡python
本文使用numpy實現卷積層和池化層,包括前向傳播和反向傳播過程。數組
在具體描述以前,先對使用符號作定義。緩存
首先,導入須要使用的工具包。網絡
import numpy as np import h5py import matplotlib.pyplot as plt %matplotlib inline plt.rcParams['figure.figsize'] = (5.0, 4.0) # set default size of plots plt.rcParams['image.interpolation'] = 'nearest' plt.rcParams['image.cmap'] = 'gray' %load_ext autoreload %autoreload 2 np.random.seed(1) # 指定隨機數種子
本文要實現的卷積神經網絡的幾個網絡塊,每一個網絡包含的功能模塊以下。框架
卷積函數Convolutiondom
池化函數Poolingide
在每一個前向傳播的函數中,在參數更新時會有一個反向傳播過程;此外,在前向傳播過程會緩存一個參數,用於在反向傳播過程當中計算梯度。函數
儘管當下存在不少深度學習框架使得卷積網絡使用更爲便捷,可是卷積網絡在深度學習中仍然是一個難以理解的運算。卷積層能將輸入轉換爲具備不一樣維度的輸出,以下圖所示。工具
接下來,咱們本身實現卷積運算。首先,實現兩個輔助函數:0填充邊界和計算卷積。學習
0值邊界填充顧名思義,使用0填充在圖片的邊界周圍。
使用邊界填充的優勢:
# GRADED FUNCTION: zero_pad def zero_pad(X, pad): """ 把數據集X的圖像邊界用0值填充。填充狀況發生在每張圖像的寬度和高度上。 參數: X -- 圖像數據集 (m, n_H, n_W, n_C),分別表示樣本數、圖像高度、圖像寬度、通道數 pad -- 整數,每一個圖像在垂直和水平方向上的填充量 返回: X_pad -- 填充後的圖像數據集 (m, n_H + 2*pad, n_W + 2*pad, n_C) """ # X數據集有4個維度,填充發生在第2個維度和第三個維度上;填充方式爲0值填充 X_pad = np.pad(X, ( (0, 0),# 樣本數維度,不填充 (pad, pad), #n_H維度,上下各填充pad個像素 (pad, pad), #n_W維度,上下各填充pad個像素 (0, 0)), #n_C維度,不填充 mode='constant', constant_values = (0, 0)) return X_pad
咱們來測試一下:
np.random.seed(1) x = np.random.randn(4, 3, 3, 2) x_pad = zero_pad(x, 2) print ("x.shape =\n", x.shape) print ("x_pad.shape =\n", x_pad.shape) print ("x[1,1] =\n", x[1,1]) print ("x_pad[1,1] =\n", x_pad[1,1]) fig, axarr = plt.subplots(1, 2) axarr[0].set_title('x') axarr[0].imshow(x[0,:,:,0]) axarr[1].set_title('x_pad') axarr[1].imshow(x_pad[0,:,:,0])
測試結果:
x.shape = (4, 3, 3, 2) x_pad.shape = (4, 7, 7, 2) x[1,1] = [[ 0.90085595 -0.68372786] [-0.12289023 -0.93576943] [-0.26788808 0.53035547]] x_pad[1,1] = [[ 0. 0.] [ 0. 0.] [ 0. 0.] [ 0. 0.] [ 0. 0.] [ 0. 0.] [ 0. 0.]]
實現一個單步卷積的計算過程,將卷積核和輸入的一個窗口片進行計算,以後使用這個函數實現真正的卷積運算。卷積運算包括:
咱們看一個卷積運算過程:
在計算機視覺應用中,左側矩陣的每一個值表明一個像素,咱們使用一個3x3的卷積核和輸入圖像對應窗口片進行element-wise相乘而後求和,最後加上bias完成一個卷積的單步運算。
# GRADED FUNCTION: conv_single_step def conv_single_step(a_slice_prev, W, b): """ 使用卷積核與上一層的輸出結果的一個分片進行卷積運算 參數: a_slice_prev -- 輸入分片, (f, f, n_C_prev) W -- 權重參數,包含在一個矩陣中 (f, f, n_C_prev) b -- 偏置參數,包含在一個矩陣中 (1, 1, 1) 返回: Z -- 一個實數,表示在輸入數據X的分片a_slice_prev和滑動窗口(W,b)的卷積計算結果 """ # 逐元素相乘,結果維度爲(f,f,n_C_prev) s = np.multiply(a_slice_prev, W) # 求和 Z = np.sum(s) # 加上偏置參數b,使用float將(1,1,1)變爲一個實數 Z = Z + float(b) return Z
咱們測試一下代碼:
np.random.seed(1) a_slice_prev = np.random.randn(4, 4, 3) W = np.random.randn(4, 4, 3) b = np.random.randn(1, 1, 1) Z = conv_single_step(a_slice_prev, W, b) print("Z =", Z)
輸出結果爲:
Z = -6.99908945068
在卷積層的前向傳播過程當中會使用多個卷積核,每一個卷積核與輸入圖片計算獲得一個2D矩陣,而後將多個卷積核的計算結果堆疊起來造成最終輸出。
咱們須要實現一個卷積函數,這個函數接收上一層的輸出結果A_prev,而後使用大小爲fxf卷積核進行卷積運算。這裏的輸入A_prev爲若干張圖片,同時卷積核的數量也可能有多個。
爲了方便理解卷積的計算過程,咱們這裏使用for循環進行描述。
提示:
若是須要在(5,5,3)的矩陣的左上角截取一個2x2的分片,可使用:a_slice_prev = a_prev[0:2,0:2,:]
;值得注意的是分片結果具備3個維度(2,2,n_C_prev)
若是想自定義分片,須要明確分片的位置,可使用vert_start, vert_end, horiz_start 和horiz_end四個值來肯定。如圖
卷積層計算結果的維度計算,可使用公式肯定:
卷積層代碼以下:
# GRADED FUNCTION: conv_forward def conv_forward(A_prev, W, b, hparameters): """ 卷積層的前向傳播 參數: A_prev --- 上一層網絡的輸出結果,(m, n_H_prev, n_W_prev, n_C_prev), W -- 權重參數,指這一層的卷積核參數 (f, f, n_C_prev, n_C),n_C個大小爲(f,f,n_C_prev)的卷積核 b -- 偏置參數 (1, 1, 1, n_C) hparameters -- 超參數字典,包含 "stride" and "pad" 返回: Z -- 卷積計算結果,維度爲 (m, n_H, n_W, n_C) cache -- 緩存卷積層反向傳播計算須要的數據 """ # 輸出參數的維度,包含m個樣from W's shape (≈1 line) (m, n_H_prev, n_W_prev, n_C_prev) = A_prev.shape # 權重參數 (f, f, n_C_prev, n_C) = W.shape # 獲取本層的超參數:步長和填充寬度 stride = hparameters['stride'] pad = hparameters['pad'] # 計算輸出結果的維度 # 使用int函數代替np.floor向下取整 n_H = int((n_H_prev + 2 * pad - f)/stride) + 1 n_W = int((n_W_prev + 2 * pad - f)/stride) + 1 # 聲明輸出結果 Z = np.zeros((m, n_H, n_W, n_C)) # 1. 對輸出數據A_prev進行0值邊界填充 A_prev_pad = zero_pad(A_prev, pad) for i in range(m): # 依次遍歷每一個樣本 a_prev_pad = A_prev_pad[i] # 獲取當前樣本 for h in range(n_H): # 在輸出結果的垂直方向上循環 for w in range(n_W):#在輸出結果的水平方向上循環 # 肯定分片邊界 vert_start = h * stride vert_end = vert_start + f horiz_start = w * stride horiz_end = horiz_start + f for c in range(n_C): # 遍歷輸出的通道 # 在輸入數據上獲取當前切片,結果是3D a_slice_prev = a_prev_pad[vert_start:vert_end,horiz_start:horiz_end,:] # 獲取當前的卷積核參數 weights = W[:,:,:, c] biases = b[:,:,:, c] # 輸出結果當前位置的計算值,使用單步卷積函數 Z[i, h, w, c] = conv_single_step(a_slice_prev, weights, biases) assert(Z.shape == (m, n_H, n_W, n_C)) # 將本層數據緩存,方便反向傳播時使用 cache = (A_prev, W, b, hparameters) return Z, cache
咱們來測試一下:
np.random.seed(1) A_prev = np.random.randn(10,5,7,4) W = np.random.randn(3,3,4,8) b = np.random.randn(1,1,1,8) hparameters = {"pad" : 1, "stride": 2} Z, cache_conv = conv_forward(A_prev, W, b, hparameters) print("Z's mean =\n", np.mean(Z)) print("Z[3,2,1] =\n", Z[3,2,1]) print("cache_conv[0][1][2][3] =\n", cache_conv[0][1][2][3])
輸出值爲:
Z's mean = 0.692360880758 Z[3,2,1] = [ -1.28912231 2.27650251 6.61941931 0.95527176 8.25132576 2.31329639 13.00689405 2.34576051] cache_conv[0][1][2][3] = [-1.1191154 1.9560789 -0.3264995 -1.34267579]
最後,卷積層應該包含一個激活函數,咱們能夠添加如下代碼完成:
# 獲取輸出個某個單元值 Z[i, h, w, c] = ... # 使用激活函數 A[i, h, w, c] = activation(Z[i, h, w, c])
池化層能夠用於縮小輸入數據的高度和寬度。池化層有利於簡化計算,同時也有助於對輸入數據的位置更加穩定。池化層有兩種類型:
池化層沒有參數須要訓練,可是它們有像窗口大小f的超參數,它指定了窗口的大小爲f x f,這個窗口用於計算最大值和平均值。
咱們這裏在同一個函數中實現最大池化和平均池化。
提示:
池化層沒有填充項,輸出結果的維度和輸入數據的維度相關,計算公式爲:
實現代碼以下:
def pool_forward(A_prev, hparameters, mode = "max"): """ 池化層的前向傳播 參數: A_prev -- 輸入數據,維度爲 (m, n_H_prev, n_W_prev, n_C_prev) hparameters -- 超參數字典,包含 "f" and "stride" mode -- string;表示池化方式, ("max" or "average") 返回: A -- 輸出結果,維度爲 (m, n_H, n_W, n_C) cache -- 緩存數據,用於池化層的反向傳播, 緩存輸入數據和池化層的超參數(f、stride) """ (m, n_H_prev, n_W_prev, n_C_prev) = A_prev.shape f = hparameters["f"] stride = hparameters["stride"] # 計算輸出數據的維度 n_H = int(1 + (n_H_prev - f) / stride) n_W = int(1 + (n_W_prev - f) / stride) n_C = n_C_prev # 定義輸出結果 A = np.zeros((m, n_H, n_W, n_C)) # 逐個計算,對A的元素進行賦值 for i in range(m): # 遍歷樣本 for h in range(n_H):# 遍歷n_H維度 # 肯定分片垂直方向上的位置 vert_start = h * stride vert_end =vert_start + f for w in range(n_W):# 遍歷n_W維度 # 肯定分片水平方向上的位置 horiz_start = w * stride horiz_end = horiz_start + f for c in range (n_C):# 遍歷通道 # 肯定當前樣本上的分片 a_prev_slice = A_prev[i, vert_start:vert_end, horiz_start:horiz_end, c] # 根據池化方式,計算當前分片上的池化結果 if mode == "max":# 最大池化 A[i, h, w, c] = np.max(a_prev_slice) elif mode == "average":# 平均池化 A[i, h, w, c] = np.mean(a_prev_slice) # 將池化層的輸入和超參數緩存 cache = (A_prev, hparameters) # 確保輸出結果維度正確 assert(A.shape == (m, n_H, n_W, n_C)) return A, cache
在深度學習框架中,你只須要實現前向傳播,框架能夠自動實現反向傳播過程,所以大多數深度學習工程師不須要關注反向傳播過程。卷積層的反向傳播過程比較複雜。
在以前的咱們實現全鏈接神經網絡,咱們使用反向傳播計算損失函數的偏導數進而對參數進行更新。相似的,在卷積神經網絡中咱們也能夠計算損失函數對參數的梯度進而進行參數更新。
咱們這裏實現卷積層的反向傳播過程。
下面是計算dA的公式:
其中\(W_c\)表示一個卷積核,\(dZ_{hw}\)是損失函數對卷積層的輸出Z的第h行第w列的梯度。值得注意的是,每次更新dA時都會用相同的\(W_c\)乘以不一樣的\(dZ\). 由於卷積層在前向傳播過程當中,同一個卷積核會和輸入數據的每個分片逐元素相乘而後求和。因此在反向傳播計算dA時,須要把全部a_slice的梯度都加進來。咱們能夠在循環中添加代碼:
da_prev_pad[vert_start:vert_end, horiz_start:horiz_end, :] += W[:,:,:,c] * dZ[i, h, w, c]
計算\(dW_c\)的公式(Wc是一個卷積核):
其中,\(a_{slice}\)表示\(Z_{hw}\)對應的輸入分片。由於咱們使用卷積核做爲一個窗口對輸入數據進行切片計算卷積,滑動了多少次就對應多少個分片,也就須要累加多少梯度數據。在代碼中咱們只須要添加一行代碼:
dW[:,:,:,c] += a_slice * dZ[i, h, w, c]
損失函數對當前卷積層的參數b的梯度db的計算公式:
和以前的神經網絡相似,db是由dZ累加計算而成。只須要將conv的輸出Z的全部梯度累加便可。在循環中添加一行代碼:
db[:,:,:,c] += dZ[i, h, w, c]
def conv_backward(dZ, cache): """ 實現卷積層的反向傳播過程 參數: dZ -- 損失函數對卷積層輸出Z的梯度, 維度和Z相同(m, n_H, n_W, n_C) cache -- 卷積層前向傳播過程當中緩存的數據 返回: dA_prev -- 損失函數對卷積層輸入A_prev的梯度,其維度和A_prev相同(m, n_H_prev, n_W_prev, n_C_prev) dW -- 損失函數對卷積層權重參數的梯度,其維度和W相同(f, f, n_C_prev, n_C) db -- 損失函數對卷積層偏置參數b的梯度,其維度和b相同(1, 1, 1, n_C) """ # 獲得前向傳播中的緩存數據,方便後續使用 (A_prev, W, b, hparameters) = cache # 輸入數據的維度 (m, n_H_prev, n_W_prev, n_C_prev) = A_prev.shape # Retrieve dimensions from W's shape (f, f, n_C_prev, n_C) = W.shape # Retrieve information from "hparameters" stride = hparameters['stride'] pad = hparameters['pad'] (m, n_H, n_W, n_C) = dZ.shape # 對輸出結果進行初始化 dA_prev = np.zeros_like(A_prev) dW = np.zeros_like(W) db = np.zeros_like(b) # 對卷積層輸入A_prev進行邊界填充;卷積運算時使用的是A_prev_pad A_prev_pad = zero_pad(A_prev, pad) dA_prev_pad = zero_pad(dA_prev, pad) # 咱們先計算dA_prev_pad,而後切片獲得dA_prev for i in range(m): # 遍歷樣本 # 選擇一個樣本 a_prev_pad = A_prev_pad[i] da_prev_pad = dA_prev_pad[i] for h in range(n_H):# 在輸出的垂直方向量循環 for w in range(n_W):# 在輸出的水平方向上循環 for c in range(n_C):# 在輸出的通道上循環 # 肯定輸入數據的切片邊界 vert_start = h * strider vert_end = vert_start + f horiz_start = w * stride horiz_end = horiz_start + f a_slice = a_prev_pad[vert_start:vert_end, horiz_start:horiz_end, :] # 計算各梯度 da_prev_pad[vert_start:vert_end, horiz_start:horiz_end, :] += W[:,:,:,c] * dZ[i, h, w, c] dW[:,:,:,c] += a_slice * dZ[i,h,w,c] db[:,:,:,c] += dZ[i,h,w,c] # 在填充後計算結果中切片獲得填充以前的dA_prev梯度; dA_prev[i, :, :, :] = da_prev_pad[pad:-pad, pad:-pad, :] assert(dA_prev.shape == (m, n_H_prev, n_W_prev, n_C_prev)) return dA_prev, dW, db
咱們來測試一下:
np.random.seed(1) A_prev = np.random.randn(10,4,4,3) W = np.random.randn(2,2,3,8) b = np.random.randn(1,1,1,8) hparameters = {"pad" : 2, "stride": 2} Z, cache_conv = conv_forward(A_prev, W, b, hparameters) # Test conv_backward dA, dW, db = conv_backward(Z, cache_conv) print("dA_mean =", np.mean(dA)) print("dW_mean =", np.mean(dW)) print("db_mean =", np.mean(db))
輸出結果:
dA_mean = 1.45243777754 dW_mean = 1.72699145831 db_mean = 7.83923256462
接下來,咱們選擇從最大池化開始實現池化層的反向傳播過程。儘管池化層沒有參數須要訓練,可是咱們仍然須要計算梯度,由於咱們須要將梯度經過池化層傳遞下去,計算下一層參數的梯度。
在實現池化層的反向傳播以前,咱們須要實現一個輔助函數create_mask_from_window(),用於實現:
這個函數依據輸入的X建立了一個掩碼,以保存輸入數據的最大值位置。掩碼中1表示對應位置爲最大值(咱們假設只有一個最大值)。
提示:
def create_mask_from_window(x): """ 根據輸入X建立一個保存最大值位置的掩碼 參數: x -- 輸入數據,維度爲 (f, f) 返回值: mask -- 形狀和x相同的保存其最大值位置的掩碼矩陣 """ mask = (x == np.max(x)) return mask
咱們來測試一下:
np.random.seed(1) x = np.random.randn(2,3) mask = create_mask_from_window(x) print('x = ', x) print("mask = ", mask)
輸出結果爲:
x = [[ 1.62434536 -0.61175641 -0.52817175] [-1.07296862 0.86540763 -2.3015387 ]] mask = [[ True False False] [False False False]]
在最大池化中,每一個輸入窗口的對輸出的影響僅僅來源於窗口的最大值;在平均池化中,窗口的每一個元素對輸出結果有相同的影響。因此咱們須要設計一個函數實現上述功能。
咱們來看一個具體的例子:
def distribute_value(dz, shape): """ 將梯度值均衡分佈在shape的矩陣中 參數: dz -- 標量,損失函數對某個參數的梯度 shape -- 輸出的維度(n_H, n_W) Returns: a -- 將dz均衡散佈在(n_H, n_W)後的矩陣 """ (n_H, n_W) = shape # 每一個元素的值 average = dz / (n_H * n_W) # 輸出結果 a = np.ones(shape) * average return a
咱們來測試一下:
a = distribute_value(2, (2,2)) print('distributed value =', a)
輸出結果:
distributed value = [[ 0.5 0.5] [ 0.5 0.5]]
將上述的輔助函數集合起來實現池化層的反向傳播過程:
def pool_backward(dA, cache, mode = "max"): """ 實現池化層的反向傳播 參數: dA -- 損失函數對池化層輸出數據A的梯度,維度和A相同 cache -- 池化層的緩存數據,包括輸入數據和超參數 mode -- 字符串,代表池化類型 ("max" or "average") 返回: dA_prev -- 對池化層輸入數據A_prv的梯度,維度和A_prev相同 """ (A_prev, hparameters) = cache stride = hparameters['stride'] f = hparameters['f'] m, n_H_prev, n_W_prev, n_C_prev = A_prev.shape m, n_H, n_W, n_C = dA.shape # 對輸出結果進行初始化 dA_prev = np.zeros_like(A_prev) for i in range(m):# 遍歷m個樣本 a_prev = A_prev[i] for h in range(n_H):# 在垂直方向量遍歷 for w in range(n_W):#在水平方向上循環 for c in range(n_C):# 在通道上循環 # 找到輸入的分片的邊界 vert_start = h * stride vert_end = vert_start + f horiz_start = w * stride horiz_end = horiz_start + f # 根據池化方式選擇不一樣的計算過程 if mode == "max": # 肯定輸入數據的切片 a_prev_slice = a_prev[vert_start:vert_end, horiz_start:horiz_end, c] # 建立掩碼 mask = create_mask_from_window(a_prev_slice) # 計算dA_prev dA_prev[i, vert_start: vert_end, horiz_start: horiz_end, c] += np.multiply(mask, dA[i,h,w,c]) elif mode == "average": # 獲取da值, 一個實數 da = dA[i,h,w,c] shape = (f, f) # 反向傳播 dA_prev[i, vert_start: vert_end, horiz_start: horiz_end, c] += distribute_value(da, shape) assert(dA_prev.shape == A_prev.shape) return dA_prev
測試一下:
np.random.seed(1) A_prev = np.random.randn(5, 5, 3, 2) hparameters = {"stride" : 1, "f": 2} A, cache = pool_forward(A_prev, hparameters) dA = np.random.randn(5, 4, 2, 2) dA_prev = pool_backward(dA, cache, mode = "max") print("mode = max") print('mean of dA = ', np.mean(dA)) print('dA_prev[1,1] = ', dA_prev[1,1]) print() dA_prev = pool_backward(dA, cache, mode = "average") print("mode = average") print('mean of dA = ', np.mean(dA)) print('dA_prev[1,1] = ', dA_prev[1,1])
輸出結果爲:
mode = max mean of dA = 0.145713902729 dA_prev[1,1] = [[ 0. 0. ] [ 5.05844394 -1.68282702] [ 0. 0. ]] mode = average mean of dA = 0.145713902729 dA_prev[1,1] = [[ 0.08485462 0.2787552 ] [ 1.26461098 -0.25749373] [ 1.17975636 -0.53624893]]
至此,咱們完成了卷積層和池化層的前向傳播和反向傳播過程,以後咱們能夠來構建卷積神經網絡。