神经网络内部卷积实现

415 阅读5分钟

Zero-Padding

在卷积神经网络(CNN)中,‌Zero-Padding(零填充)‌是一种在输入数据(如图像)的周围添加零值像素的技术。它的主要目的是控制卷积操作后输出的空间尺寸(宽度和高度),同时尽可能多地保留边缘信息。

p6.png

Zero-Padding的作用:

(1)保持输出尺寸

  • 普通卷积操作会导致特征图(Feature Map)的尺寸缩小,例如:输入尺寸为nn,卷积核为kk,输出尺寸为。

(2)保留边缘信息

  • 角落边的像素只被一个卷积计算所碰触,对输出的影响较小,图像中的大部分信息都丢失了,而中间的区域的像素,会多次参与卷积计算,输出的影响较大。Zero-Padding可以让边缘像素也能被更多的卷积核完整覆盖,避免信息损失。

(3)支持更深的网络

  • 在深层网络中,多次卷积可能导致特征图尺寸过小(甚至小于卷积核尺寸),无法继续运算。通过Zero-Padding,可以维持特征图的尺寸,从而支持更深的网络设计。

Zero-Padding实现

  • ‌通常用𝑝表示,表示在输入的每一边添加的零值行/列数。
  • 如果输入尺寸为n×nn×n,卷积核为k×kk×k,步长为ss,填充量为pp,则输出尺寸为: OutputSize=(n+2pks)1Output Size=(\frac{n+2p-k}{s})-1
def zero_pad(X, pad):
    """
    对数据集X的所有样本进行0填充
    Parameters:
    X -- numpy数组,未填充的数据集,
        shape=[num_samples, num_Height, num_Width, num_Chanel]
    pad -- 是一个整数,表示对样本高宽填充的大小
    Returns:
    numpy数组,填充后的数据集
    """
    X_pad = np.pad(X, ((0,0), (pad,pad), (pad,pad), (0,0)), mode='constant')
    return X_pad
    

import numpy as np
X = np.random.randn(5,3,3,3)
pad = 1
X_pad = zero_pad(X, pad)
print(f"X.shape={X.shape}")
print(f"X_pad.shape={X_pad.shape}")
print("填充前red:")
print(X[0,:,:,0])
print("填充后red通道:")
print(X_pad[0,:,:,0])
X.shape=(5, 3, 3, 3)
X_pad.shape=(5, 5, 5, 3)
填充前red:
[[-1.15376398 -0.75258694  0.24848238]
 [ 2.77979566 -0.32868589 -1.54132054]
 [-0.21763341 -2.30183587 -0.40836615]]
填充后red通道:
[[ 0.          0.          0.          0.          0.        ]
 [ 0.         -1.15376398 -0.75258694  0.24848238  0.        ]
 [ 0.          2.77979566 -0.32868589 -1.54132054  0.        ]
 [ 0.         -0.21763341 -2.30183587 -0.40836615  0.        ]
 [ 0.          0.          0.          0.          0.        ]]

实现一次卷积计算函数

这一部分中,实现一步卷积,也就是将滤波器应用于输入的单个位置。

np.multiply是NumPy 库中用于执行逐元素乘法(Element-wise Multiplication)的函数。它与 * 运算符在数组之间的运算效果相同,但与矩阵乘法(np.dot 或 @)完全不同

Convolution_schematic.gif

def conv_single_step(a_slice, w, b):
    """
    Arguments:
    a_slice -- numpy数组,输入数据切片,
           (n_h_filter, n_w_filter, n_c_filter)
    w -- numpy数组,过滤器权值参数,
           (n_h_filter, n_w_filter, n_c_filter)
    b -- 标量,过滤器偏置参数,(1,1,1)
    
    Returns:
    Z -- 一个数值, sum(a_slice*w) + b。
    """
    s = np.multiply(a_slice, w)
    Z = np.sum(s)
    Z = Z + float(b)
    # Z = Z + b
    return Z
    
#1、随机生成一个大小为[3,3,3]的过滤器权值矩阵w
#2、随机生成一个大小为[1,1,1]的过滤器偏置矩阵b
#3、随机生成一个供卷积运算的的输入切片x;
#4、调用conv_single_step函数并输出结果z。

w = np.random.rand(3,3,3)
b = 10
x = np.random.rand(3,3,3)
z = conv_single_step(x, w, b)
print(z)#18.384722200888632

卷积层-实现卷积前向传播

在卷积层的前向传播过程中,需要使用多个过滤器在输入数据上对其进行卷积运算。每个过滤器都会输出一个2D矩阵。然后,堆叠这些2D输出矩阵以获得3D矩阵作为卷积层的输出。

关键点:

  • 选择矩阵左上角的切片:a_slice_prev = a_prev[0:2, 0:2, :]

vert_horiz_kiank.png

  • 卷积的输出矩阵大小为[nH,nW,nC][n_H,n_W,n_C]

  • 计算公式为:

    • nH=nHprevf+2 ×padstride+1n_H = \lfloor \frac{n_{H_{prev}} - f + 2 \times pad}{stride} \rfloor +1

    • nW=nWprevf+2 ×padstride+1n_W = \lfloor \frac{n_{W_{prev}} - f + 2 \times pad}{stride} \rfloor +1

    • nC=number of filters used in the convolution n_C = \text{number of filters used in the convolution}

  • 使用for循环完成样本和卷积的移动

注意:

A_prev -- 卷积层的输入: shape=[num_samples, num_H_sample, num_W_sample, num_C_sample]

W -- 卷积层的所有卷积核组成的权值张量:shape=[num_H_filter, num_W_filter, num_C_filter, num_filters],

这个 num_C_sample==num_C_filter

import numpy as np

def zero_pad(X, pad):
    """
    对数据集X的所有样本进行0填充
    Parameters:
    X -- numpy数组,未填充的数据集,
        shape=[num_samples, num_Height, num_Width, num_Chanel]
    pad -- 是一个整数,表示对样本高宽填充的大小
    Returns:
    numpy数组,填充后的数据集
    """
    X_pad = np.pad(X, ((0,0), (pad,pad), (pad,pad), (0,0)), mode='constant')
    return X_pad

def conv_single_step(a_slice, w, b):
    """
    Arguments:
    a_slice -- numpy数组,输入数据切片,
           (n_h_filter, n_w_filter, n_c_filter)
    w -- numpy数组,过滤器权值参数,
           (n_h_filter, n_w_filter, n_c_filter)
    b -- 标量,过滤器偏置参数,(1,1,1)
    
    Returns:
    Z -- 一个数值, sum(a_slice*w) + b。
    """
    s = np.multiply(a_slice, w)
    Z = np.sum(s)
    Z = Z + float(b)
    # Z = Z + b
    return Z

def conv_3d(inp, W, B, stride, pad):
    """
    Functions:
    对3d数据进行卷积计算
    
    Parameters:
    inp -- numpy数组,卷积层的输入
           shape=[num_samples, num_H_sample, num_W_sample, num_C_sample]
    W -- numpy数组,卷积层的所有卷积核(过滤器)组成的权值张量
         shape=[num_H_filter, num_W_filter, num_C_filter, num_filters]
    B -- 卷积层所有卷积核的偏置组成的向量:
          shape=[1,1,1,num_filters]
    stride -- 卷积移动的步长
    pad -- 填充大小
    
    Returns:
    Z -- 线性输出,卷积运算后的结果
    """
    n_samples, n_H_sample, n_W_sample, n_C_sample = inp.shape # 样本
    n_h_filter, n_w_filter, n_c_filter, n_filters = W.shape #卷积核

    n_H = int((n_H_sample - n_h_filter + 2*pad)/stride) + 1
    n_W = int((n_W_sample - n_w_filter + 2*pad)/stride) + 1

    n_C = n_filters
    Z = np.zeros((n_samples, n_H, n_W, n_C)) # 构造输出矩阵 
    inp_pad = zero_pad(inp, pad) #pading

    for i in range(n_samples):          # 遍历训练集的每个样本
        sample = inp_pad[i]         # 选择第i个样本
        
        for h in range(n_H):               # 遍历输出矩阵体的高
            # 垂直方向,初始 h=0 0:stride
            vert_start = stride * h             # 固定样本垂直方向上的起始位置 
            vert_end = vert_start  + n_h_filter  # 固定样本垂直方向上的结束位置  

            for w in range(0, n_W):                   # 遍历输出矩阵的宽
                # 水平方向 初始 w=0 0:stride
                horiz_start = stride * w             # 固定样本在水平方向上的起始位置
                horiz_end = horiz_start + n_w_filter  # 固定样本在水平方向上的结束位置

                for c in range(0, n_C):
                    # 从样本上切片 (n_h_filter, n_w_filter, n_c_filter)
                    a_slice = sample[vert_start:vert_end,horiz_start:horiz_end,:]
                    Z[i,h,w,c] = conv_single_step(a_slice,W[:,:,:,c],B[:,:,:,c])
    return Z


sample=np.random.randn(10,5,5,3)
# sample =sample[0]  # 得到第i个样本
print("样本的shape:",sample.shape)
W=np.random.randn(3,3,3,8)
B=np.random.randn(1,1,1,8)
Z = conv_3d(sample, W, B, stride=1, pad=0)
print("卷积后的shape:",Z.shape)
    
#样本的shape: (10, 5, 5, 3)
#卷积后的shape: (10, 3, 3, 8)