边缘检测
我们看下下面的图
这个图反应了卷积神经网络的第一步,边缘检测,可以先检测横或者竖的线。
左侧部分为一个图片的灰度图(没有其他色RGB),中间是我们的3*3滤波器(也叫卷积核),*
号是卷积的标志,右侧也可以看成一个灰度图。注意这里不是矩阵的相乘,在python中使用conv_forward
, 在tensorflow里使用tf.nn.conv2d,
在keras中使用Conv2D
实现
- 来看一个更简单的是如何实现找到垂直边界
这里,左侧灰度图,数字大小代表亮度,卷积过后,在右侧可以看到中间亮,这个亮的区域就把垂直线找了出来,当然这里数据比较小只是6*6,右侧不太能看出来,当数据巨大时候,便很清楚显示了。
更多的边缘检测
- 对于右上,表示上下水平边缘,下图还有不同的滤波器数值,给予不同部位权重,改善鲁棒性,比如
Sobel filter
,Scharr filter
, 甚至在我们图片数量数据巨大时候,我们滤波器的参数也不需要我们自己设定,使用反向传播,交给计算机找到合适的数值,计算机可以帮我们找到垂直,水平,甚至各个角度(45°,30°,7°等等 )的数值。
Padding(填充)
当我们每进行一次卷积后,图片的维数就会变小(n-f+1,n为原图片维数,f为滤波器维数)
,原因是在图片边缘的信息利用率小,会损失图片的许多信息。
- 也就是说目前存在,图片卷积后变小以及图像信息缺失的问题。为了解决这个问题,我们可以在图像卷积之前,填充图片边缘的信息。以上为例
(n:6*6,得到图片4*4)
,我们在6*6
的图片填充一圈(P=1)
变成8*8
,再次卷积后(n-f+1)
变成(6*6)
和原来大小一样。当然如果觉得不够也可以填两圈(P=2)
- 一般来说,填充多少,我们一般分为两种情况Valid convolutions
(不填充,n-f+1)
和 Same convolutions(n+2P-f+1=n,得到p=(f-1)/2) )
一般来说,计算机视觉里面,f为奇数。
Strided convolution (卷积步长)
我们在上图,每次卷积一次后,向后移动一格(Strided=1)
,我们看下图
- 这里选取了步长为2
(Strided =2 )
那么我们得到的维数就变成了((n+2p-f)/s +1 ,(n+2p-f)/s +1 )
,其中若(n+2p-f)/s
整出不是整数,按照惯例我们对其向下取整。
三维卷积
前面灰度图像只是在一维度上的卷积,对于一个图片来说,包含(RGB)三个维度,我们看具体图像。
这里我们也设置三个滤波器(卷积核),将三个平面化成一个正方体。这里如果只是想找到红色的垂直边缘,只需要把绿色蓝色对应设置为0。
- 另一个问题是我们想要同时得到,垂直水平边界,45°,70°等,可以用多个卷积核,然后将输出叠加,矩阵维度类似一维。
单层卷积网络
对于单层卷积网络,前面的就是我们的输入层,卷积得到的结果就是w[l]*a[l-1]
,我们给卷积出来的结果加上一个不同的常数b
,得到了z[l]
,在使用ReLu函数
,得到a[l]
,在进行叠加,得到我们下一层的输入a[l]
,注意这里得到的输出层数=卷积核的数量。
下面是各个参数在n层时候的对应维度,这里维度已经写的很清楚了,就在复述:
池化层
除了卷积层之外,还经常用池化层来缩减模型的大小,提升速度的大小,提高所提取特征的鲁棒性,其中分为Max pooling and Average pooling
后者用的比较少, 用下面的图来简单理解:
这个也比较好理解,在4*4
的矩阵,选取S=2,f=2
,构成一个2*2
,选取每个值中的最大值,可以理解,我们图片信息放大后,提取重要的信息,代表这一小片区域。当然也可以选取S=1
,我们把4*4
池化成3*3
的图片,(最常使用f=2,s=2
, f=3 ,s=2
,当然也可以任意选择 )如下图:
再看一下不太常用的另一种:
这种的话,当深度很深的神经网络可以使用这种池化层。
代码可以点击这里!
残差网络
加入前馈,a[l] 连接到Relu激活之前,理论上,神经网络深度越深效果会越好,但是对于普通网络来说,深度越深会导致错误越多,但是加入ResNets残差网络
,即使网络再深,训练效果也是不错的。
Inception网络或层,使用1*1卷积核
- 使用这个层,是为了代替人工来确定卷积层中的过滤器的类型,是否创建卷积层或者池化层
对于我们直接使用5532进行卷积会造成1.2亿次的计算,运算量过大,因而采用1116的滤波器先压缩,然后在进行5*5的卷积,减少运算量,为1240万次运算,如下图。
Inception
对于一个整个神经网络来说如下图:
下面搭建一个神经网络:
导入库:
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'
#ipython很好用,但是如果在ipython里已经import过的模块修改后需要重新reload就需要这样
#在执行用户代码前,重新装入软件的扩展和模块。
%load_ext autoreload
#autoreload 2:装入所有 %aimport 不包含的模块。
%autoreload 2
np.random.seed(1) #指定随机种子
我们将实现一个卷积神经网络的一些模块,下面我们将列举我们要实现的模块的函数功能:
-
卷积模块,包含了以下函数:
使用0扩充边界
卷积窗口
前向卷积
反向卷积(可选) -
池化模块,包含了以下函数:
前向池化
创建掩码
值分配
反向池化(可选)
从底层搭建一个完整的模块,用TensorFlow实现。模型结构如下:
卷积神经网络 -
卷积层将输入转换成不同维度的输出
边界填充
使用pading为2的操作对图像(3通道,RGB)进行填充。 -
使用0填充边界有以下好处:
-
卷积了上一层之后的CONV层,没有缩小高度和宽度。 这对于建立更深的网络非常重要,否则在更深层时,高度/宽度会缩小。 一个重要的例子是“same”卷积,其中高度/宽度在卷积完一层之后会被完全保留。
-
它可以帮助我们在图像边界保留更多信息。在没有填充的情况下,卷积过程中图像边缘的极少数值会受到过滤器的影响从而导致信息丢失。我们将实现一个边界填充函数,它会把所有的样本图像X都使用0进行填充。我们可以使用
np.pad
来快速填充。
代码如下:
#constant连续一样的值填充,有constant_values=(x, y)时前面用x填充,后面用y填充。缺省参数是为constant_values=(0,0)
a = np.pad(a,( (0,0),(1,1),(0,0),(3,3),(0,0)),'constant',constant_values = (..,..))
#比如:
import numpy as np
arr3D = np.array([[[1, 1, 2, 2, 3, 4],
[1, 1, 2, 2, 3, 4],
[1, 1, 2, 2, 3, 4]],
[[0, 1, 2, 3, 4, 5],
[0, 1, 2, 3, 4, 5],
[0, 1, 2, 3, 4, 5]],
[[1, 1, 2, 2, 3, 4],
[1, 1, 2, 2, 3, 4],
[1, 1, 2, 2, 3, 4]]])
print 'constant: \n' + str(np.pad(arr3D, ((0, 0), (1, 1), (2, 2)), 'constant'))
"""
constant:
[[[0 0 0 0 0 0 0 0 0 0]
[0 0 1 1 2 2 3 4 0 0]
[0 0 1 1 2 2 3 4 0 0]
[0 0 1 1 2 2 3 4 0 0]
[0 0 0 0 0 0 0 0 0 0]]
[[0 0 0 0 0 0 0 0 0 0]
[0 0 0 1 2 3 4 5 0 0]
[0 0 0 1 2 3 4 5 0 0]
[0 0 0 1 2 3 4 5 0 0]
[0 0 0 0 0 0 0 0 0 0]]
[[0 0 0 0 0 0 0 0 0 0]
[0 0 1 1 2 2 3 4 0 0]
[0 0 1 1 2 2 3 4 0 0]
[0 0 1 1 2 2 3 4 0 0]
[0 0 0 0 0 0 0 0 0 0]]]
"""
def zero_pad(X, pad):
# 把数据集X的图像边界全部使用0来扩充pad个宽度和高度。
# X - 图像数据集,维度为(样本数,图像高度,图像宽度,图像通道数)
# pad - 整数,每个图像在垂直和水平维度上的填充量
# X_paded - 扩充后的图像数据集,维度为(样本数,图像高度 + 2*pad,图像宽度 + 2*pad,图像通道数)
X_paded = np.pad(X, (
(0, 0), # 样本数,不填充2
(pad, pad), # 图像高度,你可以视为上面填充x个,下面填充y个(x,y)
(pad, pad), # 图像宽度,你可以视为左边填充x个,右边填充y个(x,y)
(0, 0)), # 通道数,不填充
'constant', constant_values=0) # 连续一样的值填充
return X_paded
测试下填充:
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])
结果如下
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.]]
单步卷积
在这里,要实现第一步卷积,要使用一个过滤器来卷积输入的数据。如下图:
- 过滤器大小:f = 2 , 步伐:s = 1
- 在计算机视觉应用中,左侧矩阵中的每个值都对应一个像素值,我们通过将其值与原始矩阵元素相乘,然后对它们进行求和来将3x3滤波器与图像进行卷积。我们需要实现一个函数,可以将一个3x3滤波器与单独的切片块进行卷积并输出一个实数。现在我们开始实现conv_single_step()
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
卷积神经网络 - 前向传播
- 在前向传播的过程中,我们将使用多种过滤器对输入的数据进行卷积操作,每个过滤器会产生一个2D的矩阵,我们可以把它们堆叠起来,于是这些2D的卷积矩阵就变成了高维的矩阵。
- 如果我要在矩阵A_prev(shape = (5,5,3))的左上角选择一个2x2的矩阵进行切片操作,那么可以这样做:
a_slice_prev = a_prev[0:2,0:2,:]
- 想要自定义切片,我们可以这么做:先定义要切片的位置,
vert_start、vert_end、 horiz_start、 horiz_end
,它们的位置如下图:
实现卷积的前向传播:
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): # 遍历样本 [4]
a_prev_pad = A_prev_pad[i] # 选择第i个样本的扩充后的激活矩阵
for h in range(n_H): # 在输出的垂直轴上循环 [3]
for w in range(n_W): # 在输出的水平轴上循环 [2]
for c in range(n_C): # 循环遍历输出的通道 [1]
# 定位当前的切片位置
vert_start = h * stride # 竖向,开始的位置
vert_end = vert_start + f # 竖向,结束的位置
horiz_start = w * stride # 横向,开始的位置
horiz_end = horiz_start + f # 横向,结束的位置
# 切片位置定位好了我们就把它取出来,需要注意的是我们是“穿透”取出来的,
# 第一次循环,对所有图片在同一位置 ([[0, f] , [0 , f ]]) ,,可以理解某个角落,左上/下,右上/下都可以
# 第二次循环,对所有图片在同水平轴上进行卷积。
# 第三次循环,对所有水平轴在垂直方向上卷积。
# 第四次循环,样本卷积。
a_slice_prev = a_prev_pad[vert_start:vert_end, horiz_start:horiz_end, :]
# 执行单步卷积
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)
池化层
池化层会减少输入的宽度和高度,这样它会较少计算量的同时也使特征检测器对其在输入中的位置更加稳定。
-
最大值池化层:在输入矩阵中滑动一个大小为fxf的窗口,选取窗口里的值中的最大值,然后作为输出的一部分。
-
均值池化层:在输入矩阵中滑动一个大小为fxf的窗口,计算窗口里的值中的平均值,然后这个均值作为输出的一部分。
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 # 横向,结束的位置
# 定位完毕,开始切割,这里方法跟 卷积类似的。
a_slice_prev = A_prev[i, vert_start:vert_end, horiz_start:horiz_end, 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
卷积层的反向传播
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)
池化层的反向传播
def create_mask_from_window(x):
"""
从输入矩阵中创建掩码,以保存最大值的矩阵的位置。
参数:
x - 一个维度为(f,f)的矩阵
返回:
mask - 包含x的最大值的位置的矩阵
"""
mask = x == np.max(x)
return mask
均值池化层的反向传播
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
池化层的反向传播
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
以下是使用tensorflow实现的卷积模型
import math
import numpy as np
import h5py
import matplotlib.pyplot as plt
import matplotlib.image as mpimg
import tensorflow as tf
from tensorflow.python.framework import ops
import cnn_utils
%matplotlib inline
np.random.seed(1)
X_train = X_train_orig/255.
X_test = X_test_orig/255.
Y_train = cnn_utils.convert_to_one_hot(Y_train_orig, 6).T
Y_test = cnn_utils.convert_to_one_hot(Y_test_orig, 6).T
print ("number of training examples = " + str(X_train.shape[0]))
print ("number of test examples = " + str(X_test.shape[0]))
print ("X_train shape: " + str(X_train.shape))
print<