CNN简介与实现

news2024/12/26 11:01:36

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实现的卷积层代码如下:

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

在这里插入图片描述

书上的实现代码如下:

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的具体代码实现,具体图如下:
在这里插入图片描述

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

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的理论与实现》

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1804192.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Servlet-01

文章目录 Servlet创建Servlet探究Servlet的生命周期 HttpServletWebServlet注解详解 重定向与请求转发ServletContextServletContext中的接口 HttpServletRequestHttpServletResponse状态码解释Cookie Servlet Q:它能做什么呢? A:我们可以通…

使用汇编和proteus实现仿真数码管显示电路

proteus介绍: proteus是一个十分便捷的用于电路仿真的软件,可以用于实现电路的设计、仿真、调试等。并且可以在对应的代码编辑区域,使用代码实现电路功能的仿真。 汇编语言介绍: 百度百科介绍如下: 汇编语言是培养…

1-5 C语言操作符

C语言提供了非常丰富的操作符,使得C语言使用起来非常的方便 算数操作符: 加 减 乘 除 取模 【 - * / %】 注:除号的两端都是整数的时候执行的是整数的除法,如果…

Unity 编辑器扩展,获取目录下所有的预制件

先看演示效果 实现方案 1创建几个用于测试的cube 2,创建一个Editor脚本 3,编写脚本内容 附上源码 using UnityEditor; using UnityEngine;public class GetPrefeb : EditorWindow {private string folderPath "Assets/Resources"; // 指定预…

【Python】数据处理:文本文件操作

在Python中,处理文本文件是非常常见的任务。可以使用内置的open函数来打开、读取和写入文本文件。 打开文件 使用open函数打开文件。该函数有两个主要参数: open(file, moder, buffering-1, encodingNone, errorsNone, newlineNone, closefdTrue, ope…

ssm602社区医疗保健监控系统+vue【以测试】

前言:👩‍💻 计算机行业的同仁们,大家好!作为专注于Java领域多年的开发者,我非常理解实践案例的重要性。以下是一些我认为有助于提升你们技能的资源: 👩‍💻 SpringBoot…

【设计模式】行为型设计模式之 策略模式学习实践

介绍 策略模式(Strategy),就是⼀个问题有多种解决⽅案,选择其中的⼀种使⽤,这种情况下我们 使⽤策略模式来实现灵活地选择,也能够⽅便地增加新的解决⽅案。⽐如做数学题,⼀个问题的 解法可能有…

Linux shell编程基础

Shell 是一个用 C 语言编写的程序,它是用户使用 Linux 的桥梁。Shell 既是一种命令语言,又是一种程序设计语言。Shell 是指一种应用程序,这个应用程序提供了一个界面,用户通过这个界面访问 Linux 内核的服务。 Shell 脚本&#x…

GQA,MLA之外的另一种KV Cache压缩方式:动态内存压缩(DMC)

0x0. 前言 在openreview上看到最近NV的一个KV Cache压缩工作:https://openreview.net/pdf?idtDRYrAkOB7 ,感觉思路还是有一些意思的,所以这里就分享一下。 简单来说就是paper提出通过一种特殊的方式continue train一下原始的大模型&#x…

打破 AIGC 算力困境,io.net 如何实现“GPU 互联网”?

AIGC 在全球快速发展的当下,诸多项目深陷 GPU 运力不足,速度放缓、任务宕机、项目崩溃等困境,作为瞄准 AI 理念和 DePIN 赛道的 Solana 生态项目新星 io.net 来说,如何集成项目控制与云计算服务成为抢占市场的重要发力方向。第 11…

将web项目打包成electron桌面端教程(一)vue3+vite+js

说明:后续项目需要web端和桌面端,为了提高开发效率,准备直接将web端的代码打包成桌面端,在此提前记录一下demo打包的过程,需要注意的是vue2或者vue3的打包方式各不同,如果你的项目不是vue3vitejs&#xff0…

Nagios的安装和使用

*实验* *nagios安装和使用* Nagios 是一个监视系统运行状态和网络信息的监视系统。Nagios 能监视所指定的本地或远程主机以及服务,同时提供异常通知功能等. Nagios 可运行在 Linux/Unix 平台之上,同时提供一个可选的基于浏览器的 WEB 界面以方便系统管…

go语言后端开发学习(二)——基于七牛云实现的资源上传模块

前言 在之前的文章中我介绍过我们基于gin框架怎么实现本地上传图片和文本这类的文件资源(具体文章可以参考gin框架学习笔记(二) ——相关数据与文件的响应),但是在我们实际上的项目开发中一般却是不会使用本地上传资源的方式来上传的,因为文件的上传与读…

项目验收总体计划书(实际项目验收原件参考Word)

测试目标:确保项目的需求分析说明书中的所有功能需求都已实现,且能正常运行;确保项目的业务流程符合用户和产品设计要求;确保项目的界面美观、风格一致、易学习、易操作、易理解。 软件全套文档过去进主页。 一、 前言 &#xff0…

图鸟UI-Icon演示:探索多功能前端模板的魅力

在当今数字化的时代,用户界面(UI)设计在提升用户体验方面扮演着至关重要的角色。随着技术的不断进步,开发者们对于高效、统一且美观的UI组件需求日益增加。图鸟UI,作为一款功能强大且灵活的UI框架,正满足了…

一款免费文件夹同步工具,旨在帮助用户在不同磁盘或文件夹间进行文件和目录的复制、移动和同步工作

一、简介 1、一款免费文件夹同步工具,旨在帮助用户在不同磁盘或文件夹间进行文件和目录的复制、移动和同步工作。这款工具因其简单易用、高度可定制化的特点,受到了广大用户的青睐。SyncToy支持多种同步模式,包括镜像同步、单向同步以及增量同…

第四篇红队笔记-百靶精讲之Prime-wfuzz-wpscan-openssl enc

靶机Prime渗透 主机发现 nmap扫描与分析 目录爆破与模糊测试 dirb 目录扫描 dev secret.txt wfuzz发现 file参数 根据secret.txt-location.txt 和 file参数结合 secrettier360 根据filelocation.txt得到的on some other php page(改用之前扫到image.p…

GDAL 保存TIFF时的Options的可选项

使用GDAL保存文件时,高级操作需要对参数Options进行设置,但代码注释中没有这个参数的可选项,在GDAL的官网上有这部分内容,在此记录,以防遗忘,也为方便同道中人查询。 官网关于gdal Driver options参数设置的…

Oxlint 会取代 Eslint 吗?

最近,一个基于 Rust 的代码检查工具 Oxlint 在国外前端社区引起了热议,许多专家对其给予了高度评价。那么,相比于它的大哥 Eslint,Oxlint 有哪些优势?它会在未来取代 Eslint 吗?本文将讨论这个话题。 Oxc 和…

STM32高级控制定时器(STM32F103):TIM1和TIM8介绍

目录 概述 1 认识TIM1和TIM8 2 TIM1和TIM8的特性 3 TIM1和TIM6时基和分频 3.1 时基单元 3.2 预分频 3.3 时基和分频相关寄存器 3.3.1TIMx_CR1 3.3.2 TIMx_PSC 概述 本文主要介绍STM32高级定时器TIM1和TIM8的功能,还介绍了与之相关的寄存器的配置参数。包括…