深度学习入门(十二):池化层、实践

池化层

池化是缩小高、长方向上的空间的运算。如图所示:
在这里插入图片描述
图中显示的是2x2的Max池化,在指定的范围内获取最大值并保留,“2 x 2”表示目标区域的大小。
一般来说,池化的窗口大小会和步幅设定成相同的值。

池化层的特征

池化层具有以下特征:

没有要学习的参数
池化层和卷积层不同,没有要学习的参数。池化只是从目标区域中取最大值(或者平均值),所以不存在要学习的参数。

通道数不发生变化
经过池化运算,输入数据和输出数据的通道数不会发生变化。如图所示,计算是按通道独立进行的。
在这里插入图片描述
对微小的位置变化具有鲁棒性(健壮)
输入数据发生微小偏差时,池化仍会返回相同的结果。因此,池化对输入数据的微小偏差具有鲁棒性。
在这里插入图片描述
如图所示, 输入数据在宽度方向上只偏离1个元素时,输出仍为相同的结果。

卷积层和池化层的实现

大家可能会感觉卷积层和池化层的实现很复杂,但实际上,通过使用某种技巧,就可以很轻松地实现。

4维数组

如前所述,CNN中各层间传递的数据是4维数据。所谓4维数据,比如数据的形状是(10,1,28,28),则它对应10个高为28、长为28、通道为1的数据。用Python来实现的话,如下所示。

 x = np.random.rand(10, 1, 28, 28) # 随机生成数据
 x.shape

访问数据则是x[0],x[1]这样来访问。
像这样,CNN中处理的是4维数据,因此卷积运算的实现看上去会很复杂,但是通过使用下面要介绍的im2col这个技巧,问题就会变得很简单。

基于im2col的展开

如果老老实实地实现卷积运算,估计要重复好几层的for语句。这样的实现有点麻烦,而且,NumPy中存在使用for语句后处理变慢的缺点(NumPy中,访问元素时最好不要用for语句)。这里,我们不使用for语句,而是使用im2col这个便利的函数进行简单的实现。
im2col是一个函数,将输入数据展开以适合滤波器(权重)。对3维的输入数据应用im2col后,数据转换为2维矩阵(正确地讲,是把包含批数量的4维数据转换成了2维数据)。在这里插入图片描述
im2col会把输入数据展开以适合滤波器(权重)。具体地说,如图所示,对于输入数据,将应用滤波器的区域(3维方块)横向展开为1列。im2col会在所有应用滤波器的地方进行这个展开处理
在这里插入图片描述
在实际的卷积运算中,滤波器的应用区域几乎都是重叠的。在滤波器的应用区域重叠的情况下,使用im2col展开后,展开后的元素个数会多于原方块的元素个数。因此,使用im2col的实现存在比普通的实现消耗更多内存的缺点。
但是,汇总成一个大的矩阵进行计算,对计算机的计算颇有益处。比如,在矩阵计算的库(线性代数库)等中,矩阵计算的实现已被高度最优化,可以高速地进行大矩阵的乘法运算。因此,通过归结到矩阵计算上,可以有效地利用线性代数库。

im2col这个名称是“ image to column ”的缩写,翻译过来就是“从图像到矩阵”的意思。

使用im2col展开输入数据后,之后就只需将卷积层的滤波器(权重)纵向展开为1列,并计算2个矩阵的乘积即可。这和全连接层的Affi ne 层进行的处理基本相同。
在这里插入图片描述
如图所示,基于im2col方式的输出结果是2维矩阵。因为CNN中数据会保存为4维数组,所以要将2维输出数据转换为合适的形状。以上就是卷积层的实现流程。

卷积层的实现

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

    Parameters
    ----------
    input_data : 由(数据量, 通道, 高, 长)的4维数组构成的输入数据
    filter_h : 滤波器的高
    filter_w : 滤波器的长
    stride : 步幅
    pad : 填充

    Returns
    -------
    col : 2维数组
    """
    N, C, H, W = input_data.shape
    out_h = (H + 2*pad - filter_h)//stride + 1
    out_w = (W + 2*pad - filter_w)//stride + 1

    img = np.pad(input_data, [(0,0), (0,0), (pad, pad), (pad, pad)], 'constant')
    col = np.zeros((N, C, filter_h, filter_w, out_h, out_w))

    for y in range(filter_h):
        y_max = y + stride*out_h
        for x in range(filter_w):
            x_max = x + stride*out_w
            col[:, :, y, x, :, :] = img[:, :, y:y_max:stride, x:x_max:stride]

    col = col.transpose(0, 4, 5, 1, 2, 3).reshape(N*out_h*out_w, -1)
    return col

imcol2会考虑滤波器大小、步幅、填充,将输入数据展开为二维数组。实际使用如下:

 import sys, os
 sys.path.append(os.pardir)
 
 from common.util import im2col
 x1 = np.random.rand(1, 3, 7, 7)
 col1 = im2col(x1, 5, 5, stride=1, pad=0)
 print(col1.shape) # (9, 75)
 
 x2 = np.random.rand(10, 3, 7, 7) # 10个数据
 col2 = im2col(x2, 5, 5, stride=1, pad=0)
 print(col2.shape) # (90, 75)

现在使用im2col来实现卷积层。这里我们将卷积层实现为名Convolution的类。

class Convolution:
    def __init__(self, W, b, stride=1, pad=0):
        self.W = W
        self.b = b
        self.stride = stride
        self.pad = pad
        
        # 中间数据(backward时使用)
        self.x = None   
        self.col = None
        self.col_W = None
        
        # 权重和偏置参数的梯度
        self.dW = None
        self.db = None

    def forward(self, x):
        FN, C, FH, FW = self.W.shape
        N, C, H, W = x.shape
        out_h = 1 + int((H + 2*self.pad - FH) / self.stride)
        out_w = 1 + int((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)

        self.x = x
        self.col = col
        self.col_W = col_W

        return out

卷积层的初始化方法将滤波器(权重)、偏置、步幅、填充作为参数接收。滤波器是(FN, C, FH, FW)的4维形状。另外,FN、C、FH、FW分别是Filter Number(滤波器数量)、Channel 、Filter Height 、Filter Width的缩写。
这里通过reshape(FN,-1)将参数指定为-1,这是reshape的一个便利的功能。通过在reshape时指定为-1,reshape函数会自动计算-1维度上的元素个数,以使多维数组的元素个数前后一致。
forward的实现中,最后会将输出大小转换为合适的形状。转换时使用了NumPy的transpose函数。transpose会更改多维数组的轴的顺序。
在这里插入图片描述
以上就是卷积层的forward处理的实现。通过使用im2col进行展开,基本上可以像实现全连接层的Affine层一样来实现。
但有一点需要注意,在进行卷积层的反向传播时,必须进行im2col的逆处理。代码如下:

def col2im(col, input_shape, filter_h, filter_w, stride=1, pad=0):
    """

    Parameters
    ----------
    col :
    input_shape : 输入数据的形状(例:(10, 1, 28, 28))
    filter_h :
    filter_w
    stride
    pad

    Returns
    -------

    """
    N, C, H, W = input_shape
    out_h = (H + 2*pad - filter_h)//stride + 1
    out_w = (W + 2*pad - filter_w)//stride + 1
    col = col.reshape(N, out_h, out_w, C, filter_h, filter_w).transpose(0, 3, 4, 5, 1, 2)

    img = np.zeros((N, C, H + 2*pad + stride - 1, W + 2*pad + stride - 1))
    for y in range(filter_h):
        y_max = y + stride*out_h
        for x in range(filter_w):
            x_max = x + stride*out_w
            img[:, :, y:y_max:stride, x:x_max:stride] += col[:, :, y, x, :, :]

    return img[:, :, pad:H + pad, pad:W + pad]

除了使用col2im这一点,卷积层的反向传播和Affi ne层的实现方式都一样。卷积层的反向传播的实现代码如下:

     def backward(self, dout):
        FN, C, FH, FW = self.W.shape
        dout = dout.transpose(0,2,3,1).reshape(-1, FN)

        self.db = np.sum(dout, axis=0)
        self.dW = np.dot(self.col.T, dout)
        self.dW = self.dW.transpose(1, 0).reshape(FN, C, FH, FW)

        dcol = np.dot(dout, self.col_W.T)
        dx = col2im(dcol, self.x.shape, FH, FW, self.stride, self.pad)

        return dx

池化层的实现

池化层的实现和卷积层相同,都使用im2col展开输入数据。不过,池化的情况下,在通道方向上是独立的,这一点和卷积层不同。
在这里插入图片描述
如图所示,池化的应用区域按通道单独展开。像这样展开之后,只需对展开的矩阵求各行的最大值,并转换为合适的形状即可。
在这里插入图片描述
上面就是池化层的forward处理的实现流程。下面来看一Python的实现示例。

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
        
        self.x = None
        self.arg_max = None

    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)

        col = im2col(x, self.pool_h, self.pool_w, self.stride, self.pad)
        col = col.reshape(-1, self.pool_h*self.pool_w)

        arg_max = np.argmax(col, axis=1)
        out = np.max(col, axis=1)
        out = out.reshape(N, out_h, out_w, C).transpose(0, 3, 1, 2)

        self.x = x
        self.arg_max = arg_max

        return out

池化层按三个阶段进行:
1.展开输入数据。
2.求各行的最大值。
3.转换为合适的输出大小。
池化层的backward:

    def backward(self, dout):
        dout = dout.transpose(0, 2, 3, 1)
        
        pool_size = self.pool_h * self.pool_w
        dmax = np.zeros((dout.size, pool_size))
        dmax[np.arange(self.arg_max.size), self.arg_max.flatten()] = dout.flatten()
        dmax = dmax.reshape(dout.shape + (pool_size,)) 
        
        dcol = dmax.reshape(dmax.shape[0] * dmax.shape[1] * dmax.shape[2], -1)
        dx = col2im(dcol, self.x.shape, self.pool_h, self.pool_w, self.stride, self.pad)
        
        return dx

CNN的实现

接下来会实现如图所示的CNN。
在这里插入图片描述
如图所示,网络的构成是“Convolution - ReLU - Pooling -Affine - ReLU - Affine - Softmax”,我们将它实现为名为SimpleConvNet的类。
参数:
• input_dim―输入数据的维度:(通道,高,长)
• conv_param―卷积层的超参数(字典)。字典的关键字如下:
filter_num―滤波器的数量
filter_size―滤波器的大小
stride―步幅
pad―填充
• hidden_size―隐藏层(全连接)的神经元数量
• output_size―输出层(全连接)的神经元数量
• weitght_int_std―初始化时权重的标准差

这里,卷积层的超参数通过名为conv_param的字典传入。我们设想它会像{‘filter_num’:30,‘filter_size’:5, ‘pad’:0, ‘stride’:1}这样,保存必要的超参数值。SimpleConvNet的初始化最开始的部分如下:

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)

学习所需的参数是第1层的卷积层和剩余两个全连接层的权重和偏置。将这些参数保存在实例变量的params字典中。将第1层的卷积层的权重设为关键字W1W1W1,偏置设为关键字b1b1b1。同样,分别用关键字W2W2W2b2b2b2和关键字W3W3W3b3b3b3来保存第2个和第3个全连接层的权重和偏置。
最后生成必要的层:

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

从最前面开始按顺序向有序字典的 layers中添加层。只有最后的SoftmaxWithLoss层被添加到别的变量lastLayer中。
以上就是SimpleConvNet的初始化中进行的处理。像这样初始化后,进行推理的predict方法和求损失函数值的loss方法就可以像下面这样实现。

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)
#参数x是输入数据,t是教师标签。

用于推理的predict方法从头开始依次调用已添加的层,并将结果传递给下一层。
接下来是基于误差反向传播法求梯度的代码实现:

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

因为已经在各层正确实现了正向传播和反向传播的功能,所以这里只需要以合适的顺序调用即可。最后,把各个权重参数的梯度保存到grads字典中。这就是SimpleConvNet的实现。
如上所述,卷积层和池化层是图像识别中必备的模块。CNN可以有效读取图像中的某种特性,在手写数字识别中,还可以实现高精度的识别。

CNN的可视化

这个部分通过可视化探究CNN中到底进行了什么处理。

第1层权重的可视化

如图是卷积层第一层的图像:
在这里插入图片描述在这里插入图片描述

以上是学习前和学习后的第1层的卷积层的权重:虽然权重的元素是实数,但是在图像的显示上,统一将最小值显示为黑色(0),最大值显示为白色(255)。
此时的滤波器做的工作就是在观察图像的边缘和斑块。如图所示。
在这里插入图片描述
对水平方向上和垂直方向上的边缘有响应的滤波器:输出图像1中,垂直方向的边缘上出现白色像素,输出图像2中,水平方向的边缘上出现很多白色像素。
因此可以发现,滤波器1对垂直方向上的边缘有反应,滤波器2对水平方向上的边缘有反应。由此可知,卷积层的滤波器会提取边缘或斑块等原始信息。而刚才实现的CNN会将这些原始信息传递给后面的层。

基于分层结构的信息提取

随着层数堆叠,各层提取的信息也会变化,越来越抽象(准确来说是反应强烈的神经元)。
在这里插入图片描述
CNN的卷积层中提取的信息。第1层的神经元对边缘或斑块有响应,第3层对纹理有响应,第5层对物体部件有响应,最后的全连接层对物体的类别(狗或车)有响应。
如果堆叠了多层卷积层,则随着层次加深,提取的信息也愈加复杂、抽象,这是深度学习中很有意思的一个地方。最开始的层对简单的边缘有响应,接下来的层对纹理有响应,再后面的层对更加复杂的物体部件有响应。也就是说,随着层次加深,神经元从简单的形状向“高级”信息变化。

具有代表性的CNN

关于CNN,迄今为止已经提出了各种网络结构。下面将介绍两个特别重要的网络。

LeNet

LeNet在1998年被提出,是进行手写数字识别的网络。如图7-27所示,它有连续的卷积层和池化层(正确地讲,是只“抽选元素”的子采样层),最后经全连接层输出结果。
在这里插入图片描述
和“现在的CNN”相比,LeNet有几个不同点。第一个不同点在于激活函数。LeNet中使用sigmoid函数,而现在的CNN中主要使用ReLU函数。此外,原始的LeNet中使用子采样(subsampling)缩小中间数据的大小,而现在的CNN中Max池化是主流.
综上,LeNet与现在的CNN虽然有些许不同,但差别并不是那么大。

AlexNet

AlexNet是引发深度学习热潮的导火线,不过它的网络结构和LeNet基本上没有什么不同。
在这里插入图片描述
AlexNet叠有多个卷积层和池化层,最后经由全连接层输出结果。AlexNet有以下几点差异。
• 激活函数使用ReLU。
• 使用进行局部正规化的LRN(Local Response Normalization)层。
• 使用Dropout
虽然网络结构没多大变化,但是两个CNN所处的环境发生了剧烈变化,现在任何人都可以获得大量的数据。而且,擅长大规模并行计算的GPU得到普及,高速进行大量的运算已经成为可能。大数据和GPU已成为深度学习发展的巨大的原动力。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值