1、Tensorflow的 基础概念: (1)计算图(grap):tensorflow是一个基于计算图的数值计算系统,计算图是一个有向图,图中的节点代表数学操作计算中的节点(op),结点之间连接的边代表参与计算的高维数组数据,叫做tensor,计算图的执行可以看作tensor按照图的拓扑顺序,从输入节点逐步流过所有中间节点,最后流到输出节点的过程,tensorflow名称由此而来。Tensorflow计算图如图所示。
(2)张量(tensor):这一术语源于力学,在物理和数学中都有重要作用,在tensorflow系统中,tensor代表多维数组,对应神经网络计算中的高维矩阵。Tensor可以有任意维度,每个维度可以有任意长度。特别来说,一维tensor就算向量,二维tensor是矩阵。Tensorflow中一般用四维tensor表示一个mini-batch图片,四个维度分别是批大小、像素行数、像素列数、通道数。Tensor中的元素可以是任意内置类型,常用的有:int32、int64、float32、float64等。对于神经网络来说,float32是最常用的数据类型。 (3)算子(op):算子op是参与计算的基本单位,每个算子都对应一种数学运算,比如矩阵加法,矩阵乘法等。算子接收0个或多个tensor作为输入,进行一定的计算,输出0个或多个tensor的结果。在现实中,一些算子可以带有特定的属性,比如说矩阵运算算子带有数据类型dtype属性,设置数据类型可以实现计算的多态,既可以完成int32类型数据的运算,又可以完成float64类型的运算。 (4)变量(Variable):与其他一些使用数据流作为计算的抽象系统有所不同的是,tensorflow的计算图对有向图的概念进行了一些扩展,加入了图的状态表示。对于神经网络等算法来说,有向图可以表示算法的结构,但图中的权值参数并不是固定不变的,而是通过训练不断调整更新的。所谓图的状态就是图中所有参数的组合。在tensorflow中,以变量(Variable)来存储参数值。变量可以和tensor一样参与各种的运算,区别在于tensor的值在每次计算图执行完成后都会被丢弃,而变量的值在通过反响传播计算更新后会保留下来,带入到下一轮训练迭代。 (5)Session会话:session是驱动tensorflow系统执行计算交互的入口。Session负责完成多计算设备或集群分布式的节点布置和数据传输节点的添加,并负责将子图分配给相应的执行器单位来运行。从使用角度来说,典型的用法是客户端通过CreateSession接口与master建立连线,并在初始会话的过程中传入计算图。对于python接口,计算图可以在session创建之前完成,并在tf.Session对象初始化时载入到后端执行引擎。Session还提供了Extend的接口,可以在会话中修改计算图。触发计算图的执行通过run接口,也就算python的tf.Session.run()方法。通过这个接口,可以将数据代入模型,执行计算并得到执行结果。训练过程由客户端的循环来控制,一般情况下,都会在一个会话中通过Run接口执行成千上万次的计算。Session管理了运行时的一系列资源的申请和分配,所以在计算完成后,必须要关闭Session以释放资源。
2、MNIST手写字体数据库 MNIST数据集是一个手写字体数据集,数据集中每一个样本都是0~9的手写数字,该数据集由4部分组成:训练图片集,训练标签集,测试图片集和测试标签集。其中数据集中有60000个样本,测试集中有10000个样本,每张图片均由28x28大小的灰度图像组成。为了便于存储,官方对MNIST数据集进行了集中处理,将每一张图片拉伸成(1,784)的向量表示。 首先根据数据库官方给出的参数设置手写字体数据图片的大小和分类大小,在代码中使用mnist.load_data()函数从Amazon服务器中下载数据集,并把数据集分为训练图片集、训练标签集、测试图片集和测试标签集。在读入训练图片和测试图片集后,还会对图片进行x/255的计算操作,目的是将图片像素归一化处理在[0,1]区间,方便将数据输入神经网络模型中进行计算。
train_x, train_y), (test_x, test_y) = mnist.load_data() train_x = train_x.reshape(train_x.shape[0], 28, 28, 1) / 255 test_x = test_x.reshape(test_x.shape[0], 28, 28, 1) / 255 train_y = np_utils.to_categorical(train_y, num_classes=10) test_y = np_utils.to_categorical(test_y, num_classes=10)(
3、Keras核心函数详解: (1) Conv2D: keras.layers.Conv2D(filters, kernel_size, strides=(1, 1), padding='valid', data_format=None, dilation_rate=(1, 1), activation=None, use_bias=True, kernel_initializer='glorot_uniform', bias_initializer='zeros', kernel_regularizer=None, bias_regularizer=None, activity_regularizer=None, kernel_constraint=None, bias_constraint=None)
2D 卷积层 (例如对图像的空间卷积)。 该层创建了一个卷积核, 该卷积核对层输入进行卷积, 以生成输出张量。 如果 use_bias 为 True, 则会创建一个偏置向量并将其添加到输出中。 最后,如果 activation 不是 None,它也会应用于输出。 当使用该层作为模型第一层时,需要提供 input_shape 参数 (整数元组,不包含样本表示的轴),例如, input_shape=(128, 128, 3) 表示 128x128 RGB 图像, 在 data_format=“channels_last” 时。 参数 filters: 整数,输出空间的维度 (即卷积中滤波器的输出数量)。 kernel_size: 一个整数,或者 2 个整数表示的元组或列表, 指明 2D 卷积窗口的宽度和高度。 可以是一个整数,为所有空间维度指定相同的值。 strides: 一个整数,或者 2 个整数表示的元组或列表, 指明卷积沿宽度和高度方向的步长。 可以是一个整数,为所有空间维度指定相同的值。 指定任何 stride 值 != 1 与指定 dilation_rate 值 != 1 两者不兼容。 padding: “valid” 或 “same” (大小写敏感)。 data_format: 字符串, channels_last (默认) 或 channels_first 之一,表示输入中维度的顺序。 channels_last 对应输入尺寸为 (batch, height, width, channels), channels_first 对应输入尺寸为 (batch, channels, height, width)。 它默认为从 Keras 配置文件 ~/.keras/keras.json 中 找到的 image_data_format 值。 如果你从未设置它,将使用 channels_last。 dilation_rate: 一个整数或 2 个整数的元组或列表, 指定膨胀卷积的膨胀率。 可以是一个整数,为所有空间维度指定相同的值。 当前,指定任何 dilation_rate 值 != 1 与 指定 stride 值 != 1 两者不兼容。 activation: 要使用的激活函数 (详见 activations)。 如果你不指定,则不使用激活函数 (即线性激活: a(x) = x)。 use_bias: 布尔值,该层是否使用偏置向量。 kernel_initializer: kernel 权值矩阵的初始化器 (详见 initializers)。 bias_initializer: 偏置向量的初始化器 (详见 initializers)。 kernel_regularizer: 运用到 kernel 权值矩阵的正则化函数 (详见 regularizer)。 bias_regularizer: 运用到偏置向量的正则化函数 (详见 regularizer)。 activity_regularizer: 运用到层输出(它的激活值)的正则化函数 (详见 regularizer)。 kernel_constraint: 运用到 kernel 权值矩阵的约束函数 (详见 constraints)。 bias_constraint: 运用到偏置向量的约束函数 (详见 constraints)。 输入尺寸 如果 data_format=‘channels_first’, 输入 4D 张量,尺寸为 (samples, channels, rows, cols)。 如果 data_format=‘channels_last’, 输入 4D 张量,尺寸为 (samples, rows, cols, channels)。 输出尺寸 如果 data_format=‘channels_first’, 输出 4D 张量,尺寸为 (samples, filters, new_rows, new_cols)。 如果 data_format=‘channels_last’, 输出 4D 张量,尺寸为 (samples, new_rows, new_cols, filters)。 由于填充的原因, rows 和 cols 值可能已更改。 (2)MaxPooling2D: keras.layers.MaxPooling2D(pool_size=(2, 2), strides=None, padding='valid', data_format=None)
对于空间数据的最大池化。 参数 pool_size: 整数,或者 2 个整数表示的元组, 沿(垂直,水平)方向缩小比例的因数。 (2,2)会把输入张量的两个维度都缩小一半。 如果只使用一个整数,那么两个维度都会使用同样的窗口长度。 strides: 整数,2 个整数表示的元组,或者是 None。 表示步长值。 如果是 None,那么默认值是 pool_size。 padding: “valid” 或者 “same” (区分大小写)。 data_format: 字符串,channels_last (默认)或 channels_first 之一。 表示输入各维度的顺序。 channels_last 代表尺寸是 (batch, height, width, channels) 的输入张量, 而 channels_first 代表尺寸是 (batch, channels, height, width) 的输入张量。 默认值根据 Keras 配置文件 ~/.keras/keras.json 中的 image_data_format 值来设置。 如果还没有设置过,那么默认值就是 “channels_last”。 输入尺寸 如果 data_format=‘channels_last’: 尺寸是 (batch_size, rows, cols, channels) 的 4D 张量 如果 data_format=‘channels_first’: 尺寸是 (batch_size, channels, rows, cols) 的 4D 张量 输出尺寸 如果 data_format=‘channels_last’: 尺寸是 (batch_size, pooled_rows, pooled_cols, channels) 的 4D 张量 如果 data_format=‘channels_first’: 尺寸是 (batch_size, channels, pooled_rows, pooled_cols) 的 4D 张量 (3)Dropout: keras.layers.Dropout(rate, noise_shape=None, seed=None) 将 Dropout 应用于输入。 Dropout 包括在训练中每次更新时, 将输入单元的按比率随机设置为 0, 这有助于防止过拟合。 参数 rate: 在 0 和 1 之间浮动。需要丢弃的输入比例。 noise_shape: 1D 整数张量, 表示将与输入相乘的二进制 dropout 掩层的形状。 例如,如果你的输入尺寸为 (batch_size, timesteps, features),然后 你希望 dropout 掩层在所有时间步都是一样的, 你可以使用 noise_shape=(batch_size, 1, features)。 seed: 一个作为随机种子的 Python 整数。 (4)Flatten: keras.layers.Flatten(data_format=None) 将输入展平。不影响批量大小。 参数 data_format:一个字符串,其值为 channels_last(默认值)或者 channels_first。它表明输入的维度的顺序。此参数的目的是当模型从一种数据格式切换到另一种数据格式时保留权重顺序。channels_last 对应着尺寸为 (batch, …, channels) 的输入,而 channels_first 对应着尺寸为 (batch, channels, …) 的输入。默认为 image_data_format 的值,你可以在 Keras 的配置文件 ~/.keras/keras.json 中找到它。如果你从未设置过它,那么它将是 channels_last (5)Dense: keras.layers.Dense(units, activation=None, use_bias=True, kernel_initializer='glorot_uniform', bias_initializer='zeros', kernel_regularizer=None, bias_regularizer=None, activity_regularizer=None, kernel_constraint=None, bias_constraint=None)
就是你常用的的全连接层。 Dense 实现以下操作: output = activation(dot(input, kernel) + bias) 其中 activation 是按逐个元素计算的激活函数,kernel 是由网络层创建的权值矩阵,以及 bias 是其创建的偏置向量 (只在 use_bias 为 True 时才有用)。 注意: 如果该层的输入的秩大于2,那么它首先被展平然后 再计算与 kernel 的点乘。 4、LeNet5算法详解: LeNet-5出自论文Gradient-Based Learning Applied to Document Recognition,是一种用于手写体字符识别的非常高效的卷积神经网络。 本文将从卷积神经网络结构的基础说起,详细地讲解每个网络层。 (1)卷积神经网络(Convolutional Neural Network, CNN) 在讲解LeNet-5之前,让我们先看下CNN。卷积神经网络能够很好的利用图像的结构信息。LeNet-5是一个较简单的卷积神经网络。下图显示了其结构:输入的二维图像,先经过两次卷积层到池化层,再经过全连接层,最后使用softmax分类作为输出层。下面我们主要介绍卷积层和池化层。 卷积层:卷积层是卷积神经网络的核心基石。在图像识别里我们提到的卷积是二维卷积,即离散二维滤波器(也称作卷积核)与二维图像做卷积操作,简单的讲是二维滤波器滑动到二维图像上所有位置,并在每个位置上与该像素点及其领域像素点做内积。卷积操作被广泛应用与图像处理领域,不同卷积核可以提取不同的特征,例如边沿、线性、角等特征。在深层卷积神经网络中,通过卷积操作可以提取出图像低级到复杂的特征。
上图给出一个卷积计算过程的示例图,输入图像大小为H=5,W=5,D=3,即5×5大小的3通道(RGB,也称作深度)彩色图像。这个示例图中包含两(用K表示)组卷积核,即图中滤波器W0和W1。在卷积计算中,通常对不同的输入通道采用不同的卷积核,如图示例中每组卷积核包含(D=3)个3×3(用F×F表示)大小的卷积核。另外,这个示例中卷积核在图像的水平方向(W方向)和垂直方向(H方向)的滑动步长为2(用S表示);对输入图像周围各填充1(用P表示)个0,即图中输入层原始数据为蓝色部分,灰色部分是进行了大小为1的扩展,用0来进行扩展。经过卷积操作得到输出为3×3×2(用Ho×Wo×K表示)大小的特征图,即3×3大小的2通道特征图,其中Ho计算公式为:Ho=(H−F+2×P)/S+1,Wo同理。 而输出特征图中的每个像素,是每组滤波器与输入图像每个特征图的内积再求和,再加上偏置bo,偏置通常对于每个输出特征图是共享的。输出特征图o[:,:,0]中的最后一个−2计算如上图右下角公式所示。 记住这几个符号: H:图片高度; W:图片宽度; D:原始图片通道数,也是卷积核个数; F:卷积核高宽大小; P:图像边扩充大小; S:滑动步长。 在卷积操作中卷积核是可学习的参数,经过上面示例介绍,每层卷积的参数大小为D×F×F×K。卷积层的参数较少,这也是由卷积层的主要特性即局部连接和共享权重所决定。
局部连接:每个神经元仅与输入神经元的一块区域连接,这块局部区域称作感受野(receptive field)。在图像卷积操作中,即神经元在空间维度(spatial dimension,即上图示例H和W所在的平面)是局部连接,但在深度上是全部连接。对于二维图像本身而言,也是局部像素关联较强。这种局部连接保证了学习后的过滤器能够对于局部的输入特征有最强的响应。局部连接的思想,也是受启发于生物学里面的视觉系统结构,视觉皮层的神经元就是局部接受信息的。 权重共享:计算同一个深度切片的神经元时采用的滤波器是共享的。例上图中计算o[:,:,0]的每个每个神经元的滤波器均相同,都为W0,这样可以很大程度上减少参数。共享权重在一定程度上讲是有意义的,例如图片的底层边缘特征与特征在图中的具体位置无关。但是在一些场景中是无意的,比如输入的图片是人脸,眼睛和头发位于不同的位置,希望在不同的位置学到不同的特征 。请注意权重只是对于同一深度切片的神经元是共享的,在卷积层,通常采用多组卷积核提取不同特征,即对应不同深度切片的特征,不同深度切片的神经元权重是不共享。另外,偏重对同一深度切片的所有神经元都是共享的。 通过介绍卷积计算过程及其特性,可以看出卷积是线性操作,并具有平移不变性(shift-invariant),平移不变性即在图像每个位置执行相同的操作。卷积层的局部连接和权重共享使得需要学习的参数大大减小,这样也有利于训练较大卷积神经网络。 整体计算过程如下(与上图中的数据不同,但是计算过程相同):
池化层:池化是非线性下采样的一种形式,主要作用是通过减少网络的参数来减小计算量,并且能够在一定程度上控制过拟合。通常在卷积层的后面会加上一个池化层。池化包括最大池化、平均池化等。其中最大池化是用不重叠的矩形框将输入层分成不同的区域,对于每个矩形框的数取最大值作为输出层。如图所示: (2)LeNet5: LeNet5 这个网络虽然很小,但是它包含了深度学习的基本模块:卷积层,池化层,全链接层。是其他深度学习模型的基础, 这里我们对LeNet5进行深入分析。同时,通过实例分析,加深对与卷积层和池化层的理解。
LeNet-5共有7层,不包含输入,每层都包含可训练参数;每个层有多个Feature Map,每个FeatureMap通过一种卷积滤波器提取输入的一种特征,然后每个FeatureMap有多个神经元。 各层参数详解: 1、INPUT层-输入层 首先是数据 INPUT 层,输入图像的尺寸统一归一化为3232。 注意:本层不算LeNet-5的网络结构,传统上,不将输入层视为网络层次结构之一。 2、C1层-卷积层 输入图片:3232 卷积核大小:55 卷积核种类:6 输出featuremap大小:2828 (32-5+1)=28 神经元数量:28286 可训练参数:(55+1) * 6(每个滤波器55=25个unit参数和一个bias参数,一共6个滤波器) 连接数:(55+1)62828=122304 详细说明:对输入图像进行第一次卷积运算(使用 6 个大小为 55 的卷积核),得到6个C1特征图(6个大小为2828的 feature maps, 32-5+1=28)。我们再来看看需要多少个参数,卷积核的大小为55,总共就有6(55+1)=156个参数,其中+1是表示一个核有一个bias。对于卷积层C1,C1内的每个像素都与输入图像中的55个像素和1个bias有连接,所以总共有1562828=122304个连接(connection)。有122304个连接,但是我们只需要学习156个参数,主要是通过权值共享实现的。 3、S2层-池化层(下采样层) 输入:2828 采样区域:22 采样方式:4个输入相加,乘以一个可训练参数,再加上一个可训练偏置。结果通过sigmoid 采样种类:6 输出featureMap大小:1414(28/2) 神经元数量:14146 连接数:(22+1)61414 S2中每个特征图的大小是C1中特征图大小的1/4。 详细说明:第一次卷积之后紧接着就是池化运算,使用 22核 进行池化,于是得到了S2,6个1414的 特征图(28/2=14)。S2这个pooling层是对C1中的22区域内的像素求和乘以一个权值系数再加上一个偏置,然后将这个结果再做一次映射。同时有5x14x14x6=5880个连接。 4、C3层-卷积层 输入:S2中所有6个或者几个特征map组合 卷积核大小:55 卷积核种类:16 输出featureMap大小:1010 (14-5+1)=10 C3中的每个特征map是连接到S2中的所有6个或者几个特征map的,表示本层的特征map是上一层提取到的特征map的不同组合 存在的一个方式是:C3的前6个特征图以S2中3个相邻的特征图子集为输入。接下来6个特征图以S2中4个相邻特征图子集为输入。然后的3个以不相邻的4个特征图子集为输入。最后一个将S2中所有特征图为输入。 则:可训练参数:6*(355+1)+6*(455+1)+3*(455+1)+1*(655+1)=1516 连接数:10101516=151600 详细说明:第一次池化之后是第二次卷积,第二次卷积的输出是C3,16个10x10的特征图,卷积核大小是 55. 我们知道S2 有6个 1414 的特征图,怎么从6 个特征图得到 16个特征图了? 这里是通过对S2 的特征图特殊组合计算得到的16个特征图。具体如下:
C3的前6个feature map(对应上图第一个红框的6列)与S2层相连的3个feature map相连接(上图第一个红框),后面6个feature map与S2层相连的4个feature map相连接(上图第二个红框),后面3个feature map与S2层部分不相连的4个feature map相连接,最后一个与S2层的所有feature map相连。卷积核大小依然为55,所以总共有6(355+1)+6*(455+1)+3*(455+1)+1*(655+1)=1516个参数。而图像大小为10*10,所以共有151600个连接。
C3与S2中前3个图相连的卷积结构如下图所示:
上图对应的参数为 355+1,一共进行6次卷积得到6个特征图,所以有6*(355+1)参数。 为什么采用上述这样的组合了?论文中说有两个原因:1)减少参数,2)这种不对称的组合连接的方式有利于提取多种组合特征。 5、S4层-池化层(下采样层) 输入:10*10
采样区域:2*2
采样方式:4个输入相加,乘以一个可训练参数,再加上一个可训练偏置。结果通过sigmoid
采样种类:16
输出featureMap大小:5*5(10/2)
神经元数量:5516=400
连接数:16*(2*2+1)55=2000
S4中每个特征图的大小是C3中特征图大小的1/4
详细说明:S4是pooling层,窗口大小仍然是22,共计16个feature map,C3层的16个10x10的图分别进行以2x2为单位的池化得到16个5x5的特征图。有5x5x5x16=2000个连接。连接的方式与S2层类似。 6、C5层-卷积层 输入:S4层的全部16个单元特征map(与s4全相连) 卷积核大小:55 卷积核种类:120 输出featureMap大小:11(5-5+1) 可训练参数/连接:120(1655+1)=48120 详细说明:C5层是一个卷积层。由于S4层的16个图的大小为5x5,与卷积核的大小相同,所以卷积后形成的图的大小为1x1。这里形成120个卷积结果。每个都与上一层的16个图相连。所以共有(5x5x16+1)x120 = 48120个参数,同样有48120个连接。C5层的网络结构如下:
7、F6层-全连接层 输入:c5 120维向量 计算方式:计算输入向量和权重向量之间的点积,再加上一个偏置,结果通过sigmoid函数输出。 可训练参数:84*(120+1)=10164 详细说明:6层是全连接层。F6层有84个节点,对应于一个7x12的比特图,-1表示白色,1表示黑色,这样每个符号的比特图的黑白色就对应于一个编码。该层的训练参数和连接数是(120 + 1)x84=10164。 F6层的连接方式如下:
8、Output层-全连接层 Output层也是全连接层,共有10个节点,分别代表数字0到9,且如果节点i的值为0,则网络识别的结果是数字i。采用的是径向基函数(RBF)的网络连接方式。假设x是上一层的输入,y是RBF的输出,则RBF输出的计算方式是:
上式w_ij 的值由i的比特图编码确定,i从0到9,j取值从0到7*12-1。RBF输出的值越接近于0,则越接近于i,即越接近于i的ASCII编码图,表示当前网络输入的识别结果是字符i。该层有84x10=840个参数和连接。
上图是LeNet-5识别数字3的过程。 (3)总结: LeNet-5是一种用于手写体字符识别的非常高效的卷积神经网络。 卷积神经网络能够很好的利用图像的结构信息。 卷积层的参数较少,这也是由卷积层的主要特性即局部连接和共享权重所决定。 4、LeNet5代码详解:
import keras from keras.datasets import mnist from keras.layers import Conv2D, MaxPooling2D, Dense, Dropout, Flatten, Input from keras.models import Sequential, Model from keras.models import * import tensorflow as tf config = tf.ConfigProto() config.gpu_options.allow_growth = True # TensorFlow按需分配显存 config.gpu_options.per_process_gpu_memory_fraction = 0.5 # 指定显存分配比例 keras.backend.tensorflow_backend.set_session(tf.Session(config=config)) #import matplotlib.pyplot as plt #get_ipython().magic('matplotlib inline') # # 加载手写字体 # # 从keras自带的数据库中加载mnist手写字体 # In[154]: img_rows, img_cols = (28, 28) num_classes = 10 def get_mnist_data(): """ 加载mnist手写字体数据集 """ (x_train, y_train), (x_test, y_test) = mnist.load_data() x_train = x_train.reshape(x_train.shape[0], img_rows, img_cols, 1) x_test = x_test.reshape(x_test.shape[0], img_rows, img_cols, 1) x_train = x_train.astype('float32') x_test = x_test.astype('float32') x_train /= 255 x_test /= 255 y_train = keras.utils.to_categorical(y_train, num_classes) y_test = keras.utils.to_categorical(y_test, num_classes) return x_train, y_train, x_test, y_test x_train, y_train, x_test, y_test = get_mnist_data() print('x_train shape:', x_train.shape) print('x_test shape:', x_test.shape) def LeNet5(): input_shape = (img_rows, img_cols, 1) img_input = Input(shape=input_shape) x = Conv2D(32, (3, 3), activation="relu", padding="same", name="conv1")(img_input) x = MaxPooling2D((3, 3), strides=(2, 2), name='pool1')(x) x = Conv2D(64, (3, 3), activation="relu", padding='same', name='conv2')(x) x = MaxPooling2D((2, 2), strides=(2, 2), name='pool2')(x) x = Conv2D(128, (3, 3), activation="relu", padding='same', name='conv3')(x) x = Dropout(0.25)(x) x = Flatten(name='flatten')(x) x = Dense(120, activation='sigmoid', name='fc2')(x) x = Dropout(0.5)(x) x = Dense(10, activation='softmax', name='predictions')(x) model = Model(img_input, x, name='LeNet5') return model lenet5 = LeNet5() print('Model loaded.') lenet5.summary() lenet5.compile(loss=keras.losses.categorical_crossentropy, optimizer=keras.optimizers.Adadelta(), metrics=['accuracy']) lenet5.fit(x_train, y_train, batch_size = 128, epochs = 1, verbose = 1, validation_data = (x_test, y_test), ) save_model(lenet5,'model.h5')#保存模型输出结果: Model loaded. Model: “LeNet5”
input_1 (InputLayer) (None, 28, 28, 1) 0
conv1 (Conv2D) (None, 28, 28, 32) 320
pool1 (MaxPooling2D) (None, 13, 13, 32) 0
conv2 (Conv2D) (None, 13, 13, 64) 18496
pool2 (MaxPooling2D) (None, 6, 6, 64) 0
conv3 (Conv2D) (None, 6, 6, 128) 73856
dropout_1 (Dropout) (None, 6, 6, 128) 0
flatten (Flatten) (None, 4608) 0
fc2 (Dense) (None, 120) 553080
dropout_2 (Dropout) (None, 120) 0
Total params: 646,962 Trainable params: 646,962 Non-trainable params: 0
loss: 0.3948 - accuracy: 0.8820 - val_loss: 0.0842 - val_accuracy: 0.9755
可以看到精度已经达到97% 5、h5模型转pb模型: 与保存的h5模型在同一目录: h5_to_pb.py:
import tensorflow as tf from keras.models import load_model from keras import backend as K from tensorflow.python.framework import graph_io def freeze_session(session, keep_var_names=None, output_names=None, clear_devices=True): 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, input_graph_def, output_names, freeze_var_names) return frozen_graph """----------------------------------配置路径-----------------------------------""" epochs=5 h5_model_path='model.h5' output_path='.' #pb_model_name='my_model_ep{}.pb'.format(epochs) pb_model_name='model.pb' """----------------------------------导入keras模型------------------------------""" K.set_learning_phase(0) net_model = load_model(h5_model_path) print('input is :', net_model.input.name) print ('output is:', net_model.output.name) """----------------------------------保存为.pb格式------------------------------""" sess = K.get_session() frozen_graph = freeze_session(K.get_session(), output_names=[net_model.output.op.name]) graph_io.write_graph(frozen_graph, output_path, pb_model_name, as_text=False)输出: input is : input_1:0 output is: predictions/Softmax:0 记住这两个参数非常重要。此时,你的目录下应该有一个model.pb文件 6、C++调用model.pb识别图片 注意要更改cmakelists内的路径以及hello.cpp中的input_tensor_name与output_tensor_name,更改的内容就算上面h5转pb时要记下的那两个参数。 CmakeLists.txt:
cmake_minimum_required(VERSION 3.10) project(demo) set(CMAKE_CXX_STANDARD 11) link_directories(/home/lvfengkun/anaconda3/envs/tf2/lib) # 配置生成的可执行文件名为hello,要用到的源文件为hello.cpp set(TENSORFLOW_ROOT_DIR /home/lvfengkun/tensorflow-r2.0) include_directories( ${TENSORFLOW_ROOT_DIR} ${TENSORFLOW_ROOT_DIR}/bazel-include ${TENSORFLOW_ROOT_DIR}/bazel-bin/tensorflow ${TENSORFLOW_ROOT_DIR}/bazel-include ${TENSORFLOW_ROOT_DIR}/tensorflow/contrib/makefile/gen/proto ${TENSORFLOW_ROOT_DIR}/tensorflow/contrib/makefile/gen/protobuf/include ${TENSORFLOW_ROOT_DIR}/tensorflow/contrib/makefile/downloads/eigen ${TENSORFLOW_ROOT_DIR}/tensorflow/contrib/makefile/downloads/absl ) add_executable(hello hello.cpp) # 以下使能OpenCV的路径查找 find_package(OpenCV REQUIRED) # 以下是将可执行文件与一些.so文件建立动态链接关系, # 用到的有libtensorflow_cc.so,libtensorflow_framework.so,以及opencv相关的so target_link_libraries(hello tensorflow_cc tensorflow_framework ${OpenCV_LIBS})hello.cpp:
#include <fstream> #include <utility> #include <Eigen/Core> #include <Eigen/Dense> #include <iostream> #include "tensorflow/cc/ops/const_op.h" #include "tensorflow/cc/ops/image_ops.h" #include "tensorflow/cc/ops/standard_ops.h" #include "tensorflow/core/framework/graph.pb.h" #include "tensorflow/core/framework/tensor.h" #include "tensorflow/core/graph/default_device.h" #include "tensorflow/core/graph/graph_def_builder.h" #include "tensorflow/core/lib/core/errors.h" #include "tensorflow/core/lib/core/stringpiece.h" #include "tensorflow/core/lib/core/threadpool.h" #include "tensorflow/core/lib/io/path.h" #include "tensorflow/core/lib/strings/stringprintf.h" #include "tensorflow/core/public/session.h" #include "tensorflow/core/util/command_line_flags.h" #include "tensorflow/core/platform/env.h" #include "tensorflow/core/platform/init_main.h" #include "tensorflow/core/platform/logging.h" #include "tensorflow/core/platform/types.h" #include "opencv2/opencv.hpp" using namespace tensorflow::ops; using namespace tensorflow; using namespace std; using namespace cv; using tensorflow::Flag; using tensorflow::Tensor; using tensorflow::Status; using tensorflow::string; using tensorflow::int32 ; // 定义一个函数讲OpenCV的Mat数据转化为tensor,python里面只要对cv2.read读进来的矩阵进行np.reshape之后, // 数据类型就成了一个tensor,即tensor与矩阵一样,然后就可以输入到网络的入口了,但是C++版本,我们网络开放的入口 // 也需要将输入图片转化成一个tensor,所以如果用OpenCV读取图片的话,就是一个Mat,然后就要考虑怎么将Mat转化为 // Tensor了 void CVMat_to_Tensor(Mat img,Tensor* output_tensor,int input_rows,int input_cols) { //imshow("input image",img); //图像进行resize处理 resize(img,img,cv::Size(input_cols,input_rows)); //imshow("resized image",img); //归一化 img.convertTo(img,CV_32FC1); img=1-img/255; //创建一个指向tensor的内容的指针 float *p = output_tensor->flat<float>().data(); //创建一个Mat,与tensor的指针绑定,改变这个Mat的值,就相当于改变tensor的值 cv::Mat tempMat(input_rows, input_cols, CV_32FC1, p); img.convertTo(tempMat,CV_32FC1); // waitKey(0); } int main(int argc, char** argv ) { /*--------------------------------配置关键信息------------------------------*/ //string model_path="/home/lvfengkun/PycharmProjects/tf_2/saved_model/my_model/"; string model_path="/home/lvfengkun/Cpp_code/model.pb"; string image_path="/home/lvfengkun/Cpp_code/2.png"; int input_height =28; int input_width=28; //string input_tensor_name="conv2d_1_input"; string input_tensor_name="input_1"; string output_tensor_name="predictions/Softmax"; /*--------------------------------创建session------------------------------*/ Session* session; Status status = NewSession(SessionOptions(), &session);//创建新会话Session /*--------------------------------从pb文件中读取模型--------------------------------*/ GraphDef graphdef; //Graph Definition for current model //tensorflow::SessionOptions sess_options; //tensorflow::RunOptions run_options; //tensorflow::SavedModelBundle bundle; // Status status_load=LoadSavedModel(sess_options,run_options,model_path, {tensorflow::kSavedModelTagServe}, &bundle); Status status_load = ReadBinaryProto(Env::Default(), model_path, &graphdef); //从pb文件中读取图模型; if (!status_load.ok()) { cout << "ERROR: Loading model failed..." << model_path << std::endl; cout << status_load.ToString() << "\n"; return -1; } //tensorflow::MetaGraphDef graph_def = bundle.meta_graph_def; //std::unique_ptr<tensorflow::Session>& session = bundle.session;//创建新会话Session Status status_create = session->Create(graphdef); //将模型导入会话Session中; if (!status_create.ok()) { cout << "ERROR: Creating graph in session failed..." << status_create.ToString() << std::endl; return -1; } cout << "<----Successfully created session and load graph.------->"<< endl; /*---------------------------------载入测试图片-------------------------------------*/ cout<<endl<<"<------------loading test_image-------------->"<<endl; Mat img=imread(image_path,0); if(img.empty()) { cout<<"can't open the image!!!!!!!"<<endl; return -1; } //创建一个tensor作为输入网络的接口 Tensor resized_tensor(DT_FLOAT, TensorShape({1,input_height,input_width,1})); //将Opencv的Mat格式的图片存入tensor CVMat_to_Tensor(img,&resized_tensor,input_height,input_width); cout << resized_tensor.DebugString()<<endl; /*-----------------------------------用网络进行测试-----------------------------------------*/ cout<<endl<<"<-------------Running the model with test_image--------------->"<<endl; //前向运行,输出结果一定是一个tensor的vector vector<tensorflow::Tensor> outputs; //string output_node = "predictions/Softmax"; string output_node = output_tensor_name; Status status_run = session->Run({{input_tensor_name, resized_tensor}}, {output_node}, {}, &outputs); //string output_node = "softmax"; //Status status_run = session->Run({{"inputs", resized_tensor}}, {output_node}, {}, &outputs); if (!status_run.ok()) { cout << "ERROR: RUN failed..." << std::endl; cout << status_run.ToString() << "\n"; return -1; } //把输出值给提取出来 cout << "Output tensor size:" << outputs.size() << std::endl; for (std::size_t i = 0; i < outputs.size(); i++) { cout << outputs[i].DebugString()<<endl; } Tensor t = outputs[0]; // Fetch the first tensor auto tmap = t.tensor<float, 2>(); // Tensor Shape: [batch_size, target_class_num] int output_dim = t.shape().dim_size(1); // Get the target_class_num from 1st dimension // Argmax: Get Final Prediction Label and Probability int output_class_id = -1; double output_prob = 0.0; for (int j = 0; j < output_dim; j++) { cout << "Class " << j << " prob:" << tmap(0, j) << "," << std::endl; if (tmap(0, j) >= output_prob) { output_class_id = j; output_prob = tmap(0, j); } } // 输出结果 cout << "Final class id: " << output_class_id << std::endl; cout << "Final class prob: " << output_prob << std::endl; return 0; }6、运行结果: 成功调用并识别出数字6: 代码文件详见我的github:https://github.com/lvfengkun/mnist_cpp_python
参考博客:https://cuijiahua.com/blog/2018/01/dl_3.html