这里我们想用Python实现CNN中的卷积和池化操作,同时观察效果(代码在最下方)。
前篇:Understanding of a Convolutional Neural - Welt Xing’s Blog (welts.xyz)
我们接下来会用这张图片作为原始图片进行卷积操作:
在论文《Understanding of a Convolutional Neural Network 》中,作者列举了几种常见卷积核,用于不同目的:
“Identity”,这里是指卷积后图片不发生变化:
\[\begin{bmatrix} 0&0&0\\ 0&1&0\\ 0&0&0\\ \end{bmatrix}\]图片变换前和变换后看起来一模一样;
边缘检测,用于凸显边缘的三种卷积核:
\[\begin{bmatrix} 1&0&-1\\ 0&0&0\\ -1&0&1\\ \end{bmatrix},\begin{bmatrix} 0&1&0\\ 1&-4&1\\ 0&1&0\\ \end{bmatrix},\begin{bmatrix} -1&-1&-1\\ -1&8&-1\\ -1&-1&-1\\ \end{bmatrix}\]对图片进行卷积后的结果从左到右依次如下:
锐化:
\[\begin{bmatrix} 0&-1&0\\ -1&5&-1\\ 0&-1&0\\ \end{bmatrix}\]锐化操作使图片边缘更加清晰,可以看到图中的边沿明显加粗:
模糊,有两种,分别是Box blur和Gaussian blur:
\[\frac19\begin{bmatrix} 1&1&1\\ 1&1&1\\ 1&1&1\\ \end{bmatrix},\frac1{16}\begin{bmatrix} 1&2&1\\ 2&4&2\\ 1&2&1\\ \end{bmatrix}\]第一种卷积核中元素均匀分布,而第二种类似一个高斯分布,效果如下:
看起来Box blur模糊的效果更加明显,Gaussian模糊仍保留了一些原图特性。
默认Stride是1,如果将Stride设为2、3这样大于1的值,那么图片会缩小,我们设stride=3,观察卷积效果:
上面三张图片对应三种边缘检测矩阵,下面三张图片从左到右分别使用的是Identity卷积核,锐化卷积核和高斯模糊卷积核。可以发现,卷积后图片的特征被提取了出来,但变得更加模糊。
Padding通过在图片周围加0,缓解了多层卷积时图片尺寸的急剧减少,我们设stride=3,padding=25(使效果更明显):
大部分卷积核下,padding相当于给图片加了黑边,有一个卷积核对应的图片多了白色边框,无伤大雅。此外,我们发现,和上面不加padding相比,图片会更大一点。
池化层旨在降低参数数量,有两种方法:平均池化和最大池化。我们先用边缘检测卷积核对图片进行stride=1的卷积,然后分别进行最大池化和平均池化,其中步长和池化尺寸都是3:
尺寸减小的同时,图片的分辨率下降(更抽象);此外,最大池化下的图片的亮度更高,和预期相符。
当前流行的卷积神经网络框架,常常会用到多层卷积,我们尝试对图片进行多层卷积和池化,观察效果。AlexNet有5个卷积层和2个全连接层。在第一、第二和第五卷积层之后进行最大池化。
仿照AlexNet的处理步骤,我们分别用Identity,边缘检测,锐化,模糊卷积核进行卷积,第1,2,5层进行池化,效果从左到右如上所示。此时图片尺寸已经从(608,482)锐减到(72,56),而仍可以看原图的一些细节。
我们在这里列出上面进行实验的代码:
# 论文中列举出的7中卷积核
kernels = [
np.array([
[0, 0, 0],
[0, 1, 0],
[0, 0, 0],
]),
np.array([
[1, 0, -1],
[0, 0, 0],
[-1, 0, 1],
]),
np.array([
[0, 1, 0],
[1, -4, 1],
[0, 1, 0],
]),
np.array([
[-1, -1, -1],
[-1, 8, -1],
[-1, -1, -1],
]),
np.array([
[0, -1, 0],
[-1, 5, -1],
[0, -1, 0],
]),
np.ones((3, 3)) / 9,
np.array([
[1, 2, 1],
[2, 4, 2],
[1, 2, 1],
]) / 16,
]
def conv2d(array, stride=1, padding=0, kernel=0):
'''
卷积操作
parameters
----------
array : 输入的图片三维张量
stride: 卷积的步长
padding: 对图片进行0填充
kernel: 上面卷积核的index,比如为0则选用kernels[0]
return
------
ret : 卷积后的三维张量
'''
kernel = np.expand_dims(kernels[kernel], -1)
kernel_size = kernel.shape[0]
im_height, im_width, tunnel = array.shape
# padding
if padding != 0:
col = np.zeros((im_height, padding, tunnel))
raw = np.zeros((padding, im_width + 2 * padding, tunnel))
array = np.hstack((col, array, col))
array = np.vstack((raw, array, raw))
height = 1 + (im_height + 2 * padding - kernel_size) // stride
width = 1 + (im_width + 2 * padding - kernel_size) // stride
ret = np.empty((height, width, tunnel))
for i in range(0, height):
for j in range(0, width):
ret[i, j] = np.sum(
kernel * array[i * stride:i * stride + kernel_size,
j * stride:j * stride + kernel_size],
axis=(0, 1),
)
return ret
def pooling(array, pool_size=2, stride=2, padding=0, method="max"):
'''
池化操作
Parameters
----------
array : 输入的图片三维张量
pool_size : 池化计算的尺寸
stride: 池化的步长
padding: 对图片进行0填充
method: 池化方法,"max"则是最大池化,"mean"则是平均池化
return
------
ret : 卷积后的三维张量
'''
im_height, im_width, tunnel = array.shape
if padding != 0:
col = np.zeros((im_height, padding, tunnel))
raw = np.zeros((padding, im_width + 2 * padding, tunnel))
array = np.hstack((col, array, col))
array = np.vstack((raw, array, raw))
height = 1 + (im_height + 2 * padding - kernel_size) // stride
width = 1 + (im_width + 2 * padding - kernel_size) // stride
ret = np.empty((height, width, tunnel))
pool_method = {"max": np.max, "mean": np.mean}[method]
for i in range(0, height):
for j in range(0, width):
ret[i, j] = pool_method(
array[i * stride:i * stride + kernel_size,
j * stride:j * stride + kernel_size],
axis=(0, 1),
)
return ret