文章目录

  • 1. 前言
  • 2. 全连接BP神经网络的缺点
  • 3. 卷积神经网络基本单元
    • 3.1 卷积(Convolution)
    • 3.2 填充(Padding)和步长(Stride)
    • 3.3 完整的卷积过程
    • 3.4 池化(Pooling)
    • 3.5 激活函数
  • 4. 卷积神经网络的前向传播过程
  • 5. 卷积神经网络的反向传播过程
    • 5.1 卷积层的误差反传
    • 5.2 池化层的误差反传
  • 6. 代码实现
    • 6.1 Numpy实现
      • 6.1.1 卷积层前向传播
      • 6.1.2 卷积层反向传播
      • 6.1.3 池化前向传播
      • 6.1.4 池化反向传播
    • 6.2 Tensorflow框架实现
  • 7. 一些优化方法
  • 8. 后记
    • 参考资料:

1. 前言

​ 我之前写过一篇关于BP神经网络原理的文章,主要内容包括:BP神经网络的反向传播原理,以及简单的代码实现。本文和《BP 神经网络入门:从原理到应用》一文一脉相承,因此,我希望读者务必先看看这篇文章(链接在这里:BP 神经网络入门:从原理到应用,

​ 好多读者都说,之前那篇BP神经网络的文章,公式太多,阅读体验不好,所以本文我将尽量减少公式,加入更多的图解和通俗解释。

​ 本文主要包括以下内容:

  • 卷积神经网络结构

  • 前向传播原理

  • 反向传播原理

  • 代码实现

  • 模型测试

  • 简单的优化设计方法;优化设计方法有很多,本文不做过多介绍,如果有必要,我会后面会写个有关优化设计的课程。

2. 全连接BP神经网络的缺点

​ 像全连接BP神经网络一样,卷积神经网络,也有一定的生物学基础(虽然,有时候确实感觉它的生物学解释有些牵强)。

​ 我在BP 神经网络入门:从原理到应用一文,实现了一个,识别图片是不是猫的程序。在这个程序里,我们把输入的图片“展平”了,“展平”是什么意思呢?我们都知道,计算机存储图片,实际是存储了一个W×H×DW\times H\times DW×H×D 的数组(W,H,D分别表示宽,高和维数,彩色图片包含RGB三维),如下图所示,“展平”就是,把这个数组变成一列,然后,输入神经网络进行训练:

​ 这种方法,在图片尺寸较小的时候,还是可以接受的;但是,当图片尺寸比较大的时候,由于图像的像素点很多,神经网络的 层与层之间 的连接数量,会爆炸式增长,大大降低网络的性能。

​ 除此之外,把图片数据展开成一列,破坏了图片的空间信息,试想,如果把上图中的猫图片变成一列,再把这一列数据,以图片的形式展示给你,你还能看出这表示一只猫吗?并且,有相关研究表明,人类大脑,在理解图片信息的过程中,并不是同时观察整个图片,而是更倾向于,观察部分特征,然后根据特征匹配、组合,得出整图信息。以下图为例:

​ 在这张图片中,你只能看到,猫的鼻子,1只眼睛,2只耳朵和嘴,虽然没有尾巴,爪子等其它信息,但是,你依然可以判断,这里有一只猫。所以说,人在理解图像信息的过程中,更专注于,局部特征,以及这些特征之间的组合。

​ 换句话说,在BP全连接神经网络中,隐含层每一个神经元,都对输入图片 每个像素点 做出反应。这种机制包含了太多冗余连接。为了减少这些冗余,只需要每个隐含神经元,对图片的一小部分区域,做出反应就好了!卷积神经网络,正是基于这种想法而实现的。

3. 卷积神经网络基本单元

3.1 卷积(Convolution)

​ 如果,你学过信号处理,或者有相关专业的数学基础,可能需要注意一下,这里的“卷积”,和信号处理里的卷积,不太一样,虽然,在数学表达式形式上有一点类似。

​ 在卷积神经网络中,“卷积”更像是一个,特征提取算子。什么是特征提取算子呢?简单来说就是,提取图片纹理、边缘等特征信息的滤波器。下面,举个简单的例子,解释一下 边缘 特征提取算子是怎么工作的:

​ 比如有一张猫图片,人类在理解这张图片的时候,可能观察到圆圆的眼睛,可爱的耳朵,于是,判断这是一只猫。但是,机器怎么处理这个问题呢?传统的计算机视觉方法,通常设计一些算子(特征提取滤波器),来找到比如眼睛的边界,耳朵的边界,等信息,然后综合这些特征,得出结论——这是一只猫。

​ 具体是怎么做的呢:

​ 如上图所示,假设,猫的上眼皮部分(框出部分),这部分的数据,展开后如图中数组所示**(这里,为了叙述简便,忽略了rgb三维通道,把图像当成一个二维数组[类比灰度图片]),我们使用一个算子,来检测横向边沿**——这个算子,让第一行的值减去第三行的值,得出结果。具体计算过程如下,比如,原图左上角数据,经过这个算子:

200×1+199×1+189×1+203×0+23×0+34×0+177×(−1)+12×(−1)+22×(−1)=328200\times1+199\times1+189\times1+203\times0+23\times0+34\times0+177\times(-1)+12\times(-1)+22\times(-1)=328200×1+199×1+189×1+203×0+23×0+34×0+177×(−1)+12×(−1)+22×(−1)=328

​ 然后,算子向右滑动一个像素,得到第二个输出:533,依次向右滑动,最终得到第一行输出;

​ 然后,向下滑动一个像素,到达第二行;在第二行,从左向右滑动,得到第二行输出……

​ 动图演示过程如下(图片来自csdn):

​ 从输出结果可以看出,在上眼皮处颜色较深,而眼皮上方和下方,无论是眼皮上方的毛发,还是眼皮下方的眼白,颜色都很浅,因此,这样做之后,如果滤波结果中有大值出现,就意味着,这里出现了横向边沿。不断在原图上滑动这个算子,我们就可以,检测到图像中所有的横向边沿。传统的机器视觉方法,就是设计更多更复杂的算子,检测更多的特征,最后,组合这些特征,得出判断结果。

​ 卷积神经网络,做了类似的事情,所谓卷积,就是把一个算子在原图上不断滑动,得出滤波结果——这个结果,我们叫做“特征图”(Feature Map),这些算子被称为“卷积核”(Convolution Kernel)。不同的是,我们不必人工设计这些算子,而是使用随机初始化,来得到很多卷积核(算子),然后通过反向传播,优化这些卷积核,以期望得到更好的识别结果。

3.2 填充(Padding)和步长(Stride)

​ 你可能注意到了,刚刚,我们“滑动”卷积核之后,输出变小了;具体来说,猫眼睛部分数据,尺寸是6×86\times 86×8,但是,经过一个3×33\times 33×3的卷积核之后,变成了4×64\times 64×6,这是因为卷积核有尺寸,如下图所示,当卷积核(橙色)滑动到边界时,就无法向右继续滑动了:

​ 可想而知,如果经过很多层卷积的话,输出尺寸会变的很小,同时图像边缘信息,会迅速流失,这对模型的性能,有着不可忽视的影响。为了减少卷积操作导致的,边缘信息丢失,我们需要进行填充(Padding),即在原图周围,添加一圈值为“0”的像素点(zero padding),这样的话,输出维度就和输入维度一致了。

​ 还有一个很重要的因素是,步长(Stride),即卷积核每次滑动几个像素。前面,我们默认,卷积核每次滑动一个像素,其实也可以,每次滑动两个像素。其中,每次滑动的像素数,称为“步长”。

​ 你可能会有疑问,如果步长大于 1 ,那不也会造成输出尺寸变小吗?是的!但是这种情况下,不会“故意”丢失边缘信息,即使有信息丢失,也是在整张图片上,比较“温和”地,舍弃了一些无关紧要的信息(反向传播调节卷积核参数,使之“智能”地舍弃无关信息)。但是,不能频繁使用步长为2。因为,如果输出尺寸变得,过小的话,即使卷积核参数优化的再好,也会必可避免地丢失大量信息!所以,你可以看到,在应用卷积网络的时候,通常使用步长为 1 的卷积核。

​ **卷积核大小(f×ff\times ff×f)**也可以变化,比如 1×11\times11×1、5×55\times55×5 等,此时,需要根据卷积核大小,来调节填充尺寸(Padding size)。一般来说,卷积核尺寸取奇数(因为我们希望卷积核有一个中心,便于处理输出)。卷积核尺寸为奇数时,填充尺寸可以根据以下公式确定:

padding_size=f−12padding\_size=\frac{f-1}{2}padding_size=2f−1​

​ 其中,fff 表示卷积核大小。

​ 如果用sss 表示步长,www 表示图片宽度,hhh 表示图片高度,那么输出尺寸可以表示为:

w_out=w+2×padding_size−fs+1w\_out=\frac{w+2\times padding\_size-f}{s}+1w_out=sw+2×padding_size−f​+1

h_out=h+2×padding_size−fs+1h\_out=\frac{h+2\times padding\_size-f}{s}+1h_out=sh+2×padding_size−f​+1

​ 下面提供一些动图帮助大家理解(图片来自CSDN):

类别 动图
no padding, s=1, f=3 no padding, s=2, f=3
with padding, s=1, f=3 with padding, s=2, f=3

3.3 完整的卷积过程

​ 上面的卷积过程,没有考虑,彩色图片有rgb三维通道(Channel),如果考虑rgb通道,那么,每个通道,都需要一个卷积核:

​ 当输入有多个通道时,我们的卷积核也需要,有同样数量的通道。以上图为例,输入有RGB三个通道,我们的就卷积核,也有三个通道,只不过计算的时候,卷积核的每个通道,在对应通道滑动(卷积核最前面的通道在输入图片的红色通道滑动,卷积核中间的通道在输入图片的绿色通道滑动,卷积核最后面的通道在输入图片的蓝色通道滑动),三个通道的计算结果相加得到输出。注意,输出只有一个通道,因为这里我们只用了一个卷积核(只不过这个卷积核有三个通道)。用下面的动图加深理解(图片来自网络,这里我们用了 2 个卷积核,每个卷积核 3 个通道,输出有 2 个通道):

​ 三维展示如下(如果视频无法加载可以点击此链接查看):

src="//player.bilibili.com/player.html?aid=23240884&cid=38697901&page=1" scrolling="no" border="0" allowfullscreen="true">

3.4 池化(Pooling)

​ 池化(Pooling),有的地方也称汇聚,实际是一个下采样(Down-sample)过程。由于输入的图片尺寸可能比较大,这时候,我们需要下采样,减小图片尺寸。池化层,可以减小模型规模,提高运算速度,同时,提高所提取特征的鲁棒性。池化操作也有核大小 fff 和步长 sss 参数,参数意义和卷积相同。

​ 本文主要介绍最大池化(Max Pooling)和平均池化(Average Pooling)。

​ 所谓最大池化,就是对于f×ff\times ff×f大小的池化核,选取原图中数值最大的那个保留下来。比如,池化核 2×22\times22×2 大小,步长为2的池化过程如下(左边是池化前,右边是池化后),对于每个池化区域都取最大值:

​ 最大池化最为常用,并且一般都取2×22\times22×2的池化核代大小且步长为2。

​ 平均池化是取每个区域的均值(平均池化现在很少使用):

3.5 激活函数

  本文主要介绍 2 种激活函数,分别是sigmoidsigmoidsigmoid和relurelurelu函数,函数公式如下:
sigmoid(z)=11+e−zsigmoid(z)=\frac{1}{1+e^{-z}}sigmoid(z)=1+e−z1​
relu(z)={zz>00z≤0relu(z)= \left\{ \begin{array}{rcl} z & z>0\\ 0&z\leq0\end{array} \right.relu(z)={z0​z>0z≤0​
  函数图如下:

sigmoid(z)sigmoid(z)sigmoid(z)

relu(z)relu(z)relu(z)

补充说明

  引入激活函数的目的是,在模型中引入非线性。如果没有激活函数,那么,无论你的神经网络有多少层,最终都是一个线性映射,单纯的线性映射,无法解决线性不可分问题。引入非线性可以让模型解决线性不可分问题。

  一般来说,在神经网络的中间层,更加建议使用relurelurelu函数,两个原因:

  • relurelurelu函数计算简单,可以加快模型速度;
  • 由于反向传播过程中,需要计算偏导数,通过求导可以得到,sigmoidsigmoidsigmoid函数导数的最大值为0.25,如果使用sigmoidsigmoidsigmoid函数的话,每一层的反向传播,都会使梯度最少变为原来的四分之一,当层数比较多的时候可能会造成梯度消失,从而模型无法收敛。

4. 卷积神经网络的前向传播过程

​ 本节主要介绍,典型的卷积神经网络模型结构。

​ 典型的卷积网络结构,如上图所示(注意观察图中颜色对应关系)。首先输入图像,然后卷积。卷积过程采用n个卷积核,每个卷积核都有三个通道(卷积核通道数,和输入图片的通道数目相同),输出结果尺寸为W′×H′×nW^{'}\times H^{'}\times nW′×H′×n,这里的输出有n个通道,所以,下一层卷积,每个卷积核都要有n个通道;重复卷积-池化过程;模型的最后一层或两层,一般为全连接结构,输出最终结果。全连接结构部分和BP神经网络一样。最后,我们需要设计合适的损失函数,反向传播就是通过最小化损失函数,来优化网络参数,使模型达到预期结果。

​ 我们把每次卷积之后,得到输出称为“特征图(Feature map)”。

​ 我们定义以下符号,表示卷积网络结构(这里我尽可能模仿Tensorflow的API):

  • 卷积 conv2d(input, filter, strides, padding)

    • input:卷积输入(H×W×CinH \times W \times C_{in}H×W×Cin​,CinC_{in}Cin​指的是输入的通道数)
    • filter:卷积核W (fheight×fwidth×Cin×Coutf_{height}\times f_{width}\times C_{in}\times C_{out}fheight​×fwidth​×Cin​×Cout​)
    • strides:步长,一般为1×1×1×11\times1\times1\times11×1×1×1
    • padding:指明padding的算法,‘SAME’或者‘VALID’
  • 激活 relu(conv)
    • conv:卷积结果
  • 偏置相加 add(conv, b)
    • conv:卷积的激活输出
    • b:偏置系数
  • 池化 max_pool(value, ksize, strides, padding)
    • value:卷积输出
    • ksize:池化尺寸,一般2×22\times22×2
    • strides:步长
    • padding:指明padding的算法,‘SAME’或者‘VALID’

5. 卷积神经网络的反向传播过程

​ 终于讲到反向传播了。现在大多数深度学习框架,都帮我们做好了误差反传的工作。大部分工程师,仅仅需要构建前向传播过程,就可以了。但是,对于研究人员,有必要了解一下误差反传的原理。

​ 在BP神经网络中,我们已经介绍过,全连接神经网络的反向传播过程。很多教程,在介绍卷积网络反向传播的时候,总是说,类比全连接神经网络,然后一笔带过。其实,卷积神经网络的误差反传过程,要复杂得多。原因有以下几点:

  • 卷积神经网络的卷积核,每次滑动只与部分输入相连,具有局部映射的特点,在误差反传的时候,需要确定卷积核的局部连接方式;

  • 卷积神经网络的池化过程,丢失了大量信息,误差反传,需要恢复这些丢失的信息;

  • ……

      总之,卷积神经网络的误差反传过程,很复杂。之前,在写BP神经网络那篇文章的时候,推导的公式过多,导致很多读者反应,文章晦涩难懂;所以,这次我尽可能用通俗的语言来表述。
    
本小节的符号定义,和BP神经网络一文相同,如果出现了新的符号,会在文中对应位置说明。

5.1 卷积层的误差反传

​ 假设损失函数为LossLossLoss, 我们希望求解,损失函数对参数 W 和偏置 b 的梯度。

​ 首先,来看损失函数对卷积核 W 的梯度:

​ lthl^{th}lth层的卷积输出为:

Z[l]=conv2d(Zpool[l−1],(W,b),strides=1,padding=′SAME′)=∑m∑nWm′,n′[l−1]Zpool_m,n[l−1]+b[l]Z^{[l]}=conv2d(Z^{[l-1]}_{pool}, (W, b) , strides = 1, padding='SAME')\\ \ \ \ \ \ \ = \sum_m\sum_nW^{[l-1]}_{m^{'},n^{'}}Z^{[l-1]}_{pool\_m, n}+b^{[l]}Z[l]=conv2d(Zpool[l−1]​,(W,b),strides=1,padding=′SAME′)      =m∑​n∑​Wm′,n′[l−1]​Zpool_m,n[l−1]​+b[l]

​ 其中,Zpool_m,n[l−1]Z^{[l-1]}_{pool\_m, n}Zpool_m,n[l−1]​表示上一层池化的输出;m′m^{'}m′和n′n^{'}n′表示卷积核尺寸;mmm 和 nnn 表示输入的长和宽。

​ 卷积层反向传播的基础,依旧是链式法则,所以,我们必须搞清楚,权重W的局部连接方式。

​ 考虑前向传播过程:

​ 可以发现,由于卷积核在输入的特征图上滑动,所以,特征图的所有像素都参与了卷积核运算,与此对应,输出特征图的每个像素,都和卷积核 W 直接相连。

​ 损失函数对参数W的梯度,根据链式法则:

∂Loss∂Wm′,n′[l]=∑i=0H∑i=0H∂Loss∂Zi,j[l]∂Zi,j[l]∂Wm′,n′[l]=∑i=0H∑i=0HdZi,j[l]∂Zi,j[l]∂Wm′,n′[l]\frac{\partial Loss}{\partial W^{[l]}_{m^{'}, n^{'}}}=\sum_{i=0}^{H}\sum_{i=0}^{H}\frac{\partial Loss}{\partial Z^{[l]}_{i, j}}\frac{\partial Z^{[l]}_{i, j}}{\partial W^{[l]}_{m^{'}, n^{'}}}\\ \qquad \ =\sum_{i=0}^{H}\sum_{i=0}^{H}dZ^{[l]}_{i,j}\frac{\partial Z^{[l]}_{i, j}}{\partial W^{[l]}_{m^{'}, n^{'}}}∂Wm′,n′[l]​∂Loss​=i=0∑H​i=0∑H​∂Zi,j[l]​∂Loss​∂Wm′,n′[l]​∂Zi,j[l]​​ =i=0∑H​i=0∑H​dZi,j[l]​∂Wm′,n′[l]​∂Zi,j[l]​​

​ 其中,Z表示没有激活的输出,dZ[l]dZ^{[l]}dZ[l]表示lthl^{th}lth层的误差。现在我们来求解∂Zi,j[l]∂Wm,n[l]\frac{\partial Z^{[l]}_{i, j}}{\partial W^{[l]}_{m, n}}∂Wm,n[l]​∂Zi,j[l]​​部分:

∂Zi,j[l]∂Wm′,n′[l]=∂∂Wm′,n′[l](∑m∑nWm′,n′[l−1]Zpool_m,n[l−1]+b[l])=∂∂Wm′,n′[l](W0,0[l]Zpool_i+0,j+0[l−1]+...+Wm′,n′[l]Zpool_i+m′,j+n′[l−1])=Zpool_i+m′,j+n′[l−1]\frac{\partial Z^{[l]}_{i, j}}{\partial W^{[l]}_{m^{'}, n^{'}}}=\frac{\partial}{\partial W^{[l]}_{m^{'}, n^{'}}}(\sum_m\sum_nW^{[l-1]}_{m^{'},n^{'}}Z^{[l-1]}_{pool\_m, n}+b^{[l]})\\ \qquad \ = \frac{\partial}{\partial W^{[l]}_{m^{'}, n^{'}}}(W^{[l]}_{0,0}Z^{[l-1]}_{pool\_i+0, j+0}+ . . . +W^{[l]}_{m^{'},n^{'}}Z^{[l-1]}_{pool\_i+m^{'},j+n^{'}})\\ \qquad \ =Z^{[l-1]}_{pool\_i+m^{'},j+n^{'}}∂Wm′,n′[l]​∂Zi,j[l]​​=∂Wm′,n′[l]​∂​(m∑​n∑​Wm′,n′[l−1]​Zpool_m,n[l−1]​+b[l]) =∂Wm′,n′[l]​∂​(W0,0[l]​Zpool_i+0,j+0[l−1]​+...+Wm′,n′[l]​Zpool_i+m′,j+n′[l−1]​) =Zpool_i+m′,j+n′[l−1]​

​ 综上,

∂Loss∂Wm′,n′[l]=∑i=0H∑i=0HdZi,j[l]∂Zi,j[l]∂Wm′,n′[l]=∑i=0H∑i=0HdZi,j[l]Zpool_i+m′,j+n′[l−1]\frac{\partial Loss}{\partial W^{[l]}_{m^{'}, n^{'}}}=\sum_{i=0}^{H}\sum_{i=0}^{H}dZ^{[l]}_{i,j}\frac{\partial Z^{[l]}_{i, j}}{\partial W^{[l]}_{m^{'}, n^{'}}}\\ \qquad \quad=\sum_{i=0}^{H}\sum_{i=0}^{H}dZ^{[l]}_{i,j}Z^{[l-1]}_{pool\_i+m^{'},j+n^{'}}\\ \qquad \quad∂Wm′,n′[l]​∂Loss​=i=0∑H​i=0∑H​dZi,j[l]​∂Wm′,n′[l]​∂Zi,j[l]​​=i=0∑H​i=0∑H​dZi,j[l]​Zpool_i+m′,j+n′[l−1]​

​ 同理,可得:

∂Loss∂b[l]=∑i=0H∑i=0HdZi,j[l]\frac{\partial Loss}{\partial b^{[l]}}=\sum_{i=0}^{H}\sum_{i=0}^{H}dZ^{[l]}_{i,j}∂b[l]∂Loss​=i=0∑H​i=0∑H​dZi,j[l]​

​ 下面来看看dZ[l]dZ^{[l]}dZ[l]如何求解:

dZi,j[l]=∂Loss∂Zi,j[l]=∂Loss∂Z[l+1]∂Z[l+1]∂Z[l]=dZ[l+1]∂Z[l+1]∂Z[l]dZ^{[l]}_{i, j}=\frac{\partial Loss}{\partial Z^{[l]}_{i, j}}=\frac{\partial Loss}{\partial Z^{[l+1]}}\frac{\partial Z^{[l+1]}}{\partial Z^{[l]}}=dZ^{[l+1]}\frac{\partial Z^{[l+1]}}{\partial Z^{[l]}}dZi,j[l]​=∂Zi,j[l]​∂Loss​=∂Z[l+1]∂Loss​∂Z[l]∂Z[l+1]​=dZ[l+1]∂Z[l]∂Z[l+1]​

​ 也就是说我们需要求解∂Z[l+1]∂Z[l]\frac{\partial Z^{[l+1]}}{\partial Z^{[l]}}∂Z[l]∂Z[l+1]​,所以我们需要知道在lthl^{th}lth层的一个像素值如何影响卷积输出:

​ 上图说明,一个像素点,通过卷积运算,会影响输出的一个区域(输入特征图的蓝色像素点,影响了输出特征图的蓝色虚线框出的部分)。所以反向传播就是要找到,卷积核滑动过程中,一个像素点,如何通过 W 影响输出,然后逐步应用链式法则即可。

5.2 池化层的误差反传

​ 池化层没有参数需要学习,但是,由于池化层的下采样操作损失了大量信息,因此,在反向传播过程中,我们需要恢复这些信息。

​ 对于最大池化而言,我们需要知道在池化之前最大元素的位置:

​ 然后,在反向传播的时候将对应值填入最大位置,这可以用池化后误差乘以M得到,这样一个数就可以恢复为2×22\times 22×2的矩阵,对池化层误差矩阵的每个元素都应用这样的上采样,我们就可以恢复维度信息,从而误差可以顺利反传。

​ 对于平均池化而言,我们保存的是均值信息:

​ 反传过程中依旧是用池化层误差乘以dZ,将一个数恢复为2×22\times 22×2的矩阵即,可恢复维度信息。

6. 代码实现

​ 本小节,给出了Numpy实现卷积网络的核心代码。另外,因为卷积神经网络的计算过程很复杂,因此,使用自己实现的核心代码效率很低。所以,完整模型的训练采用TensorFlow库来实现。

6.1 Numpy实现

6.1.1 卷积层前向传播

​ 首先实现一个0填充的函数:

def zero_pad(X, pad):"""零填充。Args:X: python numpy array of shape (m, n_H, n_W, n_C) representing a batch of m imagespad: integer, amount of padding around each image on vertical and horizontal dimensionsReturns:X_pad -- padded image of shape (m, n_H + 2*pad, n_W + 2*pad, n_C)"""X_pad = np.pad(X, ((0,0),(pad,pad),(pad,pad),(0,0)), 'constant')return X_pad

​ 接下来实现一个单步卷积,即考虑全图一个切片的卷积:

def conv_single_step(a_slice_prev, W, b):""""Args:a_slice_prev: slice of input data of shape (f, f, n_C_prev)W: Weight parameters contained in a window - matrix of shape (f, f, n_C_prev)b: Bias parameters contained in a window - matrix of shape (1, 1, 1)Returns:Z: a float value."""s = a_slice_prev * WZ = np.sum(s)Z = Z + breturn Z

​ 完整的一步卷积,即卷积核在输入特征图滑动:

def conv_forward(A_prev, W, b, hparameters):"""前向传播Args:A_prev: output activations of the previous layer, numpy array of shape (m, n_H_prev, n_W_prev, n_C_prev)W: Weights, numpy array of shape (f, f, n_C_prev, n_C)b: Biases, numpy array of shape (1, 1, 1, n_C)hparameters: a python dictionary containing "stride" and "pad"Returns:Z: conv output, numpy array of shape (m, n_H, n_W, n_C)cache: cache of values needed for the conv_backward() function"""(m, n_H_prev, n_W_prev, n_C_prev) = A_prev.shape(f, f, n_C_prev, n_C) = W.shapestride = hparameters['stride']pad = hparameters['pad']n_H = int((n_H_prev+2*pad-f)/stride) + 1n_W = int((n_W_prev+2*pad-f)/stride) + 1Z = np.zeros((m,n_H,n_W,n_C))A_prev_pad = zero_pad(A_prev, pad)for i in range(m):                              a_prev_pad = A_prev_pad[i,:,:,:]for h in range(n_H):  for w in range(n_W):  for c in range(n_C):vert_start = h * stridevert_end = vert_start+fhoriz_start = w * stridehoriz_end = horiz_start+fa_slice_prev = a_prev_pad[vert_start:vert_end,horiz_start:horiz_end, :]Z[i, h, w, c] = conv_single_step(a_slice_prev, W[:,:,:,c], b[:,:,:,c])                       cache = (A_prev, W, b, hparameters)return Z, cache

6.1.2 卷积层反向传播

def conv_backward(dZ, cache):"""卷积层反向传播Args:dZ: gradient of the cost with respect to the output of the conv layer (Z), numpy array of shape (m, n_H, n_W, n_C)cache: cache of values needed for the conv_backward(), output of conv_forward()Returns:dA_prev: gradient of the cost with respect to the input of the conv layer (A_prev), numpy array of shape (m, n_H_prev, n_W_prev, n_C_prev)dW: gradient of the cost with respect to the weights of the conv layer (W)numpy array of shape (f, f, n_C_prev, n_C)db: gradient of the cost with respect to the biases of the conv layer (b)numpy array of shape (1, 1, 1, n_C)"""(A_prev, W, b, hparameters) = cache(m, n_H_prev, n_W_prev, n_C_prev) = A_prev.shape(f, f, n_C_prev, n_C) = W.shapestride = hparameters['stride']pad = hparameters['pad'](m, n_H, n_W, n_C) = dZ.shapedA_prev = np.zeros(A_prev.shape)                          dW = np.zeros(W.shape)db = np.zeros(b.shape)A_prev_pad = zero_pad(A_prev, pad)dA_prev_pad = zero_pad(dA_prev, pad)for i in range(m):    a_prev_pad = A_prev_pad[i]da_prev_pad = dA_prev_pad[i]for h in range(n_H):                   for w in range(n_W):               for c in range(n_C): vert_start = h * stridevert_end = vert_start + fhoriz_start = w * stridehoriz_end = horiz_start + fa_slice = a_prev_pad[vert_start:vert_end, horiz_start:horiz_end, :]da_prev_pad[vert_start:vert_end, horiz_start:horiz_end, :] += W[:, :, :, c] * dZ[i, h, w, c]dW[:,:,:,c] += a_slice * dZ[i, h, w, c]db[:,:,:,c] += dZ[i, h, w, c]dA_prev[i, :, :, :] = da_prev_pad[pad:-pad, pad:-pad, :]return dA_prev, dW, db

6.1.3 池化前向传播

​ 池化前向传播:

def pool_forward(A_prev, hparameters, mode = "max"):"""Args:A_prev: Input data, numpy array of shape (m, n_H_prev, n_W_prev, n_C_prev)hparameters: python dictionary containing "f" and "stride"mode: "max" or "average"Returns:A: output of the pool layer, a numpy array of shape (m, n_H, n_W, n_C)cache: cache used in the backward pass of the pooling layer, contains the input and hparameters """(m, n_H_prev, n_W_prev, n_C_prev) = A_prev.shapef = hparameters["f"]stride = hparameters["stride"]n_H = int(1 + (n_H_prev - f) / stride)n_W = int(1 + (n_W_prev - f) / stride)n_C = n_C_prevA = np.zeros((m, n_H, n_W, n_C))              for i in range(m):                         for h in range(n_H):                     for w in range(n_W):                 for c in range (n_C):            vert_start = h*stridevert_end = vert_start+fhoriz_start = w*stridehoriz_end = horiz_start+fa_prev_slice = A_prev[i, vert_start:vert_end, horiz_start:horiz_end, c]if mode == "max":A[i, h, w, c] = np.max(a_prev_slice)elif mode == "average":A[i, h, w, c] = np.mean(a_prev_slice)cache = (A_prev, hparameters)return A, cache

6.1.4 池化反向传播

​ 首先,对于最大池化,我们需要记录前向传播过程中最大值在哪个位置:

def create_mask_from_window(x):"""Args:x: Array of shape (f, f)Returns:maskL: Array of the same shape as window, contains a True at the position corresponding to the max entry of x."""mask = x == np.max(x)return mask

​ 对于平均池化,我们需要把误差平均分到每个位置:

def distribute_value(dz, shape):"""Args:dz: input scalarshape: the shape (n_H, n_W) of the output matrix for which we want to distribute the value of dzReturns:a: Array of size (n_H, n_W) for which we distributed the value of dz"""(n_H, n_W) = shapeaverage = dz / (n_H * n_W)a = np.ones((n_H, n_W)) * averagereturn a

​ 接下来,我们实现完整的池化层反向传播过程:

def pool_backward(dA, cache, mode = "max"):"""Args:dA: gradient of cost with respect to the output of the pooling layer, same shape as Acache: cache output from the forward pass of the pooling layer, contains the layer's input and hparameters mode: the pooling mode you would like to use, defined as a string ("max" or "average")Returns:dA_prev: gradient of cost with respect to the input of the pooling layer, same shape as A_prev"""(A_prev, hparameters) = cachestride = hparameters['stride']f = hparameters['f']m, n_H_prev, n_W_prev, n_C_prev = A_prev.shapem, n_H, n_W, n_C = dA.shapedA_prev = np.zeros(A_prev.shape)for i in range(m):                       a_prev = A_prev[i]for h in range(n_H):                   for w in range(n_W):               for c in range(n_C):           vert_start = h * stridevert_end = vert_start + fhoriz_start = w * stridehoriz_end = horiz_start + fif mode == "max":a_prev_slice = a_prev[vert_start:vert_end, horiz_start:horiz_end, c]mask = create_mask_from_window(a_prev_slice)dA_prev[i, vert_start: vert_end, horiz_start: horiz_end, c] += np.multiply(mask, dA[i, h, w, c])elif mode == "average":da = dA[i, h, w, c]shape = (f, f)dA_prev[i, vert_start:vert_end, horiz_start:horiz_end, c] += distribute_value(da, shape)return dA_prev

6.2 Tensorflow框架实现

​ 由于自己实现的卷积网络核心代码,计算速度过慢(只能在CPU运行,且包含太多for循环,效率过低),所以最终决定用TensorFlow来完成一个测试模型。

​ 我们选用TensorFlow实现一个CIFAR-10数据集分类的例子。CIFAR-10是一个用于图片分类的数据集。此数据集由60000张图片组成,包涵10类。其中50000张图片用于训练,10000张图片用于测试。

​ 本文不会具体介绍TensorFlow的详细用法,如果需要,可以参考TensoFlow官网教程,或者gitchat也有人写过一些入门文章。TensorFlow实现深度学习模型一般包涵以下步骤:

  • 定义占位符

  • 构建模型的前向传播过程

  • 定义损失函数

  • 指定优化方法

  • 运行优化方法最小化损失函数

    我使用jupyter notebook编程环境。首先,导入必须的包,导入数据文件:

import os
import tensorflow as tf
import numpy as np
import math
import timeit
import matplotlib.pyplot as plt
gpu_no = '0'  # or '1'os.environ["CUDA_VISIBLE_DEVICES"] = gpu_no
config = tf.ConfigProto()config.gpu_options.allow_growth = True%matplotlib inlinedef load_cifar10(num_training=49000, num_validation=1000, num_test=10000):"""导入CIFAR数据集。"""cifar10 = tf.keras.datasets.cifar10.load_data()(X_train, y_train), (X_test, y_test) = cifar10X_train = np.asarray(X_train, dtype=np.float32)y_train = np.asarray(y_train, dtype=np.int32).flatten()X_test = np.asarray(X_test, dtype=np.float32)y_test = np.asarray(y_test, dtype=np.int32).flatten()mask = range(num_training, num_training + num_validation)X_val = X_train[mask]y_val = y_train[mask]mask = range(num_training)X_train = X_train[mask]y_train = y_train[mask]mask = range(num_test)X_test = X_test[mask]y_test = y_test[mask]mean_pixel = X_train.mean(axis=(0, 1, 2), keepdims=True)std_pixel = X_train.std(axis=(0, 1, 2), keepdims=True)X_train = (X_train - mean_pixel) / std_pixelX_val = (X_val - mean_pixel) / std_pixelX_test = (X_test - mean_pixel) / std_pixelreturn X_train, y_train, X_val, y_val, X_test, y_testX_train, y_train, X_val, y_val, X_test, y_test = load_cifar10()class Dataset(object):def __init__(self, X, y, batch_size, shuffle=False):assert X.shape[0] == y.shape[0], 'Got different numbers of data and labels'self.X, self.y = X, yself.batch_size, self.shuffle = batch_size, shuffledef __iter__(self):N, B = self.X.shape[0], self.batch_sizeidxs = np.arange(N)if self.shuffle:np.random.shuffle(idxs)return iter((self.X[i:i+B], self.y[i:i+B]) for i in range(0, N, B))train_dset = Dataset(X_train, y_train, batch_size=64, shuffle=True)
val_dset = Dataset(X_val, y_val, batch_size=64, shuffle=False)
test_dset = Dataset(X_test, y_test, batch_size=64)

​ 构建计算图(前向传播过程):

def conv2d(x, scope, W_shape, b_shape, stddev = 5e-2, stride = [1, 1, 1, 1], lambda_ = 0.005):"""定义卷积层"""with tf.variable_scope(scope, reuse = tf.AUTO_REUSE) as scope:initializer = tf.truncated_normal_initializer(stddev=stddev, dtype=tf.float32)W = tf.get_variable('W', W_shape, initializer=initializer, dtype=tf.float32)b = tf.get_variable('b', b_shape, initializer=tf.constant_initializer(0.0), dtype=tf.float32)tf.add_to_collection("l2_loss", tf.nn.l2_loss(W)*lambda_)conv = tf.nn.conv2d(x, W, stride, padding='SAME')conv = tf.add(tf.nn.relu(conv), b)return convdef max_pool(x, ksize = [1, 2, 2,1], stride = [1, 2, 2, 1]):"""池化层"""out = tf.nn.max_pool(x, ksize, stride, padding='SAME')return outdef fully_connect(x, scope, indim, outdim, stddev=5e-2):"""全连接层"""with tf.variable_scope(scope, reuse=tf.AUTO_REUSE) as scope:reshape = tf.reshape(x, [tf.shape(x)[0], -1])initializer = tf.truncated_normal_initializer(stddev=stddev, dtype=tf.float32)W = tf.get_variable('W', [indim, outdim], initializer=initializer, dtype=tf.float32)b = tf.get_variable('b', [outdim], initializer=tf.constant_initializer(0.0), dtype=tf.float32)tf.add_to_collection("l2_loss", tf.nn.l2_loss(W)*0.004)fc_out = tf.nn.relu(tf.matmul(reshape, W) + b)return fc_outdef build_model(images):"""构建完整模型 conv1->relu->pool->norm->conv2->pool->norm->fully1->relu->fully2->relu->softmax"""temp_conv = conv2d(images, "conv1", [5, 5, 3, 64], [64], stddev = 5e-2)temp_pool = max_pool(temp_conv, [1, 3, 3, 1])temp_norm = tf.nn.lrn(temp_pool, 4, bias=1.0, alpha=0.001/9, beta=0.75)temp_conv = conv2d(temp_norm, "conv2", [5, 5, 64, 64], [64], stddev = 5e-2)temp_pool = max_pool(temp_conv)temp_norm = tf.nn.lrn(temp_pool, 4, bias=1.0, alpha=0.001/9, beta=0.75)fc = fully_connect(temp_pool, 'fully1', 8*8*64, 256)fc = fully_connect(fc, 'fully2', 256, 10)out = tf.nn.softmax(fc)return outdef loss(y_pred, y):"""损失函数"""labels = tf.reshape(tf.one_hot(tf.reshape(tf.cast(y, dtype=tf.int64),[1, -1]), 10), [-1, 10])softmax_loss = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits_v2(logits=y_pred, labels=labels))loss = tf.reduce_sum(tf.get_collection("l2_loss")) + softmax_loss#loss = softmax_lossreturn lossdef accuracy(y_pred, y):"""准确率计算"""y_pred = tf.argmax(y_pred, axis=1)y = tf.reshape(tf.cast(y,tf.int64),[1, -1])acc = tf.reduce_mean(tf.cast(tf.equal(y_pred,y), tf.float32))return accdef train(train_dst, sess, epochs=100):"""训练图"""train_accs = []val_accs = []#占位符images = tf.placeholder(tf.float32, [None, 32, 32, 3])y_label = tf.placeholder(tf.float32, [None, 1])#模型model = build_model(images)y_pred = model#准确率acc = accuracy(y_pred, y_label)#损失函数losses = loss(y_pred, y_label)#优化器with tf.variable_scope('opt', reuse = tf.AUTO_REUSE):opt = tf.train.AdamOptimizer(learning_rate=0.0001, use_locking=True)train_step = opt.minimize(losses)#参数初始化init = tf.global_variables_initializer()sess.run(init)for epoch in range(epochs):#迭代训练print("****************epoch "+str(epoch))l = 0for (X, y) in train_dst:y_pred_ = sess.run(model, feed_dict={images:X})#print(y_pred.shape,type(y_pred))l = sess.run(losses,  feed_dict={images:X, y_label: y.reshape((-1, 1))} )train_acc = sess.run(acc, feed_dict={images:X, y_label: y.reshape((-1, 1))})train_accs.append(train_acc)sess.run(train_step, feed_dict={images:X, y_label:y.reshape((-1, 1))})print("loss: " +str(l))if epoch%1==0:print("train_acc: "+str(train_acc))val_acc = 0for i, (X_v, y_v) in enumerate(val_dset):y_pred_val = sess.run(model,feed_dict={images: X_v})val_acc += sess.run(acc, feed_dict={images: X_v, y_label: y_v.reshape((-1, 1))})val_acc /= (i+1)val_accs.append(val_acc)print("val_acc: "+ str(val_acc))#测试集test_acc = 0for i, (X_t, y_t) in enumerate(test_dset):y_pred_test = sess.run(model, feed_dict={images: X_t})test_acc += sess.run(acc, feed_dict={images: X_t, y_label:y_t.reshape((-1, 1))})test_acc /= (i+1) print("%%%%%%%%%%%%% Test accuracy")print(test_acc)return train_accs, val_accs, test_acc

​ 运行计算图:

train_acc, val_acc, test = [], [], 0.0
with tf.Session(config=config) as sess:train_acc, val_acc, tes=train(train_dset, sess )

​ 训练过程中,模型在训练集的准确率变化如下:

​ 在测试集的准确率为69.7%。

​ 可以看出,模型表现还是可以的(我们仅仅迭代了100次),但是距离完美还有很大差距。如果想要更好的结果,需要设计更复杂的网络结构,以及更加精细的调参过程。考虑到本文主要目的不是如何优化网络,而是为大家展示卷积网络原理,所以没有实现很复杂的网络。

7. 一些优化方法

​ 本部分内容主要是作者个人理解,仅仅是为了给读者说一下优化方法的概念,不必深究。如果需要深入了解,可以查阅相关论文。

​ 常用的优化方法有正则化(Regularization),批规范化(Batch Normalization),动量法(Momentum),Adam梯度下降法等。这里主要提一下正则化和规范化。

​ 所谓正则化,就是在损失函数里,考虑网络参数的复杂度(事实上,在我们刚刚的TensorFlow模型中已经用到了正则化)。举个简单的例子,如果你想要拟合一条曲线(图片来自网络):

​ 假设你有数据集(X, Y),如上图蓝色圈所示(共N个数据)。你希望对这些数据,拟合一条曲线,如果你把拟合公式定义为:

y^=f(x)=w1x+w2x2+w3x3+w4x4+w5x5+b\hat y=f(x)=w_1x+w_2x^2+w_3x^3+w_4x^4+w_5x^5+by^​=f(x)=w1​x+w2​x2+w3​x3+w4​x4+w5​x5+b

​ 损失函数定义为:

loss=1N∑i=1N(y^i−yi)2loss=\frac{1}{N}\sum_{i=1}^N(\hat y_i-y_i)^2loss=N1​i=1∑N​(y^​i​−yi​)2

​ 经过梯度下降法之后,得到红色的拟合曲线。这时候,曲线经过所有的样本点,loss为0,可以说,完美地达到了你的要求。但是,可达鸭眉头一皱,发现事情并不简单。

​ 即使没有机器学习基础,只学过初等数学的人,也知道“过拟合”(Over fit)的概念。没错!这里显然过拟合了!这些参数w1−w5w_1-w_5w1​−w5​过于“肆意妄为”,以至于得到一个很不靠谱的模型。

​ 正则化要做的,就是“约束”这些参数,防止模型过拟合。具体来说,加入正则化之后,损失函数变为:

loss=1N∑i=1N(y^i−yi)2+λ∑j=15wj2loss = \frac{1}{N}\sum_{i=1}^N(\hat y_i-y_i)^2+\lambda \sum_{j=1}^5w_j^2loss=N1​i=1∑N​(y^​i​−yi​)2+λj=1∑5​wj2​

​ 上述公式的最后一项被称为L2正则化项(可以看作是对模型参数复杂度的度量),λ\lambdaλ 需要人工选取。用新的损失函数,我们可以得到绿色的拟合曲线,这条曲线显然看起来更合理。

​ 那么,你可能会问,为什么正则化项的加入,会产生这么神奇的效果呢?先来看看,我们那条“过拟合”的红色曲线,这条曲线弯弯曲曲的,曲线各点的斜率,剧烈变化(斜率是导数):

f′(x)=w1+2w2x+3w3x2+4w4x3+5w5x4f^{'}(x)=w_1+2w_2x+3w_3x^2+4w_4x^3+5w_5x^4f′(x)=w1​+2w2​x+3w3​x2+4w4​x3+5w5​x4

​ 在x相同的时候,w的值越大,斜率变化越复杂。因此,当加入正则化项之后,参数w趋向变小,从而拟合曲线的斜率变化趋于平缓,换言之,拟合曲线变得更加“平滑”,因此在很大程度上解决了过拟合问题。总结一下,正则化主要是,让参数W趋向于小值,让模型趋向于简单

​ 在深度神经网络中,正则化依旧是有用的。在深度神经网络中,除了上面提到的优点,正则化还趋向于让W参数趋于“分散”,即,不会让 W 矩阵的某个元素的绝对值过大或过小,而是趋向于,让W矩阵的每个元素,都对模型有差不多的贡献,从而可以在一定程度上,防止模型过拟合。

​ 下面,再来说一说批规范化(Batch Normalization)。批规范化,即每次输入一批数据后,先对数据进行处理,使数据均值为0,方差为1。在卷积神经网络中,批规范化一般用在激活函数之前(也可以在之后)。现在,一些论文研究了各种各样的规范化,除了批规范化(Batch Normalization)还有层规范化(Layer Normalization),组规范化(Group Normalization)等。虽然目前对规范化的作用机理有各种解释,但是从个人角度来看,大多数解释都有些牵强。个人角度看,规范化的主要作用是,强行稳定各层特征图的分布,使模型的训练更加容易,并且,批规范化解决了模型对参数初始化过于敏感的问题。如下图所示,蓝色表示“等高线”(损失函数值相等的线),这些等高线的中心表示最低点(损失函数最小的地方)。规范化之前,你的特征空间可能“又细又长”,梯度下降法的路径,可能如下图左所示,震荡幅度很大;规范化之后,由于各个方向的特征比较均衡,因此梯度下降更加稳定。

​ 关于深度神经网络的优化方法还有很多,比如Adam变步长算法,学习率衰减算法以及各种巧妙的初始化方法等。目前来看,深度神经网络的可解释性还比较差,并且,神经网络的损失函数是非凸的,非凸优化的求解难度很大,因为可能存在无数个局部最优点,而通常求解全局最优的复杂度是指数级。目前有研究表明,深度神经网络模型很复杂,因此存在足够多的局部最优点,而这些局部最优点接近全局最优,并且,由于深度神经网络的损失函数,维度很高,因此存在大量鞍点,所以总有一个梯度方向,能朝着一个合理的局部最优点前进。因此,只要模型结构设计地合理,训练方法得当,我们总能得到一个可接受的结果。

​ 总之,目前来看,深度神经网络,黑箱的成分很大,不仅没有足够的数学理论支撑,而且,即使得到了一个优秀的模型,也很难说清楚模型为什么表现的好。所以,如果你读论文比较多的话,就会发现,好多论文更像是得到好的实验结果之后,再去尝试为模型添加理论解释,而不是以理论突破来指导模型创新。

​ 添加各种优化算法之后的误差反传更加繁琐,感兴趣的读者可以自行推导(强烈不建议,我自己推过,还是很麻烦的),本文不再赘述。

8. 后记

​ 卷积神经网络,在图像领域有着广泛的应用,但是,目前可解释性较差,就像一个“黑箱”一样,虽然不断进化,人们却很难说清楚模型为什么表现地好。

​ 误差反传是深度神经网络的核心,也是最难推理的部分。虽然误差反传的基础是链式法则,但是当模型变得复杂的时候,试图推导出每一层的误差反传解析式极其困难。现在,大多数深度学习框架都用到了一种叫做“自动微分”(Automatic Differentiation)的技术,来进行误差反传的工作。对于大多数工程师而言,不需要关心误差反传的过程。自动微分的知识可以参见书籍Evaluating Derivatives: Principles and Techniques of Algorithmic Differentiation, Second Edition。

​ 另外,关于深度学习框架,我也有一点想说。目前流行的深度学习框架,主推TensorFlow(Google)和PyTorch(Facebook)。TensorFlow大而全,但是对新手不是很友好,应该说,由于其使用静态计算图(static graph),先构建计算图,再计算,和普通的编程思路有所不同,大部分新手在学习TensorFlow的时候,会懵逼好一阵子。PyTorch对新手友好,使用动态计算图(dynamic graph),使用PyTorch编写代码和普通的python代码几乎没有区别,并且和numpy无缝结合(主要指张量和numpy.ndarray可以相互转换),特别方便。我的建议是,如果你之前没有用过任何深度学习框架,推荐你从PyTorch入手,当然,如果你是老手的话,就根据自己的需要选择吧。

​ 好了,本次内容分享就到这里了,谢谢大家。


参考资料:

[1] cs231n http://cs231n.stanford.edu/
[2] Coursera Deep learning https://www.coursera.org/specializations/deep-learning
[3] Deep Learning http://www.deeplearningbook.org/

一文搞定卷积神经网络——从原理到应用相关推荐

  1. bp神经网络图像特征提取,一文搞定bp神经网络

    本人新手,在做BP神经网络的时候遇到了一个问题 5 不知你是不是用matlab的神经网络工具箱,因为一般神经网络都是成批处理的,每一次调整都会综合所有样本的误差进行调整,而不是一类一类图片的去调整,所 ...

  2. 一文搞定bp神经网络,bp神经网络的实现

    1.自学bp神经网络要有什么基础?? 简介:BP(Back Propagation)网络是1986年由Rumelhart和McCelland为首的科学家小组提出,是一种按误差逆传播算法训练的多层前馈网 ...

  3. BP神经网络的基本思想,一文搞定bp神经网络

    自学bp神经网络要有什么基础?? . 简介:BP(BackPropagation)网络是1986年由Rumelhart和McCelland为首的科学家小组提出,是一种按误差逆传播算法训练的多层前馈网络 ...

  4. koa 接口返回数据_一文搞定 Koa 中间件实现原理

    Koa是一个新的 web 框架,由 Express 幕后的原班人马打造, 致力于成为 web 应用和 API 开发领域中的一个更小.更富有表现力.更健壮的基石. 通过利用 async 函数, Koa ...

  5. bp神经网络的主要功能,一文搞定bp神经网络

    张力传感器的工作原理 张力传感器(tensionsensor):张力传感器是张力控制过程中,用于测量卷材张力值大小的仪器. 张力传感器适用于各种光纤.纱线.化纤等的张力测量:广泛应用于电子.化工.纺织 ...

  6. 【嵌入式开发-AD19】六文搞定Altium Designer-第一章:AD介绍及原理图库的创建

    [嵌入式开发-AD19]六文搞定Altium Designer-第一章:AD介绍及原理图库的创建 在文章的开头我想首先简单介绍一下国产全免费EDA软件,嘉立创EDA.嘉立创EDA拥有网页版和安装版两种 ...

  7. 【语义分割】综述——一文搞定语义分割

    本文记录了博主阅读的关于语义分割(Semantic Segmentation)的综述类文章的笔记,更新于2019.02.19. [语义分割]综述--一文搞定语义分割 参考文献网址 An overvie ...

  8. 一文看懂卷积神经网络-CNN(基本原理+独特价值+实际应用)

    http://blog.itpub.net/29829936/viewspace-2648775/ 2019-06-25 21:31:18 卷积神经网络 – CNN 最擅长的就是图片的处理.它受到人类 ...

  9. 技术向:一文读懂卷积神经网络

     技术向:一文读懂卷积神经网络 技术网络 36大数据(张雨石) · 2015-03-06 05:47 自今年七月份以来,一直在实验室负责卷积神经网络(Convolutional Neural Ne ...

  10. 一文看懂卷积神经网络CNN的核心

    在之前,我总结了关于计算机神经网络与梯度下降的核心,详见下文链接 : 一文看懂计算机神经网络与梯度下降 本文主要会对图像相关的机器学习中最为重要的网络,卷积神经网络作个人的理解分析. 1. 为什么要使 ...

最新文章

  1. Shell test 命令
  2. 步步为营 .NET 设计模式学习笔记 十九、Chain of Responsibility(职责链模式)
  3. 你知道element中el-table的列名中不能含有 . 吗?
  4. 类型与通用语言运行时
  5. 退出出库复核是什么意思_细思极恐!为什么是黄晓明退出而不是李菲儿?因为女方是芒果艺人...
  6. 【OpenCV】OpenCV实战从入门到精通之 -- 离散傅里叶变换相关函数详解
  7. C#中的HashTable 和Dictionary对象
  8. Atitit GRASP(General Responsibility Assignment Software Patterns),中文名称为“通用职责分配软件模式”
  9. 三星线刷工具Odin3_V3.12.7
  10. 利用第三方软件识别图片文字并转换为文本
  11. 新浪云sae部署php,如何在新浪云 SAE 上安装部署 Laravel 5.1 应用并测试数据库连接...
  12. 第一次上手小项目(宜宾)中的困难
  13. 人一生必看的100部电影(全球最佳电影排名榜TOP250)
  14. android 读取 build.prop,读取 android /system/build.prop 的最简单方法
  15. 小米实习---推荐系统--二面
  16. AcWing133. 蚯蚓
  17. c语入门,心得和自我反省
  18. oracle安装缺少依赖包,ORACLE 安装提示缺少依赖包
  19. html导航栏圆角,怎么实现css圆角?
  20. LoRa Gateway 笔记汇总

热门文章

  1. 51单片机AD模块PCF8591 1路AD采样+数码管显示+Proteus仿真
  2. element-tree 实现部门-人员选择(支持ID相同)
  3. 【WhatsApp营销】如何设置欢迎消息?WhatsAppBusiness问候语示例
  4. 如何才能让自己成为一个自律的人?
  5. 我的世界贝爷生存用什么Java_我的世界贝爷生存MOD教程 [MITE] MC实在是太简单了教程详解 | 我的世界 | MC世界侠...
  6. SpringAOP简单案例
  7. 开篇:内容提要 (《蓝调口琴指南》名作拙译)
  8. 现代控制工程(三)状态方程的解
  9. 小米手机开发者模式怎么打开?简单,只要三步
  10. ubuntu16.04 联想拯救者y7000笔记本电脑安装1060显卡驱动,及ubuntu16.04更新内核