「Dl Code」卷积神经网络反向传播

2019-02-02 笔记

Posted by koko on February 2, 2019

卷积神经网络中的反向传播

卷积层的反向传播

This is the formula for computing $dA$ with respect to the cost for a certain filter $W_c$ and a given training example:

\[dA += \sum _{h=0} ^{n_H} \sum_{w=0} ^{n_W} W_c \times dZ_{hw} \tag{1}\]

  其中, $W_c$是过滤器, $dZ_{hw}$ 是一个标量, $Z_{hw}$是卷积层第$h$行第$w$列的使用点乘计算后的输出$Z$的梯度。需要注意的是在每次更新$dA$的时候,都会用相同的过滤器$W_c$乘以不同的$dZ$,因为在前向传播的时候,每个过滤器都与a_slice进行了点乘相加,所以在计算$dA$的时候,我们需要把a_slice的梯度也加进来,我们可以在循环中加一句代码:

da_prev_pad[vert_start:vert_end, horiz_start:horiz_end, :] += W[:,:,:,c] * dZ[i, h, w, c]
\[dW_c += \sum _{h=0} ^{n_H} \sum_{w=0} ^ {n_W} a_{slice} \times dZ_{hw} \tag{2}\]

其中, $a_{slice}$ 对应着 $Z_{ij}$ 的激活值。由此,我们就可以推导 $W$的 梯度,因为我们使用了过滤器来对数据进行窗口滑动,在这里,我们实际上是切出了和过滤器一样大小的切片,切了多少次就产生了多少个梯度,所以我们需要把它们加起来得到这个数据集的整体 $dW$。

\[db = \sum_h \sum_w dZ_{hw} \tag{3}\]
db[:,:,:,c] += dZ[ i, h, w, c]
def conv_backward(dZ,cache):
    """
    实现卷积层的反向传播

    参数:
        dZ - 卷积层的输出Z的 梯度,维度为(m, n_H, n_W, n_C)
        cache - 反向传播所需要的参数,conv_forward()的输出之一

    返回:
        dA_prev - 卷积层的输入(A_prev)的梯度值,维度为(m, n_H_prev, n_W_prev, n_C_prev)
        dW - 卷积层的权值的梯度,维度为(f,f,n_C_prev,n_C)
        db - 卷积层的偏置的梯度,维度为(1,1,1,n_C)

    """
    #获取cache的值
    (A_prev, W, b, hparameters) = cache

    #获取A_prev的基本信息
    (m, n_H_prev, n_W_prev, n_C_prev) = A_prev.shape

    #获取dZ的基本信息
    (m,n_H,n_W,n_C) = dZ.shape

    #获取权值的基本信息
    (f, f, n_C_prev, n_C) = W.shape

    #获取hparaeters的值
    pad = hparameters["pad"]
    stride = hparameters["stride"]

    #初始化各个梯度的结构
    dA_prev = np.zeros((m,n_H_prev,n_W_prev,n_C_prev))
    dW = np.zeros((f,f,n_C_prev,n_C))
    db = np.zeros((1,1,1,n_C))

    #前向传播中我们使用了pad,反向传播也需要使用,这是为了保证数据结构一致
    A_prev_pad = zero_pad(A_prev,pad)
    dA_prev_pad = zero_pad(dA_prev,pad)

    #现在处理数据
    for i in range(m):
        #选择第i个扩充了的数据的样本,降了一维。
        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
                    vert_end = vert_start + f
                    horiz_start = w
                    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]
        #设置第i个样本最终的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": 1}
# 前向传播
Z , cache_conv = conv_forward(A_prev,W,b,hparameters)
# 反向传播
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))

池化层的反向传播

最大池化反向传播

过程:

\[X = \begin{bmatrix} 1 && 3 \\ 4 && 2 \end{bmatrix} \quad \rightarrow \quad M =\begin{bmatrix} 0 && 0 \\ 1 && 0 \end{bmatrix}\tag{4}\]

If you have a matrix X and a scalar x: A = (X == x) will return a matrix A of the same size as X such that:

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 - 一个维度为(f,f)的矩阵

    返回:
        mask - 包含x的最大值的位置的矩阵
    """
    mask = x == np.max(x)

    return mask

平均池化反向传播

\[dZ = 1 \quad \rightarrow \quad dZ =\begin{bmatrix} 1/4 && 1/4 \\ 1/4 && 1/4 \end{bmatrix}\tag{5}\]
def distribute_value(dz,shape):
    """
    给定一个值,为按矩阵大小平均分配到每一个矩阵位置中。

    参数:
        dz - 输入的实数
        shape - 元组,两个值,分别为n_H , n_W

    返回:
        a - 已经分配好了值的矩阵,里面的值全部一样。

    """
    #获取矩阵的大小
    (n_H , n_W) = shape

    #计算平均值
    average = dz / (n_H * n_W)

    #填充入矩阵
    a = np.ones(shape) * average

    return a

池化的整体流程:

1、获取池化层前那一层的特征图,获取池化层输出的矩阵信息的某一点。

2、找到池化层那一点对应的特征图的位置,求一个mask或者均值矩阵。

3、把求到的矩阵添加进池化层的输入

def pool_backward(dA,cache,mode = "max"):
    """
    实现池化层的反向传播

    参数:
        dA - 池化层的输出的梯度,和池化层的输出的维度一样
        cache - 池化层前向传播时所存储的参数。
        mode - 模式选择,【"max" | "average"】

    返回:
        dA_prev - 池化层的输入的梯度,和A_prev的维度相同

    """
    #获取cache中的值
    (A_prev , hparaeters) = cache

    #获取hparaeters的值
    f = hparaeters["f"]
    stride = hparaeters["stride"]

    #获取A_prev和dA的基本信息
    (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):
        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
                    vert_end = vert_start + f
                    horiz_start = w
                    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
                        print("caculate:")
                        print(np.multiply(mask,dA[i,h,w,c]))
                        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