将TensorFlow的Model转换到iOS设备中

前言

苹果在iOS 11中发布了CoreML框架。其实很多人对于CoreML有个误区,那就是他们认为CoreML“无所不能”,既可以训练模型又可以应用模型。其实CoreML只是将训练好的模型放在应用层面上的一个工具。

既然它不能训练模型,那总得有能训练模型的工具吧。那就是TensorFlow该干的活了。其实大体思路就是这样的:TensorFlow构建并训练模型,然后训练好的模型经过一层转换(因为CoreML不能直接支持TensorFlow)交给CoreML来应用。

先上几个效果图:

识别数字5
识别数字0
识别数字4

构建并训练TensorFlow模型

其实小弟我也是刚刚折腾TensorFlow没几天,什么事情都是摸索着做,大牛勿喷。现在手里还没有适合的资源。没办法,就用机器学习界的“Hello World”----MNIST手写数据集来当练习吧。

首先第一步是先构建用于训练MNIST手写数据集的模型,所谓的模型,其实就是SoftMax回归模型。何为SoftMax?

Softmax回归介绍 

我们知道MNIST的每一张图片都表示一个数字,从0到9。我们希望得到给定图片代表每个数字的概率。比如说,我们的模型可能推测一张包含9的图片代表数字9的概率是80%但是判断它是8的概率是5%(因为8和9都有上半部分的小圆),然后给予它代表其他数字的概率更小的值。

这是一个使用softmax回归模型的经典案例。softmax模型可以用来给不同的对象分配概率。即使在之后,我们训练更加精细的模型时,最后一步也需要用softmax来分配概率。

softmax回归分两步:第一步

为了得到一张给定图片属于某个特定数字类的证据(evidence),我们对图片像素值进行加权求和。如果这个像素具有很强的证据说明这张图片不属于该类,那么相应的权值为负数,相反如果这个像素拥有有利的证据支持这张图片属于这个类,那么权值是正数

下面的图片显示了一个模型学习到的图片上每个像素对于特定数字类的权值。红色代表负数权值,蓝色代表正数权值。

我们也需要加入一个额外的偏置量(bias),因为输入往往会带有一些无关的干扰量。因此对于给定的输入图片 x 它代表的是数字 i 的证据可以表示为

其中 

代表Wi权重,bi代表数字 i 类的偏置量,j 代表给定图片 x 的像素索引用于像素求和。然后用softmax函数可以把这些证据转换成概率 y

这里的softmax可以看成是一个激励(activation)函数或者链接(link)函数,把我们定义的线性函数的输出转换成我们想要的格式,也就是关于10个数字类的概率分布。因此,给定一张图片,它对于每一个数字的吻合度可以被softmax函数转换成为一个概率值。softmax函数可以定义为:

展开等式上边的子式,可以得到:

但是更多的时候把softmax模型函数定义为前一种形式:把输入值当成幂指数求值,再正则化这些结果值。这个幂运算表示,更大的证据对应更大的假设模型(hypothesis)里面的乘数权重值。反之,拥有更少的证据意味着在假设模型里面拥有更小的乘数系数。假设模型里的权值不可以是0值或者负值。Softmax然后会正则化这些权重值,使它们的总和等于1,以此构造一个有效的概率分布。对于softmax回归模型可以用下面的图解释,对于输入的xs加权求和,再分别加上一个偏置量,最后再输入到softmax函数中:

如果把它写成一个等式,我们可以得到:

我们也可以用向量表示这个计算过程:用矩阵乘法和向量相加。这有助于提高计算效率。


有了数学理论,就要开始实践了。

构建模型

首先我们的模型肯定得读取数据对不对?而MNIST数据集是有图像和图像对应的标签所组成的,所以我们不仅得读取图像,还得读取图像标签做个对应,代码如下:

def read32(bytestream):

# 由于网络数据的编码是大端,所以需要加上>

dt = numpy.dtype(numpy.int32).newbyteorder('>')

data = bytestream.read(4)

return numpy.frombuffer(data, dt)[0]

def read_labels(filename):

with gzip.open(filename) as bytestream:

magic = read32(bytestream) #读取标签数量60000

numberOfLabels = read32(bytestream) #取出60000个标签组成一个数组

labels = numpy.frombuffer(bytestream.read(numberOfLabels), numpy.uint8)

#声明一个60000*10的二维数组

data = numpy.zeros((numberOfLabels, 10))

for i in range(len(labels)):

data[i][labels[i]] = 1

bytestream.close()

return data

def read_images(filename):

# 把文件解压成字节流

with gzip.open(filename) as bytestream:

magic = read32(bytestream)

numberOfImages = read32(bytestream)

rows = read32(bytestream)

columns = read32(bytestream)

images = numpy.frombuffer(bytestream.read(numberOfImages * rows * columns), numpy.uint8)

images.shape = (numberOfImages, rows * columns)

images = images.astype(numpy.float32)

images = numpy.multiply(images, 1.0 / 255.0)

bytestream.close()

return images

read_images为读取图像的函数,read_labels为读取图像标签的函数。gzip用import gzip就可以导入。代码不是很难,就不班门弄斧了。

既然定义了读取标签和读取图像的函数,那么就得利用它对不对?代码如下:

train_labels = read_labels(train_labels_file)

train_images = read_images(train_images_file)

test_labels = read_labels(t10k_labels_file)

test_images = read_images(t10k_images_file)

其中这四个读取的分别是测试集上的图像和标签、验证集上的图像和标签。

为什么要使用测试集,还要在使用验证集呢?

机器就好比一个学生,平常复习的再好,是骡子是马得出来溜溜不是?模型也一样,在测试集上正确率再高,也不一定代表这个模型就好使(比如过拟合)。所以,验证集才是验证这个模型正确率的关键。一般将训练集和测试集划为7:3。

上边的代码中四个传参分别为:

train_images_file = "MNIST_data/train-images-idx3-ubyte.gz"

train_labels_file = "MNIST_data/train-labels-idx1-ubyte.gz"

t10k_images_file = "MNIST_data/t10k-images-idx3-ubyte.gz"

t10k_labels_file = "MNIST_data/t10k-labels-idx1-ubyte.gz"

这些为我本地放置MNIST数据集的地方,这些.gz文件是从http://yann.lecun.com/exdb/mnist/下载的。

既然读取了数据,那么下一步就是要构建softmax回归模型了,代码如下:

import tensorflow as tf

 x = tf.placeholder("float", [None, 784.],name='input/x_input')

W = tf.Variable(tf.zeros([784., 10.]))

b = tf.Variable(tf.zeros([10.]))

y = tf.nn.softmax(tf.matmul(x, W) + b)

y_ = tf.placeholder("float",name='input/y_input')

cross_entropy = -tf.reduce_sum(y_ * tf.log(y))

train_step = tf.train.GradientDescentOptimizer(0.01).minimize(cross_entropy)

init = tf.initialize_all_variables()

sess = tf.Session()sess.run(init)

for i in range(1200):

batch_xs = train_images[50 * i:50 * i + 50]

batch_ys = train_labels[50 * i:50 * i + 50]

sess.run(train_step, feed_dict={x: batch_xs, y_: batch_ys})

correct_prediction = tf.equal(tf.argmax(y, 1, output_type='int32', name='output'), tf.argmax(y_, 1, output_type='int32'))

# correct_prediction = tf.equal(tf.argmax(y, 1), tf.argmax(y_, 1))

accuracy = tf.reduce_mean(tf.cast(correct_prediction, "float"))

print (sess.run(accuracy, feed_dict={x: test_images, y_: test_labels}))

解释一下,首先我们定义x,w,b变量,其中x应该为55000*784的向量,b这个变量前文也说了,是对应10个数字的偏移项,所以自然为55000*10,w因为是要连接x和b的桥梁,所以自然是784*10。然后因为是softmax回归,所以直接用TensorFlow自带的tf.nn.softmax就好,matmul函数代表两个矩阵相乘,也就是w*x。任何一个模型都要定义一个损失函数,则利用的是交叉熵损失函数,cross_entropy = -tf.reduce_sum(y_ * tf.log(y))。我们的目标是为了正确率更高,那就是利用梯度下降要让交叉熵损失更小,用TensorFlow自带的优化器就可以,train_step = tf.train.GradientDescentOptimizer(0.01).minimize(cross_entropy)。下面要做的就是初始化变量(init = tf.initialize_all_variables()   sess = tf.Session()sess.run(init))并且迭代训练了(for i in range(1200):)。

训练代码为sess.run(train_step, feed_dict={x: batch_xs, y_: batch_ys}),其中batch_xs为测试集中的图像,batch_ys为测试集中的标签。

训练的时候就得检测正确率:

correct_prediction = tf.equal(tf.argmax(y, 1, output_type='int32', name='output'), tf.argmax(y_, 1, output_type='int32'))

accuracy = tf.reduce_mean(tf.cast(correct_prediction, "float"))

前文说过,模型得在验证集上运行来验证模型,所以下面代码就是在验证集上运行的

sess.run(accuracy, feed_dict={x: test_images, y_: test_labels})


导出TensorFlow模型

现在已经训练好了模型并通过了验证,下面就要把这个模型导出出来。在这里我们导出的是.pb格式的文件。代码如下:

output_graph_def = graph_util.convert_variables_to_constants(sess, sess.graph_def, output_node_names=['output'])

with tf.gfile.FastGFile('model/mnist.pb', mode='wb') as f:

# ’wb’中w代表写文件,b代表将数据以二进制方式写入文件。 f.write(output_graph_def.SerializeToString())

sess.close()

这样我们就把模型保存了下来。下一步,就是转换成CoreML支持的格式了。

转换成.mlmodel文件

需要安装一个CoreMLTools的工具,安装方式其实很简单:

pip install -U coremltools即可安装。

然后新建一个tf2ml.py文件,写入一下代码:

import tfcoreml as tf_converter

tf_converter.convert(tf_model_path='model/mnist.pb', mlmodel_path='model/my_mnist.mlmodel', output_feature_names=['Softmax:0'],input_name_shape_dict={"input/x_input:0":[1,784]})

以上代码中,tf_model_path是保存的.pb文件的路径,mlmodel_path是转换后的.mlmodel文件所在路径,output_feature_names和input_name_shape_dict都是CoreML中所需要的,下文会讲到。

然后运行这个python文件,就可以生成.mlmodel文件了。

在CoreML中使用.mlmodel文件

将.mlmodel导入工程中,然后重新编译一下,Xcode会自动生成一个模型类。查看一下,会发现input_x_input__0,Softmax__0是不是很眼熟?没错,这个就是coremltool转换时我们定义的名字。

然后就是iOS敲代码时间了,CoreML的应用代码我就不详述了,最后运行的样子就是最开头展示的那个样子。


以后我会尝试着把InceptionV3转换到CoreML中。

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

推荐阅读更多精彩内容