深度学习(七):卷积神经网络

93 阅读6分钟

一,整体结构

卷积神经网络,英文为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