《斋藤康毅-深度学习入门》读书笔记07-卷积神经网络CNN

448 阅读9分钟

卷积神经网络(Convolutional Neural Network, CNN)是一种利用物体边缘的图像识别技术,被广泛应用于图像识别领域,在图像识别比赛中几乎全部的深度学习方法都基于CNN来实现。

整体结构

基于全连接的神经网络

在以往的学习中,我们认识到全连接的神经网络,通常是由Affine(仿射函数,线性层)+ReLU(激活函数,也可替换为SigmoidTanh的等)构成,Affine为模型建立了线性的表达能力,而ReLU则在此基础上增加非线性表达因素,增强了整个模型的表达能力,可以拟合更加复杂的曲线。实际生活中的场景大多是非线性的,因此,非线性特征越多,才能训练出符合各种特征的曲线与模型,提高模型的泛化能力。

最终由Softmax进行输出的正则化处理,得到每个分类的概率。

image.png 图:基于全连接层(Affine层)的网络

基于CNN的网络

CNN在此基础上,增加了卷积层(Convolution)和池化层(Pooling),将原始的Affine-ReLU替换成Conv-ReLU-Pooling,在下图中靠近输出的层依旧使用了Affine-ReLU,更前面一层则是Conv-ReLU,没有使用Pooling

image.png 图:基于CNN的网络

卷积层

全连接层存在的问题

以图像识别为例,全连接层将三通道(1,28,28)拉平成一通道,虽然简化了数据形式,但这会导致丢失数据在不同维度上的信息。全连接层忽视了数据形状,将全部输入数据作为相同神经元处理,无法利用与形状相关信息。

卷积层可以保持形状不变,在不同层之间以三维数据形式进行传递,有效利用形状信息。

卷积层的输入输出数据被称为“特征图(featuer map)”。

卷积运算

卷积运算相当于图像处理中的滤波器运算,卷积运算的符号是,其含义是,通过滑动过滤器窗口,将输入元素与滤波器相乘后相加,得到新的位置上的元素。因此卷积运算也可以称作乘积累加运算

卷积运算也可以包含偏置,偏置通常是1*1的大小,被分别加到全部元素中。

image.png 图:卷积运算的计算顺序,4*4的矩阵经过卷积得到2*2的输出

image.png 图:偏置,所有应用了滤波器的元素被加上某个固定值

填充

由上图可知,44的矩阵在卷积后变小(22)了,如果应用多个卷积层,最终会导致输出变成1*1的矩阵,这还怎么玩?因此提出了“填充(padding)”的概念,填充过程有2个参数可以配置,分别是填充大小(指外围增加的行、列数)与填充数值(0、1、2等)。

image.png 图:对输入数据进行大小为1、值为0的填充后,经过卷积运算,能得到与输入维度4*4相同的输出矩阵

步幅

应用滤波器的位置间隔被称为“步幅(stride)”,步幅决定了输出(尺寸)大小,步幅越大,输出越小。

image.png

输入滤波器输出填充步幅 这五个元素之间的关系可以用下式表示:

image.png

3维数据的卷积

在三维数据中,第三维被称为“通道数”,输入数据和滤波器的通道数要设置为相同的值,且所有滤波器的大小要设置为相同的值。

运算过程是对每一个通道计算完成后,进行相加,最后得到单通道的数据。

image.png 图:对三维数据进行卷积运算

运算得到三维数据

前面讲到,卷积会将三维数据运算后累加,得到二维数据,那么如何保证运算后的数据仍然是三维呢?这里需要用到多个滤波器,假设滤波器的个数是N,则运算后会得到(N, W, H)大小的新数据,如下图所示:

image.png 图:应用多个滤波器,输出三维数据,通道数=滤波器个数

池化层

卷积运算中,通过设置Padding(填充)可以扩大输出的尺寸,设置Stride(步幅)则可以缩小输出尺寸。池化层也有类似的作用,可以通过设置池化层大小,来对池内的数据进行最大值平均值运算。

  • Max池化:对池内数据求最大值,作为输出元素,图像识别领域通常使用Max池化
  • Average池化:对池内数据求平均值后,作为输出元素

Max池化就是对池内元素求最大值的操作,池的大小通常设置成跟Stride(步幅)相同。

image.png 图:Max池化处理顺序

池化层有如下特征:

  • 没有要学习的参数:只是求Max/Average,没有参数要学习
  • 通道数不发生变化:按通道独立计算,不会增加或减少通道数
  • 对微小的位置变化具有鲁棒性:即使输入数据发生微小偏差,池化结果仍然可以保持不变,即池化会吸收输入数据的偏差

根据池化层的特征,可以总结出它有如下作用:

  • 降低数据大小:减少计算量
  • 平移不变性:对特征相应一致
  • 鲁棒性:从而让模型具备泛化能力

卷积层和池化层实现

im2col

im2colimage to column的简称,该函数能够将4维的输入数据降低成2维矩阵。具体地说,是将应用滤波器的区域(3维方块)横向展开为1列。

image.png 图:im2col将滤波器应用区域展开为一列

在实际的卷积运算中,步幅较小,滤波器的应用区域都是重叠的,这会导致展开后的元素个数多于原始输入数据中的元素,占据的内存上升。消耗内存是im2col的一个缺点,但这带来了运算速度的大幅提升——在线性代数库中对于矩阵计算已经实现了高度最优化。因此这是一种空间换时间的方式。

经过im2col处理,输入数据被横向展开为1列。同时,将滤波器纵向展开为1列,并且计算这两个矩阵的乘积,就可以得到卷积层的输出结果。

image.png 图:卷积运算的滤波器处理

im2col的函数签名如下,参数名都很好理解。

im2col(input_data, filter_h, filter_w, stride=1, pad=0)

以下用通道数为37*7数据举例,两组数据的批大小分别是110

import sys, os
sys.path.append(os.pardir)
from common.util import im2col
x1 = np.random.rand(1, 3, 7, 7) # 批大小为1
col1 = im2col(x1, 5, 5, stride=1, pad=0)
print(col1.shape) # (9, 75) # 第二维75个元素(3*5*5,滤波器元素个数总和)
x2 = np.random.rand(10, 3, 7, 7) # 10个数据
col2 = im2col(x2, 5, 5, stride=1, pad=0)
print(col2.shape) # (90, 75)

卷积层实现

具备forward函数的卷积层实现如下:

class Convolution:
    def __init__(self, W, b, stride=1, pad=0):
        sellf.W = W
        self.b = b
        self.stride = strinde
        self.pad = pad
        
    def forward(self, x):
        FN, C, FH, FW = self.W.shape # 初始化滤波器参数
        N, C, H, W = x.shape # 输入,C=channel通道数
        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 = sef.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) # transpose调整多维数组轴的顺序
        
        return out

image.png 图:transpose按照新顺序对轴进行排序

池化层的实现

MAX池化的实现相对简单,对每一行求最大值即可。

image.png 图:对输入数据展开池化

# 池化层实现

class Pooling:
    def__init__(self, pool_h, pool_w, stride=1, pad=0):
        sef.pool_h = pool_h
        self.pooll_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) # 对指定轴求max

        # 3 转换
        out = out.reshape(N, out_h, out_w, C).transpose(0, 3, 1, 2)
        
        return out

CNN实现

以一个名为SimpleConvNet的简单卷积网络为例,它的组成是Conv-ReLU-Pooling--Affine-ReLU--Affine-Softmax

image.png

类的初始化部分,保存卷积的各项参数。

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))

第二步,根据权重标准差以及各层的大小,进行权重参数初始化,所有权重都保存在params变量中

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)
# 全连接1
self.params['W2'] = weight_init_std * np.random.randn(pool_output_size, hidden_size)
self.params['b2'] = np.zeros(hidden_size)
# 全连接2
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['Affine1'] = Affine(self.params['W2'], self.params['b2'])

self.layers['Relu2'] = Relu()
self.layers['Affine2'] = Affine(self.params['W3'], self.params['b3'])

self.last_year = 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() # 反序
    
    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

CNN可视化

经过学习,滤波器掌握到了图像中的物体边缘信息,反映到滤波器上就是横向&纵向的滤波器。

image.png

随着层次加深,卷积层所提取到的信息越来越抽象。

image.png

CNN经典模型:LeNet和AlexNet

LeNet

image.png

1998年被提出,用于手写数字识别,和当代CNN相比,LeNet有如下不同点:

  • 使用sigmoid作为激活函数
  • 使用子采样(subsampling)缩小中间数据的大小

AlexNet

image.png

它具有如下特点:

  • 激活函数使用ReLU
  • 使用进行局部正规化的LRNLocal Response Normalization)层
  • 使用Dropout

数据规模的增大以及GPU运算速度提升,已经成为深度学习发展的两项巨大动力。