TensorFlow Lite(实战系列二) :自定义网络实现MNIST数据集的训练迁移边缘设备

摘要

在本篇文章中,少奶奶将详细的讲解如何把自定义训练的网络模型转换成TensorFlow Lite能识别的模型,并嵌入到手机设备中,让大家能够摆脱只能使用Google官网提供的模型的苦恼。此次教程使用的是MNIST数据集,在下一篇文章中,少奶奶将会使用YOLOV3模型来训练更为复杂的模型结构。当然,若有小伙伴不理解YOLOV3模型,少奶奶也会在近期使用浅显易懂的方式讲解一下YOLOV3.但总的来说,无论你是使用官网提供的模型还是自定义训练的模型,其整体思路是不变的。还请大家细心浏览和理解。
感谢前辈的分享
备注:大家在实践过程中有什么问题,可以在评论区留言,少奶奶会定期回复。

思路

1,使用Python实现一个简单的识别MNIST数据集的神经网络,得到网络模型结构和网络模型参数。
2,通过tensorboard查看网络模型图结构。
3,手动剪裁drop层。
4,模型转换。
5,边缘设备接入。

开发环境

window 10,pycharm、TensorFlow 1.13.0、Python 3.6、anaconda、android studio3.6

自定义网络模型(MNIST数据集)

下述代码会自动下载MNIST数据集(二进制),大家只需要配好环境,然后运行即可。少奶奶强烈建议大家阅读一下这套代码。关键的地方,少奶奶都写了注解。

import tensorflow as tf
from tensorflow.examples.tutorials.mnist import input_data
from tensorflow.python.framework import graph_util
from tensorflow.python.platform import gfile

# 定义初始化权重的函数
def weight_variavles(shape):
    w = tf.Variable(tf.truncated_normal(shape, stddev=0.1))
    return w

# 定义一个初始化偏置的函数
def bias_variavles(shape):
    b = tf.Variable(tf.constant(0.1, shape=shape))
    return b

def model():
    # 1.建立数据的占位符 x [None, 784]  y_true [None, 10]
    with tf.variable_scope("date"):
        x = tf.placeholder(tf.float32, [None, 28,28,1],"input_node")
        y_true = tf.placeholder(tf.float32, [None, 10])

    # 2.卷积层1  卷积:5*5*1,32个filter,strides= 1-激活-池化
    with tf.variable_scope("conv1"):
        # 随机初始化权重
        w_conv1 = weight_variavles([5, 5, 1, 32])
        b_conv1 = bias_variavles([32])

        # 对x进行形状的改变[None, 784] ----- [None,28,28,1]
        # x_reshape = tf.reshape(x, [-1, 28, 28, 1])  # 不能填None,不知道就填-1

        # [None,28, 28, 1] -------- [None, 28, 28, 32]
        x_relu1 = tf.nn.relu(tf.nn.conv2d(x, w_conv1, strides=[1, 1, 1, 1], padding="SAME") + b_conv1)

        # 池化 2*2,步长为2,【None, 28,28, 32]--------[None,14, 14, 32]
        x_pool1 = tf.nn.max_pool(x_relu1, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding="SAME")

    # 3.卷积层2  卷积:5*5*32,64个filter,strides= 1-激活-池化
    with tf.variable_scope("conv2"):
        # 随机初始化权重和偏置
        w_conv2 = weight_variavles([5, 5, 32, 64])
        b_conv2 = bias_variavles([64])

        # 卷积、激活、池化
        # [None,14, 14, 32]----------【NOne, 14, 14, 64]
        x_relu2 = tf.nn.relu(tf.nn.conv2d(x_pool1, w_conv2, strides=[1, 1, 1, 1], padding="SAME") + b_conv2)

        # 池化 2*2,步长为2 【None, 14,14,64]--------[None,7, 7, 64]
        x_pool2 = tf.nn.max_pool(x_relu2, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding="SAME")

    # 4.全连接层 [None,7, 7, 64] --------- [None, 7*7*64] * [7*7*64, 10]+[10] = [none, 10]
    with tf.variable_scope("arg"):
        w_fc1 = weight_variavles([1024, 10])
        b_fc1 = bias_variavles([10])
        keep_prob = tf.placeholder("float", name='keep_prod')
    with tf.variable_scope("fc"):
        # 随机初始化权重和偏置:
        w_fc = weight_variavles([7 * 7 * 64, 1024])
        b_fc = bias_variavles([1024])
        # 修改形状 [none, 7, 7, 64] ----------[None, 7*7*64]
        x_fc_reshape = tf.reshape(x_pool2, [-1, 7 * 7 * 64])
        h_fc1 = tf.matmul(x_fc_reshape, w_fc) + b_fc
        h_fc1 = tf.nn.relu(h_fc1)
    with tf.variable_scope("drop"):
        # 在输出之前加入dropout以减少过拟合
        h_fc1_drop = tf.nn.dropout(h_fc1, keep_prob)
    with tf.variable_scope("output"):
        # 进行矩阵运算得出每个样本的10个结果[NONE, 10],输出
        y_predict = tf.nn.softmax(tf.matmul(h_fc1_drop, w_fc1) + b_fc1,name="softmax_tensor")
    return x, y_true, y_predict, keep_prob
    # return x, y_true, y_predict

def conv_fc():
    # 获取数据,MNIST_data是楼主用来存放官方的数据集,如果你要这样表示的话,那MNIST_data这个文件夹应该和这个python文件在同一目录
    mnist = input_data.read_data_sets('MNIST_data', one_hot=True)

    # 定义模型,得出输出
    x, y_true, y_predict, keep_prob = model()
    # x, y_true, y_predict = model()

    # 进行交叉熵损失计算
    # 3.计算交叉熵损失
    with tf.variable_scope("soft_cross"):
        # 求平均交叉熵损失,tf.reduce_mean对列表求平均值
        loss = -tf.reduce_sum(y_true * tf.log(y_predict))

    # 4.梯度下降求出最小损失,注意在深度学习中,或者网络层次比较复杂的情况下,学习率通常不能太高
    with tf.variable_scope("optimizer"):

        train_op = tf.train.AdamOptimizer(1e-4).minimize(loss)

    # 5.计算准确率
    with tf.variable_scope("acc"):

        equal_list = tf.equal(tf.argmax(y_true, 1), tf.argmax(y_predict, 1))
        # equal_list None个样本 类型为列表1为预测正确,0为预测错误[1, 0, 1, 0......]

        accuray = tf.reduce_mean(tf.cast(equal_list, tf.float32))

    init_op = tf.global_variables_initializer()

    saver = tf.train.Saver()

    # 开启会话运行
    with tf.Session() as sess:
        sess.run(init_op)
        for i in range(3000):
            mnist_x, mnist_y = mnist.train.next_batch(50)
            mnist_x = mnist_x.reshape(-1,28,28,1)
            if i % 100 == 0:
                # 评估模型准确度,此阶段不使用Dropout
                train_accuracy = accuray.eval(feed_dict={x: mnist_x, y_true: mnist_y, keep_prob: 1.0})
                # train_accuracy = accuray.eval(feed_dict={x: mnist_x, y_true: mnist_y})
                print("step %d, training accuracy %g" % (i, train_accuracy))

            # 训练模型,此阶段使用50%的Dropout
            train_op.run(feed_dict={x: mnist_x, y_true: mnist_y, keep_prob: 0.5})
            # train_op.run(feed_dict={x: mnist_x, y_true: mnist_y})
            # 将模型保存在你自己想保存的位置
            # write_meta_graph=False 设置是否在生成ckpt时 生成图结构
        # saver.save(sess, "./fcc_model.ckpt",write_meta_graph=False)
        # tf.train.write_graph(sess.graph_def,"./","nsfw-graph.pb",as_text=False)

        output_graph_def = graph_util.convert_variables_to_constants(
            sess, sess.graph.as_graph_def(), ['output/softmax_tensor'])

        with gfile.FastGFile('./frozen_model_str.pb', 'wb') as f:
            f.write(output_graph_def.SerializeToString())

    return None
if __name__ == "__main__":
    conv_fc()
# 备注:输出名称是fc/softmax_tensor 而不是softmax_tensor 是因为原文中加入了with tf.variable_scope("fc"):

特别留意:由于,我们使用的数据集是二进制结构。所以,少奶奶在喂数据时,先reshape成[-1,28,28,1]。其维度是一维,并不是三维。
在这里插入图片描述
下图是模型入口节点,名称为"input_node"。
在这里插入图片描述
下图是模型出口节点,名称为"output/softmax_tensor"。
在这里插入图片描述备注:我们必须为入口节点和出口节点设置节点名称,因为在模型转换时,需要用到这些名称。
下图中的代码,可以直接把网络模型结构图和网络模型权重参数冻结(绑定)在一起。若不用该接口,我们就需要存放图结构的pb文件和存放权重的ckpt文件进行冻结成新的pb文件。
在这里插入图片描述

tensorboard查看生成的网络图结构

运行下面的代码,然后在pycharm的终端中,激活anaconda虚拟环境,输入tensorboard --lodg/即可在浏览器中查看生产的图结构。

import tensorflow as tf

model = 'frozen_model_str.pb'    #pb文件名称
# model = 'frozen_model_str.pb'    #pb文件名称
graph = tf.get_default_graph()
graph_def = graph.as_graph_def()
graph_def.ParseFromString(tf.gfile.FastGFile(model, 'rb').read())
tf.import_graph_def(graph_def, name='graph')
summaryWriter = tf.summary.FileWriter('log/', graph)   #log存放地址
# 网站中查看图结构 tensorboard --logdir log/

如下图所示,我们可以看出自定义网络模型的图结构,其中,少奶奶使用了dropout,在转换成tflite时,需要删除掉该节点。
在这里插入图片描述
这篇文章中,使用了optimize_for_inference和freeze_graph之类的工具对dropout节点进行剪裁,但奇怪的是,少奶奶使用这些命令后并没有对dropout节点进行剪裁。所以采用手动剪裁的方式实现。具体如下:

from __future__ import print_function
from tensorflow.core.framework import graph_pb2
import tensorflow as tf

def display_nodes(nodes):
    for i, node in enumerate(nodes):
        print('%d %s %s' % (i, node.name, node.op))
        [print(u'└─── %d ─ %s' % (i, n)) for i, n in enumerate(node.input)]
# read frozen graph and display nodes
graph = tf.GraphDef()
with tf.gfile.Open('frozen_model_str.pb', 'rb') as f:
    data = f.read()
    graph.ParseFromString(data)

display_nodes(graph.node)

运行上述代码,可以得到自定义网络的模型结构,找到第一个以drop开头的节点和最后一个以drop结尾的节点,这两个节点就是我们需要剪裁drop节点的起始位置和结束位置,
在这里插入图片描述
运行下述代码进行图结构的剪裁

from __future__ import print_function
from tensorflow.core.framework import graph_pb2
import tensorflow as tf
# Connect 'MatMul_1' with 'Relu_2'
graph.node[46].input[0] = 'fc/Relu'
# Remove dropout nodes
nodes = graph.node[:31] + graph.node[46:]
# del nodes[1] # 1 -> keep_prob

# Save graph
output_graph = graph_pb2.GraphDef()
output_graph.node.extend(nodes)
with tf.gfile.GFile('./frozen_model_without_dropout.pb', 'w') as f:
    f.write(output_graph.SerializeToString())

再次运行tensorboard代码,可以看到drop节点已经被剪裁掉了
在这里插入图片描述

tflite模型转换

执行一下代码,实现pb转tflite模型

# -*- coding:utf-8 -*-
import tensorflow as tf

in_path = "frozen_model_without_dropout.pb"
# in_path = "frozen_mnist.pb"
out_path = "frozen_graph.tflite"
# out_path = "./model/quantize_frozen_graph.tflite"

# 模型输入节点
input_tensor_name = ["date/input_node"]
input_tensor_shape = {"date/input_node":[1, 28,28,1]}
# 模型输出节点
classes_tensor_name = ["output/softmax_tensor"]

converter = tf.lite.TFLiteConverter.from_frozen_graph(in_path,
                                            input_tensor_name, classes_tensor_name,
                                            input_shapes = input_tensor_shape)
converter.post_training_quantize = True

converter.allow_custom_ops=True

tflite_model = converter.convert()

with open(out_path, "wb") as f:
    f.write(tflite_model)

备注:
1,转换模型时,需要网络模型中输出层和输入层的名称,这些名称可以到对应py文件中查找。
2,由于我们在训练时,是直接使用了MNIST官网中的文件,并没有转化成图片,所以,输入图片尺寸是1x28x28x1。

APP设备的移植

步骤一:下载Google官网的花卉识别模型android代码,并使用android studio导入。

代码下载
在这里插入图片描述这些代码是Google官网提供给开发者使用的花卉分类示例代码,大家可以使用这套代码重训练花卉识别模型,并嵌入app中,少奶奶写过一篇关于该套代码的使用教程[放上链接]。在本章中,我们只需要导入android文件夹中的代码即可。

步骤二:导入模型和标签,修改示例代码中的细节

标签制作
在这里插入图片描述
导入模型文件和标签
在这里插入图片描述
修改细节
在这里插入图片描述
修改ImageClassfier.java文件中的参数在这里插入图片描述
在这里插入图片描述
备注:IMAGE_MEAN和IMAGE_STD分别是均值和方差,用于对图片数据进行标准化操作,由于少奶奶的自定义网络模型没有进行标准化或者归一化操作,所以设置为0和1.0f。且找到如下代码,注释前面两行,因为我们的输入是[1,28,28,1],既1通道,所以在获取数据时,只需要获取一个通道即可,若大家使用的是[1,28,28,3],那么就不能注释前面两行(imgData.putFloat()执行了三次,代表读取三通道的数据)。
在这里插入图片描述

步骤三:运行代码,真机调试,测试结果

使用usb数据线把手机和电脑进行连接,并打开手机的开发者模式,android studio会自动识别手机驱动,点击运行后,手机会自动下载安装app
在这里插入图片描述以下是测试结果
在这里插入图片描述

总结

自此,自定义网络实现MNIST数据集的训练和边缘设备迁移教程完结。大家在实践过程中会遇到很多问题,出现很多bug,但这是很正常的现象。记住要静下心来多看看博客,多查查资料。在下一篇中,少奶奶将为大家介绍一些关于YOLOV3模型的知识,并实现自定义YOLOV3训练模型使用TensorFlow Lite嵌入手机设备中。

  • 5
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 9
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值