卷积神经网络在物联网场景中的应用初探

概述

机器学习发展到今天,得益于数据量的增长、算力的丰富、和深度神经网络技术的不断创新和广泛应用,像计算机视觉、自动控制、图像识别、语音识别、自然语言处理和音频识别等领域,在最近几年中,不断迎来突破。这直接导致了AI技术的蓬勃发展,像Alapha Go、自动翻译、自动驾驶汽车、机器人技术、无人机技术等前沿领域,不断涌现出新的“黑科技”,持续吸引着大众的眼球,成为资本争相追捧的宠儿。

针对最近物联网实验室关心的几个课题,我们尝试通过机器学习的手段,借助图像识别技术来解决一些自动分类和预警的问题,这些业务场景包括:

  • 火情识别

  • 高塔识别

希望通过解决这些问题,对机器学习在物联网领域中的应用进行一些探索,找到一些好的方案,在实际业务中提供帮助。

本文先介绍卷积神经网络的基本知识,在理解卷积神经网络的基础上,我们使用keras搭建起一个卷积神经网络,使用事先搜集的样本数据对网络进行训练,得到一个图像识别模型,并测试模型的准确性。

神经网络(Neural Network)

神经网络技术起源于上世纪五、六十年代,当时叫感知机(Perceptron),拥有输入层、输出层和一个隐藏层。输入的特征向量通过隐藏层变换后到达输出层,在输出层得到分类结果。

但感知机的拟合能力太弱了,对稍复杂一些的函数都无能为力。随着数学的发展,这个问题到了上世纪八十年代才被Rumelhart、Williams、Hinton、LeCun等人发明的多层感知机(Multilayer Perceptron,MP)克服。多层感知机,就是有多个隐藏层的感知机,它使用Sigmoid或Tanh等连续函数模拟神经元对激励的响应,在训练算法上则使用反向传播(Back Propagation,BP)算法,使得各个神经元的参数在训练过程中能够不断被调整优化。

多层感知机,其实就是神经网络(Neural Network,NN)了,它具备几个很明显的特点。

  • 有一个输入层

  • 有一个输出层

  • 有一个或多个隐藏层,隐藏层越多,神经网络越深

神经网络(Neural Network)

神经网络的层数,直接关系到它对现实问题的刻画能力,换句话说就是足够多的层数,足够多的神经元,经过合理的组织和训练,能够拟合任意复杂的函数。理论证明,有两层隐藏层的神经网络可以无限逼近任意连续函数。隐藏层越多,拟合能力越强,拥有很多隐藏层的神经网络,我们称为深度神经网络(Deep Neural Network,DNN)。那有多少隐藏层算深?没有统一答案,视场景而定,在语音识别中4层网络就被认为是较深的,而在自动驾驶、增强学习中20层以上的网络屡见不鲜。

全连接神经网络应用于图像识别遇到的问题

在神经网络中每条边都代表着神经元之间的参数,它在训练过程中将被不断调整优化。实际工作中,最常见的网络是全连接神经网络,每相邻两层之间下层神经元和上层所有神经元都能够形成连接,带来的潜在问题是参数数量的膨胀。

考虑一个数字识别的神经网络,假设输入的是一幅像素为28×28的灰度图像,那么输入层的神经元就有28×28=784个,输出层有0-9共10个神经元,代表着图像被识别为哪个数字,如果在中间只使用一层15个神经元的隐藏层,那么参数就有 28×28×15×10=117600 个。看下图感受一下这些边有多密集。

全连接神经网络

如果输入图像包含RGB三个色彩通道,那么参数的数量还要再乘以3,如果在隐藏层中再增加几个神经元,或再增加几个隐藏层,那么需要训练的参数数量会继续膨胀。

参数膨胀还会带来另一个问题,如果在训练过程中使用梯度下降,那么参数泛滥会导致解空间中出现大量局部最优解,梯度下降将极易陷入局部最优解中而导致训练过程早早收敛,很难收到好的效果。

另一方面,每一层都全部接收了上一层的所有特征,没有结合图像固有的模式提取出一些关键特征,这很容易走向过拟合,使模型失去泛化能力。

总结起来,全连接神经网络在处理图像识别问题时有如下几个弊端:

  • 参数膨胀,计算量庞大,性能较差

  • 极易陷入局部最优解

  • 很容易走向过拟合

卷积神经网络

卷积神经网络(Convolutional Neural Network,CNN)中,卷积层的神经元只与前一层的部分神经元节点相连,即它的神经元间的连接是非全连接的,且同一层中某些神经元之间的连接的权重 w 和偏移量 b 是共享的,这样大大地减少了需要训练的参数数量。

卷积神经网络CNN的结构一般包含这几个层:

  • 输入层:用于数据的输入
  • 卷积层:使用卷积核进行特征提取和特征映射
  • 激活层:由于卷积也是一种线性运算,因此需要增加非线性映射,使模型具备拟合非线性函数的能力
  • 池化层:进行采样,提取关键特征,对特征图稀疏处理,减少数据运算量
  • 全连接层:在神经网络尾部进行重新拟合,减少特征信息的损失
  • 输出层:用于输出结果

在不同场景中,还可以使用一些其他的功能层:

  • 归一化层(Batch Normalization):对特征进行归一化
  • 切分层:对某些数据(比如图片)进行分区域的单独学习
  • 融合层:对独立进行特征学习的分支进行融合

输入层和输出层

对于图像识别的问题来讲,通常卷积神经网络的数据输入格式与全连接神经网络的输入层不一样,我们希望保留原始图片的结构,假设图片的尺寸是28×28,每个像素有3个颜色通道(RGB),那么图片的数据结构如下图。

图像的像素

如果希望每次迭代输入100张图片,那么输入层的张量结构就是(100, 28, 28, 3)。

对应于输入层的数据,输出层就是输入图片所属的分类标签,输入了100张图片,那么输出层就是这100张图片所属的分类标签。

卷积层

卷积神经网络的理论中有个概念叫感受视野(local receptive fields),比如我们定义一个5×5的区域,隐藏层的神经元与输入层的5×5个神经元相连,这个5×5的区域就称之为感受视野,如下图所示:

感受视野(local receptive fields)

换一个角度理解,隐藏层中的神经元具有一个固定大小的感受视野去感受上一层的部分特征。从这个角度看来,在全连接神经网络中,隐藏层中的神经元的感受视野足够大以至于可以看到上一层的所有特征,所以它不需要卷积。

而在卷积神经网络中,隐藏层中的神经元的感受视野比较小,只能看到上一层的部分特征,可以通过平移感受视野来得到上一层的其他特征,从而得到同一层的其他神经元,如下图:

平移感受视野

可以看出,卷积层的神经元是只与前一层的部分神经元节点相连,每一条相连的线对应一个权重w。

一个感受视野带有一个卷积核,我们将感受视野中的权重 w 矩阵称为卷积核(convolutional kernel),将感受视野对输入的扫描间隔称为步长(stride),当步长比较大时(stride>1),为了扫描到边缘的一些特征,感受视野可能会“出界”,这时需要对边界扩充(pad),边界扩充可以设为 0 或其他值。

我们可以定义感受视野的大小,也就是卷积核的大小,卷积核的权重矩阵的值便是卷积神经网络的参数,卷积核可附带一个偏移项 b ,它们的初值可以随机生成或填充为 0,将会通过训练进行调整。

那么,感受视野扫描时计算出下一层神经元的值的公式如下:

对下一层的所有神经元来说,它们从不同的位置去探测了上一层神经元的特征。

我们将通过一个带有卷积核的感受视野扫描生成的下一层神经元矩阵称为一个特征映射图(feature map),同一个特征映射图上的神经元使用的卷积核是相同的,因此这些神经元共享卷积核中的权值和偏移量。

正是有了感受视野和共享参数,所以我们需要训练的参数大大减少了,可以节省很大的计算量。

在图像识别这个场景中,卷积层的工作原理,就是定义一个卷积核矩阵(常用正态分布矩阵),以一定的步长,用卷积核矩阵的长和宽对图像进行采样,把图像切分成很多个区域(感受视野,边缘通常填充为0),使用卷积核矩阵与这些区域的值做内积运算(两两相乘再求和),得到这些区域的特征映射图,这就是所谓的卷积运算,卷积神经网络的名字也源于此。

如果觉得这段描述太抽象,形象的图解如下:

卷积运算

定义卷积行为的有几个参数,下面这个图就是一个步长为2,卷积核是3x3,深度是2(有两个卷积核)的卷积层。

卷积运算的过程

所以对卷积层的训练,其实就是在训练卷积核的参数(包括权重和偏移量),从另一个角度理解,就是找到合适的方式,把图像进行重新采样,并提取出特征,相当于以更有利于计算的方式重新刻画了这个图像。

激活层

激活层主要对卷积层的输出进行一个非线性映射,因为从卷积层的卷积计算公式可以看出卷积运算还是一种线性计算,现实中线性计算所能解决的问题非常有限,因为并非所有问题都是线性可分的,只有对线性计算的结果进行非线性映射,才有可能拟合各种各样的函数,所以激活层在神经网络中扮演着非常重要的角色。

激活函数通常需要具备如下的性质:

  • 非线性: 当激活函数是非线性的时候,一个两层的神经网络就可以逼近基本上所有的函数了

  • 可微性: 当优化方法是基于梯度的时候,这个性质是必须的,反向传播的过程需要依靠这个性质来更新神经元的参数

  • 单调性: 当激活函数是单调时,单层神经网络的误差函数是凸函数,好优化

传统神经网络中常用Sigmoid系(Logistic-Sigmoid、Tanh-Sigmoid)的激活函数,这是个很经典的激活函数。从数学上来看,非线性的Sigmoid函数对中央区的信号增益较大,对两侧区的信号增益小,在信号的特征空间映射上,可以很好的将线性结果映射为非线性结果(通俗地讲就是将直线扭曲为曲线)。从神经科学上来看,中央区类似神经元的兴奋态,两侧区类似神经元的抑制态,因而在神经网络学习时,可以将重点特征推向中央区(通过映射可以被激活放大),而非重点特征推向两侧区(通过映射将被抑制)。

激活函数

Relu函数是现在深度学习中使用比较广泛的激活函数,相比于Sigmoid系激活函数,其优点在于计算简单,导数简单,收敛快,单侧抑制,相对宽阔的兴奋边界,稀疏激活性(在负半区的导数为0,节省计算量)。

Relu函数

池化层

经过卷积层和激活层计算后,若感受视野比较小,或步长比较小,得到的特征映射图还是比较大的,所以还需要通过池化层来对每一个特征映射图进行降维操作,输出的深度还是不变的,仍然是特征映射图的个数。

在这个降维操作中,我们需要定义一个类似感受视野的过滤矩阵来对特征映射图矩阵进行扫描,对过滤矩阵中的值进行计算,一般有两种计算方式:

  • Max pooling:取过滤矩阵中的最大值

  • Average pooling:取过滤矩阵中的平均值

池化层

扫描的过程与卷积层类似,每一个特征映射图都会得到一个降维后的特征矩阵。

池化的目的主要有两个

  • 进行特征压缩,提取主要特征,提高网络的鲁棒性,防止过拟合

  • 使用特征图变小,降低网络计算复杂度

下面这个例子以 2X2 为单位进行池化,为了使特征更突出,使用了最大化池化(这个过程有点类似于主成分分析)。

最大化池化

全连接层

全连接层包含一系列分类器(如softmax分类器),以上所有层进行计算的结果,得到了一系列特征矩阵,这些特征矩阵将被重新映射为一维向量,将这个一维向量输入到全连接层的分类器进行计算,得到这些特征属于各个分类的概率,这也就是整个卷积神经网络的输出层。

准备数据

我们的目标是从输入的图片中,自动识别包含火焰和包含高塔的图片。所以我们通过各种途径搜集了一批包含火焰和包含高塔的图片,并人工给它们做好了分类。

类别 标签 用途 数量 目录
火焰 00001 训练 900 image/train/00001
高塔 00002 训练 583 image/train/00002

先对样本数据来一个预览,每个分类加载若干张图片,看看数据是什么样的。

import numpy as np
import pandas as pd
import os

work_dir = 'D:/ml/cr-fire-warning'
assert os.path.exists(work_dir), '工作目录不存在'

data_dir = os.path.join(work_dir, 'data')
assert os.path.exists(data_dir), '样本目录不存在'

train_dir = os.path.join(data_dir, 'train')
assert os.path.exists(train_dir), '训练样本目录不存在'

test_dir = os.path.join(data_dir, 'test')
assert os.path.exists(test_dir), '测试样本目录不存在'

model_dir = os.path.join(work_dir, 'model')
if not os.path.exists(model_dir):
    os.mkdir(model_dir)

# Keras模型
model_name_keras = 'recognize_model_keras.h5'
# TensorFlow模型
model_name_tf = 'recognize_model_tf.pb'

os.chdir(work_dir)
import matplotlib.pyplot as plt
import matplotlib.pylab as pylab

# 设置画图参数
params = {
        'axes.titlesize': '18',
        'axes.labelsize': '13',
        'xtick.labelsize': '13',
        'ytick.labelsize': '13',
        'lines.linewidth': '2',
        'legend.fontsize': '13',
        'figure.figsize': '6, 5',
#         'figure.facecolor': 'white',
        'figure.facecolor': 'snow',
        # 正常显示中文
        'font.sans-serif': 'SimHei',
        # 正常显示负号
        'axes.unicode_minus': False
    }
pylab.rcParams.update(params)
import skimage.data
import skimage.transform
from PIL import Image

# 加载图片数据
def load_data(label_image_dir, limit_in_class = None, transform = False, image_width = 32, image_height = 32):
    assert os.path.exists(label_image_dir), '样本目录不存在'

    labels = []
    images = []
    dirs = [os.path.join(label_image_dir, dir)
        for dir in os.listdir(label_image_dir) if dir.startswith("0")]
    
    for dir in dirs:
        filenames = [os.path.join(dir, f)
                           for f in os.listdir(dir)
                               if f.lower().endswith(".jpg") or f.lower().endswith(".jpeg")
                          ]
        for i in range(0, len(filenames)):
            if (limit_in_class != None and i >= limit_in_class):
                break
            
            filename = filenames[i]
            
            file_type = Image.open(filename).format
            if (file_type != 'JPEG'):
                print('无效图片格式:' + filename, '文件类型:' + file_type)
                continue
                
            img = skimage.data.imread(filename)
            # 统一成 32*32 的图像
            if (transform):
                img = skimage.transform.resize(img, (image_width, image_height), mode='constant')
            
            if not hasattr(img[0][0], "__len__"):
                print('无效图片:' + filename)
                continue
                
            images.append(img)
            labels.append(int(os.path.basename(dir)[:5]))
            
    return images, labels
# 加载图片
images, labels = load_data(train_dir, limit_in_class = 6, transform = False)
labels_preview = np.array(labels)
images_preview = np.array(images)

# 显示图片
plt.figure(figsize=(12, 200))
for i in range(0, len(labels_preview)):
    plt.subplot(50, 3, i + 1)
    plt.title("标签 {0}".format(labels_preview[i]))
    plt.axis('off')
    plt.imshow(images_preview[i])

plt.show()
样本数据

原始图片尺寸、格式、分辨率都不一样,在输入到神经网络之前,我们还需要统一做一个转换。

构建卷积神经网络

我们来构建一个两个卷积层,一个全连接层的神经网络。

(输入层)

-->(卷积层1) --> (激活层1) --> (池化层1) -->

-->(卷积层2) --> (激活层2) --> (池化层2) -->

-->(全连接层)

--> (输出层)

  • 输入层:我们希望保留原始图片的结构,先把样本图片统一转换成32×32尺寸,每个像素有3个颜色通道,我们希望每次迭代输入100张图片,那么输入层的张量结构就是(100, 32, 32, 3)
  • 卷积层1:使用一个5×5的正态分布的卷积核,对输入层数据进行卷积运算
  • 激活层1:使用ReLu激活函数
  • 池化层1:使用一个2×2的过滤矩阵,对上一层的特征映射图矩阵进行降维处理,为了突出图像特征,池化层使用max pool
  • 卷积层2:使用一个5×5的正态分布的卷积核,对上一层池化层的结果进行卷积运算
  • 激活层2:使用ReLu激活函数
  • 池化层2:使用一个2×2的过滤矩阵,对上一层的特征映射图矩阵进行降维处理,为了突出图像特征,池化层使用max pool
  • 全连接层:把上一层的结果重新映射成一维特征向量,再使用一个dropout运行把特征向量映射到一个1024长度的向量中,并且把概率小于0.4的丢掉
  • 输出层:把上一层计算的结果,通过softmax分类器进行分类,得到属于各个分类的概率,取得概率最大的分类,判定属于该分类

使用均方根优化器RMSprop(root mean square prop),主要目的是为了减缓参数下降时的摆动,并允许使用一个更大的学习率α,从而加快算法的迭代速率。

RMSprop(root mean square prop)是AdaGrad算法的改进,经验上,RMSProp被证明是有效且实用的深度学习网络优化算法。
在每轮迭代中,使用如下公式来更新网络参数。



w的在横轴上变化率很小,所以dw的值很小,所以Sdw也小,而b在纵轴上波动很大,所以斜率在b方向上特别大。在这些微分中,db较大,dw较小。这样W减去一个较小的数,总体来说,W的变化很大。而b的除数是一个较大的数,这样b的更新就会被减缓,纵向的变化相对平缓。
总的来说,不论w波动幅度大还是b波动幅度大,这个公式总是能够把幅度大的调小,幅度小的调大,从而达到减缓参数摆动的效果,防止参数调整的过程中总是在目标点附近摆动。
实际使用中为了防止除0,在分母中加上常数ϵ,使用如下公式:

使用了ReduceLROnPlateau来动态更新学习率α,在迭代的过程中,监测准确率val_acc的值,如果若干轮迭代之后,值仍没有变化,就按照一定的幅度动态调整学习率,以加快迭代速率。

另外在训练的过程中,我们还使用ImageDataGenerator来对输入的图片随机做缩放、旋转、漂移的操作,以增加图像的多样化,提升模型的泛化能力,也可以有效的避免模型的过拟合。

import numpy as np
from keras.models import Sequential
from keras.layers import Conv2D, MaxPool2D, Dense, Dropout, Activation, Flatten
from keras.optimizers import RMSprop
from keras.preprocessing.image import ImageDataGenerator
from keras.callbacks import ReduceLROnPlateau
from keras.utils import np_utils

# 定义变量
target_classes = 3 # 生成的目标分类

image_width = 32 # 图像宽度
image_height = 32 # 图像高度
num_channels = 3 # 3 个颜色通道

conv1_filters = 32 # 第一层训练 32 个特征映射图
conv1_kernel_size = (5, 5) # 第一层卷积核大小
conv2_kernel_size = (5, 5) # 第二层卷积核大小
conv2_filters = 64 # 第二层训练 64 个特征映射图
max_pool_size1 = 2 # 第一个 max pool 的大小
max_pool_size2 = 2 # 第二个 max pool 的大小
flatten_size = 256 # 全连接层神经元数量
dropout_rate = 0.25 # 随机失活比率

batch_size = 128 #每批输入神经网络的数据量
epochs = 100 #迭代次数

# 设置为学习模式
from keras import backend as K
K.set_learning_phase(0)

input_shape = [image_width, image_height, num_channels]

# np.random.seed(1337)

#构建模型
model = Sequential()
# 第一个卷积层,32个卷积核,大小5x5,卷积模式SAME,激活函数relu,输入张量的大小
model.add(Conv2D(input_shape=input_shape, filters= conv1_filters, kernel_size=(conv1_kernel_size[0], conv1_kernel_size[1]),
                 padding='Same', activation='relu'))
# 池化层
model.add(MaxPool2D(pool_size=(max_pool_size1, max_pool_size1)))
# 随机失活,丢弃一部分网络连接,防止过拟合
model.add(Dropout(dropout_rate))

model.add(Conv2D(filters= conv2_filters, kernel_size=(conv2_kernel_size[0], conv2_kernel_size[1]), padding='Same', activation='relu'))
model.add(MaxPool2D(pool_size=(max_pool_size2, max_pool_size2), strides=(2,2)))
model.add(Dropout(dropout_rate))

# 全连接层,展开操作
model.add(Flatten())
# 添加全连接层和激活函数
model.add(Dense(flatten_size, activation='relu'))
model.add(Dropout(dropout_rate))

# 输出层
model.add(Dense(target_classes, activation='softmax'))

# #编译模型
# model.compile(loss='categorical_crossentropy',
#               optimizer='adadelta',
#               metrics=['accuracy'])

# 使用均方根优化器RMSprop (root mean square prop)
# lr :学习效率, decay :lr的衰减值
optimizer = RMSprop(lr = 0.001, decay=0.0)

# 编译模型
# loss:损失函数,metrics:对应性能评估函数
model.compile(optimizer=optimizer, loss = 'categorical_crossentropy', metrics=['accuracy'])

# keras的callback类提供了可以跟踪目标值,和动态调整学习效率
# moitor : 要监测的量,这里是验证准确率
# patience: 当经过3轮的迭代,监测的目标量,仍没有变化,就会调整学习效率
# verbose : 信息展示模式,取0或1
# factor : 每次减少学习率的因子,学习率将以lr = lr*factor的形式被减少
# mode:‘auto’,‘min’,‘max’之一,在min模式下,如果检测值触发学习率减少。在max模式下,当检测值不再上升则触发学习率减少。
# epsilon:阈值,用来确定是否进入检测值的“平原区”
# cooldown:学习率减少后,会经过cooldown个epoch才重新进行正常操作
# min_lr:学习率的下限
learning_rate_reduction = ReduceLROnPlateau(monitor = 'val_acc', patience = 3,
                                            verbose = 1, factor=0.5, min_lr = 0.00001)

# 数据增强处理,提升模型的泛化能力,也可以有效的避免模型的过拟合
# rotation_range : 旋转的角度
# zoom_range : 随机缩放图像
# width_shift_range : 水平移动占图像宽度的比例
# height_shift_range 
# horizontal_filp : 水平反转
# vertical_filp : 纵轴方向上反转
data_augment = ImageDataGenerator(rotation_range= 10, zoom_range= 0.1,
                                  width_shift_range = 0.1, height_shift_range = 0.1,
                                  horizontal_flip = False, vertical_flip = False)
Using TensorFlow backend.

训练神经网络

神经网络搭建好了,我们把样本数据输入到神经网络中进行训练。

训练的步骤如下:

  1. 加载所有分类的样本图片,统一转换为32×32的尺寸,每张图片有RGB一共3个颜色通道
  2. 将数据切分为训练集和测试集,并对每一个像素值进行归一化处理
  3. 将训练集数据输入到神经网络中进行迭代训练
  4. 训练完成后,画出loss和accuracy曲线,观察训练是否收敛,loss和accuracy是否满足要求
  5. 使用模型对测试数据进行预测,根据预测结果计算混淆矩阵,评估模型效果
from sklearn.model_selection import train_test_split

images, labels = load_data(train_dir, limit_in_class = None, transform = True, image_width = image_width, image_height = image_height)
images = np.array(images)
labels = np.array(labels)


# 从训练数据中分出十分之一的数据作为验证数据
X_train , X_test , y_train, y_test = train_test_split(images, labels, test_size=0.1, random_state=3)

X_train = X_train.astype('float32')
X_test = X_test.astype('float32')
X_train /= 255
X_test /= 255
print('训练集样本数', X_train.shape[0])
print('测试集样本数', X_test.shape[0])

# 转换为one_hot类型
Y_train = np_utils.to_categorical(y_train, target_classes)
Y_test = np_utils.to_categorical(y_test, target_classes)
无效图片格式:D:/ml/cr-fire-warning\data\train\00001\2105469964_9022d70cab.jpg 文件类型:PNG
无效图片:D:/ml/cr-fire-warning\data\train\00002\tower (12).jpg
训练集样本数 1331
测试集样本数 148
#训练模型
history = model.fit_generator(data_augment.flow(X_train, Y_train, batch_size=batch_size), epochs=epochs,
          callbacks=[learning_rate_reduction], verbose=1, validation_data=(X_test, Y_test))

#评估模型
score = model.evaluate(X_test, Y_test, verbose=1)

print('Test score:', score[0])
print('Test accuracy:', score[1])
Epoch 1/100
11/11 [==============================] - 8s 724ms/step - loss: 0.7897 - acc: 0.5528 - val_loss: 0.7190 - val_acc: 0.5608
Epoch 2/100
11/11 [==============================] - 7s 667ms/step - loss: 0.6974 - acc: 0.5781 - val_loss: 0.7217 - val_acc: 0.5608
Epoch 3/100
11/11 [==============================] - 7s 671ms/step - loss: 0.6339 - acc: 0.6398 - val_loss: 0.4619 - val_acc: 0.8514

Epoch 99/100
11/11 [==============================] - 7s 669ms/step - loss: 0.1609 - acc: 0.9417 - val_loss: 0.1391 - val_acc: 0.9730
Epoch 100/100
11/11 [==============================] - 7s 669ms/step - loss: 0.1494 - acc: 0.9464 - val_loss: 0.1383 - val_acc: 0.9662
148/148 [==============================] - 0s 3ms/step
Test score: 0.138349657627
Test accuracy: 0.966216216216

把损失函数的值画出来,看看训练过程是否收敛。

def p():
    fig,ax = plt.subplots(2,1,figsize=(10,10))
    ax[0].plot(history.history['loss'], color='r', label='Training Loss')
    ax[0].plot(history.history['val_loss'], color='g', label='Validation Loss')
    ax[0].legend(loc='best',shadow=True)
    ax[0].grid(True)


    ax[1].plot(history.history['acc'], color='r', label='Training Accuracy')
    ax[1].plot(history.history['val_acc'], color='g', label='Validation Accuracy')
    ax[1].legend(loc='best',shadow=True)
    ax[1].grid(True)

    plt.show()
    
plot_learning_curves()
import itertools
from sklearn.metrics import confusion_matrix

# 混淆矩阵
def plot_confusion_matrix(cm, classes, normalize=False, title='混淆矩阵',cmap=plt.cm.Blues):
    plt.imshow(cm, interpolation='nearest', cmap=cmap)
    plt.title(title)
    plt.colorbar()
    tick_marks = np.arange(len(classes))
    plt.xticks(tick_marks, classes, rotation=45)
    plt.yticks(tick_marks, classes)
    if normalize:
        cm = cm.astype('float')/cm.sum(axis=1)[:, np.newaxis]
    thresh = cm.max()/2.0
    for i,j in itertools.product(range(cm.shape[0]), range(cm.shape[1])):
        plt.text(j,i,cm[i,j], horizontalalignment='center',color='white' if cm[i,j] > thresh else 'black')
    plt.tight_layout()
    plt.ylabel('真值')
    plt.xlabel('预测值')
    
    plt.show()

    
pred_y = model.predict(X_test)
pred_label = np.argmax(pred_y, axis=1)

cm = confusion_matrix(y_test, pred_label)

plot_confusion_matrix(cm, classes = range(4))
混淆矩阵

保存模型

训练完成后,我们得到了神经网络中所有神经元之间连接的权重和偏移量参数,这就是我们的训练成果,我们把它作为模型保存起来,这样下次要进行预测时直接加载模型即可,不用每次都重新训练模型。

我们使用的是keras计算框架,调用模型自身的保存接口,保存模型。keras自身保存的模型文件格式是hdf5。

为方便模型的传递交换和多语言调用,我们把神经网络的结构和所有定义的变量、所有训练得到的参数都固化下来,以protocal buffer(pb)格式,保存到TensorFlow可以解析的单独模型文件中。

# 使用keras自身的接口保存模型
def save_model_keras(model, model_dir, model_name):
    model_path = os.path.join(model_dir, model_name)
    model.save(model_path)
    
    return model_path

# 保存keras模型
save_model_keras(model, model_dir, model_name_keras)
'D:/ml/cr-fire-warning\\model\\recognize_model_keras.h5'
import tensorflow as tf

def freeze_session(session, keep_var_names=None, output_names=None, clear_devices=True):
    """
    Freezes the state of a session into a prunned computation graph.

    Creates a new computation graph where variable nodes are replaced by
    constants taking their current value in the session. The new graph will be
    prunned so subgraphs that are not neccesary to compute the requested
    outputs are removed.
    @param session The TensorFlow session to be frozen.
    @param keep_var_names A list of variable names that should not be frozen,
                          or None to freeze all the variables in the graph.
    @param output_names Names of the relevant graph outputs.
    @param clear_devices Remove the device directives from the graph for better portability.
    @return The frozen graph definition.
    """
    from tensorflow.python.framework.graph_util import convert_variables_to_constants
    graph = session.graph
    with graph.as_default():
        freeze_var_names = list(set(v.op.name for v in tf.global_variables()).difference(keep_var_names or []))
        output_names = output_names or []
        output_names += [v.op.name for v in tf.global_variables()]
        input_graph_def = graph.as_graph_def()
        if clear_devices:
            for node in input_graph_def.node:
                node.device = ""
        frozen_graph = convert_variables_to_constants(session, session.graph_def, output_names)
#         frozen_graph = convert_variables_to_constants(session, input_graph_def, output_names, freeze_var_names)
        return frozen_graph
# 将模型保存为TensorFlow可以解析的pb格式
def save_model_tf(model_dir, model_name):
    frozen_graph = freeze_session(K.get_session(), output_names=[model.output.op.name])
#     tf.train.write_graph(frozen_graph, model_dir, model_name, as_text=False)
    from tensorflow.python.framework import graph_io
    graph_io.write_graph(frozen_graph, model_dir, model_name, as_text=False)
    return os.path.join(model_dir, model_name)
    
save_model_tf(model_dir, model_name_tf)
INFO:tensorflow:Froze 20 variables.
Converted 20 variables to const ops.
'D:/ml/cr-fire-warning\\model\\recognize_model_tf.pb'

使用模型进行预测

训练已经完成,我们得到一个模型,验证了模型的准确率,并且作为文件保存起来。接下来我们重新加载模型,使用模型对全新的图片进行预测,看看模型的表现怎么样。

import skimage.data
import skimage.transform
from PIL import Image

# 加载图片数据
def load_predict_data(image_dir, limit_in_class = None, transform = False, image_width = 32, image_height = 32):
    assert os.path.exists(image_dir), '样本目录不存在'

    images = []
    dirs = [image_dir]
    
    for dir in dirs:
        filenames = [os.path.join(dir, f)
                           for f in os.listdir(dir)
                               if f.lower().endswith(".jpg") or f.lower().endswith(".jpeg")
                          ]
        for i in range(0, len(filenames)):
            if (limit_in_class != None and i >= limit_in_class):
                break
            
            filename = filenames[i]
            
            file_type = Image.open(filename).format
            if (file_type != 'JPEG'):
                print('无效图片格式:' + filename, '文件类型:' + file_type)
                continue
                
            img = skimage.data.imread(filename)
            # 统一成 32*32 的图像
            if (transform):
                img = skimage.transform.resize(img, (image_width, image_height), mode='constant')
            
            if not hasattr(img[0][0], "__len__"):
                print('无效图片:' + filename)
                continue
                
            images.append(img)
            
    return images
predict_dir = os.path.join(data_dir, 'predict')
assert os.path.exists(predict_dir), '图片目录不存在'

predict_images = load_predict_data(predict_dir, transform = True, image_width = image_width, image_height = image_height)
predict_images_a = np.array(predict_images)
image_predict = predict_images_a.astype('float32')
image_predict /= 255

print("images shape {0}".format(image_predict.shape))
images shape (13, 32, 32, 3)
model_path_keras = os.path.join(model_dir, model_name_keras)
assert os.path.exists(model_path_keras), '模型文件不存在'

from keras.models import load_model
model_keras = load_model(model_path_keras)

def recognize_by_keras_model(model_keras, predict_data):
    predict_y = model_keras.predict(predict_data)
    predict_labels = np.argmax(predict_y, axis=1)
    
    return predict_labels
predicted_labels = recognize_by_keras_model(model_keras, image_predict)
print(predicted_labels)
[1 1 1 2 2 2 2 2 1 2 2 1 1]
# 定义神经网络的输入层
# 图片的大小是32*32*3的,对应图片的是长宽和三色通道,第一个参数表示一批数据的大小。
image_input_shape = [None, image_width, image_height, num_channels]
images_ph = tf.placeholder(tf.float32, image_input_shape)

input_name = model.input.name
output_name = model.output.name

def load_tf_model(model_path):
    with tf.Session() as sess:
        with open(model_path, 'rb') as f:
            graph_def = tf.GraphDef()
            graph_def.ParseFromString(f.read())
    output = tf.import_graph_def(graph_def, input_map={input_name:images_ph}, return_elements=[output_name], name='')

    return (sess, output)
model_path_tf = os.path.join(model_dir, model_name_tf)
assert os.path.exists(model_path_tf), '模型文件不存在'
print(model_path_tf)

(sess, output) = load_tf_model(model_path_tf)
D:/ml/cr-fire-warning\model\recognize_model_tf.pb
def recognize_by_tf_model(session, output, predict_data):
    feed_dict = {images_ph: predict_data}
    predict_y = session.run(output, feed_dict=feed_dict)[0]
    print(predict_y)
    predict_labels = np.argmax(predict_y, axis=1)

    return predict_labels

predicted_labels = recognize_by_tf_model(sess, output, image_predict)
print(predicted_labels)
[[  4.58848517e-05   9.88525569e-01   1.14285639e-02]
 [  1.96375791e-02   6.36299133e-01   3.44063252e-01]
 [  2.28518602e-05   9.92211938e-01   7.76528846e-03]
 [  1.33850204e-03   3.42030311e-03   9.95241165e-01]
 [  6.35846576e-04   6.99755270e-03   9.92366612e-01]
 [  6.32785785e-04   3.95553000e-03   9.95411694e-01]
 [  6.32785785e-04   3.95553000e-03   9.95411694e-01]
 [  1.27463706e-03   6.01934362e-03   9.92706001e-01]
 [  1.28740809e-04   9.79152739e-01   2.07185037e-02]
 [  3.23435441e-02   2.13835035e-02   9.46272910e-01]
 [  1.83394831e-02   2.83331811e-01   6.98328733e-01]
 [  7.92038257e-09   9.99818265e-01   1.81791780e-04]
 [  2.32125126e-06   9.95894074e-01   4.10356373e-03]]
[1 1 1 2 2 2 2 2 1 2 2 1 1]
# 可视化预测结果
def plot_predicted():
    fig = plt.figure(figsize=(30, 10))
    for i in range(len(predict_images_a)):
        prediction = predicted_labels[i]
        plt.subplot(5, 5, 1+i)
        plt.axis('off')
        color = 'blue'
        plt.text(40, 15, "预测分类 {0}".format(prediction), fontsize=16, color=color)
        plt.imshow(predict_images_a[i])

    plt.show()
    
plot_predicted()
预测结果
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,723评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,485评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,998评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,323评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,355评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,079评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,389评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,019评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,519评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,971评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,100评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,738评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,293评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,289评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,517评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,547评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,834评论 2 345

推荐阅读更多精彩内容