Conmajia

Stop stealing sheep!

导航

< 20253 >
23 24 25 26 27 28 1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31 1 2 3 4 5

统计

卷积神经网络和图像分类识别

Andrew Kirillov 著
Conmajia 译
2019 年 1 月 15 日

原文发表于 CodeProject2018 年 10 月 28 日. 中文版有小幅修改已获作者本人授权.

本文介绍了如何使用 ANNT 神经网络库生成卷积神经网络进行图像分类识别.

全文约 11,000 字建议阅读时间 30 分钟. 本文数学内容较多如果感到不适可以放弃.

这个库最终命名为 ANNTArtificial Neural Networks Technology是 AForge.NET 科学计算库 AForge.Neuro 的组成部分AForge.NET 是 Andrew Kirillov 的杰作之一主要用于计算机视觉人工智能机器学习图像处理机器人等领域

源码 491 KB

简介

本文继续上一篇前馈全连接神经网络讨论使用 ANNT 生成卷积神经网络并应用到图像分类处理任务中. 在前馈我介绍了随机梯度下降SGD误差反向传播EBP等算法还引入了一个 MNIST 手写文字识别的简单例子. 例子虽然简单但还是达到了 96.5% 的准确率. 这篇文章里我打算介绍一个不同的人工神经网络架构卷积神经网络convolutional neural networksCNN. 这是专为计算机视觉领域设计的架构适宜处理诸如图像分类图像识别之类的任务. 文中附带的例子里我把手写文字分类识别准确率提高到了 99%.

卷积神经网络Yann LeCun 在 1998 年提出. 然而那时候公众和业界对人工智能相关领域的关注度很低他的研究在当时无人问津. 直到 14 年后ImageNet 比赛中获胜团队使用了这一架构拔得头筹这才引起了广泛的关注. 随后 CNN 一飞冲天迅速流行起来并应用到了大量计算机视觉领域研究中. 如今最先进的卷积神经网络算法在进行图像识别时甚至可以超过人类肉眼识别的准确率.

理论背景

前馈全连接人工神经网络的思路来源于对生物细胞的生理连接规律的研究. 类似的卷积网络则是从动物大脑的学习方式获得灵感. 1950 年代至 1960 年代HubelWiesel 的研究揭示了猫与猴子的大脑皮层中负责视觉的部分包含了能响应极小视野的神经元. 如果眼睛不动视觉刺激影响单个神经元放电的视觉空间区域称为感受野receptive field. 相邻的细胞有相似和重叠的接收区. 感受野的大小和位置在整个大脑皮层上有系统的变化从而形成完整的视觉空间图.

虽然汉语里字有 field 的意思但是 receptive field 翻译成感受还真是***朗朗上口呢

在 Hubel 等的论文中他们描述了大脑中两种基本类型的视觉神经细胞简单细胞和复杂细胞每种的行为方式都不同. 例如当识别到某个固定区域里呈某一角度的线条时简单细胞就会激活. 复杂细胞的感受野更大其输出对其中的特定位置不敏感. 这些细胞即便在视网膜的位置发生了变化也会继续对某种刺激作出反应.

1980 年日本的福岛邦彦提出了种层次化的神经网络模型命名为新认知机neocongnitron. 这个模型受简单和复杂细胞的概念的启发新认知者能够通过学习物体的形状来识别模式.

福岛邦彦老爷子奔 90 去的人了对于技术发展还是很关注的. 感兴趣的读者可以通过 [〒电子邮件] 向他请教.

再后来1998 年Yann LeCun 等人引入了卷积神经网络. 第一版的 CNN 叫做 Lenet-5能够分类手写数字.

卷积网络的架构

在开始构建卷积神经网络的细节之前先来看神经网络的组成基础. 正如前一篇文章提到的人工神经网络的许多概念可以作为单独的实体来实现用于执行推理和训练阶段的计算. 由于核心结构已经在前面的文章中列出这里我将直接在顶层添加模块然后把它们粘在一起.

卷积层

卷积层是卷积神经网络的核心部分. 它假定输入是具有一定宽度高度和深度的三维形状. 对于第一个卷积层通常是一个图像最常见的深度是 1灰度图像或 3带 RGB 通道的彩色图像. 前一层生成一组特征映射这里的深度是输入特征映射的数量输入到后一层. 这里假设需要处理深度为 1 的输入然后转换为二维结构.

所以卷积层所做的本质上是一个具有图像卷积一种非常常见的图像处理操作. 例如可以用来模糊化或者锐化图像. 但讨论卷积网络时并不关心这些. 根据使用的核图像卷积可以用来寻找图像中的某些特征如垂直水平边缘角或圆等更复杂的特征. 想想我前面介绍的视觉皮层中简单细胞的概念

数字图像处理里两个矩阵相乘如果其中一个保持不变那么相当于用它代表的操作对另一个进行某种运算. 所以有时也称为算子operator.

现在来计算一下卷积. 假设有 n×m高度×宽度矩阵 KI图像那么卷积可以写成这些矩阵的点积

(1)KI=i=1nj=1mKni+1,mj+1Ii,j

举个例子对于 3×3 的矩阵可以这么计算它们的卷积

[abcdefghi][123456789]=i1+h2+g3+f4+e5+d6+c7+b8+a9

(1) 的卷积定义是从信号处理领域借鉴过来的核经过了垂直和水平翻转. 更直接的计算方法是 KI 不进行翻转直接进行正常点积. 这种操作称为互相关定义如下

KI=i=1nj=1mKi,jIi,j

在信号处理里卷积和互相关具有不同的性质并且用于不同的目的. 但是在图像处理和神经网络里这些差异变得很细微通常使用互相关来计算. 对于神经网络来说这点差异并不重要. 稍后可以看到这些“卷积”核实际上是神经网络需要学习的权重. 所以由网络决定哪个核需要学习翻转还是不翻转.

本文提到的卷积是两个矩阵的点积互相关.

好了现在知道了如何计算两个相同大小的矩阵的卷积. 但是实际图像处理中这种福利局很少有一般通常是一个 3×35×57×7 等大小的正方形矩阵作为核而图像可以是任意大小的. 那么怎么计算图像卷积呢为了计算图像卷积在整个图像上移动核并在每个可能位置计算加权和. 图像处理中这个概念被称为滑动窗口从图像的左上角开始计算这一小区域大小和核相同的卷积. 然后将核右移一个像素计算出另一个卷积. 不断重复完成第一每个位置的计算然后从第二行开始继续重复前面的计算. 这样当整个图像处理后就能得到一个特征图其中包含了原图每个位置的卷积值.

图 1 说明了图像卷积的计算过程. 对于 8×8 的输入图像input image和 3×3 的核kernel计算得到 6×6 的特征图feature map.

图 1 图像卷积演示

注意卷积只在核完全匹配图像的位置计算图形边缘无法计算卷积. 于是计算得到的特征图总是小于原图.

图 1 的 3×3 卷积核是设计来查找对象的左边缘的从滑动窗口的中心看右侧有一条垂直直线. 特征图中的高正值表示存在要查找的特征零表示没有特征. 对于这个例子负值表示存在“反转”特征也就是对象的右边缘.

当计算卷积时输出特征映射的大小比原图小. 使用的核越大得到的特征图就越小. 对于 n×m 大小的核输入图像的大小将丢失 (n1)×(m1). 因此上面的例子如果用 5×5 的核那特征图将只有 4×4. 多数情况下需要特征图和原图等大这时就要填充特征图一般用 0 填充. 假设原图大小为 8×8而核为 5×5那么需要先把原图填充到 12×12添加 4 个额外的行和列每侧各 2 行/列.

现在读者应该已经可以计算卷积了. 接下来要研究这些内容怎样运用到前面定义的卷积层中. 为了保持简单继续使用图 1 的例子. 在这种情况下输入层有 64 个节点卷积层有 36 个神经元. 和全连接层不同的是卷积层的神经元只与前一层的一小部分神经元相连. 卷积层中的每个神经元的连接数与它所实现的卷积核中的权重数相同在上面的例子中是 9 个连接核大小 3×3. 因为假定卷积层的输入具有二维形状一般是三维的我这里简化一下便于研究所以这些连接是对先前神经元的矩形组进行的该组神经元的形状与使用中的内核相同. 以前连接的神经元组对于卷积层的每个神经元是不同的但是它确实与相邻的神经元重叠. 使用滑动窗口法计算图像卷积时这些连接的方式与选择原图像素的方式相同.

忽略全连接层和卷积层的神经元与前一层的连接数不同并且这些连接具有一定的结构这样的事实后这两个层可以看作基本相同的计算输入的加权和以产生输出. 不过还有一个区别就是卷积层的神经元共享权重. 因此如果一个层做一个 3×3 的卷积它只有一组权重即 9. 每个神经元都共享这个权重用于计算加权和. 而且尽管没有提到卷积层也为加权和增加了偏差值这也是共享的. 表 1 总结了全连接层和卷积层之间的区别

表 1 全连接层和卷积层对比
全连接层卷积层
不假设输入结构假设输入为 2D 形状通常是 3D
每个神经元都连接到前一层所有神经元
每神经元 64 个连接
每个神经元连接到前一层的矩形组连接数等于卷积核的权重数
每神经元 9 个连接
每个神经元有自身的权重和偏差值
共 2304 权重36 偏差值
共享权重和偏差值
共 9 权重1 偏差值

前面的思考都基于卷积层的输入和输出都是二维这个假设. 但是实际上通常输入和输出都具有三维形状. 首先从输出开始每个卷积层计算不止一个卷积. 设计人工神经网络时可以对它所能做的卷积数量进行配置每个卷积使用自己的一组权重和偏差值从而生成不同的特征图. 前面提到过不同的核可以用来寻找不同的特征直线曲线角等. 因此通常会求得一些特征图以突出不同特征. 这些图的计算方法很简单只要在卷积层中添加额外的神经元群这些神经元以单核的方式连接到输入端就可以完成卷积的计算. 尽管这些神经元具有相同的连接模式但它们共享不同的权重和偏差值. 还是用上面的例子假设将卷积层配置为执行 5 个卷积每个执行 3×3这种情况下输出数量神经元数量是 36×5=180. 5 组神经元组织成二维形状并重复相同的连接模式每组都有自己的权重/偏差集于是可得 45 个权重和 5 个偏差值.

来讨论一下输入的三维性质. 对于第一层卷积层多半都是些图像要么是灰度图2D要么是 RGB 彩图3D. 对于后续的卷积层输入的深度等于前一层计算的特征图的数量卷积的数量. 输入深度越大与前一层连接的数量越多卷积层中的神经元数量就越少. 此时使用的实际上是 3D 的卷积核大小为 n×m×dd 是输入深度. 可以认为每个神经元都从各自的输入特征图增加了额外的连接. 2D 输入的情况下每个神经元连接到输入特征图的 n×m 矩形区域. 3D 输入的情况下每个神经元连接的是这些区域同样的位置只是它们具有来自不同输入特征图的数字 d.

现在已经将卷积层推广到了三维上也提到了偏差值针对卷积核每个 (x,y)(1) 可以表示为

(2)K(f)Iy,x=l=1di=1nj=1m[Kl,i,j(f)Il,y+i1,x+j1+b(f)],f=1,2,,z

总结一下卷积层的参数. 在生成全连接层时只用到输入神经元数量和输出神经元数量两个参数. 生成卷积层时不需要指定输出的数量只用指定输入的形状h×w×d以及核的形状 n×m 和数量 z. 因此有 6 个数字

  • w输入特征图的宽度
  • h输入特征图的高度
  • d输入深度特征图的数量
  • m卷积核宽度
  • n卷积核高度
  • z卷积核数量输出特征图的数量

卷积核的实际大小取决于指定的输入因此可以得到 zn×m×d 大小的卷积核假设没有填充输入这时输出的大小应为 (hn+1)×(wm+1)×z.

上面是计算输出的概念性内容接下来训练卷积层时还会再次提到.

ReLU 激活函数

ReLU 激活函数也就是 rectifier 激活函数对卷积神经网络来说它不是什么新东西. 随着更深层次的神经网络的兴起它得到了广泛的推广.

深度神经网络遇到的问题之一就是消失梯度问题. 当使用基于梯度的学习算法和反向传播算法训练人工神经网络时每个神经网络的权重都与当前权重相关的误差函数偏导数成比例变化. 问题是在某些情况下梯度值可能小到权重值不会改变. 这一问题的原因之一是使用传统的激活函数如 sigmoid 和 tanh. 这些函数的梯度在 (0,1) 范围内大部分的值接近于 0. 由于误差的偏导数是用链式法则计算出来的对于一个 n 层网络这些小数字会乘上 n梯度将呈指数递减. 结果就是深度神经网络在训练“前面的”层时非常缓慢.

ReLU 函数的定义为 f(x)=x+=max(0,x). 它最大的优点是对于 x>0 的值它的导数总是 1所以它允许更好的梯度传播从而加快深度人工神经网络的训练速度. 和 sigmoid 和 tanh 相比它的计算效率更高速度更快.

(a) ReLU 函数
(b) sigmoid 函数
图 2 ReLU 函数和 sigmoid 函数

虽然 ReLU 函数存在一些潜在的问题但到目前为止它依然是深度神经网络中最成功最广泛的激活函数之一.

池化层

实践中经常会为卷积层生成一个池化层pooling layer. 池化的目的是减少输入的空间尺寸减少神经网络中的参数和计算量. 这也有助于控制过拟合over-fitting.

最常见的池化技术是平均池化最大池化. 以最大池化为例使用 2×2 大小过滤器跨距为 2 的 MAX 池化. 对于 n×m 的输入通过将输入中的每个 2×2 区域替换为单个值该区域中 4 个值的最大值得到 n2×m2 的结果. 通过设置与池化区域大小相等的跨距可以保证这些区域相邻而不重叠. 图 3 演示了用于 6×6 输入图的过程.

图 3 池化

池化层的过滤器和跨距值. 例如一些应用程序使用具有 2 跨距的 3×3 大小过滤器这样存在部分重叠的池化. 一般来说跨距不会大于过滤器大小图像里很多内容会完全丢失.

池化层使用二维特征图但并且不影响输入深度. 如果输入包含由前一个卷积层生成的 10 个特征图那么池化将分别应用于每个图. 所以通过池化能生成相同数量的特征图但尺寸更小.

建立卷积神经网络

多数情况下卷积网络从卷积层开始卷积层执行初始特征的提取然后是全连接层后者执行最终的分类.

LeNet-5 为例. 这是 Yann LeCun 提出的卷积神经网络结构并应用于手写数字分类. 它输入 32×32 的灰度图像产生 10 个值的向量这些值代表数字属于某一类数字 0 到 9的概率. 表 2 总结了网络的结构输出的尺寸和可训练参数权重+偏差的数量.

表 2 LeNet-5 结构
层类型 可训练参数 输出大小
输入图像   32×32×1
卷积层 1核大小 5×5核数量 6
ReLU 激活
156 28×28×6
最大池化 1   14×14×6
卷积层 2核大小 5×5核数量 16
ReLU 激活函数
416 10×10×16
最大池化 2   5×5×16
卷积层 3核大小 5×5核数量 120 3120 1×1×120
全连接层 1输入 120输出 84
Sigmoid 激活函数
10164 84
全连接层 2输入 84输出 10
SoftMax 激活函数
850 10

这里只有 14706 个可训练参数算是非常简单的卷积神经网络结构了. 业界实用的更复杂的深度神经网络包含了超过几百万个训练参数.

训练卷积网络

到目前为止本文还只局限于推导卷积神经网络即计算给定输入的输出. 但是要从中得到有意义的东西需要先对网络进行训练. 对于图像处理中的卷积算子卷积核通常是人工设计的具有特定的用途比如查找物体边缘锐化图像或是模糊图像等. 设计正确的卷积核来执行所需的任务是一个耗时的过程. 但是对于卷积神经网络情况却完全不同. 在设计这种网络时只用考虑层数完成的卷积的数量和大小等而不会设置这些卷积核. 相反网络将在训练阶段学习这些内容. 从本质上说这些核只不过是权重.

卷积人工网络的训练使用与全连接网络训练完全相同的算法——随机梯度下降和反向传播. 正如前馈中写到的为了计算神经网络误差的偏导数可以使用链式法则. 这样可以为任何可训练层的权重变化定义完整的方程. 我将针对神经网络每个构建块building block比如全连接和卷积层激活函数成本函数等写一些小点的方程而不是那种一个式子占半页纸的大玩意儿.

通过链式法则可以发现神经网络的每个构建块都将其误差梯度计算为输出相对于输入的偏导数并与后面块的误差梯度相乘. 要记住信息流是向后移动的所以计算要从最后一个块开始然后流到前一个块即第一个块. 训练阶段的最后一个块始终是一个成本函数它将误差梯度作为成本其输出相对于神经网络输出成本函数的输入的导数进行计算. 这可以通过以下方式定义

δi(net)=Costneti

所有其他构建块都从下一个块中获取误差梯度并乘以其输出相对于输入的偏导数.

δi(k)=out(k)ini(k)δ(k+1)

回忆一下全连接网络的导数. 首先从 MSE 成本函数相对于网络输出的误差梯度开始yi 为网络产生的输出ti 为目标输出

δi(net)=yiti

当误差梯度通过 sigmoid 激活函数后移时它会以这种方式重新计算这里的 oi 是 sigmoid 的输出这是从下一块无所谓是什么也可以是成本函数或多层网络中的另一层得到的梯度乘以 sigmoid 的导数

δi(k)=oi(1oi)δi(k+1)

或者如果使用 tanh 作为激活函数

δi(k)=oi(1oi2)δi(k+1)

当需要通过一个全连接层向后传播误差梯度时鉴于每个输入输出都各自相连可以得到一个偏导数的和

δi(k)=j=1mωi,jδj(k+1)

其中n 是全连接层中的神经元数ij 分别表示第 i 个输出和第 j 个输入.

由于全连接层是一个可训练的层它不仅需要将误差梯度向后传递给前一个层还需要计算权重. 使用上述定义的命名约定权重和偏差的计算规则可以写成经典SGD

ωi,j(t+1)=ωi,j(t)λ(δi(k+1)xj)bi(t+1)=bi(t)λδi(k+1)

上面的方程实际上都是前馈中反向传播的内容. 为什么我要再写一遍首先是要提醒一下基础知识其次我用了不同的方式重写其中每个构建块定义自己的误差梯度反向传播方程. 前馈里给出的权重方程有助于理解基本知识以及链规则的工作原理但是作为一个单一的方程它没法通用. 如果成本函数不是 MSE 呢如果需要 tanh 或者 ReLU 激活函数而不是 sigmoid 呢本文介绍的方法更加灵活允许以各种方式混合人工神经网络的构建块并在不假设哪一层之后进行激活使用哪一个成本函数的情况下进行培训. 此外这样的写法和我实际的 C++ 代码实现类似我把不同的构建块实现为单独的类在训练过程中让它们各自计算前向传递和后向传递.

如果你对上面的内容有点摸不着头脑建议你先阅读前馈全连接神经网络.

交叉熵成本函数

卷积神经网络最常用的用途之一是图像分类. 给定一个图像网络需要把它分类到相互排斥的类里去. 比如手写数字分类有 10 个可能的类对应于从 0 到 9 的数字. 或者可以训练一个网络来识别汽车卡车轮船飞机等交通工具. 这种分类的要点是每个输入图像必须只属于一个类别.

在处理多类分类问题时人工神经网络输出的类数应当与要区分的类数相同. 在训练阶段目标输出是独热编码也就是用零向量表示在与类对应的索引处只有一个元素设置为值“1”例如对于 4 类分类的任务目标输出可能是第 2 类 \(\{0100\}\)第 4 类 \(\{0001\}\) 等. 任何目标输出都不允许将多个元素设置为1或其他非零值. 这可以看作是目标概率\(\{0100\}\) 输出意味着输入属于第 2 类的概率为 100%以及属于其他类的概率为 0%.

上面说的是理想情况实际训练中的神经网络输出不会是非黑即白这么极端比如它可以输出 0.30.350.250.1 之类的小数. 这些输出对应着不同的实际含义. 这表示神经网络没法十分清楚判断目标应该分到哪一类它只能根据计算得到的概率分析第 2 类的概率有0.35也就是 35% 的可能性而且这是 4 个输出中最高的那么它将猜测这很可能应该属于第 2 类.

所以说需要一个成本函数来量化目标和实际输出之间的差异并指导神经网络计算其参数. 在处理互斥类的概率模型时通常需要处理预测概率和真实值ground-truth概率. 这种情况下最常见的选择是交叉熵成本函数cross-entropy. 交叉熵信息论当中的概念. 通过最小化交叉熵通过最小化额外的数据比特量估计的概率 yi 对出现概率分布 ti目标或实际分布的某些事件进行编码. 为了最小化交叉熵需要使估计概率与实际概率相同.

交叉熵成本函数定义如下

Cost=i=1ntilog(yi)

其中ti 是目标输出yi 是神经网络输出.

对上式求导成本函数对神经网络输出的偏导数为

δi(net)=tiyi

这就得到了可以代替 MSE 的交叉熵成本函数. 接下来可以开始处理其他构建块并观察误差梯度是如何反向传播的.

SoftMax 激活函数

前馈中已经介绍过在分类问题中用到的神经网络最后一层使用 sigmoid 作为激活函数. 它的输出值域为 (0,1)可以理解为从 0% 到 100% 表示的概率. 如果神经网络输出层采用 sigmoid它的确可能得到接近于真实值的概率. 但是现在要处理的是互斥类很多情况下 sigmoid 的输出是无意义的. 比如上面的 4 类分类例子一个输出向量是 ${0.6,0.55,0.1,0.1}这是用 sigmoid 可能得到的结果. 问题在哪乍一看这表明应该是第 1 类60% 概率但是第 2 类的可能性也很大55%. 而且这个输出结果有一个很大的问题它的各概率和达到了 1.35也就是目标属于这 4 类之一的可能性是 135%. 这在物理上是毫无意义

这里要指出两个问题第一各分类概率和应为 100%不能多也不能少. 第二对于难以识别的分类目标如果目标既像第 1 类又像第 2 类那么怎么能确定 60% 这么高的概率一定是可信的

为了解决这两个问题需要用到另一个激活函数SoftMax. SoftMax 类似 sigmoid值域也是 (0,1). 不同的是它处理整个输入向量而不是其中的单个值这就保证了输出向量概率的和恒为 1. SoftMax 定义为

f(xi)=exij=1mexj

将上面的例子改用 SoftMax 进行处理后输出向量变得更合理了{0.316,0.3,0.192,0.192}. 可以看到向量中各概率的和等于 1也就是 100%. 最可能的第 1 类它的概率也不再高得离谱只有 31.6%.

和其他激活函数一样SoftMax 也需要定义它的梯度反向传播方程

δi(k)=j=1m(δj(k+1){j=i,oi(1oj)ji,oioj})

表 2 里可以看到 LeNet-5 神经网络架构中包含了全连接层和 sigmoid 激活函数. 这两者的方程也定义完毕现在就可以继续讨论其他构建块了.

ReLU 激活函数

前面提到过ReLU 激活函数在深度神经网络经常用到它对于大于 0 的输入向量梯度恒为 1所以能保证误差梯度在网络中更好地传播. 现在来定义它的梯度反向传播方程

δi(k)=δj(k+1){1,oi>00,oi0}

池化层

为了尽量简洁地说明误差梯度如何通过池化层反向传播假设使用的池化层卷积核大小 2×2跨度 2不填充输入只池化有效位置. 这个假设意味着每个输出特征图的值都是基于 4 个值计算得到的.

尽管池化层假设输入向量是二维数据但是下面的数学定义也可以处理输入输出是一维向量的情况. 首先定义 i2j(i) 函数这个函数接受输入向量第 i 个值作为索引并返回输出向量对应的第 j 个值作为索引. 由于每个输出都是用 4 个输入值计算出来的所以这意味着有 4 个 i 会让 i2j(i) 函数输出同一个 j.

先从最大池化开始定义误差梯度反向传播方程之前还有一件事要做. 在正向传递计算神经网络的输出也会用与输出向量长度相同的最大索引值max indices向量填充池化层. 如果输出向量包含对应输入值的最大值则最大索引值向量包含最大值的索引. 综上所述可以定义最大池化层的梯度反向传播方程

δi(k)=δi2j(i)(k+1){1,i=pi2j(i)0,ipi2j(i)}

其中p 是最大索引值向量.

平均池化来说就更简单了

δi(k)=δi2j(i)(k+1)q

其中q 是卷积核大小在这个例子里q=4.

卷积层

最后来定义卷积层的反向传播过程. 牢记一点它和全连接层的区别就在于共享权重和偏差值.

从卷积层的权重计算开始. 对于全连接层误差对权重 ωi,j 的偏导数等于下一个块的误差梯度乘以相应的输入值 δi(k+1)xj. 这是因为每个输入输出连接都在全连接层中分配了自己的权重而全连接层是不共享的. 但是卷积层和这不一样图 4 显示了卷积核的每个权重都用于多个输入输出连接. 图中的例子突出显示的卷积核权重每个使用了 9 次对应输入图像中的 9 个不同位置. 因此与权重有关的误差的偏导数也需要有 9 个.

图 4 卷积层计算

和处理池化层时类似这里忽略了卷积层处理的是二维三维数据这一事实而假设它们是普通的向量数组就像 C++ 编程时用到的那样. 对于上面的示例第一个权重红线框出应用于输入 {1,2,3,5,6,7,9,10,11,13,14,15}而第四个权重应用于输入 {6,7,8,10,11,12,14,15,16}. 用 ri 表示每个权重使用的输入索引向量. 另外定义 i2o(i,j) 函数它为第 i 个权重和第 j 个输入提供输出值索引. 上图中有几个例子i2o(1,1)=1i2o(4,6)=1i2o(1,11)=9i2o(4,16)=9. 根据这些约定可以定义卷积网络的权重

(3)ωi(t+1)=ωi(t)λjjriδi2o(i,j)(k+1)xj

上面的玩意儿有什么意义它的意义很丰富. 你想得越多意义就越多. 这里的目标是为所有输出取误差梯度因为每个核的权重用于计算所有的输出然后将它们乘以相应的输入. 尽管有多个核但是它们都以相同的模式应用所以即使需要计算不同核的权重权重输入向量也保持不变. 然而i2o(i,j) 是每个核特定的它可以使用核的索引作为额外的参数进行扩展.

更新偏差值要简单得多. 由于每个核偏差都用于计算输出值所以只需为当前核生成的特性图的误差梯度求和即可

(4)b(t+1)=b(t)λjsδj(k+1)

其中s 是特性图.

(3)(4) 都是依据特征图卷积核来完成的权重和偏差值没有用核索引参数化.

现在来求卷积层误差梯度反向传播的最终方程. 这意味要计算与层输入相关的误差偏导数. 每个输入元素可以多次用于生成要素图的输出值它的使用次数可以与卷积核中的元素数权重数相同. 但是有些输入只能用于一个输出比如二维特征图的四角. 还要记住每个输入特征图都可以用不同的核进行多次处理从而生成更多的输出图. 假设另一组名为 γi 的辅助向量用于保存第 i 个输入所贡献的输出索引. 再定义 i2w(i,j) 函数它返回连接第 i 个输入到第 j 个输出的权重. 还是以图 4 为例i2w(1,1)=1i2w(6,1)=4i2w(16,9)=4. 利用这些定义误差梯度通过卷积层后向传播的方程可以写为

δi(k)=jjγiωi2w(i,j)δj(k+1)

数学分析到此结束所有需要计算的内容都已经完成了.

ANNT 库

卷积人工神经网络很大程度上是基于前馈所述的全连接网络实现的设计集. 所有核心类都保持原样只实现了新的构建块允许将它们构建成卷积神经网络. 新的类关系图如下所示跟原来的没有什么区别.

图 5 类关系图

与以前的设置方式类似新的构建块负责计算正向传递上的输出和反向传递上传播误差梯度以及在可训练层的情况下计算初始权重. 因此所有的神经网络训练代码都可以原样照搬. 和其他代码一样新的构建块尽可能使用了 SIMD 指令向量化计算以及 OpenMP 并行计算.

编译源码

源码里附带 MSVC2015文件和 GCC make 文件. 用 MSVC 非常简单每个例子的解决方案文件都包括例子本身和库的项目编译也只需点击一下按钮. 如果使用 GCC则需要运行 make 来编译程序.

使用例程

分析了那么久的原理和数学推导是时候开始实践并实际生成一些用于图像分类任务的网络了例如分类识别手写数字和汽车卡车轮船飞机之类不同的对象.

这些例子唯一的目的是用来演示 ANNT 库的使用方法并不代表用到的神经网络结构就是最适于它们的. 这些代码片段只是范例的一小部分要查看示例的完整代码你需要参阅本文提供的源码.

MNIST 手写数字分类

第一个例子是对 MNIST 数据库里的手写数字进行分类. 这个数据库包含了 60000 个神经网络训练样本和 10000 个测试样本. 图 6 展示了其中的一部分.

图 6 MNIST 数据库部分

例子使用的卷积神经网络的结构与 LeNet-5 网络非常相似只是规模小得多. 它只有一个全连接网络

Conv(32x32x1, 5x5x6  ) -> ReLU -> AvgPool(2x2)
Conv(14x14x6, 5x5x16 ) -> ReLU -> AvgPool(2x2)
Conv(5x5x16,  5x5x120) -> ReLU
FC(120, 10) -> SoftMax

上面设置了每个卷积层的输入大小以及它们执行的卷积的大小和数量全连接层的输入输出数量. 接下来生成卷积神经网络.

// 连接表用于指定第一卷积层要使用的由第二层生成的特征图
vector<bool> connectionTable( {
    true,  true,  true,  false, false, false,
    false, true,  true,  true,  false, false,
    false, false, true,  true,  true,  false,
    false, false, false, true,  true,  true,
    true,  false, false, false, true,  true,
    true,  true,  false, false, false, true,
    true,  true,  true,  true,  false, false,
    false, true,  true,  true,  true,  false,
    false, false, true,  true,  true,  true,
    true,  false, false, true,  true,  true,
    true,  true,  false, false, true,  true,
    true,  true,  true,  false, false, true,
    true,  true,  false, true,  true,  false,
    false, true,  true,  false, true,  true,
    true,  false, true,  true,  false, true,
    true,  true,  true,  true,  true,  true
} );

// 准备卷积神经网络
shared_ptr<XNeuralNetwork> net = make_shared<XNeuralNetwork>( );

net->AddLayer( make_shared<XConvolutionLayer>( 32, 32, 1, 5, 5, 6 ) );
net->AddLayer( make_shared<XReLuActivation>( ) );
net->AddLayer( make_shared<XAveragePooling>( 28, 28, 6, 2 ) );

net->AddLayer( make_shared<XConvolutionLayer>( 14, 14, 6, 5, 5, 16, connectionTable ) );
net->AddLayer( make_shared<XReLuActivation>( ) );
net->AddLayer( make_shared<XAveragePooling>( 10, 10, 16, 2 ) );

net->AddLayer( make_shared<XConvolutionLayer>( 5, 5, 16, 5, 5, 120 ) );
net->AddLayer( make_shared<XReLuActivation>( ) );

net->AddLayer( make_shared<XFullyConnectedLayer>( 120, 10 ) );
net->AddLayer( make_shared<XLogSoftMaxActivation>( ) );

从源码可以清楚看到上面的神经网络配置是如何转换成代码的只是这个连接表是首次出现的. 这很容易理解从网络结构和代码可以看出第一层做 6 个卷积因此生成 6 个特征图第二层做 16 个卷积. 在某些情况下需要配置层的卷积只在输入特征映射的子集上操作. 如代码所示第二层的前 6 个卷积使用第一层生成的 3 个特征图的不同模式接下来的 9 个卷积使用 4 个特征图的不同模式. 最后一个卷积使用第一层的所有 6 个特征映射. 这样做是为了减少要训练的参数数量并确保第二层的不同特征图不会基于相同的输入特征图.

当创建卷积网络时可以像处理全连接网络一样进行操作创建一个训练内容指定成本函数和权重的优化器然后全部传递给一个助手类由它运行训练验证循环并测试.

// 生成训练内容,用到了 Adam 优化器和负对数似然函数(SoftMax)
shared_ptr<XNetworkTraining> netTraining = make_shared<XNetworkTraining>( net,
                                           make_shared<XAdamOptimizer>( 0.002f ),
                                           make_shared<XNegativeLogLikelihoodCost>( ) );

// 使用助手类训练神经网络分类
XClassificationTrainingHelper trainingHelper( netTraining, argc, argv );
trainingHelper.SetValidationSamples( validationImages, encodedValidationLabels, validationLabels );
trainingHelper.SetTestSamples( testImages, encodedTestLabels, testLabels );

// 20 世代, 每批 50 样本
trainingHelper.RunTraining( 20, 50, trainImages, encodedTrainLabels, trainLabels );

下面是输出显示了训练进度和测试数据集的最终结果分类精度. 可以看到精度达到了 99.01%比起前馈中 96.55% 的精度更准确了.

MNIST handwritten digits classification example with Convolution ANN

Loaded 60000 training data samples
Loaded 10000 test data samples

Samples usage: training = 50000, validation = 10000, test = 10000

Learning rate: 0.0020, Epochs: 20, Batch Size: 50

Before training: accuracy = 5.00% (2500/50000), cost = 2.3175, 34.324s

Epoch   1 : [==================================================] 123.060s
Training accuracy = 97.07% (48536/50000), cost = 0.0878, 32.930s
Validation accuracy = 97.49% (9749/10000), cost = 0.0799, 6.825s
Epoch   2 : [==================================================] 145.140s
Training accuracy = 97.87% (48935/50000), cost = 0.0657, 36.821s
Validation accuracy = 97.94% (9794/10000), cost = 0.0669, 5.939s
...
Epoch  19 : [==================================================] 101.305s
Training accuracy = 99.75% (49877/50000), cost = 0.0077, 26.094s
Validation accuracy = 98.96% (9896/10000), cost = 0.0684, 6.345s
Epoch  20 : [==================================================] 104.519s
Training accuracy = 99.73% (49865/50000), cost = 0.0107, 28.545s
Validation accuracy = 99.02% (9902/10000), cost = 0.0718, 7.885s

Test accuracy = 99.01% (9901/10000), cost = 0.0542, 5.910s

Total time taken : 3187s (53.12min)

CIFAR10 图片分类

第二个示例对来自 CIFAR-10 数据集的 32×32 彩色图像进行分类. 这个数据集包含 60000 个图像其中 50000 个用于训练另外 10000 个用于测试. 图像分为 10 类飞机汽车鹿青蛙船和卡车. 图 7 展示了部分内容.

图 7 CIFAR10 数据集部分

可以看到CIFAR-10 数据集比 MNIST 手写数字复杂得多. 首先图像是彩色的. 其次它们不那么明显. 有些图如果不经提醒我都认不出来. 网络的结构变得更大了但并不是说它变得更深了而是执行卷积和训练权重的数量在增加. 它的网络结构如下

Conv(32x32x3,  5x5x32, BorderMode::Same) -> ReLU -> MaxPool -> BatchNorm
Conv(16x16x32, 5x5x32, BorderMode::Same) -> ReLU -> MaxPool -> BatchNorm
Conv(8x8x32,   5x5x64, BorderMode::Same) -> ReLU -> MaxPool -> BatchNorm
FC(1024, 64) -> ReLU -> BatchNorm
FC(64, 10) -> SoftMax

将上述神经网络结构转化为代码得到以下结果

ReLU(MaxPool)MaxPool(ReLU) 计算结果相同但计算量减少 75%所以这里选用前者.

// 准备卷积神经网络
shared_ptr<XNeuralNetwork> net = make_shared<XNeuralNetwork>( );

net->AddLayer( make_shared<XConvolutionLayer>( 32, 32, 3, 5, 5, 32, BorderMode::Same ) );
net->AddLayer( make_shared<XMaxPooling>( 32, 32, 32, 2 ) );
net->AddLayer( make_shared<XReLuActivation>( ) );
net->AddLayer( make_shared<XBatchNormalization>( 16, 16, 32 ) );

net->AddLayer( make_shared<XConvolutionLayer>( 16, 16, 32, 5, 5, 32, BorderMode::Same ) );
net->AddLayer( make_shared<XMaxPooling>( 16, 16, 32, 2 ) );
net->AddLayer( make_shared<XReLuActivation>( ) );
net->AddLayer( make_shared<XBatchNormalization>( 8, 8, 32 ) );

net->AddLayer( make_shared<XConvolutionLayer>( 8, 8, 32, 5, 5, 64, BorderMode::Same ) );
net->AddLayer( make_shared<XMaxPooling>( 8, 8, 64, 2 ) );
net->AddLayer( make_shared<XReLuActivation>( ) );
net->AddLayer( make_shared<XBatchNormalization>( 4, 4, 64 ) );

net->AddLayer( make_shared<XFullyConnectedLayer>( 4 * 4 * 64, 64 ) );
net->AddLayer( make_shared<XReLuActivation>( ) );
net->AddLayer( make_shared<XBatchNormalization>( 64, 1, 1 ) );

net->AddLayer( make_shared<XFullyConnectedLayer>( 64, 10 ) );
net->AddLayer( make_shared<XLogSoftMaxActivation>( ) );

剩下部分代码和前面的例子类似也是生成训练内容传递给助手类执行. 下面是这个例子的输出

CIFAR-10 dataset classification example with Convolutional ANN

Loaded 50000 training data samples
Loaded 10000 test data samples

Samples usage: training = 43750, validation = 6250, test = 10000

Learning rate: 0.0010, Epochs: 20, Batch Size: 50

Before training: accuracy = 9.91% (4336/43750), cost = 2.3293, 844.825s

Epoch   1 : [==================================================] 1725.516s
Training accuracy = 48.25% (21110/43750), cost = 1.9622, 543.087s
Validation accuracy = 47.46% (2966/6250), cost = 2.0036, 77.284s
Epoch   2 : [==================================================] 1742.268s
Training accuracy = 54.38% (23793/43750), cost = 1.3972, 568.358s
Validation accuracy = 52.93% (3308/6250), cost = 1.4675, 76.287s
...
Epoch  19 : [==================================================] 1642.750s
Training accuracy = 90.34% (39522/43750), cost = 0.2750, 599.431s
Validation accuracy = 69.07% (4317/6250), cost = 1.2472, 81.053s
Epoch  20 : [==================================================] 1708.940s
Training accuracy = 91.27% (39931/43750), cost = 0.2484, 578.551s
Validation accuracy = 69.15% (4322/6250), cost = 1.2735, 81.037s

Test accuracy = 68.34% (6834/10000), cost = 1.3218, 122.455s

Total time taken : 48304s (805.07min)

前面提到了CIFAR-10 数据集来得更复杂计算的结果远远达不到 MNIST 那样 99% 的准确度训练集的准确度约 91%测试验证的准确度约 68-69%. 就是这样的精度区区 20 个世代的计算就花了我 13 个小时这也说明了对于卷积网络来说如果不用分布式集群或者超级计算机普通 PC 仅仅使用 CPU 来计算显然不够看.

结论

本文中讨论了用 ANNT 库生成卷积神经网络. 在这一点上它只能生成相对简单的网络到目前为止还不支持生产更高级更流行的架构. 但是正如 CIFAR-10 一例中看到的一旦神经网络变大就需要更多的计算能力来进行训练所以仅仅使用 CPU 是不够的目前我只实现了用 CPU 计算网络. 随着学习深入这个弱点还会不断放大. 所以接下来我会优先研究如何实现 GPU 计算. 至于更复杂的神经网络架构先往后放一放.

现在已经讨论了全连接和卷积的神经网络在接下来的文章里我将介绍递归神经网络recurrent neural networks架构.

如果想关注 ANNT 库的进展或者挖掘更多的代码可以在 Github 上找到这个项目.

许可

本文以及任何相关的源代码和文件都是根据 GNU通用公共许可证GPLv3授权.

关于作者


Andrew Kirillov来自英国🇬🇧目前就职于 IBM.


  1. Yann LeCun中文名杨立昆Facebook 首席人工智能专家人工智能研究院院长被称为“深度学习三大巨头”之一另外两位是 Geoffrey Hinton 和 Yoshua Bengio. ↩︎

posted on2019-01-16   Conmajia  阅读(9749)  评论(0编辑  收藏  举报

编辑推荐:
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
点击右上角即可分享
微信分享提示