使用底层代码(无框架)实现卷积神经网络理解CNN逻辑

news2024/10/2 6:25:31

首先将数据集放入和底下代码同一目录中,然后导入一些相关函数的文件cnn_utils.py:

import math
import numpy as np
import h5py
import matplotlib.pyplot as plt
import tensorflow as tf
from tensorflow.python.framework import ops
def load_dataset():
    train_dataset = h5py.File('datasets/train_signs.h5', "r")
    train_set_x_orig = np.array(train_dataset["train_set_x"][:]) # your train set features
    train_set_y_orig = np.array(train_dataset["train_set_y"][:]) # your train set labels
    test_dataset = h5py.File('datasets/test_signs.h5', "r")
    test_set_x_orig = np.array(test_dataset["test_set_x"][:]) # your test set features
    test_set_y_orig = np.array(test_dataset["test_set_y"][:]) # your test set labels
    classes = np.array(test_dataset["list_classes"][:]) # the list of classes
    train_set_y_orig = train_set_y_orig.reshape((1, train_set_y_orig.shape[0]))
    test_set_y_orig = test_set_y_orig.reshape((1, test_set_y_orig.shape[0]))
    return train_set_x_orig, train_set_y_orig, test_set_x_orig, test_set_y_orig, classes
def random_mini_batches(X, Y, mini_batch_size = 64, seed = 0):
    """
    Creates a list of random minibatches from (X, Y)
    Arguments:
    X -- input data, of shape (input size, number of examples) (m, Hi, Wi, Ci)
    Y -- true "label" vector (containing 0 if cat, 1 if non-cat), of shape (1, number of examples) (m, n_y)
    mini_batch_size - size of the mini-batches, integer
    seed -- this is only for the purpose of grading, so that you're "random minibatches are the same as ours.
    Returns:
    mini_batches -- list of synchronous (mini_batch_X, mini_batch_Y)
    """
    m = X.shape[0]                  # number of training examples
    mini_batches = []
    np.random.seed(seed)
    # Step 1: Shuffle (X, Y)
    permutation = list(np.random.permutation(m))
    shuffled_X = X[permutation,:,:,:]
    shuffled_Y = Y[permutation,:]
    # Step 2: Partition (shuffled_X, shuffled_Y). Minus the end case.
    num_complete_minibatches = math.floor(m/mini_batch_size) # number of mini batches of size mini_batch_size in your partitionning
    for k in range(0, num_complete_minibatches):
        mini_batch_X = shuffled_X[k * mini_batch_size : k * mini_batch_size + mini_batch_size,:,:,:]
        mini_batch_Y = shuffled_Y[k * mini_batch_size : k * mini_batch_size + mini_batch_size,:]
        mini_batch = (mini_batch_X, mini_batch_Y)
        mini_batches.append(mini_batch)
    # Handling the end case (last mini-batch < mini_batch_size)
    if m % mini_batch_size != 0:
        mini_batch_X = shuffled_X[num_complete_minibatches * mini_batch_size : m,:,:,:]
        mini_batch_Y = shuffled_Y[num_complete_minibatches * mini_batch_size : m,:]
        mini_batch = (mini_batch_X, mini_batch_Y)
        mini_batches.append(mini_batch)
    return mini_batches
def convert_to_one_hot(Y, C):
    Y = np.eye(C)[Y.reshape(-1)].T
    return Y
def forward_propagation_for_predict(X, parameters):
    """
    Implements the forward propagation for the model: LINEAR -> RELU -> LINEAR -> RELU -> LINEAR -> SOFTMAX
    Arguments:
    X -- input dataset placeholder, of shape (input size, number of examples)
    parameters -- python dictionary containing your parameters "W1", "b1", "W2", "b2", "W3", "b3"
                  the shapes are given in initialize_parameters
    Returns:
    Z3 -- the output of the last LINEAR unit
    """
    # Retrieve the parameters from the dictionary "parameters" 
    W1 = parameters['W1']
    b1 = parameters['b1']
    W2 = parameters['W2']
    b2 = parameters['b2']
    W3 = parameters['W3']
    b3 = parameters['b3'] 
                                                           # Numpy Equivalents:
    Z1 = tf.add(tf.matmul(W1, X), b1)                      # Z1 = np.dot(W1, X) + b1
    A1 = tf.nn.relu(Z1)                                    # A1 = relu(Z1)
    Z2 = tf.add(tf.matmul(W2, A1), b2)                     # Z2 = np.dot(W2, a1) + b2
    A2 = tf.nn.relu(Z2)                                    # A2 = relu(Z2)
    Z3 = tf.add(tf.matmul(W3, A2), b3)                     # Z3 = np.dot(W3,Z2) + b3
    return Z3
def predict(X, parameters):
    W1 = tf.convert_to_tensor(parameters["W1"])
    b1 = tf.convert_to_tensor(parameters["b1"])
    W2 = tf.convert_to_tensor(parameters["W2"])
    b2 = tf.convert_to_tensor(parameters["b2"])
    W3 = tf.convert_to_tensor(parameters["W3"])
    b3 = tf.convert_to_tensor(parameters["b3"])
    params = {"W1": W1,
              "b1": b1,
              "W2": W2,
              "b2": b2,
              "W3": W3,
              "b3": b3}
    x = tf.placeholder("float", [12288, 1])
    z3 = forward_propagation_for_predict(x, params)
    p = tf.argmax(z3)
    sess = tf.Session()
    prediction = sess.run(p, feed_dict = {x: X})
    return prediction

padding

卷积网络通常会出现两大问题:

  1. 图像缩小问题。正常卷积后会缩小高度和宽度,特别如果是深层的CNN,这样的问题会影响网络性能。
  2. 边缘信息丢失,因为边缘的值被卷积核卷积的次数较。
    而padding可以很好的解决这两个问题。
    首先实现padding填充的代码:
def zero_pad(X, pad):
    """
    把数据集X的图像边界全部使用0来扩充pad个宽度和高度。

    参数:
        X - 图像数据集,维度为(样本数,图像高度,图像宽度,图像通道数)
        pad - 整数,每个图像在垂直和水平维度上的填充量
    返回:
        X_paded - 扩充后的图像数据集,维度为(样本数,图像高度 + 2*pad,图像宽度 + 2*pad,图像通道数)

    """

    X_paded = np.pad(X, (
        (0, 0),  # 样本数,不填充
        (pad, pad),  # 图像高度,你可以视为上面填充x个,下面填充y个(x,y)
        (pad, pad),  # 图像宽度,你可以视为左边填充x个,右边填充y个(x,y)
        (0, 0)),  # 通道数,不填充
                     'constant', constant_values=0)  # 连续一样的值填充

    return X_paded

可以测试代码看padding后的效果:

np.random.seed(1)
x = np.random.randn(4, 3, 3, 2)
x_paded = zero_pad(x, 2)
# 查看信息
print("x.shape =", x.shape)
print("x_paded.shape =", x_paded.shape)
print("x[1, 1] =", x[1, 1])
print("x_paded[1, 1] =", x_paded[1, 1])

# 绘制图
fig, axarr = plt.subplots(1, 2)  # 一行两列
axarr[0].set_title('x')
axarr[0].imshow(x[0, :, :, 0])
axarr[1].set_title('x_paded')
axarr[1].imshow(x_paded[0, :, :, 0])
plt.show()

输出:

x.shape = (4, 3, 3, 2)
x_paded.shape = (4, 7, 7, 2)
x[1, 1] = [[ 0.90085595 -0.68372786]
 [-0.12289023 -0.93576943]
 [-0.26788808  0.53035547]]
x_paded[1, 1] = [[0. 0.]
 [0. 0.]
 [0. 0.]
 [0. 0.]
 [0. 0.]
 [0. 0.]
 [0. 0.]]

在这里插入图片描述

Conv

前向传播

单个卷积核的卷积过程如图(2D),3D则是相同通道的卷积核的每个通道与输入对应通道的进行2D的卷积然后相加,3D卷积的输出的通道数=卷积核的个数。
在这里插入图片描述
n × n × n c − − C o n v ( f × f × n c , n 1 c 个 , s t r i d e , p a d d i n g ) − − > m × m × n 1 c n×n×n_c--Conv(f×f×n_c,n1_c个, stride, padding)-->m×m×n1_c n×n×ncConv(f×f×ncn1c,stride,padding)>m×m×n1c
m = ⌊ n − f + 2 × p a d d i n g s t r i d e ⌋ + 1 m = ⌊ \frac{n − f + 2 × p a dding }{ st r i d e }⌋ + 1 m=stridenf+2×padding+1
若除以步幅不为整数时要向下取整。​
因为卷积核的个数决定输出的通道数,第l层的卷积核通道数=第l-1层的输出(即l层的输入)的通道数,第l层的卷积核的个数=第l+1层的输入(即第l层的输出)的通道数,所以l层的卷积核(大小×通道数×个数)为:
f × f × n c l − 1 × n c l f×f×n^{l-1}_c×n^{l}_c f×f×ncl1×ncl
n c l − 1 为 l − 1 层的卷积核个数, n c l 为 l 层的卷积核个数 n^{l-1}_c为l-1层的卷积核个数,n^{l}_c为l层的卷积核个数 ncl1l1层的卷积核个数,ncll层的卷积核个数

# 单个卷积操作
def conv_single_step(a_slice_prev, W, b):
    """
    在前一层的激活输出的一个片段上应用一个由参数W定义的过滤器。
    这里切片大小和过滤器大小相同

    参数:
        a_slice_prev - 输入数据的一个片段,维度为(过滤器大小,过滤器大小,上一通道数)
        W - 权重参数,包含在了一个矩阵中,维度为(过滤器大小,过滤器大小,上一通道数)
        b - 偏置参数,包含在了一个矩阵中,维度为(1,1,1)

    返回:
        Z - 在输入数据的片X上卷积滑动窗口(w,b)的结果。
    """

    s = np.multiply(a_slice_prev, W) + b

    Z = np.sum(s)

    return Z

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),(过滤器大小,过滤器大小,上一层的过滤器数量,这一层的过滤器数量)
        b - 偏置矩阵,维度为(1, 1, 1, n_C),(1,1,1,这一层的过滤器数量)
        hparameters - 包含了"stride"与 "pad"的超参数字典。

    返回:
        Z - 卷积输出,维度为(m, n_H, n_W, n_C),(样本数,图像的高度,图像的宽度,过滤器数量)
        cache - 缓存了一些反向传播函数conv_backward()需要的一些数据
    """

    # 获取来自上一层数据的基本信息
    (m, n_H_prev, n_W_prev, n_C_prev) = A_prev.shape

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

    # 获取超参数hparameters的值
    stride = hparameters["stride"]
    pad = hparameters["pad"]

    # 计算卷积后的图像的宽度高度,参考上面的公式,使用int()来进行板除
    n_H = int((n_H_prev - f + 2 * pad) / stride) + 1
    n_W = int((n_W_prev - f + 2 * pad) / stride) + 1

    # 使用0来初始化卷积输出Z
    Z = np.zeros((m, n_H, n_W, n_C))

    # 通过A_prev创建填充过了的A_prev_pad
    A_prev_pad = zero_pad(A_prev, pad)

    for i in range(m):  # 遍历样本
        a_prev_pad = A_prev_pad[i]  # 选择第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  # 横向,结束的位置
                    # 切片位置定位好了我们就把它取出来,需要注意的是我们是“穿透”取每层通道的(3D层面)
                    # 自行脑补一下吸管插入一层层的橡皮泥就明白了
                    a_slice_prev = a_prev_pad[vert_start:vert_end, horiz_start:horiz_end, :]
                    # 执行单步卷积,遍历到的切片与第c个卷积核进行单卷集操作,得到输出的第c层的对应的一个值
                    Z[i, h, w, c] = conv_single_step(a_slice_prev, W[:, :, :, c], b[0, 0, 0, c])

    # 数据处理完毕,验证数据格式是否正确
    assert (Z.shape == (m, n_H, n_W, n_C))

    # 存储一些缓存值,以便于反向传播使用
    cache = (A_prev, W, b, hparameters)

    return (Z, cache)

'''
卷积层的最后应该要进行激活的,activation可以是sigmoid、relu、tanh等,但这里我们就不这么做了,
只是用无框架代码解释其底层逻辑
A[i, h, w, c] = activation(Z[i, h, w, c])
'''

这里遍历切片的位置定义可参考下图:
在这里插入图片描述
测试代码:

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)

print("np.mean(Z) = ", np.mean(Z))
print("A_prev.shape =", cache_conv[0].shape)  
print("W.shape =", cache_conv[1].shape)
print("b.shape =", cache_conv[2].shape)

输出:

np.mean(Z) =  0.15585932488906465
A_prev.shape = (10, 4, 4, 3)
W.shape = (2, 2, 3, 8)
b.shape = (1, 1, 1, 8)

可以看到输入、卷积核及偏差的shape与测试代码中的一致。

反向传播

若是使用DL框架,只需要写出前向传播框架就会自动计算出反向传播的梯度值。
首先列出反向传播所需要的计算公式:

  1. 计算dA的:
    d A + = ∑ h = 0 n H ∑ w = 0 n W W c × d Z h w (1) dA += \sum ^{n_H} _{h=0} \sum ^{n_W} _{w=0} W_{c} \times dZ_{hw} \tag{1} dA+=h=0nHw=0nWWc×dZhw(1)
    其中 W c W_c Wc是过滤器, Z h w Z_{hw} Zhw是一个标量,是卷积层第h行第w列的使用点乘计算后的输出Z的梯度(即输出Z的每个切片对应的梯度)。
    需要注意的是在每次更新dA的时候,都会用第c个过滤器 W c W_c Wc乘以对应的第c层通道的dZ,因为在前向传播的时候,每个过滤器都与a_slice进行了点乘相加得到某一层通道的一个值(即输入的每一块切片都是用不同的卷积核卷积过,第c个卷积核与其卷积得到第c层通道的一个值),所以在计算dA的每一块的切片的梯度的时候,我们是计算每个卷积核与对应层的dZ的反向操作的结果之和。公式可以如下表示:
da_perv_pad[vert_start:vert_end,horiz_start:horiz_end,:] += W[:,:,:,c] * dZ[i,h,w,c]

在这里插入图片描述

如图(网图加上自己的画笔)是一个单个卷积操作的正向过程,可以看到是一个卷积核对一个切片(3D)进行卷积是得到了某一层的一个值(2D)(图中的红蓝框);而反向计算我们也按这个顺序反向,每个卷积核与dZ对应层(通道)的一个值(2D)相乘然后每个结果相加,就得到对应切片的范围的dA(3D)。大概更直观的过程就是:
正向过程: 3 D ∗ 卷积核 − − > 2 D 正向过程:3D * 卷积核 --> 2D 正向过程:3D卷积核>2D
反向过程: 2 D ∗ 卷积核 − − > 3 D 反向过程:2D * 卷积核 --> 3D 反向过程:2D卷积核>3D

  1. ​计算dW的:

d W c + = ∑ h = 0 n H ∑ w = 0 n W a s l i c e × d Z h w (2) dW_c += \sum^{n_H}_{h=0} \sum^{n_W}_{w=0}a_{slice} \times dZ_{hw} \tag{2} dWc+=h=0nHw=0nWaslice×dZhw(2)
​其中, a s l i c e a_{slice} aslice 对应着 Z i j Z_{ij} Zij的激活值。由此,我们就可以推导W的梯度,因为我们使用了卷积核来对数据进行窗口滑动,在这里,我们实际上是切出了和过滤器一样大小的切片,切了多少次就产生了多少个梯度,所以我们需要把它们加起来得到这个数据集的整体dW。用公式概括:

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

同样可以参考上面的dA求解过程即配图。单步卷积的正向是一个切片与一个卷积核卷积得到一层的一个值,而输入的每个切片都是用到这个卷积核(但一个卷积层有一个或多个卷积核),所以反向操作是每个卷积核的梯度是所有切片与某一层通道(这个卷积核卷积后得到的对应层)的所有值一一对应相乘后相加。大概更直观的过程就是:
正向过程:卷积核 1 − − > 输出结果的第 1 层通道,卷积核 2 − − > 输出结果的第 2 层通道 正向过程:卷积核1-->输出结果的第1层通道,卷积核2-->输出结果的第2层通道 正向过程:卷积核1>输出结果的第1层通道,卷积核2>输出结果的第2层通道
反向过程:输出结果的第 1 层通道 − − > 卷积核 1 ,输出结果的第 2 层通道 − − > 卷积核 2 反向过程:输出结果的第1层通道-->卷积核1,输出结果的第2层通道-->卷积核2 反向过程:输出结果的第1层通道>卷积核1,输出结果的第2层通道>卷积核2

  1. 计算db的:
    每个卷积核只对应一个b的值,所以b的梯度比较好计算:
    d b = ∑ h ∑ w d Z h w (3) db = \sum^{}_{h} \sum^{}_{w}dZ_{hw} \tag{3} db=hwdZhw(3)
    可以用一行代码进行概括:
db[:,:,:,c] += dZ[ i, h, w, c]

因为一个卷积核对应一个b,输出结果的每一层对应的是一个卷积核,所以输出结果的每一层对应的是一个b。
接下来用这3个公式求卷积的反向过程:

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

                    # 切片完毕,使用上面的公式计算梯度,第c个卷积核与dZ的第c层通道反向计算得到对应的3D切片
                    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的shape要和A_prev的保持一致才是正确的,下面assert验证
        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.shape =", dA.shape)
print("dW.shape =", dW.shape)
print("db.shape =", db.shape)

输出:

dA.shape = (10, 4, 4, 3)
dW.shape = (2, 2, 3, 8)
db.shape = (1, 1, 1, 8)

可以看到输入的dA、卷积核的dW及偏差的db的shape都与之相对应,从正向传播的输出输入到反向传播过程中而产生的结果无误。

pooling

正向传播

池化层可以减小图像的宽高(通道数不变),池化核对每层分别进行池化操作。池化层是CNN的静态属性,没有参数进行学习更新,只需要设置超参即可(size、stride、pooling等)。
池化层可以缩减模型大小,提高计算速度;提高提取特征的鲁棒性。
池化分为最大值池化和均值池化:

  1. max pooling:取池化核区域内的最大值
  2. 项目2取池化核区域内的平均值
    在这里插入图片描述

在这里插入图片描述
池化操作后的高宽的计算跟卷积一样,不同的是输出的通道数不变:
n c l = n c l − 1 n^l_c = n^{l-1}_c ncl=ncl1

def pool_forward(A_prev, hparameters, mode="max"):
    """
    实现池化层的前向传播

    参数:
        A_prev - 输入数据,维度为(m, n_H_prev, n_W_prev, n_C_prev)
        hparameters - 包含了 "f" 和 "stride"的超参数字典
        mode - 模式选择【"max" | "average"】

    返回:
        A - 池化层的输出,维度为 (m, n_H, n_W, n_C)
        cache - 存储了一些反向传播需要用到的值,包含了输入和超参数的字典。
    """

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

    # 获取超参数的信息
    f = hparameters["f"]
    stride = hparameters["stride"]

    # 计算输出维度
    n_H = int((n_H_prev - f) / stride) + 1
    n_W = int((n_W_prev - f) / stride) + 1
    n_C = n_C_prev

    # 初始化输出矩阵
    A = np.zeros((m, n_H, n_W, n_C))

    for i in range(m):  # 遍历样本
        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  # 横向,结束的位置
                    # 遍历到的区域切片,这里是对c层通道进行切片(2D层面)
                    a_slice_prev = A_prev[i, vert_start:vert_end, horiz_start:horiz_end, c]

                    # 对切片进行池化操作,对遍历到的第c层的区域进行池化操作
                    if mode == "max":
                        A[i, h, w, c] = np.max(a_slice_prev)
                    elif mode == "average":
                        A[i, h, w, c] = np.mean(a_slice_prev)

    # 池化完毕,校验数据格式
    assert (A.shape == (m, n_H, n_W, n_C))

    # 校验完毕,开始存储用于反向传播的值
    cache = (A_prev, hparameters)

    return A, cache

测试代码:

np.random.seed(1)
A_prev = np.random.randn(2, 4, 4, 3)
hparameters = {"f": 4, "stride": 1}

A, cache = pool_forward(A_prev, hparameters, mode="max")
print("mode = max")
print("A =", A)
print("----------------------------")
A, cache = pool_forward(A_prev, hparameters, mode="average")
print("mode = average")
print("A =", A)

输出结果:

mode = max
A = [[[[1.74481176 1.6924546  2.10025514]]]


 [[[1.19891788 1.51981682 2.18557541]]]]
----------------------------
mode = average
A = [[[[-0.09498456  0.11180064 -0.14263511]]]


 [[[-0.09525108  0.28325018  0.33035185]]]]

4×4×3的输入经过stride为1的4×4的池化之后输出为1×1×3,样本数为2,输出的shape为(2, 1, 1, 3),没有错误。

反向传播

即使池化层没有反向传播过程中要更新的参数,我们仍然需要通过池化层反向传播梯度,以便为在池化层之前的层(比如卷积层)计算梯度。

  1. max pooling的反向传播:
    创建一个create_mask_from_window()的函数,用来记池化核区域的最大值的位置。因为正向的操作是取输入的池化核区域内的最大值一个值,而反向操作是还是取这个输入,在对应的池化核区域内只保留最大值的值其余置0,所以我们先记录最大值位置的矩阵通过相乘来达到我们的目的。
    X = [ 1 3 4 2 ] → M = [ 0 0 1 0 ] X = \begin{bmatrix} 1 && 3 \\ 4 && 2 \end {bmatrix} \quad \rightarrow \quad M = \begin{bmatrix} 0 && 0 \\ 1 && 0 \end {bmatrix} X=[1432]M=[0100]
    正向传播首先是经过卷积层,然后滑动地取卷积层最大值构成了池化层,要记录最大值的位置,才能反向传播到卷积层。
def create_mask_from_window(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 = " + str(x))
print("mask = " + str(mask))

输出:

x = [[ 1.62434536 -0.61175641 -0.52817175]
 [-1.07296862  0.86540763 -2.3015387 ]]
mask = [[ True False False]
 [False False False]]

是以布尔值为矩阵来记录最大值的位置。

  1. average pooling的反向传播:
    而均值池化的正向操作是取池化核内的平均值得到一个值,所以反向操作就根据后一层的梯度的其中一个值来得到对应区域内的所有平均值。
    d Z = 1 → d Z = [ 1 4 1 4 1 4 1 4 ] dZ=1 \quad \rightarrow \quad dZ = \begin{bmatrix} \frac{1}{4} & \frac{1}{4} \\ \frac{1}{4} & \frac{1}{4} \\ \end{bmatrix} dZ=1dZ=[41414141]
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

测试:

dz = 2
shape = (2, 2)

a = distribute_value(dz, shape)
print("a = " + str(a))

输出:

a = [[0.5 0.5]
 [0.5 0.5]]

可以看到2平均分为2×2=4份平均值为0.5验正确。
池化层的反向操作的实现:

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,一层一层对应的反操作
                        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.14571390272918056
dA_prev[1,1] =  [[ 0.          0.        ]
 [ 5.05844394 -1.68282702]
 [ 0.          0.        ]]

mode = average
mean of dA =  0.14571390272918056
dA_prev[1,1] =  [[ 0.08485462  0.2787552 ]
 [ 1.26461098 -0.25749373]
 [ 1.17975636 -0.53624893]]

无报错证明通过了assert验证,dA_prev[1, 1]输出的是第二个样本的第二行的切片,因为宽度为3,通道数为2,所以输出了个shape为(3, 2)的矩阵也没问题。
因为池化是对每一层分别池化进行池化操作(不改变通道数的操作),所以反操作也是一层一层对应的反操作。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/544724.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Win10搭建Docker Desktop并安装vim

Win10搭建Docker Desktop 1 介绍 Docker Desktop是适用于Windows的Docker桌面,是Docker设计用于在Windows 10上运行。它是一个本地 Windows 应用程序,为构建、交付和运行dockerized应用程序提供易于使用的开发环境。Docker Desktop for Windows 使用 Wi…

shell脚本基本操作及案例

本文介绍了shell脚本的基本建立过程,并举了4个案例。关键是例3的shell脚本检测局域网ip地址机器是否宕机,例4的shell脚本获取本机ip地址 一、第一个shell脚本 1、定义解释器 #!/bin/bash echo "hello world" #! 是一个约定的标记…

今天面了个京东拿28K 出来的,让我见识到了测试界的天花板

今年的春招基本已经结束了,很多小伙伴收获不错,拿到了心仪的 offer。 各大论坛和社区里也看见不少小伙伴慷慨地分享了常见的软件测试面试题和八股文,为此咱这里也统一做一次大整理和大归类,这也算是划重点了。 俗话说得好&#…

【Android】基于Airtest实现大麦网app自动抢票程序

0x01 缘起 疫情结束的2023年5月,大家对出去玩都有点疯狂,歌手们也扎堆开演唱会。但演唱会多,票一点也不好抢,抢五月天的门票难度不亚于买五一的高铁票。所以想尝试找一些脚本来辅助抢票,之前经常用selenium和request做…

探索未来智能交通:网联汽车与汽车互联

网联汽车是指配备多种传感器和通信设备,并且能够接入互联网的汽车。这种汽车可以与外部环境进行交互,并利用各种技术(如 GPS 导航、娱乐系统、诊断传感器和通信工具等)实现数据的传输和接收。 网联汽车能够与其他车辆、交通基础设…

ChatGPT 引发AI服务器霸屏?AI服务器和普通服务器不同在哪?

​  近阶段,由于 ChatGPT 的横空问世,引发 AI 服务器再一次热潮来袭。随之而来的是,越来越多的企业和机构开始关注 AI 服务器和普通服务器之间的区别和对比。那么AI服务器到底是什么意思,它与普通服务器相比又有哪些差异呢? AI…

MySQL高级——第15章_锁

第15章_锁 1. 概述 锁是计算机协调多个进程或线程并发访问某一资源的机制。在程序开发中会存在多线程同步的问题,当多个线程并发访问某个数据的时候,尤其是针对一-些敏感的数据(比如订单、金额等),我们就需要保证这个数据在任何 时刻最多只…

动态规划进阶

文章目录 状压dp小国王玉米田炮兵阵地 状压dp 小国王 import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.io.PrintWriter;public class Main{static BufferedReader br new Bu…

Day25力扣刷題

131.分割回文串 此題思維和前幾題不一樣,思維容量更大,主要在於返回的時候,還是會取值。 運行代碼: class Solution:def partition(self, s: str) -> List[List[str]]:result[]tt[]def backtrack(s,index):if len(s)index:res…

node.js PM2部署项目

pm2 是什么 pm2 是一个守护进程管理工具,它能帮你守护和管理你的应用程序。通常一般会在服务上线的时候使用 pm2 进行管理。本文围绕以下重点进行讲解:安装pm2;命令行部署到PM2;PM2查看日志等命令;PM2进行负载均衡;PM…

Python心经(4)

这节记录一些内置模块的使用 目录 hashlib模块,,加密用 json模块 os模块 一个实用的案例: os模块的找文件的操作 随机生成random模块 时间相关模块 time,datetime hashlib模块,,加密用 以md5加密…

什么是Spring Cache?Spring项目如何使用?

前言 目前Spring Cloud微服务在Web项目中占据了主流地位,如果去面试关于Spring Cloud的岗位时,面试官一般都会提问你的项目是如何优化的,从哪些方面入手去优化。而缓存技术绝对是项目必不可少的,所以我们必须掌握好Java项目的缓存…

unity 3种办法实现血条效果并实现3d世界血条一直看向摄像机

普通血条栏: 渐变色血条栏: 缓冲血条栏: 3D场景血条栏跟随玩家移动: 普通血条栏: 在Canvas下创建一个空物体HP bar,在空物体下方创建3个Image,分别为血条框bar 黑色,最大HP maxHP 白色,和当前HP currentHP 红色。(PS:注意先后顺序以调整显示的图层) 效果: …

java中排序

1.传统比较器格式 2.stream 3.结果 4.源码 List<String> list Arrays.asList("201305", "200305", "199009", "200208");Collections.sort(list, new Comparator<String>() {Overridepublic int compare(String o1, Stri…

SpringMVC 万字通关

文章目录 1. 什么是 Spring MVC?1.1 MVC 定义1.2 MVC 和 Spring MVC 的关系 2. Spring MVC 有什么用 ?3. 如何学 Spring MVC ?3.1 Spring MVC 的创建3.2 实现连接功能3.2.1 RquestMapping 详解1. RequestMapping 支持什么请求?2. 请求限定3. GetMapping 和 PostMapping4. c…

Sharding-Sphere系列-主从配置和分库分表

主从配置和分库分表 Sharding-Sphere组成 Sharding-JDBC Sharding-Proxy Sharding-Sidecar&#xff08;TODO&#xff09; Sharding-JDBC表的概念 逻辑表 广播表 绑定表 Sharding-JDBC中的分片策略 自动分片算法 标准分片算法 复合分片算法 自定义分片算法 分布式…

Mybatis-Plus 自动属性填充与自定义Insert into语句顺序

前言&#xff1a;系统中使用了Mybatis-Plus 自动属性填充为实体统一进行属性的填值&#xff0c;在Mapper的xml 文件中 insert into 语句 使用 <if test"id ! null">id,</if> 进行判断会发现该属性是空的&#xff0c;明明已经为改字段进行了属性的自动填充…

百度2023年Q1财报解析:AI+生态战略加速助推

原创 | 文 BFT机器人 01 百度靠AI实现翻身 &#xff08;一&#xff09;盈利能力 百度凭借着强大的AI能力&#xff0c;成功地实现了从依赖搜索业务的互联网公司到AI公司的转型。 从盈利能力层面上看&#xff0c;在第一季度&#xff0c;百度实现了营收311.44亿元&#xff0c;同比…

unity愤怒的小鸟学习制作(二)

终于又开始了啦啦啦&#xff0c;我有一个自己的相机了&#xff0c;真开心&#xff0c;诶嘿 视频链接和素材如下&#xff1a;视频 小鸟的飞出 想要让小鸟在拉开弹弓之后能飞出去&#xff0c;就必须让这个组件失活&#xff0c;如下 所以我们更改脚本内容&#xff0c;加入&#…

HarmonyOS应用端云一体化开发主要流程

图示 主要步骤 序号 阶段 任务 说明 1 创建端云一体化开发工程 选择工程类型与云开发模板 确定工程类型&#xff1a;选择“Application”或“Atomic Service”页签&#xff0c;确定创建的是HarmonyOS应用工程还是原子化服务工程。选择云开发模板&#xff0c;包括通用云开…