【DeepLearning.AI】使用numpy搭建卷積神經網絡

使用numpy搭建卷積神經網絡

主要內容來自DeepLearning.AI的卷積神經網絡

本文使用numpy實現卷積層和池化層,包括前向傳播和反向傳播過程。

在具體描述之前,先對使用符號做定義。

  • 上標[I]表示神經網絡的第Ith層。
    • a[4]a^{[4]}表示第4層神經網絡的激活值;W[5]W^{[5]}b[5]b^{[5]}表示神經網絡第5層的參數;
  • 上標(i)表示第i個數據樣本
    • x(i)x^{(i)}表示第i個輸入樣本
  • 下標i表示向量的第i個元素
    • ai[l]a_i^{[l]}表示神經網絡第l層的激活向量的第i個元素。
  • nH,nW,nCn_H,n_W,n_C表示當前層神經網絡的高度、寬度和通道數。
  • nHprevnWprevnCprevn_{H_{prev}},n_{W_{prev}},n_{C_{prev}}表示上一層神經網絡的高度、寬度和通道數。

1. 導入包


首先,導入需要使用的工具包。

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) # 指定隨機數種子

2. 大綱


本文要實現的卷積神經網絡的幾個網絡塊,每個網絡包含的功能模塊如下。

  • 卷積函數Convolution

    • 0填充邊界
    • 卷積窗口
    • 卷積運算前向傳播
    • 卷積運算反向傳播
  • 池化函數Pooling

    • 池化函數前向傳播
    • 掩碼創建
    • 值分配
    • 池化的反向傳播

    [外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-WdueA44q-1589298710418)(https://dijvuftadtswhbvbrhckte.coursera-apps.org/notebooks/week1/images/model.png)]

在每個前向傳播的函數中,在參數更新時會有一個反向傳播過程;此外,在前向傳播過程會緩存一個參數,用於在反向傳播過程中計算梯度。

3. 卷積神經網絡


儘管當下存在很多深度學習框架使得卷積網絡使用更爲便捷,但是卷積網絡在深度學習中仍然是一個難以理解的運算。卷積層能將輸入轉換爲具有不同維度的輸出,如下圖所示。

image-20200512164859913

接下來,我們自己實現卷積運算。首先,實現兩個輔助函數:0填充邊界和計算卷積。

3.1 0值邊界填充


0值邊界填充顧名思義,使用0填充在圖片的邊界周圍。

PAD

使用邊界填充的優點:

  • 可以保證使用上一層的輸出結果經過卷積運算後,其高度和寬度不會發生變化。這個特性對於構建深層網絡非常重要,否則隨着網絡深度的增加,計算結果會逐步縮水,直至降爲1。一種特殊的的“Same”卷積,可以保證計算結果的寬度和高度不發生變化
  • 可以在圖像邊界保留更多的信息。不適用填充的情況下,圖像的邊緣像素對下一層結果的影響小(圖像的中間像素使用滑動窗口時會遍歷多次,而邊界元素則比較少,有可能只使用1次)。
# 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.]]

image-20200512172310736

3.2 單步卷積(單個滑動窗口計算過程)

實現一個單步卷積的計算過程,將卷積核和輸入的一個窗口片進行計算,之後使用這個函數實現真正的卷積運算。卷積運算包括:

  • 接受一個輸入
  • 將卷積核和輸入的每個窗口分片進行單步卷積計算
  • 輸出計算結果(維度通常與輸入維度不同)

我們看一個卷積運算過程:

Convolution_schematic

在計算機視覺應用中,左側矩陣的每個值代表一個像素,我們使用一個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

3.3 卷積層的前向傳播

在卷積層的前向傳播過程中會使用多個卷積核,每個卷積核與輸入圖片計算得到一個2D矩陣,然後將多個卷積核的計算結果堆疊起來形成最終輸出。

image-20200512175118831

我們需要實現一個卷積函數,這個函數接收上一層的輸出結果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四個值來確定。如圖

    image-20200512180058292

  • 卷積層計算結果的維度計算,可以使用公式確定:image-20200512180205603

卷積層代碼如下:

# 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])

4. 池化層


池化層可以用於縮小輸入數據的高度和寬度。池化層有利於簡化計算,同時也有助於對輸入數據的位置更加穩定。池化層有兩種類型:

  • 最大池化:在輸入數據上用一個(f, f)的窗口進行滑動,在輸出中保存窗口的最大值
  • 平均池化:在輸入數據上用一個(f, f)的窗口進行滑動,在輸出中保存窗口的平均值

image-20200512212008383

池化層沒有參數需要訓練,但是它們有像窗口大小f的超參數,它指定了窗口的大小爲f x f,這個窗口用於計算最大值和平均值。

4.1 前向傳播

我們這裏在同一個函數中實現最大池化和平均池化。

提示:

池化層沒有填充項,輸出結果的維度和輸入數據的維度相關,計算公式爲:

image-20200512212445859

實現代碼如下:

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

5. 反向傳播


在深度學習框架中,你只需要實現前向傳播,框架可以自動實現反向傳播過程,因此大多數深度學習工程師不需要關注反向傳播過程。卷積層的反向傳播過程比較複雜。

在之前的我們實現全連接神經網絡,我們使用反向傳播計算損失函數的偏導數進而對參數進行更新。類似的,在卷積神經網絡中我們也可以計算損失函數對參數的梯度進而進行參數更新。

5.1 卷積層的反向傳播

我們這裏實現卷積層的反向傳播過程。

5.1.1 計算dA

下面是計算dA的公式:

image-20200512222648436

其中WcW_c表示一個卷積核,dZhwdZ_{hw}是損失函數對卷積層的輸出Z的第h行第w列的梯度。值得注意的是,每次更新dA時都會用相同的WcW_c乘以不同的dZdZ. 因爲卷積層在前向傳播過程中,同一個卷積核會和輸入數據的每一個分片逐元素相乘然後求和。所以在反向傳播計算dA時,需要把所有a_slice的梯度都加進來。我們可以在循環中添加代碼:

da_prev_pad[vert_start:vert_end, horiz_start:horiz_end, :] += W[:,:,:,c] * dZ[i, h, w, c]

5.1.2 計算dW

計算dWcdW_c的公式(Wc是一個卷積核):

image-20200512223715501

其中,aslicea_{slice}表示ZhwZ_{hw}對應的輸入分片。因爲我們使用卷積核作爲一個窗口對輸入數據進行切片計算卷積,滑動了多少次就對應多少個分片,也就需要累加多少梯度數據。在代碼中我們只需要添加一行代碼:

dW[:,:,:,c] += a_slice * dZ[i, h, w, c]

5.1.3 計算db

損失函數對當前卷積層的參數b的梯度db的計算公式:

image-20200512224249941

和之前的神經網絡類似,db是由dZ累加計算而成。只需要將conv的輸出Z的所有梯度累加即可。在循環中添加一行代碼:

db[:,:,:,c] += dZ[i, h, w, c]

5.1.4 函數實現

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

5.2 池化層的反向傳播

接下來,我們選擇從最大池化開始實現池化層的反向傳播過程。儘管池化層沒有參數需要訓練,但是我們仍然需要計算梯度,因爲我們需要將梯度通過池化層傳遞下去,計算下一層參數的梯度。

5.2.1 最大池化的反向傳播

在實現池化層的反向傳播之前,我們需要實現一個輔助函數create_mask_from_window(),用於實現:

image-20200512231556297

這個函數依據輸入的X創建了一個掩碼,以保存輸入數據的最大值位置。掩碼中1表示對應位置爲最大值(我們假設只有一個最大值)。

提示:

  • np.max()用於計算輸入數組的最大值
  • 矩陣X和實數x,那麼,A = (X == x)將返回一個和X相同的矩陣,其中:
    • A[i, j] = True if X[i, j] = x
    • A[i, j] = False if X[i, j] != x
  • 不考慮矩陣中存在多個最大值的情況
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]]

5.2.2 平均池化的反向傳播

在最大池化中,每個輸入窗口的對輸出的影響僅僅來源於窗口的最大值;在平均池化中,窗口的每個元素對輸出結果有相同的影響。所以我們需要設計一個函數實現上述功能。

我們來看一個具體的例子:

image-20200512233007472

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]]

5.2.3 代碼實現

將上述的輔助函數集合起來實現池化層的反向傳播過程:

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]]

至此,我們完成了卷積層和池化層的前向傳播和反向傳播過程,之後我們可以來構建卷積神經網絡。


歡迎大家關注公衆號,一起學習成長

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章