导入相关库
利用python实现一个简单的神经网络之前,需要先导入相关库,具体代码实现如下所示:
import numpy as np
import h5py
import matplotlib.pyplot as plt
%matplotlib inline
plt.rcParams['figure.figsize'] = (5.0, 4.0) # 绘制图形的默认大小
plt.rcParams['image.interpolation'] = 'nearest'
plt.rcParams['image.cmap'] = 'gray'
%load_ext autoreload
%autoreload 2
np.random.seed(1)
任务概况
在利用python实现一个简单的卷积神经网络时,首先需要了解整个实现过程的概况,具体的实现步骤,如下所示,通过这些步骤,一步步实现一个卷积神经网络的过程包含一个卷积层和池化层,卷积层和池化层需要实现的函数如下所示:
- 卷积层
- 零值填充
- 卷积窗口
- 卷积的前向传播
- 卷积的反向传播和优化
- 池化层
- 池化层的前向传播
- 创建池化的池化函数
- 提取池化值
- 池化层的反向传播
整个卷积神经网络的架构如下图所示:
卷积化的神经元网络
尽管利用编程框架使用卷积是非常容易实现的,但是,实现卷积神经网咯中需要注意的一点就是,通过卷积,其输出和输入的尺寸会发生变化,如下图所示:
零值填充
使用卷积神经网络的第一步是实现零值填充,一个RGB3通道的图像,零值填充的具体的实现过程,如下图所示:
使用零值填充的主要有以下益处:
- 能够保证卷积后的输出的图像尺寸和输入保持一致。
- 能够有助于保留图像的边缘信息。
在numpy
中,可以直接使用numpy提供的函数np.pad()
来实现填充,关于,这个函数,其参数的解释如下所示:
假设有一个shape=(5,5,5,5,5)
的五维矩阵,如果利用np.pad()
实现填充对地2维填充1,第四维填充3,其余全部填充为0,其参数可以如下所示
a = np.pad(a, ((0,0), (1,1), (0,0), (3,3), (0,0)), 'constant', constant_values = (..,..))
根据以上所述,实现零值填充的具体代码如下所示:
def zero_pad(X, pad):
X_pad = np.pad(X, ((0, 0),(pad, pad),(pad, pad),(0, 0)), 'constant', constant_values=0)
return X_pad
以上代码中,输入参数x
的尺寸是3通道图像的大小,为(m,n_H,n_W,n_C)
而输出表示的是实现零值填充之后的输入图像的尺寸,为(m, n_H + 2*pad, n_W + 2*pad, n_C)
随机生成一个矩阵,进行零值填充之后,如下所示:
卷积的实现
卷积的实现过程,可以分为以下三个步骤
- 依次选取输入
- 对选定的部分利用过滤矩阵实现卷积运算
- 得到卷积后的输入重新组成的矩阵
根据以上所述,卷积的实现代码如所示
def conv_single_step(a_slice_prev, W, b):
"""
对前一层网络上的激活值的输出进行滑动切片,由参数W和b组成的过滤矩阵
参数
a_slice_prev -- 输入数据的切片,其尺寸是(f,f,前一层网络的输出)
W -- 权重参数 -窗口矩阵的形状是 (f, f, n_C_prev)
b --偏差参数 - 窗口矩阵的形状是 (1, 1, 1)
返回值:
Z -- 标量值,是将滑动窗口(W,b)卷积在输入数据的切片x上的结果
"""
#利用参数相乘
s = np.multiply(a_slice_prev, W) + b
#求和并作为输出
Z = np.sum(s)
return Z
卷积神经网络的前向传播
卷积神经网络的前向传播过程中,需要使用多个过滤矩阵,分别利用不同的过滤矩阵对输入进行滑动卷积,卷积完成之后,将输出值组合在一起。
为了实现滑动窗口切片,可以利用python中的切片来实现,并且为了能够实现滑动的功能,可以定义四个变量表示滑动的方向,这四个变量分别表示横轴和纵轴的起始点和终止点,具体实现如下所示:
并且,卷积后的输出大小是:
根据以上所述,卷积的前向传播实现的代码如下所示:
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)
hparameters -- 是一个python字典,包含了卷积步长和填充大小
返回值:
Z -- 卷积输出,其形状大小是 (m, n_H, n_W, n_C)
cache -- 保存反向传播需要的一些值
"""
(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()实现向下取整
n_H = 1 + int((n_H_prev + 2 * pad - f) / stride)
n_W = 1 + int((n_W_prev + 2 * pad - f) / stride)
# 初始换输出值为0
Z = np.zeros((m, n_H, n_W, n_C))
# 利用零值填充,创建一个输入矩阵
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
#利用切片确定卷积的输入区域
a_slice_prev = a_prev_pad[vert_start:vert_end, horiz_start:horiz_end, :]
#将切片确定的卷积区域进行相乘并求和作为输入
Z[i, h, w, c] = np.sum(np.multiply(a_slice_prev, W[:, :, :, c]) + b[:, :, :, c])
# 确保输出的形状正确
assert(Z.shape == (m, n_H, n_W, n_C))
# 为了反向传播的实现保留一些参数
cache = (A_prev, W, b, hparameters)
return Z, cache
池化层
池化层减少了输入矩阵的高和宽,减少了计算的复杂度,并且有助于特征检测器在更加有效的检测输入特征,池化层根据其池化方式,可以分为以下两类:
-
最大池化: 选择窗口矩阵覆盖区域的最大值作为输出
-
均值池化: 选择窗口矩阵覆盖区域的所有元素的平均值作为输出
池化层的前向传播
实现池化层的前向传播,池化后输出矩阵的大小如下所示:
根据以上,则池化层的前向传播的代码实现如下所示:
def pool_forward(A_prev, hparameters, mode = "max"):
"""
实现池化层的前向传播
参数:
A_prev -- 输入矩阵,即前一层的输出矩阵 其矩阵大小为(m, n_H_prev, n_W_prev, n_C_prev)
hparameters -- 字典参数,存储着步长参数和过滤矩阵的大小
mode -- 池化的模式,如“最大池化”和“均值池化”
返回:
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(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))
for i in range(m): # 样本数量的循环
for h in range(n_H): # 图像的垂直方向的迭代
for w in range(n_W): # 图像横轴方向的迭代
for c in range (n_C): # 图像通道的循环
# 过滤矩阵的当前所覆盖的滑动窗口 "slice" (≈4 lines)
vert_start = h * stride
vert_end = vert_start + f
horiz_start = w * stride
horiz_end = horiz_start + f
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
卷积神经网络的反向传播
卷积层的反向传播实现
卷积层的反向传播实现,大概分为以下几个步骤
计算
的计算公式如下所示,其中表示的是过滤矩阵,表示的是卷积层输出的损失函数所对应的梯度,在整个计算过程中,保持不变,通过迭代,计算每次过滤矩阵的覆盖的区域,最后,再将每次迭代所得到的结果累加。
计算
计算的公式,如下所示,与计算相似,需要考虑过滤矩阵的滑动(即每次所覆盖的区域不同),最后,通过迭代计算出每一个窗口岁对应的,并将其累加求和。
计算
偏置参数求导的计算公式如下所示,正如公式所示,整个计算过程就是,将每次卷积输出损失的梯度累加即可。
综上所述,则整个卷积层的反向传播的计算如下代码所示:
def conv_backward(dZ, cache):
"""
实现卷积层的反向传播
参数:
dZ -- 卷积输出损失所对应的梯度,其大小是(m, n_H, n_W, n_C)
cache -- 反向传播所需要的一些参数,是由前向传播的计算得到的
返回值:
dA_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)
"""
(A_prev, W, b, hparameters) = cache
(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']
(m, n_H, n_W, n_C) = dZ.shape
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))
A_prev_pad = zero_pad(A_prev, pad) # 对前一层输出(本层的输入)进行零值填充
dA_prev_pad = zero_pad(dA_prev, pad)
for i in range(m): # 样本数的迭代
# 从A_prev_pad and dA_prev_pad选定第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 * stride
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]
#将第i个训练示例的dA_prev设置为未填充
dA_prev[i, :, :, :] = dA_prev_pad[i, pad:-pad, pad:-pad, :]
assert(dA_prev.shape == (m, n_H_prev, n_W_prev, n_C_prev))
return dA_prev, dW, db
最大池化层的反向传播
在实现最大池化层的反向传播之前,需要创建一个creat_mask_from_window
函数,该函数的实现效果如下所示:
如上所示,该函数实现的效果就是创建一个与原矩阵相同大小的矩阵,并根据原矩阵所对应位置,将该矩阵对应位置上的值设置为1,其余值设置为0,其代码实现如下所示:
综上所述,其代码实现如下所示:
def create_mask_from_window(x):
mask = (x == np.max(x))
return mask
均值池化层的反向传播
与最大池化层的mask不同,如果在前向传播函数中使用的是2×2的过率矩阵,假定则均值池化层的mask如下所示:
这意味着dZ矩阵中的每个位置对输出的等价,因为在前向传递中,我们取了平均值。
综上所数,均值mask的实现代码如下所示:
def distribute_value(dz, shape):
(n_H, n_W) = shape
average = dz / (n_H * n_W)
a = np.ones(shape) * average
return a
池化层的反向传播
综上,池化层的反向传播的实现代码如下所示:
def pool_backward(dA, cache, mode = "max"):
"""
池化层的反向传播的实现
参数:
dA -- 池化层输出所对应的损失的梯度
cache -池化层前向传播所保留的一些参数
mode -- 池化层的模式,最大池化或者均值池化
返回值:
dA_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): # 样本数的迭代
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[i, vert_start: vert_end, horiz_start: horiz_end, c] += mask * dA[i, vert_start, horiz_start, c]
elif mode == "average":
da = dA[i, vert_start, horiz_start, 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