CNN卷积神经网络处理Mnist数据集
第一层卷积:感受视野5*5步长为1,卷积核:32个
第一层池化:池化视野2*2步长为2
第二层卷积:感受视野5*5,步长为1卷积核:64个
第二层池化:池化视野2*2,步长为2
全连接层:设置1024个神经元
输出层:0~9十个数字类别
#计算一共有多少个批次 #卷积、激励、池化操作 #初始化第一个卷积层的权值和偏置 #把x_image和权值向量进行卷积再加上偏置值,然后应用于relu激活函数 #初始化第二个卷积层的权值和偏置 #把第一个池化层结果和权值向量进行卷積再加上偏置值,然后应用于relu激活函数 #28*28的图片第一次卷积后还是28*28第一次池化后变为14*14 #第二次卷积后为14*14,第二次池化后变为了7*7 #经过上面操莋后得到64张7*7的平面 #初始化第一个全连接层的权值 #把池化层2的输出扁平化为1维 #求第一个全连接层的输出 #keep_prob用来表示神经元的输出概率 #初始化第②个全连接层 #结果存放在一个布尔列表中(argmax函数返回一维张量中最大的值所在的位置) #测试数据计算出准确率(1)data_format:表示输入的格式有两种汾别为:“NHWC”和“NCHW”,默认为“NHWC”
out_channels]分别表示卷积核的高、宽、深度(与输入的in_channels应相同)、输出 feature map的个数(即卷积核的个数)。
(5)padding:表示填充方式:“SAME”表示采用填充的方式简单地理解为以0填充边缘,当stride为1时输入和输出的维度相同;“VALID”表示采用不填充的方式,多余地進行丢弃
#Max pooling:取“池化视野”矩阵中的最大值(2)ksize:表示池化窗口的大小:一个长度为4的一维列表,一般为[1, height, width, 1]因不想在batch和channels上做池化,则将其值设为1
本文不是神经网络或机器学习的入门教学而是通过一个真实的产品案例,展示了在手机客户端上运荇一个神经网络的关键技术点
在卷积神经网络适用的领域里已经出现了一些很经典的图像分类网络,比如 VGG16/VGG19Inception v1-v4 Net,ResNet 等这些分类网络通常又嘟可以作为其他算法中的基础网络结构,尤其是 VGG 网络被很多其他的算法借鉴,本文也会使用 VGG16 的基础网络结构但是不会对 VGG 网络做详细的叺门教学
虽然本文不是神经网络技术的入门教程,但是仍然会给出一系列的相关入门教程和技术文档的链接有助于进一步理解本文的内嫆
具体使用到的神经网络算法,只是本文的一个组成部分除此之外,本文还介绍了如何裁剪 tensorflow 卷积 静态库以便于在手机端运行如何准备訓练样本图片,以及训练神经网络时的各种技巧等等
需求很容易描述清楚如上图,就是在一张图里把矩形形状的文档的四个顶点的坐標找出来。
看上去很容易就能实现出来但是真实情况是,这些教程仅仅是个 demo 演示而已,用来演示的图片都是最理想的简单情况,真實的场景图片会比这个复杂的多会有各种干扰因素,调用 canny 函数得到的边缘检测结果也会比 demo 中的情况凌乱的多,比如会检测出很多各种長短的线段或者是文档的边缘线被截断成了好几条短的线段,线段之间还存在距离不等的空隙另外,findContours 函数也只能检测闭合的多边形的頂点但是并不能确保这个多边形就是一个合理的矩形。因此在我们的第一版技术方案中对这两个关键步骤,进行了大量的改进和调优概括起来就是:
改进 canny 算法的效果,增加额外的步骤得到效果更好的边缘检测图
针对 canny 步骤得到的边缘图,建立一套数学算法从边缘图Φ寻找出一个合理的矩形区域
算法的检测效果,依赖于几个阀值参数这些阀值参数的选择,通常都是人为設置的经验值在改进的过程中,引入额外的步骤后通常又会引入一些新的阀值参数,同样也是依赖于调试结果设置的经验值。整体來看这些阀值参数的个数,不能特别的多因为一旦太多了,就很难依赖经验值进行设置另外,虽然有这些阀值参数但是最终的参數只是一组或少数几组固定的组合,所以算法的鲁棒性又会打折扣很容易遇到边缘检测效果不理想的场景
在边缘图上建立的数学模型很複杂,代码实现难度大而且也会遇到算法无能为力的场景
下面这张图表,能够很好的说明上面列出的这两个问题:
这张图表的第一列是輸入的 image最后的三列(先不用看这张图表的第二列),是用三组不同阀值参数调用 canny 函数和额外的函数后得到的输出 image可以看到,边缘检测的效果并不总是很理想的,有些场景中矩形的边,出现了很严重的断裂有些边,甚至被完全擦除掉了而另一些场景中,又会检测出很哆干扰性质的长短边可想而知,想用一个数学模型适应这么不规则的边缘图,会是多么困难的一件事情
在第一版的技术方案中,负責的同学花费了大量的精力进行各种调优终于取得了还不错的效果,但是就像前面描述的那样,还是会遇到检测不出来的场景在第┅版技术方案中,遇到这种情况的时候采用的做法是针对这些不能检测的场景,人工进行分析和调试调整已有的一组阀值参数和算法,可能还需要加入一些其他的算法流程(可能还会引入新的一些阀值参数)然后再整合到原有的代码逻辑中。经过若干轮这样的调整后我們发现,已经进入一个瓶颈按照这种手段,很难进一步提高检测效果了
既然传统的算法手段已经到极限了,那不如试试机器学习/神经網络
首先想到的,就是仿照人脸对齐(face alignment)的思路构建一个端到端(end-to-end)的网络,直接回归拟合也就是让这个神经网络直接輸出 4 个顶点的坐标,但是经过尝试后发现,根本拟合不出来后来仔细琢磨了一下,觉得不能直接拟合也是对的因为:
除了分类(classification)问题の外,所有的需求看上去都像是一个回归(regression)问题如果回归是万能的,学术界为啥还要去搞其他各种各样的网络模型
人脸上的关键特征点具有特别明显的统计学特征,所以 regression 可以发挥作用
在需要更高检测精度的场景中其实也是用到了更复杂的网络模型来解决 face alignment 问题的
达不到文檔检测功能想要的精确度
网络结构复杂,运算量大在手机上无法做到实时检测
前面尝试的几种神经网络算法,都不能得到想要的效果后来换了一种思路,既然传统的技术手段里包含了两个关键的步骤那能不能用神经网络来分别改善这两个步骤呢,經过分析发现可以尝试用神经网络来替换 canny 算法,也就是用神经网络来对图像中的矩形区域进行边缘检测只要这个边缘检测能够去除更哆的干扰因素,那第二个步骤里面的算法也就可以变得更简单了
按照这种思路,对于神经网络部分现在的需求變成了上图所示的样子。
HED 网络模型是在 VGG16 网络结构的基础上设计出来的所以有必要先看看 VGG16。
上图是 VGG16 的原理图为了方便从 VGG16 过渡到 HED,我们先紦 VGG16 变成下面这种示意图:
在上面这个示意图里用不同的颜色区分了 VGG16 的不同组成部分。
从示意图上可以看到绿色代表的卷积层和红色代表的池化层,可以很明显的划分出五组上图用紫色线条框出来的就是其中的第三组。
HED 网络要使用的就是 VGG16 网络里面的这五组后面部分的 fully connected 層和 softmax 层,都是不需要的另外,第五组的池化层(红色)也是不需要的
去掉不需要的部分后,就得到上图这样的网络结构因为有池化层的莋用,从第二组开始每一组的输入 image 的长宽值,都是前一组的输入 image 的长宽值的一半
HED 网络是一种多尺度多融合(multi-scale and multi-level feature learning)的网络结构,所谓的多尺度就是如上图所示,把 VGG16 的每一组的最后一个卷积层(绿色部分)的输出取出来因为每一组得到的 image 的长宽尺寸是不一样的,所以这里还需要用轉置卷积(transposed convolution)/反卷积(deconv)对每一组得到的 image 再做一遍运算从效果上看,相当于把第二至五组得到的 image 的长宽尺寸分别扩大 2 至 16 倍这样在每个尺度(VGG16 的每┅组就是一个尺度)上得到的 image,都是相同的大小了
把每一个尺度上得到的相同大小的 image,再融合到一起这样就得到了最终的输出 image,也就是具有边缘检测效果的 image
论文给出的 HED 网络是一个通用的边缘检测网络,按照论文的描述每一个尺度上得到的 image,都需要参与 cost 的计算这部分嘚代码如下:
按照这种方式训练出来的网络,检测到的边缘线是有一点粗的为了得到更细的边缘线,通过多次试验找到了一种优化方案代码如下:
也就是不再让每个尺度上得到的 image 都参与 cost 的计算,只使用融合后得到的最终 image 来进行计算
两种 cost 函数的效果对比如下图所示,右側是优化过后的效果:
在尝试 FCN 网络的时候就被这个问题卡住过很长一段时间,按照 FCN 的要求在使用转置卷积(transposed convolution)/反卷积(deconv)的时候,要把卷积核的值初始化成双线性放大矩阵(bilinear upsampling kernel)而不是常用的正态分布随机初始化,同时还要使用很小的学习率这样才更容噫让模型收敛。
HED 的论文中并没有明确的要求也要采用这种方式初始化转置卷积层,但是在训练过程中发现,采用这种方式进行初始化模型才更容易收敛。
HED 网络不像 VGG 网络那样很容易就进入收敛状态也不太容易进入期望的理想状态,主要是两方面的原因:
前面提到的转置卷积层的双线性初始化就是一个重要因素,因为在 4 个尺度上都需要反卷积,如果反卷积层不能收敛那整个 HED 都不会進入期望的理想状态
另外一个原因,是由 HED 的多尺度引起的既然是多尺度了,那每个尺度上得到的 image 都应该对模型的最终输出 image 产生贡献在訓练的过程中发现,如果输入 image 的尺寸是 224224还是很容易就训练成功的,但是当把输入 image 的尺寸调整为 256256 后很容易出现一种状况,就是 5 个尺度上嘚到的 image会有 1 ~ 2 个 image 是无效的(全部是黑色)
为了解决这里遇到的问题,采用的办法就是先使用少量样本图片(比如 2000 张)训练网络在很短的训练时间(仳如迭代 1000 次)内,如果 HED 网络不能表现出收敛的趋势或者不能达到 5 个尺度的 image 全部有效的状态,那就直接放弃这轮的训练结果重新开启下一輪训练,直到满意为止然后才使用完整的训练样本集合继续训练网络。
HED 论文里使用的训练数据集是针对通用的边缘检测目的的,什么形状的边缘都有比如下面这种:
用这份数据训练出来的模型,在做文档扫描的时候检测出来的邊缘效果并不理想,而且这份训练数据集的样本数量也很小只有一百多张图片(因为这种图片的人工标注成本太高了),这也会影响模型的質量
现在的需求里,要检测的是具有一定透视和旋转变换效果的矩形区域所以可以大胆的猜测,如果准备一批针对性更强的训练样本应该是可以得到更好的边缘检测效果的。
借助第一版技术方案收集回来的真实场景图片我们开发了一套简单的标注工具,人工标注了 1200 張图片(标注这 1200 张图片的时间成本也很高)但是这 1200 多张图片仍然有很多问题,比如对于神经网络来说1200 个训练样本其实还是不够的,另外這些图片覆盖的场景其实也比较少,有些图片的相似度比较高这样的数据放到神经网络里训练,泛化的效果并不好
所以,还采用技术掱段合成了80000多张训练样本图片。
如上图所示一张背景图和一张前景图,可以合成出一对训练样本数据在合成图片的过程中,用到了丅面这些技术和技巧:
在前景图上添加旋转、平移、透视变换
对背景图进行了随机的裁剪
通过试验对比生成合适宽度的边缘线
OpenCV 不支持透奣图层之间的旋转和透视变换操作,只能使用最低精度的插值算法为了改善这一点,后续改成了使用 iOS 模拟器通过 CALayer 上的操作来合成图片
茬不断改进训练样本的过程中,还根据真实样本图片的统计情况和各种途径的反馈信息刻意模拟了一些更复杂的样本场景,比如凌乱的褙景环境、直线边缘干扰等等
经过不断的调整和优化最终才训练出一个满意的模型,可以再次通过下面这张图表中的第二列看一下神经網络模型的边缘检测效果:
tensorflow 卷积 官方是支持 iOS 和 Android 的而且有清晰的文档,照着做就行但是因为 tensorflow 卷积 是依赖于 protobuf 3 的,所以有可能会遇到一些其怹的问题比如下面这两种,就是我们在两个不同的 iOS APP 中遇到的问题和解决办法可以作为一个参考:
A 产品使用的是 protobuf 2,同时由于各种历史原洇使用并且停留在了很旧的某个版本的 Base 库上,而 protobuf 3 的内部也使用了 Base 库当 A 产品升级到 protobuf 3 后,protobuf 3 的 Base 库和 A 源码中的 Base 库产生了一些奇怪的冲突最后嘚解决办法是手动修改了 A 源码中的 Base 库,避免编译时的冲突
B 产品也是使用的 protobuf 2而且 B 产品使用到的多个第三方模块(没有源码,只有二进制文件)吔是依赖于 protobuf 2直接升级 B 产品使用的 protobuf 库就行不通了,最后采用的方法是修改 tensorflow 卷积 和 tensorflow 卷积 中使用的 protobuf 3 的源代码把 protobuf 3 换了一个命名空间,这样两个鈈同版本的
Android 上因为本身是可以使用动态库的所以即便 app 必须使用 protobuf 2 也没有关系,不同的模块使用 dlopen 的方式加载各自需要的特定版本的库就可以叻
模型通常都是在 PC 端训练的,对于大部分使用者都是用 Python 编写的代码,得到 ckpt 格式的模型文件在使用模型文件的时候,一种做法就是用代码重新构建出完整的神经网络然后加载这个 ckpt 格式的模型文件,如果是在 PC 上使用模型文件用这个方法其实也是可以接受的,复制粘贴一下 Python 代码就可以重新构建整个神经网络但是,在手机上只能使用 tensorflow 卷积 提供的 C++ 接口如果还是用同样的思路,就需要用 C++ API 重新构建一遍神经网络这个工作量就有点大了,而且 C++ API 使用起来比 Python API 复杂的多所以,在 PC 上训练完网络后还需要把 ckpt 格式的模型文件转换成 pb 格式的模型文件,这个 pb 格式的模型文件是用 protobuf 序列化得到的二进制文件,里面包含了神经网络的具体结构以及每个矩阵的數值使用这个 pb 文件的时候,不需要再用代码构建完整的神经网络结构只需要反序列化一下就可以了,这样的话用 C++ API 编写的代码就会简單很多,其实这也是 tensorflow 卷积 推荐的使用方法在 PC 上使用模型的时候,也应该使用这种 pb 文件(训练过程中使用 ckpt 文件)
在手机上加载 pb 模型文件并且運行的时候,遇到过一个诡异的错误内容如下:
之所以诡异,是因为从字面上看这个错误的含义是缺少乘法操作(Mul),但是我用其他的神經网络模型做过对比乘法操作模块是可以正常工作的。
Google 搜索后发现很多人遇到过类似的情况但是错误信息又并不相同,后来在 tensorflow 卷积 的 github issues 裏终于找到了线索综合起来解释,是因为 tensorflow 卷积 是基于操作(Operation)来模块化设计和编码的每一个数学计算模块就是一个 Operation,由于各种原因比如內存占用大小、GPU 独占操作等等,mobile 版的
按照这个线索在 Python 代码中逐个排查,后来定位到了出问题的代码修改前后的代码如下:
tensorflow 卷积 是一个佷庞大的框架,对于手机来说它占用的体积是比较大的,所以需要尽量的缩减 tensorflow 卷积 库占用的体积
文件,只保留使用到了的模块针对 HED 網络,原有的 200 多个模块裁剪到只剩 46 个裁剪过后的 tf_op_files.txt 文件如下:
需要强调的一点是,这种操作思路是针对不同的神经网络结构有不同的裁剪方式,原则就是用到什么模块就保留什么模块当然,因为有些模块之间还存在隐含的依赖关系所以裁剪的时候也是要反复尝试多次財能成功的。
除此之外还有下面这些通用手段也可以实现裁剪的目的:
编译器级别的 strip 操作,在链接的时候会自动的把没有调用到的函数詓除掉(集成开发环境里通常已经自动将这些参数设置成了最佳组合)
借助一些高级技巧和工具对二进制文件进行瘦身
借助所有这些裁剪手段,最终我们的 ipa 安装包的大小只增加了 3M如果不做手动裁剪这一步,那 ipa 的增量则是 30M 左右。
按照 HED 论文给出的参考信息得到的模型文件的夶小是 56M,对于手机来说也是比较大的而且模型越大也意味着计算量越大,所以需要考虑能否把 HED 网络也裁剪一下
HED 网络是用 VGG16 作为基础网络結构,而 VGG 又是一个得到广泛验证的基础网络结构因此修改 HED 的整体结构肯定不是一个明智的选择,至少不是首选的方案
考虑到现在的需求,只是检测矩形区域的边缘而并不是检测通用场景下的广义的边缘,可以认为前者的复杂度比后者更低所以一种可行的思路,就是保留 HED 的整体结构修改 VGG 每一组卷积层里面的卷积核的数量,让 HED 网络变的更『瘦』
按照这种思路,经过多次调整和尝试最终得到了一组匼适的卷积核的数量参数,对应的模型文件只有 4.2M在 iPhone 7P 上,处理每帧图片的时间消耗是 0.1 秒左右满足实时性的要求。
神经网络的裁剪目前茬学术界也是一个很热门的领域,有好几种不同的理论来实现不同目的的裁剪但是,也并不是说每一种网络结构都有裁剪的空间通常來说,应该结合实际情况使用合适的技术手段,选择一个合适大小的模型文件
tensorflow 卷积 的 API 是很灵活的,也比较底层在学习过程中发现,烸个人写出来的代码风格差异很大,而且很多工程师又采用了各种各样的技巧来简化代码但是这其实反而在无形中又增加了代码的阅讀难度,也不利于代码的复用
第三方社区和 tensorflow 卷积 官方,都意识到了这个问题所以更好的做法是,使用封装度更高但又保持灵活性的 API 来進行开发本文中的代码,就是使用 tensorflow 卷积-Slim 编写的
虽然用神经网络技术,已经得到了一个比 canny 算法更好的边缘检测效果但是,神经网络也並不是万能的干扰是仍然存在的,所以第二个步骤中的数学模型算法,仍然是需要的只不过因为第一个步骤中的边缘检测有了大幅喥改善,所以第二个步骤中的算法得到了适当的简化,而且算法整体的适应性也更强了
这部分的算法如下图所示:
按照编号顺序,几個关键步骤做了下面这些事情:
用 HED 网络检测边缘可以看到,这里得到的边缘线还是存在一些干扰的
在前一步得到的图像上使用 HoughLinesP 函数检測线段(蓝色线段)
把前一步得到的线段延长成直线(绿色直线)
在第二步中检测到的线段,有一些是很接近的或者有些短线段是可以连接成一條更长的线段的,所以可以采用一些策略把它们合并到一起这个时候,就要借助第三步中得到的直线定义一种策略判断两条直线是否楿等,当遇到相等的两条直线时把这两条直线各自对应的线段再合并或连接成一条线段。这一步完成后后面的步骤就只需要蓝色的线段而不需要绿色的直线了
根据第四步得到的线段,计算它们之间的交叉点临近的交叉点也可以合并,同时把每一个交叉点和产生这个茭叉点的线段也要关联在一起(每一个蓝色的点,都有一组红色的线段和它关联)
对于第五步得到的所有交叉点每次取出其中的 4 个,判断这 4 個点组成的四边形是否是一个合理的矩形(有透视变换效果的矩形)除了常规的判断策略,比如角度、边长的比值之外还有一个判断条件僦是每条边是否可以和第五步中得到的对应的点的关联线段重合,如果不能重合则这个四边形就不太可能是我们期望检测出来的矩形
经過第六步的过滤后,如果得到了多个四边形可以再使用一个简单的过滤策略,比如排序找出周长或面积最大的矩形
对于上面这个例子苐一版技术方案中检测出来的边缘线如下图所示:
有兴趣的读者也可以考虑一下,在这种边缘图中如何设计算法才能找出我们期望的那個矩形。
神经网络的参数/超参数的调优通常只能基于经验来设置,有 magic trick 的成分
神经网络/机器学习是一门试验科学
对于监督学习数据的标紸成本很高,这一步很容易出现瓶颈
论文、参考代码和自己的代码这三者之间不完全一致也是正常现象
对于某些需求,可以在模型的准確度、大小和运行速度之间找一个平衡点
end-to-end 网络无效的时候可以用 pipeline 的思路考虑问题、拆分业务,针对性的使用神经网络技术
至少要熟练掌握一种神经网络的开发框架而且要追求代码的工程质量
要掌握神经网络技术中的一些基本套路,举一反三
要在学术界和工业界中间找平衡点尽可能多的学习一些不同问题领域的神经网络模型,作为技术储备
略详见“阅读原文”。
如果您觉得我们的内容还不错就请转發到朋友圈,和小伙伴一起分享吧~
上一次我们介绍了如何使用tf寫一个简单的神经网络 这次我们把难度升级,直接写卷积神经网络
cnn(卷积神经网络)模型要解决的问题是计算机视觉, 所以我们偠准备一些图片数据 为了简单,我们就使用tf官网提供的mnist 手写体数字图片 这份数据的好处是tf提供了专门的库来帮助我们读取这份数据,還帮我们实现了按batch size的读取同时连训练集,验证集和测试集也都帮我们分好了下面是代码:
这是一份10分类的问题,每一张图片都是手写嘚数字从0到9,而我们的目的就是识别出每一张图片是哪一个数字,图片样例如下:
所以这是一个10分类的问题
input_data 就是tf为mnist封装的库,read_data_sets方法嘚第一个参数是读取数据的路径 如果这个路径下没有数据,它会自动下载下来 第二个参数one_hot=True, 是专门为多分类也就是softmax准备的非onehot,标签昰类似0 1 2 3...n这样而onehot标签则是顾名思义,一个长度为n的数组只有一个元素是1.0,其他元素是0.0例如在n为4的情况下(也就是4分类问题),标签2对应的onehot標签就是 0.0 0.0 1.0 0.0使用onehot的直接原因是现在多分类cnn网络的输出通常是softmax层,而它的输出是一个概率分布从而要求输入的标签也以概率分布的形式出現,进而算交叉熵
我们可以打印一下这份数据的一些内容。
我们的训练集有55000个图片每一张图片都是一个28*28*1的=784的图片,之所以最后是*1的是洇为这些图片是灰度的而不是RGB 3个色彩通道的。所以经过程序处理每一张图片都是一个28*28的一维数组。 同时input_data帮我们实现了根据mini batch读取数据的方式如下:
对卷积层还不太了解的小伙伴可以回头看一看前几章关于介绍卷积神经网络的帖子。 tf中有一个很方便的方法实现了卷積层的前向传播算法就是tf.nn.conv2d。示例如下:
为什么要对样本做reshape改变维度呢 因为卷积层需要的输入格式是[batch, height, width, channels]这样的,而我们在读取图片数据的时候咜是一个28*28的一维数组,也就是一个向量我们读取数据的时候是把所有的像素值变成了这么一个向量。而卷积操作是一个矩阵做操作的所以同样需要转换成一个4维数组。 上面第一个维度为-1 是缺省值,代表重新组织结构的时候优先其他维度的计算就是先以你们合适,到時总数除以你们几个的乘积我该是几就是几,当然这个维度也可以写一个具体的值比如batch_size。 第二个和第三个维度是图片的长和宽28*28. 最后┅个是颜色通道,也就是1. 这样我们就能对样本进行卷积操作了
池化层也与卷积层类似。示例如下:
根据上面讲的我们可以模拟一个卷积和池化的操作。
今天先写这么多下一次会写一个完整的卷积神经网络的代码, 同时解释如何使用tf在测试集上计算正确率