深度学习入门(十一):向更深的网络出发(感受野、加深层的动机、深度学习的高速化、实现深层CNN)

本文为《深度学习入门 基于Python的理论与实现》的部分读书笔记
代码以及图片均参考此书

加深层的动机

感受野(receptive field)

参考:https://blog.csdn.net/program_developer/article/details/80958716
https://blog.csdn.net/u010725283/article/details/78593410/

理解感受野是理解卷积神经网络工作的基础,尤其是对于使用Anchor作为强先验区域的物体检测算法,如Faster RCNN和SSD。如何设置Anchor的大小,Anchor应该对应特征图的哪一层,都应该考虑感受野。通常来讲,Anchor的大小应该与感受野相匹配,尤其是有效感受野,过大或过小都不好

定义

感受野(Receptive Field)的定义是卷积神经网络每一层输出的特征图(feature map)上的像素点在输入图片上映射的区域大小。再通俗点的解释是,特征图上的一个点对应输入图上的区域
在这里插入图片描述
由上图可以看出,经过几个卷积层之后,特征图的大小逐渐变小,一个特征所表示的信息量越来越多

感受野的计算

在这里插入图片描述
注意上图中Layer1到Layer2的过程中,padding=1,这里的1是指特征所占的区域,即一个特征所占的感受野,所以Conv2这张图上才会在外面加上三个格。stride=2也是同样的道理,2表示跨过两个特征。

从上图可以整理出以下公式:
jl=jl1sl\begin{aligned} j_{l} = j_{l-1} * s_{l} \end{aligned}rl+1=rl+(kl+11)jl\begin{aligned} r_{l+1} = r_l + (k_{l+1} - 1) *j_{l} \end{aligned}其中rlr_l为第ll层的感受野大小,kk为卷积核大小,jlj_l为第ll层中两个特征相隔的距离

RFl+1RF_{l+1}为第ll层的感受野大小,kk为卷积核大小,StridelStride_l为第ll层的卷积核步长
整理一下就可以得到感受野RFRF的计算公式:
Sl=i=1lStridei\begin{aligned} S_{l} = \prod_{i=1}^l Stride_i \end{aligned}RFl+1=RFl+(k1)Sl\begin{aligned} RF_{l+1} =RF_l + (k - 1) *S_{l} \end{aligned}

由上述公式计算的感受野通常很大,而因为输入层中边缘点的使用次数明显比中间点少,因此做出的贡献不同。经过多层的卷积堆叠之后,输入层对于特征图点做出的贡献分布呈高斯分布形状。
因此,实际的有效感受野 Effective Receptive Field 往往小于理论感受野。

减少网络的参数数量

说得详细一点,就是与没有加深层的网络相比,加深了层的网络可以用更少的参数达到同等水平(或者更强)的表现力。这一点结合卷积运算中的滤波器大小来思考就好理解了。
在这里插入图片描述
使用 5×55 \times 5 的滤波器每个输出节点都是从输入数据的某个 5×55 \times 5 的区域算出来的。

在这里插入图片描述
由上图可以看出,重复两次 3×33 \times 3 的卷积层后,输出数据同样是观察了输入数据的某个5×55 \times 5 的区域后计算出来的

也就是说,一次 5×55 \times 5 的卷积运算的区域可以看作由两次 3×33 \times 3 的卷积运算抵充,而且参数数量更少了。这个参数数量之差会随着层的加深而变大。比如,重复三次 3×33 \times 3 的卷积运算时,参数的数量总共是27。而为了用一次卷积运算“观察”与之相同的区域,需要一个 7×77 \times 7 的滤波器,此时的参数数量是49。

由此可以看出叠加小型滤波器来加深网络的好处:减少参数的数量,扩大感受野(receptive field,给神经元施加变化的某个局部空间区域)。并且,通过叠加层,将ReLU 等激活函数夹在卷积层的中间,进一步提高了网络的表现力。这是因为向网络添加了基于激活函数的“非线性”表现力,通过非线性函数的叠加,可以表现更加复杂的东西。

使学习更加高效

卷积层中,神经元会对边缘等简单的形状有响应,随着层的加深,开始对纹理、物体部件等更加复杂的东西有响应。CNN的卷积层会分层次地提取信息,从而高效地进行学习

例如,考虑一下 “狗”的识别问题。要用浅层网络解决这个问题的话,卷积层需要一下子理解很多“狗”的特征。“狗”有各种各样的种类,根据拍摄环境的不同,外观变化也很大。因此,要理解“狗”的特征,需要大量富有差异性的学习数据,而这会导致学习需要花费很多时间。不过,通过加深网络,就可以分层次地分解需要学习的问题。因此,各层需要学习的问题就变成了更简单的问题。比如,最开始的层只要专注于学习边缘就好,这样一来,只需用较少的学习数据就可以高效地进行学习。这是为什么呢?因为和印有“狗”的照片相比,包含边缘的图像数量众多,并且边缘的模式比“狗”的模式结构更简单。

深度学习的高速化

基于GPU的高速化

百度的AI Studio现在每天都可以白嫖12小时的GPU使用时间 😏真香

GPU原本是作为图像专用的显卡使用的,但最近不仅用于图像处理,也用于通用的数值计算。由于GPU可以高速地进行并行数值计算,因此GPU计算的目标就是将这种压倒性的计算能力用于各种用途。

深度学习中需要进行大量的乘积累加运算(或者大型矩阵的乘积运算)。这种大量的并行运算正是GPU所擅长的(反过来说,CPU比较擅长连续的、复杂的计算)。因此,与使用单个CPU相比,使用GPU进行深度学习的运算可以达到惊人的高速化。

GPU主要由NVIDIA和AMD两家公司提供。虽然两家的GPU都可以用于通用的数值计算,但与深度学习比较“亲近”的是NVIDIA的GPU。实际上,大多数深度学习框架只受益于NVIDIA的GPU。这是因为深度学习的框架中使用了NVIDIA提供的CUDA这个面向GPU计算的综合开发环境

下图中中出现的cuDNN是在CUDA上运行的库,它里面实现了为深度学习最优化过的函数等。
在这里插入图片描述

  • 同时要注意Numpy这个库是不会主动检测并使用GPU的,如果要使用GPU进行运算,可以使用它的替代品minpy或者直接使用其他的深度学习框架。因为之前的实现一直都使用的是Numpy,因此只能用CPU进行网络训练

分布式学习

为了进一步提高深度学习所需的计算的速度,可以考虑在多个GPU或者多台机器上进行分布式计算。
在这里插入图片描述
“如何进行分布式计算”是一个非常难的课题。它包含了机器间的通信、数据的同步等多个无法轻易解决的问题。可以将这些难题都交给TensorFlow 等优秀的框架。

运算精度的位数缩减

在深度学习的高速化中,除了计算量之外,内存容量、总线带宽等也有可能成为瓶颈。关于内存容量,需要考虑将大量的权重参数或中间数据放在内存中。关于总线带宽,当流经GPU(或者CPU)总线的数据超过某个限制时,就会成为瓶颈。考虑到这些情况,我们希望尽可能减少流经网络的数据的位数。

深度学习并不那么需要数值精度的位数。这是神经网络的一个重要性质。这个性质是基于神经网络的健壮性而产生的。这里所说的健壮性是指,比如,即便输入图像附有一些小的噪声,输出结果也仍然保持不变。可以认为,正是因为有了这个健壮性,流经网络的数据即便有所“劣化”,对输出结果的影响也较小。

根据以往的实验结果,在深度学习中,即便是16位的半精度浮点数(half float),也可以顺利地进行学习。实际上,NVIDIA的下一代GPU框架Pascal也支持半精度浮点数的运算,由此可以认为今后半精度浮点数将被作为标准使用。

以往的深度学习的实现中并没有注意数值的精度,不过Python 中一般使用64 位的浮点数。NumPy中提供了16 位的半精度浮点数类型(不过,只有16位类型的存储,运算本身不用16位进行),即便使用NumPy的半精度浮点数,识别精度也不会下降。

特别是在面向嵌入式应用程序中使用深度学习时,位数缩减非常重要。

实现深层CNN

这个网络结构参考了VGG。卷积层全部使用 3×33 \times 3 的小型滤波器,pad=1,stride=1pad=1,stride=1(有一层的卷积层滤波器pad=2pad=2以保证输入数据在经过最后一层池化层之前长宽均为偶数),使输入数据每经过一个卷积层,长宽不变而通道数变大(通道数从前面的层开始按顺序以16、16、32、32、64、64的方式增加)。池化层用于逐渐减小中间数据的空间大小,使用 2×22 \times 2 的滤波器,pad=0,stride=2pad=0,stride=2,使输入数据每经过一个池化层,通道数不变而长宽均减半。同时也加入了Dropout层,用于抑制过拟合。

在这里插入图片描述

import sys
file_path = __file__.replace('\\', '/')
dir_path = file_path[: file_path.rfind('/')] # 当前文件夹的路径
pardir_path = dir_path[: dir_path.rfind('/')]
sys.path.append(pardir_path) # 添加上上级目录到python模块搜索路径

import numpy as np
from func.gradient import numerical_gradient, gradient_check
from layer.common import *
from collections import OrderedDict
import os
import pickle

class DeepConvNet:
    """
    网络结构如下所示
        conv - relu - conv- relu - pool -
        conv - relu - conv- relu - pool -
        conv - relu - conv- relu - pool -
        affine - relu - dropout - affine - dropout - softmax

    输入数据(1,28,28)的形状变化
        (16, 28, 28) - (16, 28, 28) - (16, 14, 14) -
        (32, 14, 14) - (32, 16, 16) - (32, 8, 8) -
        (64, 8, 8) - (64, 8, 8) - (64, 4, 4) -
        (50) - (10)

    如果要改变卷积核的参数或者全连接层中的神经元个数的话,则不仅需要调整初始化方法中的参数,还要在代码中手动更改初始化权重的标准差以及第一个隐藏层的输入数据形状
    """
    def __init__(self, input_dim=(1, 28, 28),
                 conv_param_1 = {'filter_num':16, 'filter_size':3, 'pad':1, 'stride':1},
                 conv_param_2 = {'filter_num':16, 'filter_size':3, 'pad':1, 'stride':1},
                 conv_param_3 = {'filter_num':32, 'filter_size':3, 'pad':1, 'stride':1},
                 conv_param_4 = {'filter_num':32, 'filter_size':3, 'pad':2, 'stride':1},
                 conv_param_5 = {'filter_num':64, 'filter_size':3, 'pad':1, 'stride':1},
                 conv_param_6 = {'filter_num':64, 'filter_size':3, 'pad':1, 'stride':1},
                 hidden_size=50, output_size=10, dropout_ratio=0.5,
                 pretrain_flag=True, pkl_file_name=None):
        self.pkl_file_name = pkl_file_name
        if pretrain_flag == 1 and os.path.exists(self.pkl_file_name):
            self.load_pretrain_model()
        else:
            # 初始化权重===========
            # 各层的神经元平均与前一层的几个神经元有连接
            #                         conv1   conv2   conv3   conv4   conv5   conv6   affine1  affine2
            pre_node_nums = np.array([1*3*3, 16*3*3, 16*3*3, 32*3*3, 32*3*3, 64*3*3, 64*4*4, hidden_size])
            wight_init_scales = np.sqrt(2.0 / pre_node_nums)  # 使用ReLU的情况下推荐的初始值
            
            self.params = {}
            pre_channel_num = input_dim[0]
            for idx, conv_param in enumerate([conv_param_1, conv_param_2, conv_param_3, conv_param_4, conv_param_5, conv_param_6]):
                self.params['W' + str(idx+1)] = wight_init_scales[idx] * np.random.randn(conv_param['filter_num'], pre_channel_num, conv_param['filter_size'], conv_param['filter_size'])
                self.params['b' + str(idx+1)] = np.zeros(conv_param['filter_num'])
                pre_channel_num = conv_param['filter_num']
            self.params['W7'] = wight_init_scales[6] * np.random.randn(64*4*4, hidden_size)
            self.params['b7'] = np.zeros(hidden_size)
            self.params['W8'] = wight_init_scales[7] * np.random.randn(hidden_size, output_size)
            self.params['b8'] = np.zeros(output_size)

            # 生成层===========
            self.layers = []
            self.layers.append(Convolution(self.params['W1'], self.params['b1'], 
                            conv_param_1['stride'], conv_param_1['pad']))
            self.layers.append(Relu())
            self.layers.append(Convolution(self.params['W2'], self.params['b2'], 
                            conv_param_2['stride'], conv_param_2['pad']))
            self.layers.append(Relu())
            self.layers.append(Pooling(pool_h=2, pool_w=2, stride=2))
            self.layers.append(Convolution(self.params['W3'], self.params['b3'], 
                            conv_param_3['stride'], conv_param_3['pad']))
            self.layers.append(Relu())
            self.layers.append(Convolution(self.params['W4'], self.params['b4'],
                            conv_param_4['stride'], conv_param_4['pad']))
            self.layers.append(Relu())
            self.layers.append(Pooling(pool_h=2, pool_w=2, stride=2))
            self.layers.append(Convolution(self.params['W5'], self.params['b5'],
                            conv_param_5['stride'], conv_param_5['pad']))
            self.layers.append(Relu())
            self.layers.append(Convolution(self.params['W6'], self.params['b6'],
                            conv_param_6['stride'], conv_param_6['pad']))
            self.layers.append(Relu())
            self.layers.append(Pooling(pool_h=2, pool_w=2, stride=2))
            self.layers.append(Affine(self.params['W7'], self.params['b7']))
            self.layers.append(Relu())
            self.layers.append(Dropout(dropout_ratio))
            self.layers.append(Affine(self.params['W8'], self.params['b8']))
            self.layers.append(Dropout(dropout_ratio))
            
            self.last_layer = SoftmaxWithLoss()

    def load_pretrain_model(self):
        with open(self.pkl_file_name, 'rb') as f:
            model = pickle.load(f)
            for key in ('params', 'layers', 'last_layer'):
                exec('self.' + key + '=model.' + key)
            print('params loaded!')

    def predict(self, x, train_flg=False):
        for layer in self.layers:
            if isinstance(layer, Dropout):
                x = layer.forward(x, train_flg)
            else:
                x = layer.forward(x)
        return x

    def loss(self, x, t):
        y = self.predict(x, train_flg=True)
        return self.last_layer.forward(y, t)

    def accuracy(self, x, t, batch_size=100):
        if t.ndim != 1: 
            t = np.argmax(t, axis=1)

        acc = 0.0

        for i in range(int(x.shape[0] / batch_size)):
            tx = x[i*batch_size:(i+1)*batch_size]
            tt = t[i*batch_size:(i+1)*batch_size]
            y = self.predict(tx, train_flg=False)
            y = np.argmax(y, axis=1)
            acc += np.sum(y == tt)

        return acc / x.shape[0]

    def gradient(self, x, t):
        # forward
        self.loss(x, t)

        # backward
        dout = 1
        dout = self.last_layer.backward(dout)
        for layer in reversed(self.layers):
            dout = layer.backward(dout)

        # 设定
        grads = {}
        for i, layer_idx in enumerate((0, 2, 5, 7, 10, 12, 15, 18)):
            grads['W' + str(i+1)] = self.layers[layer_idx].dW
            grads['b' + str(i+1)] = self.layers[layer_idx].db

        return grads

if __name__ == '__main__':
    from dataset.mnist import load_mnist
    from trainer.trainer import Trainer

    (x_train, t_train),  (x_test, t_test) = load_mnist(normalize=True, flatten=False, one_hot_label=True, shuffle_data=True)

    # setting
    train_flag = 1 # 进行训练还是预测
    gradcheck_flag = 0 # 对已训练的网络进行梯度检验
    
    pkl_file_name = dir_path + '/deep_convnet.pkl'
    fig_name = dir_path + '/deep_convnet.png'

    net = DeepConvNet(input_dim=(1, 28, 28),
                        conv_param_1 = {'filter_num':16, 'filter_size':3, 'pad':1, 'stride':1},
                        conv_param_2 = {'filter_num':16, 'filter_size':3, 'pad':1, 'stride':1},
                        conv_param_3 = {'filter_num':32, 'filter_size':3, 'pad':1, 'stride':1},
                        conv_param_4 = {'filter_num':32, 'filter_size':3, 'pad':2, 'stride':1},
                        conv_param_5 = {'filter_num':64, 'filter_size':3, 'pad':1, 'stride':1},
                        conv_param_6 = {'filter_num':64, 'filter_size':3, 'pad':1, 'stride':1},
                        hidden_size=50, output_size=10, dropout_ratio=0.5,
                        pretrain_flag=True, pkl_file_name=pkl_file_name)

    trainer = Trainer(net, x_train, t_train, x_test, t_test,
                 epochs=5, mini_batch_size=128,
                 optimizer='Adam', optimizer_param={}, 
                 save_model_flag=True, pkl_file_name=pkl_file_name, plot_flag=True, fig_name=fig_name,
                 evaluate_sample_num_per_epoch=1000, verbose=True)

    if gradcheck_flag == 1:
        gradient_check(net, x_train[:2], t_train[:2])

    if train_flag:
        trainer.train()
    else:           
        acc = net.accuracy(x_train, t_train)
        print('accuracy:', acc)  

训练了3个epoch之后,精度成功突破了99%,没有过拟合现象发生

=============== Final Test Accuracy ===============
test acc:0.9917

在这里插入图片描述

深度学习的应用案例(简介)

物体识别

物体检测

图像分割

图像分割是指在像素水平上对图像进行分类。如下图所示,使用以像素为单位对各个对象分别着色的监督数据进行学习。然后,在推理时,对输入图像的所有像素进行分类。
在这里插入图片描述
要基于神经网络进行图像分割,最简单的方法是以所有像素为对象,对每个像素执行推理处理。比如,准备一个对某个矩形区域中心的像素进行分类的网络,以所有像素为对象执行推理处理。正如大家能想到的,这样的方法需要按照像素数量进行相应次forward处理,因而需要耗费大量的时间(正确地说,卷积运算中会发生重复计算很多区域的无意义的计算)。为了解决这个无意义的计算问题,有人提出了一个名为FCNFully Convolutional Network)的方法。该方法通过一次forward处理,对所有像素进行分类

相对于一般的CNN包含全连接层,FCN将全连接层替换成发挥相同作用的卷积层。在物体识别中使用的网络的全连接层中,中间数据的空间容量被作为排成一列的节点进行处理,而只由卷积层构成的网络中,空间容量可以保持原样直到最后的输出。

全连接层中,输出和全部的输入相连。使用卷积层也可以实现与此结构完全相同的连接。比如,针对输入大小是32×10×10(通道数32、高10、长10)的数据的全连接层可以替换成滤波器大小为32×10×10 的卷积层。如果全连接层的输出节点数是100,那么在卷积层准备100 个32×10×10 的滤波器就可以实现完全相同的处理。像这样,全连接层可以替换成进行相同处理的卷积层。

如下图所示,FCN的特征在于最后导入了扩大空间大小的处理。基于这个处理,变小了的中间数据可以一下子扩大到和输入图像一样的大小。FCN最后进行的扩大处理是基于双线性插值法的扩大(双线性插值扩大)。FCN中,这个双线性插值扩大是通过去卷积(逆卷积运算)来实现的

在这里插入图片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章