一,整体结构
卷积神经网络,英文为Convolutional Neural Network,所以也被简称为CNN
CNN和之前介绍的神经网络一样,也可以通过组装层来构建,只是在CNN中新出现了卷积层和池化层。
之前的文章中已经介绍了Affine层,也就是全连接层,相邻的所有神经元之间都有连接
那么在一个全连接的神经网络中,应该就是一个Affine层后面会跟着激活函数层(ReLU或者Sigmoid),例如下:
那么CNN是什么样的呢?如下所示
可以看到,有把一些“Affine - ReLU”连接被替换成了“Convolution - ReLU -(Pooling)”连接
二,卷积层
2.1 卷积层的意义
在之前的全连接层中,输入图像是1通道、高28像素、长28像素的(1, 28, 28)形状,但是被换成了1列,以784个数据的形式输入。这种转换下可能会丢失很多信息。 而卷积层可以保持形状不变,它会以3维数据接收,再以3维数据输出。另外,我们把卷积层的输入输出数据称为特征图
2.2 卷积运算
在卷积层进行的就是卷积运算。现在我们输入一个(4,4)的数据,和一个(3,3)的滤波器(核)进行卷积运算
具体怎么计算的,是将各个位置上滤波器的元素和输入的对应元素相乘,然后再求和,将这个结果保存到输出的对应位置; 将这个过程在所有位置都进行一遍,就可以得到卷积运算的输出
在CNN中,滤波器的参数就对应了之前的权重,除此之外,CNN中还存在着偏置,计算方式如下
2.3 填充
这是卷积层的一种处理,在计算之前,要向输入数据的周围填入固定的数据,这叫填充
如上图中,进行的操作就是对大小为(4, 4)的输入数据应用了幅度为1的填充,就是用幅度为1像素的0填充周围。这个填充的数值也可以是其他值自行设置
使用填充主要是为了调整输出的大小,因为每次进行卷积运算都会缩小空间,那么在某个时刻输出大小就有可能变为 1,导致无法再应用卷积运算,所以可以使用填充
2.4 步幅
滤波器每次应用移动的距离就称为步幅,像假如把步幅设为2,如图所示
增大步幅后,输出大小会变小,增大填充后,输出大小会变大 假设输入大小为(H, W),滤波器大小为(FH, FW),输出大小为(OH, OW),填充为P,步幅为S。输出大小可如下方式计算
2.5 3维数据的卷积运算
假如要处理加上通道方向的3维数据来进行计算,如下所示
通道方向上有多个特征图时,会按通道进行输入数据和滤波器的卷积运算,并将结果相加,从而得到输出
2.6 结合方块思考
将其表示成方块来考虑,输入数据为(C,H,W)的形状,滤波器为(C,FH,FW)的形状
数据输出就是通道数为1的特征图。如果使用多个滤波器,那么在通道方向上也拥有多个卷积运算的输出,如下
如果再加上偏置,如下
2.7 批处理
上面是传入的一个数据,那假如要一次性输入N个数据呢,处理流程如下
注意此时各个阶段的形状
三,池化层
池化是缩小高、长方向上的空间的运算,如下所示
如上所示就是按步幅2进行的22的Max池化。就是以在22的区域中找到最大值,然后以步幅2进行移动
除了Max池化还有Average池化,也就是求目标区域的平均值
池化层的特征
- 没有要学习的参数
- 通道数不发生变化
- 对微小的位置变化具有鲁棒性:输入数据发生微小偏差时,池化仍会返回相同的结果
四,卷积层和池化层的实现
使用Python封装这两个层,方便调用
4.1 im2col
因为在CNN中处理的事多维数据,在取数据进行运算的时候,如果用普通运算会比较复杂,所以可以借用im2col函数 它能将将输入数据展开以适合滤波器
如在上图中,把包含批数量的4维数据转换成了2维数据
使用im2col展开输入数据后,之后就只需将卷积层的滤波器(权重)纵向展开为1列,并计算2个矩阵的乘积即可
4.2 卷积层的实现
class Convolution:
def __init__(self, W, b, stride=1, pad=0):
self.W = W
self.b = b
self.stride = stride
self.pad = pad
def forward(self, x):
FN, C, FH, FW = self.W.shape
N, C, H, W = x.shape
out_h = int(1 + (H + 2*self.pad - FH) / self.stride)
out_w = int(1 + (W + 2*self.pad - FW) / self.stride)
col = im2col(x, FH, FW, self.stride, self.pad)
col_W = self.W.reshape(FN, -1).T # 滤波器的展开
out = np.dot(col, col_W) + self.b
out = out.reshape(N, out_h, out_w, -1).transpose(0, 3, 1, 2)
return out
4.3 池化层的实现
class Pooling:
def __init__(self, pool_h, pool_w, stride=1, pad=0):
self.pool_h = pool_h
self.pool_w = pool_w
self.stride = stride
self.pad = pad
def forward(self, x):
N, C, H, W = x.shape
out_h = int(1 + (H - self.pool_h) / self.stride)
out_w = int(1 + (W - self.pool_w) / self.stride)
# 展开(1)
col = im2col(x, self.pool_h, self.pool_w, self.stride, self.pad)
col = col.reshape(-1, self.pool_h*self.pool_w)
# 最大值(2)
out = np.max(col, axis=1)
# 转换(3)
out = out.reshape(N, out_h, out_w, C).transpose(0, 3, 1, 2)
return out
五,CNN的实现
将卷积层和池化层调用起来,搭建进行手写数字识别的CNN
代码如下
class SimpleConvNet:
def __init__(self, input_dim=(1, 28, 28),conv_param={'filter_num':30, 'filter_size':5, 'pad':0, 'stride':1}, hidden_size=100, output_size=10, weight_init_std=0.01):
filter_num = conv_param['filter_num']
filter_size = conv_param['filter_size']
filter_pad = conv_param['pad']
filter_stride = conv_param['stride']
input_size = input_dim[1]
conv_output_size = (input_size - filter_size + 2*filter_pad) / filter_stride + 1
pool_output_size = int(filter_num *(conv_output_size/2) * (conv_output_size/2))
self.params = {}
self.params['W1'] = weight_init_std * np.random.randn(filter_num, input_dim[0], filter_size, filter_size)
self.params['b1'] = np.zeros(filter_num)
self.params['W2'] = weight_init_std * np.random.randn(pool_output_size, hidden_size)
self.params['b2'] = np.zeros(hidden_size)
self.params['W3'] = weight_init_std * np.random.randn(hidden_size, output_size)
self.params['b3'] = np.zeros(output_size)
self.layers = OrderedDict()
self.layers['Conv1'] = Convolution(self.params['W1'],self.params['b1'],conv_param['stride'],conv_param['pad'])
self.layers['Relu1'] = Relu()
self.layers['Pool1'] = Pooling(pool_h=2, pool_w=2, stride=2)
self.layers['Affine1'] = Affine(self.params['W2'],self.params['b2'])
self.layers['Relu2'] = Relu()
self.layers['Affine2'] = Affine(self.params['W3'],self.params['b3'])
self.last_layer = softmaxwithloss()
def predict(self, x):
for layer in self.layers.values():
x = layer.forward(x)
return x
def loss(self, x, t):
y = self.predict(x)
return self.lastLayer.forward(y, t)
def gradient(self, x, t):
# forward
self.loss(x, t)
# backward
dout = 1
dout = self.lastLayer.backward(dout)
layers = list(self.layers.values())
layers.reverse()
for layer in layers:
dout = layer.backward(dout)
# 设定
grads = {}
grads['W1'] = self.layers['Conv1'].dW
grads['b1'] = self.layers['Conv1'].db
grads['W2'] = self.layers['Affine1'].dW
grads['b2'] = self.layers['Affine1'].db
grads['W3'] = self.layers['Affine2'].dW
grads['b3'] = self.layers['Affine2'].db
return grads