CNN简介与实现

CNN简介与实现

导语

CNN全称卷积神经网络,可谓声名远扬,被用于生活中的各个领域,也是最好理解的神经网络结构之一。

整体结构

相较于先前的神经网络,CNN出现了卷积层和池化层的概念,基本的组成模块是"卷积-ReLU-池化",并且,在靠近输出或最后输出时时仍会采用"Affine-ReLU"、"Affine-ReLU"的组合,书上给出的示例图如下:

卷积层

在思考为什么要用卷积层之前,我们可以先来看看卷积层之前的全连接层有什么局限性,全连接层通常要求输入是一个一维的数组,即使原始数据是更高维的数据,如高、长、通道的三维图像,这个时候,使用全连接层,原始数据中的几何信息、点之间的相对位置等空间信息就都被清除了,这些信息其实很重要,因为点与点之间在高维空间的关联性是比一维更强的。

相比之下,卷积层就考虑到了这些空间信息,当输入为图像时,卷积层会以三维数据的形式接受输入数据,并且输出也是三维数据。

CNN中卷积层的输入输出数据被称作特征图,输入叫输入特征图,输出叫输出特征图。

卷积

卷积是卷积层的运算,类似与图像中的滤波器处理,具体做法如图(图源自网络,侵删):

此图省略了卷积核,只给出了输入和结果,以该图为例,输入是一个4×4的矩阵,在矩阵上存在一个3×3的滑动窗口,窗口每次移动一个单位,每次对窗口内的矩阵A进行一次权重累和,具体的权重为同等大小的卷积核矩阵,具体的例子如下, 36 = 1 × 1 + 2 × 1 + 0 × 3 + 4 × 0 + 5 × 2 + 6 × 0 + 7 × 1 + 8 × 2 + 1 × 1 36=1×1+2×1+0×3+4×0+5×2+6×0+7×1+8×2+1×1 36=1×1+2×1+0×3+4×0+5×2+6×0+7×1+8×2+1×1。

与全连接层一样,CNN中也存在偏置,对于算出的结果矩阵,对矩阵中的所有元素可以加上一个相同的偏置值。

填充

在进行卷积前,有时候要把数据拓宽,例如把4×4拓成6×6,如何拓宽呢很简单,把不够的部分都设置为同一个值就可以(一般是0或者1),具体操作如图(图源网络,侵删):

这种做法,就叫做填充,使用填充主要是为了调整输出大小,在使用卷积核运算的时候,如果不进行填充,卷积的结果势必会在整体上变小(如4×4变成2×2),多次使用后,最后的结果就可能只有一个1,因此使用填充来避免这种情况的发生。

步幅

步幅很容易理解,就是滑动窗口的每次的移动距离,像下面这张图,就是步幅为2时候的卷积(图源网络,侵删):

可以看到,增大步幅会使得输出变小,加上填充会变大,这个时候就可以根据两者关系列出卷积输出结果的公式了。

书上的描述如下(值除不尽四舍五入):

三维卷积

在现实使用中,CNN的输入并不是一个单纯的二维矩阵,输入的图像时一个带有高、宽、通道的具体的特征图,以RGB为例,RGB图像是三通道,如果对RGB图像进行卷积,那么就要对图像上的每一个通道都使用一个卷积核,通道方向有多个特征图时,需要按照通道方向进行输入数据和滤波器的卷积运算,并将结果累和,生成一个新的二维矩阵。

立体化

当我们把输入和输出推向更一般的适用情况,多通道输入数据使用对应的多通道核,最后输出一张单个图,书上的例子如下,其中C为通道数、H为高度、W为长度。

如果要再通道方向上也拥有多个卷积运算的输出,就需要使用多个滤波器(权重),书上的图如下:

如果再考虑上偏置,书上给出的图如下:

批处理

通常,为了加快效率,神经网络会将输入数据进行一批批的打包,一次性处理一堆数据,为了处理一批数据,需要在上一张图的基础上加上批次,书上给出的图如下:

数据作为4维数据在各层之间传递,批处理将N次处理汇总成了1次进行。

实现

如果直接实现卷积运算,利用for循环,效率其实是不高的,况且python给出了更好的选择:im2col函数。

im2col将输入数据展开来适合卷积核的计算,书上给出的图如下:

这里更详细的解释一下,输入的是一个三维的数据,把每一面(二维)从左到右,从上到下,拉成一个一维的数组,然后把每个通道的一维数组拼起来,形成一个二维的矩阵,如果是多批次,就把这些矩阵首尾相连,形成一个更大的二维矩阵即可。

实际的卷积运算中,卷积核的应用区域几乎彼此重叠,因此,在使用im2col之后,展开的元素个数会多于原来的输入元素个数,所以会消耗更多的内存。

书上给出了用im2col进行卷积的流程:

还需要明晰的一点是,im2col的使用并不会损失原数据在空间上的信息,它只是为了方便进行矩阵对数据进行了一些处理,并且在最后恢复了原来的数据模式。

书上给出了im2col和基于im2col实现的卷积层代码如下:

python 复制代码
def im2col(input_data, filter_h, filter_w, stride=1, pad=0):
    #输入,高,长,步幅,填充
    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

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)#拉伸
		#卷积层反向传播的时候,需要进行im2col的逆处理
		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)
		#更改轴的顺序,NHWC变成NCHW
		return out

  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展开,但展开时在通道方向上是独立的,书上给的图示如下:

书上的实现代码如下:

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

    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,书上给出了一个简单CNN的具体代码实现,具体图如下:

书上加上注释的代码如下:

python 复制代码
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.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)
            y = np.argmax(y, axis=1)
            acc += np.sum(y == tt) 
        
        return acc / x.shape[0]

    def numerical_gradient(self, x, t):#求梯度,用数值微分方法
        loss_w = lambda w: self.loss(x, t)

        grads = {}
        for idx in (1, 2, 3):
            grads['W' + str(idx)] = numerical_gradient(loss_w, self.params['W' + str(idx)])
            grads['b' + str(idx)] = numerical_gradient(loss_w, self.params['b' + str(idx)])

        return grads

    def gradient(self, x, t):#误差反向传播求梯度
        # forward
        self.loss(x, t)

        # backward
        dout = 1
        dout = self.last_layer.backward(dout)

        layers = list(self.layers.values())
        layers.reverse()
        for layer in layers:
            dout = layer.backward(dout)

        # 设定
        grads = {}
        grads['W1'], grads['b1'] = self.layers['Conv1'].dW, self.layers['Conv1'].db
        grads['W2'], grads['b2'] = self.layers['Affine1'].dW, self.layers['Affine1'].db
        grads['W3'], grads['b3'] = self.layers['Affine2'].dW, self.layers['Affine2'].db

        return grads
        

训练所需要的时间相较于先前的方法比较久,但是得到的结果识别率更高,具体训练结果如下:

可视化

"卷积"是一种数学运算,逻辑上其实很难理解到它的用处,因此,书上给出了对卷积作用更加直接的展现方式,以上一部分学习前和学习后的卷积核为例,各个卷积核的权重图如下:

学习前:

学习后:

可以明显的看到,学习前杂乱无章的权重矩阵,在学习后变得有迹可循,明显有些区域的权重更深一些,那么,这些权重更大的部分对应的目标究竟是什么呢?

书上给出了答案:这些卷积核在学习边缘(颜色变化的分界线)和斑块(局部的块状区域),例如黑白分界线,可以根据手写数字识别的例子想象,手写的数字是黑色,背景是白色,那么卷积核的目标就是使得模型对黑色的部分更加敏感,权重更大。

上述的结果是只进行了一次卷积得到的,随着层次的加深,提取的信息也会越来越抽象,在深度学习中,最开始层会对简单的边缘有响应,接下来是对纹理,在接下来是对更复杂的性质,随着层次递增,模型的目标会从简单的形状进化到更高级的信息。

总结

本章详细介绍了CNN的构造,对卷积层、池化层进行了从零开始的实现,但是对反向传播的部分只给出了代码实现。最重要的还是对im2col的理解,明白了im2col的原理,卷积层、池化层乃至反向传播的实现,这些问题就迎刃而解了。

基于最基本的CNN,后续还有更多功能强大,网络结构更深的CNN网络,如LeNet(激活函数为sigmod,使用子采样缩小中间数据大小,而不是卷积、池化)还有AlexNet(多个卷积层和池化层,激活函数为sigmod,使用进行局部正规化的LRN层,使用Dropout)等。

参考文献

  1. 【Pytorch实现】------深入理解im2col(详细图解)
  2. 12张动图帮你看懂卷积神经网络到底是什么
  3. 《深度学习入门------基于Python的理论与实现》
相关推荐
网易独家音乐人Mike Zhou2 小时前
【卡尔曼滤波】数据预测Prediction观测器的理论推导及应用 C语言、Python实现(Kalman Filter)
c语言·python·单片机·物联网·算法·嵌入式·iot
安静读书2 小时前
Python解析视频FPS(帧率)、分辨率信息
python·opencv·音视频
小陈phd2 小时前
OpenCV从入门到精通实战(九)——基于dlib的疲劳监测 ear计算
人工智能·opencv·计算机视觉
Guofu_Liao4 小时前
大语言模型---LoRA简介;LoRA的优势;LoRA训练步骤;总结
人工智能·语言模型·自然语言处理·矩阵·llama
小二·4 小时前
java基础面试题笔记(基础篇)
java·笔记·python
小喵要摸鱼5 小时前
Python 神经网络项目常用语法
python
秀儿还能再秀6 小时前
神经网络(系统性学习三):多层感知机(MLP)
神经网络·学习笔记·mlp·多层感知机
一念之坤7 小时前
零基础学Python之数据结构 -- 01篇
数据结构·python
wxl7812277 小时前
如何使用本地大模型做数据分析
python·数据挖掘·数据分析·代码解释器
NoneCoder7 小时前
Python入门(12)--数据处理
开发语言·python