IOS平台TensorFlow实践

1 天前

作者简介:

MATTHIJS HOLLEMANS

荷兰人,独立开发者,专注于底层编码,GPU优化和算法研究。目前研究方向为IOS上的深度学习及其在APP上的应用。

推特地址:https://twitter.com/mhollemans

邮件地址:mailto:matt@machinethink.net

github地址:hollance (Matthijs Hollemans)

个人博客:Machine, Think!

一、逻辑斯蒂回归

在使用深度学习网络(deep learning network)进行预测任务之前,首先要训练它。目前有很多训练神经网络的工具,TensorFlow是大部分人的首选。

你可以使用TensorFlow训练你的机器学习模型,然后使用这些模型来进行预测。训练过程通常是在一台强大的机器或者云上进行,但是TensorFlow也可以运行在IOS上,虽然存在一些限制。

本文中,作者详细介绍了如何使用TensorFlow训练一个简单的分类器并应用在IOS app上。本文将会使用Gender Recognition by Voice and Speech Analysis dataset数据集,项目源码已托管至GitHub

TensorFlow简介

TensorFlow是一个构建计算图(computational graphs)用来做机器学习的软件库。其他很多工具都以一种高抽象层次(higher level of abstraction)的方式工作着,比如通过Caffe,你能够设计一个不同层(layers)之间相互链接的神经网络(neural network)。 这和IOS上基础神经网络子程序(Basic Neural Network Subroutines, BNNS)和Metal 渲染(Metal Performance Shaders Convolution Neural Network,BPSCNN)提供的功能很相似。

你可以认为TensorFlow是一个实现新机器学习算法的工具包(toolkit),而其他的深度学习工具则是使用已实现的算法。这意味着你不必从头开始构建一切,TensorFlow拥有很多可复用的构件集(reusable building blocks),以及能够在TensorFlow上层提供便利模块的其他库,如Keras

使用逻辑斯蒂回归的二值分类

在本文中,我们将会创建了一个使用逻辑斯蒂回归算法(logistic regression)的分类器。该分类器接收输入数据然后返回这条数据所属的类别。项目中只有两个类别:男性(male)和女性(female),因此这是一个二值分类器(binary classifier)。

Note: 二值分类器虽然是最简单的分类器,但是其思想和那些能够区分成百上千类的分类器一样。虽然这篇文章中并没有进行深度学习,但某些理论基础是共同的。

每条输入数据由代表用户声音的声学特征的20个数字组成,后面会详细说明,现在你将其看作是声频和其他信息就可以了。如图所示,20个数字和一个sum块连接,这些连接有不同的权重(weights),对应着这20个代表特征的数字的重要程度。

图中,x0 – x19表示输入特征,w0 - w19表示连接的权重,在sum 块中,按如下方式进行运算(就是普通的点乘):

训练分类器就是要找到w和b的正确的数值。初始化时,将w和b全部置0。训练多轮之后,分类器就会使用合适的w和b将男性声音和女性声音区分开。为了将sum转化成0到1之间的概率,我们采用logistic sigmod函数:

如果sum是一个大的正数,sigmod函数将返回1或者概率100%。如果sum是一个大的负数,sigmod函数会返回0。所以对于大的正数和负数,我们就能得到确定的“是”和“否”的预测结果。然而,如果sum接近0,sigmod函数就会返回一个接近50%的概率。当我们开始训练分类器的时候,初始预测会是50/50,这是因为分类器还没有学到任何东西,对所有的输出并不确定。但是随着训练次数的增加,概率就会越接近1和0,分类结果就会变得更加明确。

y_pred即语音来自男性的概率。如果这个概率大于0.5,我们就认为这是男性的声音,否则,就认为是女性的声音。

使用逻辑斯蒂回归的二值分类器的原理:分类器的输入数据由描述音频记录声学特征的20个数字组成,加权求和再使用sigmod函数,最后输出是男性语音的概率。

在TensorFlow上实现分类器

在TensorFlow上使用该分类器,首先需要创建一个计算图(computational graph)。计算图由表示进行运算的节点(nodes)以及节点之间流动的数据(data)组成。逻辑斯蒂回归的图如下所示:

这个图和前面的图看起来有一些区别,输入数据x不再是20个独立的数字,而是一个有20个元素的向量,权重用矩阵w表示,点积用简单的矩阵乘法代替。

这里的输入y用来检验分类器的效果。实验所用的数据集有3168条语音记录,并且我们知道每条记录是男性还是女性的。这些已知的输出(男性/女性)被称为标签(labels),这些标签保存在输入y中。

由于权重初始化时全部置为0,分类器可能会做出错误的预测。所以,我们使用损失函数(loss function)来衡量分类器的分类水平。损失函数会比较预测结果y_pred和正确标签y。计算完训练样本的损失值(loss),我们使用反向传播(back propagation)修正权重w和b的值。训练过程要在所有样本上重复进行,直到计算图得出最佳权重数据。衡量分类器效果的损失值会随时间变得越来越小。

张量简介

上图中数据从左边流向右边,从输入流向输出。这就是TensorFlow中“flow”的来源。图中的数据都是以张量(tensor)的形式流动的。张量其实就是n维数组(n-dimensional array)。前面提到w是权重矩阵,TensorFlow认为它是一个二阶张量(second-order tensor),其实也就是二维数组(two-dimensional array)。如:

1.标量数字就是0阶张量;

2.向量是一阶张量;

3.矩阵是二阶张量;

4.三维数组是三阶张量

深度学习中,比如卷积神经网络(convolutional neural networks, CNN)经常需要处理四维张量,但本文中的逻辑斯蒂分类器比较简单,不会超过二阶张量,即矩阵。之前提到x是一个向量,现在把x和y都当作一个矩阵。如此一来,损失值就可以一次性计算出来。单个样本有20个数据,如果载入全部3168个样本,那么x将变成一个3168 X 20的矩阵。在x和w相乘之后,输出y_pred是一个3168 X 1的矩阵。即为数据集中的每一个样本都进行了预测。总之,用矩阵/张量表示计算图,就可以一次性为多个样本进行预测。

安装TensorFlow

环境:Python3.6

在Mac上使用Homebrew包管理器安装Python 3.6很简单,如果还没有安装Homebrew,可以在线安装。然后打开终端(Terminal)输入如下命令安装最新版Python:

brew install python3

然后使用Python的包管理器pip来安装所需要的包,在终端输入如下命令:

pip3 install numpypip3 install scipypip3 install scikit-learnpip3 install pandaspip3 install tensorflow

除了TensorFlow,我们还安装了Numpy, Scipy, pandas和scikit-learn库,这些包会安装在/usr/local/lib/python3.6/site-packages目录下,你可以随时查看。pip可以自动安装最适合你系统的TensorFlow版本。如果你想安装其它版本,请参照离线安装指南

下面测试一下所有的东西是否都已被正确安装。创建tryit.py,如下:

import tensorflow as tfa = tf.constant([1, 2, 3])b = tf.constant([4, 5, 6])sess = tf.Session(config=tf.ConfigProto(log_device_placement=True))print(sess.run(a + b))

然后从终端运行这个脚本,就会输出关于设备的一些调试信息,最可能是关于CPU的,如果你的Mac装有NVIDIA GPU,就会输出GPU相关的情况。最后会输出:

[5 7 9]

这是两个向量a和b的和。亦有可能出现如下信息:

W tensorflow/core/platform/cpu_feature_guard.cc:45] The TensorFlow library wasn't compiled to use SSE4.1 instructions, but these are available on your machine and could speed up CPU computations.

如果出现上述调试信息,这意味着你的系统上安装的TensorFlow版本并不适合你的CPU。一种解决办法是从源(from source)安装TensorFlow,你可以自行配置所有的选项。

数据的详细分析

在本文的实验中,我们并没有使用TensorFlow教程中常用的MNIST手写数字是被数据集,而是使用了根据语音识别性别的数据集,voice.csv文件如下所示。这些数字代表语音记录不同的声学特征(acoustic properties)。通过脚本从录音中抽取出这些特征,然后转换为这个CSV文件。如果感兴趣的话可以参照R语言源码

这个数据集包含3168条样本数据,表格中每一行是一条样本,基本上男女各占一半。每条样本数据包含20个声学特征,如图所示:

虽然不清楚这些特征代表的含义,但这并不重要,我们关心的仅是从这些数据中训练出一个能够区分男性和女性声音的分类器。如果要在你的APP中检测音频是男性还是女性产生的,你首先需要从这些音频数据中抽取这些声学特征。只要找到了这20个声学特征,就可以使用我们的分类器进行预测。所以,这个分类器并不是直接作用在音频上的,而仅仅是作用在这些抽取出来的特征。

Note: 这里需要指出深度学习和传统算法如逻辑斯蒂回归的区别。我们训练的分类器不能学习非常复杂的东西,需要在数据预处理阶段抽取特征。而深度学习系统可以直接将原始音频数据作为输入,抽取重要的声学特征,然后再进行分类。

创建训练集和测试集

我创建了一个名为split_data.py的Python脚本来分割训练集和数据集,如下:

# This script loads the original dataset and splits it into a training set and test set.    import numpy as np  import pandas as pd    # Read the CSV file.  df = pd.read_csv("voice.csv", header=0)    # Extract the labels into a numpy array. The original labels are text but we convert  # this to numbers: 1 = male, 0 = female.  labels = (df["label"] == "male").values * 1    # labels is a row vector but TensorFlow expects a column vector, so reshape it.  labels = labels.reshape(-1, 1)    # Remove the column with the labels.  del df["label"]    # OPTIONAL: Do additional preprocessing, such as scaling the features.  # for column in df.columns:  #    mean = df[column].mean()  #    std = df[column].std()  #    df[column] = (df[column] - mean) / std    # Convert the training data to a numpy array.  data = df.values  print("Full dataset size:", data.shape)    # Split into a random training set and a test set.  from sklearn.model_selection import train_test_split  X_train, X_test, y_train, y_test = train_test_split(data, labels, test_size=0.3, random_state=123456)    print("Training set size:", X_train.shape)  print("Test set size:", X_test.shape)    # Save the matrices using numpy's native format.  np.save("X_train.npy", X_train)  np.save("X_test.npy", X_test)  np.save("y_train.npy", y_train)  np.save("y_test.npy", y_test)

在本例的二分类器中,我们用1表示男性,0表示女性。在终端运行这个脚本文件,最终会生成4个文件:训练数据(X_train.npy)及其标签(y_train.npy),测试数据(X_test.npy)及其标签(y_test.npy)

构建计算图

下面将使用train.py脚本,用TensorFlow训练逻辑斯蒂分类器,可在GitHub上查看完整代码。

先导入训练数据(X_train和y_train):

下面开始构建计算图。首先使用placeholders定义输入数据x和y:

tf.name_scope()将图的不同部分分成不同域,每个层都是在一个唯一的tf.name_scope()下创建,作为在该作用域内创建的元素的前缀,x的独特名字将会是‘inputs/x-input’,这里将输入数据x和y定义在inputs域下,分别命名为“x_input”和“y_put”,方便后面使用。

每条输入数据是有20个元素的一个向量,并且有一个对应的标签(1表示男性,0表示女性)。如果将所有的训练数据构成矩阵,那么就可以一次性完成计算。所以上面定义x和y为二维张量:x的维度是[None, 20],y的维度是[None, 1]。None表示第一个维度未知。实验中的训练集中有2217条样本,测试集有951条样本。

导入训练数据后,下面开始定义分类器参数(parameters):

with tf.name_scope("model"):        W = tf.Variable(tf.zeros([num_inputs, num_classes]), name="W")        b = tf.Variable(tf.zeros([num_classes]), name="b")

张量w是权重矩阵(一个20×1的矩阵),b是偏置。W和b被声明为TensorFlow的变量(variables),会在反向传播的过程中被更新。

下面声明逻辑斯蒂回归分类器的核心公式:

y_pred = tf.sigmoid(tf.matmul(x, W) + b)

这里将x和w相乘再加上b,然后输入sigmod函数中,得到预测值y_pred,表示x中音频数据是男性声音的概率。

Note:实际上,这行代码现在还没有计算任何东西,目前只是在构建计算图。这行代码将矩阵乘法和加法的节点,以及sigmod函数(tf.sigmoid)加入图中。当计算图构建完成时,创建一个TensorFlow会话(session),就可以测试真实数据了。

为了训练模型还需要定义一个损失函数(loss function),对于二值逻辑斯蒂回归分类器,TensorFlow已经内置了log_loss函数:

with tf.name_scope("loss-function"):        loss = tf.losses.log_loss(labels=y, predictions=y_pred)        loss += regularization * tf.nn.l2_loss(W)

log_loss节点接收样本数据的真实标签y作为输入,与预测值y_pred比较,比较的结果代表损失值(loss)。第一次训练时,在所有的样本上预测值y_pred都会是0.5,因为分类器现在并不知道真实答案。初始损失值为-ln(0.5),即0.693146,。随着不断训练,损失值会变得越来越小。

上面第三行代码加入了L2正则化项防止过拟合。正则项系数regularization 定义在另一个placeholder中:

with tf.name_scope("hyperparameters"):        regularization = tf.placeholder(tf.float32, name="regularization")        learning_rate = tf.placeholder(tf.float32, name="learning-rate")

前面我们使用了placeholder来定义输入x和y,这里又定义了超参(hyperparameters)。这些参数不像权重w和偏置b能够通过模型学习得到,你只能根据经验来设置。另一超参learning-rate定义了步长。

optimizer 进行反向传播运算:以loss作为输入,决定如何更新权重和偏置。TensorFlow中各种优化类提供了为损失函数计算梯度的方法,这里我们选用AdamOptimizer:

with tf.name_scope("train"):        optimizer = tf.train.AdamOptimizer(learning_rate)        train_op = optimizer.minimize(loss)

这里添加了操作节点train_op,用于最小化loss,后面会运行这个节点来训练分类器。在训练过程中,我们使用快照技术与准确率确定分类器效果。定义一个计算预测结果准确率的图节点accuracy:

with tf.name_scope("score"):        correct_prediction = tf.equal(tf.to_float(y_pred > 0.5), y)        accuracy = tf.reduce_mean(tf.to_float(correct_prediction), name="accuracy")

之前有说过y_pred是0到1之间的概率。通过tf.to_float(y_pred > 0.5),如果预测是女性,返回0;如果是男性,就返回1。通过tf.equal方法可以比较预测结果y_pred与实际结果y是否相等,返回布尔值。先把布尔值转换成浮点数,tf.reduce_mean()计算均值,最后的结果就是准确率。后面在测试集上也会使用这个accuracy节点确定分类器的真实效果。

对于没有标签的新数据,定义inference节点进行预测:

with tf.name_scope("inference"):        inference = tf.to_float(y_pred > 0.5, name="inference")

训练分类器

这个简单的逻辑斯蒂分类器可能很快就能训练好,但一个深度神经网络可能就需要数小时甚至几天才能达到足够好的准确率。下面是train.py的第一部分:

with tf.Session() as sess:        tf.train.write_graph(sess.graph_def, checkpoint_dir, "graph.pb", False)        sess.run(init)        step = 0        while True:        # here comes the training code

我们创建了一个session对象来运行图。调用sess.run(init)将w和b置为0。同时,将图保存在/tmp/voice/graph.pb文件。后面测试分类器在测试集上的效果以及将分类器用在IOS app上都需要用到这个图。

在while True:循环内,操作如下:

perm = np.arange(len(X_train))        np.random.shuffle(perm)        X_train = X_train[perm]        y_train = y_train[perm]

在每次进行训练时,将训练集中的数据随机打乱,避免让分类器根据样本的顺序来进行预测。下面session将会运行train_op节点,进行一次训练:

feed = {x: X_train, y: y_train, learning_rate: 1e-2,                regularization: 1e-5}        sess.run(train_op, feed_dict=feed)

通过sess.run()函数传入feed_dict参数,给使用placeholder中的张量赋值,启动运算过程。

本文所采用的是个简单分类器,每次都采用完整训练集进行训练,所以将x_train数组放入x中,将y_train数组放入y中。如果数据非常多,每次迭代就应该使用一小批数据(100到1000个样本)进行训练。

train_op节点会运行很多次,反向传播机制每次都会对权重w和偏置b进行微调,随着迭代次数增多,w和b就会逐渐达到最优值。为了帮助理解训练过程,在每迭代1000次时,运行accuracy和loss节点,输出相关信息:

if step % print_every == 0:            train_accuracy, loss_value = sess.run([accuracy, loss],                                                  feed_dict=feed)            print("step: %4d, loss: %.4f, training accuracy: %.4f" % \                    (step, loss_value, train_accuracy))

注意的是,在训练集上高准确率并不意味着在测试集上也能表现良好,但是这个值应该随着训练过程逐渐上升,loss值不断减小。

然后定义可以用来后续恢复模型以进一步训练或评估的检查点(checkpoint)文件。分类器目前学习到的w和b被保存到/tmp/voice/目录下:

然后运行train.py,得到如下结果:

当你发现loss不再下降,当下一个*** SAVED MODEL ***消息出现,这个时候你就可以按 Ctrl+C停止训练。

我选用learning_rate = 1e-2, regularization = 1e-5,在训练集上能够达到97%准确率和0.157左右的损失值。如果feed中regularization = 0,loss值会更低。

分类器效果

分类器训练好之后,就可以在测试数据上检验分类器的实际效果。我们创建一个新的脚本test.py,载入计算图和测试集,然后计算预测准确率。

Note: 测试集上的准确率会比训练集中的准确率(97%)低,但是不应该太低。如果你的训练器出现过拟合,那就需要重新调整训练过程了。

还是先导入包,然后载入测试数据:

import numpy as npimport tensorflow as tffrom sklearn import metricsX_test = np.load("X_test.npy")y_test = np.load("y_test.npy")

由于现在只是验证分类器的效果,所以并不需要整个图,只需要train_op 和 loss节点。之前已经将计算图保存到graph.pb文件,所以这里只需要载入就可以了:

with tf.Session() as sess:    graph_file = os.path.join(checkpoint_dir, "graph.pb")    with tf.gfile.FastGFile(graph_file, "rb") as f:        graph_def = tf.GraphDef()        graph_def.ParseFromString(f.read())        tf.import_graph_def(graph_def, name="")

TensorFlow推荐使用*.Pb保存数据,所以这里只需要一些辅助代码就可以载入这个文件,并导入会话(session)中。再从检查点文件中载入w和b的值:

W = sess.graph.get_tensor_by_name("model/W:0")    b = sess.graph.get_tensor_by_name("model/b:0")    checkpoint_file = os.path.join(checkpoint_dir, "model")    saver = tf.train.Saver([W, b])    saver.restore(sess, checkpoint_file)

我们将节点都放在域(scope)中并命名,就可以使用get_tensor_by_name()轻易找到。如果你没有给他们一个明确的命名,那么你只能在整个图中寻找TensorFlow默认名称,这将会很麻烦。还需要引用其他的节点,尤其是输入x和y以及进行预测的节点:

x = sess.graph.get_tensor_by_name("inputs/x-input:0")    y = sess.graph.get_tensor_by_name("inputs/y-input:0")    accuracy = sess.graph.get_tensor_by_name("score/accuracy:0")    inference = sess.graph.get_tensor_by_name("inference/inference:0")

现在就可以对测试集中的数据进行预测:

feed = {x: X_test, y: y_test}    print("Test set accuracy:", sess.run(accuracy, feed_dict=feed))

使用scikit-learn输出一些其他的信息:

predictions = sess.run(inference, feed_dict={x: X_test})    print("Classification report:")    print(metrics.classification_report(y_test.ravel(), predictions))    print("Confusion matrix:")    print(metrics.confusion_matrix(y_test.ravel(), predictions))

在终端运行test.py,结果如下:

如上图所示,在测试集上的准确率达到了96%,比训练集上的准确率略低。这意味着训练出来的分类器对未知数据也能准确分类。分类结果报告(Classification report)和混淆矩阵( confusion matrix)说明有些样本是预测错误的。混肴矩阵说明女性样本中446个预测正确,28个预测错误。男性样本中466个预测正确,11个预测错误。这说明分类器在预测女性声音时会出现更多错误。

下一节中,我们将会介绍如何将这个分类器运用到实际的app中。

-------------------------------------------------------------------------------------------

二、实际应用教程(附源码)

上一节中,我们介绍了在如何用TnesorFlow创建一个逻辑斯蒂回归分类器,接下来介绍如何将这个分类器运用在实际的app中。

在IOS上安装TensorFlow

前面已经训练好模型,下面创建一个利用TensorFlow C++ 库和这个模型的app。坏消息是你不得不从源构建TensorFlow,还需要使用Java环境;好消息是这个过程相对简单。完整的指导在这里,但是下面几步很重要(测试环境为TensorFlow 1.0)。

首先你得安装好Xcode 8,确定开发者目录指向你安装Xcode的位置并且已经被激活。(如果你在安装Xcode之前已经安装了Homebrew,这可能会指向错误的地址,导致TensorFlow安装失败):

sudo xcode-select -s /Applications/Xcode.app/Contents/Developer

我们将使用名为bazel的工具来安装TensorFlow。先使用Homebrew安装所需要的包:

brew cask install javabrew install bazelbrew install automakebrew install libtool

完成之后,你需要克隆TensorFlow GitHub仓库。注意,一定要保存在没有空格的路径下,否则bazel会拒绝构建。我是克隆到我的主目录下:

cd /Users/matthijsgit clone https://github.com/tensorflow/tensorflow -b r1.0

-b r1.0表明克隆的是r1.0分支。当然你也可以随时获取最新的分支或者主分支。

Note:在MacOS Sierra 上,运行下面的配置脚本报错了,我只能克隆主分支来代替。在OS X EI Caption 上使用r1.0分支就不会有任何问题。

一旦GitHub仓库克隆完毕,你就需要运行配置脚本(configure script):

cd tensorflow./configure

这里有些地方可能需要你自行配置,比如:

Please specify the location of python. [Default is /usr/bin/python]:

我写的是/usr/local/bin/python3,因为我使用的是Python 3.6。如果你选择默认选项,就会使用Python 2.7来创建TensorFlow。

Please specify optimization flags to use during compilation [Default is -march=native]:

这里只需要按Enter键。后面两个问题,只需要选择n(表示 no)。当询问使用哪个Python库时,按Enter键选择默认选项(应该是Python 3.6 库)。剩下的问题都选择n。随后,这个脚本将会下载大量的依赖项并准备构建TensorFlow所需的一切。

构建静态库

有两种方法构建TensorFlow:1.在Mac上使用bazel工具;2.在IOS上,使用Makefile。我们是在IOS上构建,所以选择第2种方式。不过因为会用到一些工具,也会用到第一种方式。

在TensorFlow的目录中执行以下脚本:

tensorflow/contrib/makefile/build_all_ios.sh

这个脚本首先会下载一些依赖项,然后开始构建。一切顺利的话,它会创建三个链入你的app的静态库:libtensorflow-core.a, libprotobuf.a, libprotobuf-lite.a。

还有另外两个工具需要构建,在终端运行如下两行命令:

bazel build tensorflow/python/tools:freeze_graphbazel build tensorflow/python/tools:optimize_for_inference

Note: 这个过程至少需要20分钟,因为它会从头开始构建TensorFlow(本次使用的是bazel)。如果遇到问题,请参考官方指导

在Mac上构建TensorFlow

这一步是可选的,不过因为已经安装了所有需要的包,在Mac上构建TensorFlow就没那么困难了。使用pip包代替官方的TensorFlow包进行安装。

现在你就可以创建一个自定义的TensorFlow版本。例如,当运行train.py脚本时,如果出现“The TensorFlow library wasn’t compiled to use SSE4.1 instructions”提醒,你可以编译一个允许这些指令的TensorFlow版本。

在终端运行如下命令来构建TensorFlow:

bazel build --copt=-march=native -c opt //tensorflow/tools/pip_package:build_pip_packagebazel-bin/tensorflow/tools/pip_package/build_pip_package /tmp/tensorflow_pkg

-march=native选项添加了对SSE,AVX,AVX2,FMA等指令的支持(如果这些指令能够在你的CPU上运行)。然后安装包:

pip3 uninstall tensorflowsudo -H pip3 install /tmp/tensorflow_pkg/tensorflow-1.0.0-XXXXXX.whl

更多详细指令请参考TensorFlow网站

固化计算图

我们将要创建的app会载入之前训练好的模型,并作出预测。之前在train.py中,我们将图保存到了 /tmp/voice/graph.pb文件中。但是你不能在IOS app中直接载入这个计算图,因为图中的部分操作是TensorFlow C++库并不支持。所以就需要用到上面我们构建的那两个工具。

freeze_graph将包含训练好的w和b的graph.pb和检查点文件合成为一个文件,并移除IOS不支持的操作。在终端运行TensorFlow目录下的这个工具:

bazel-bin/tensorflow/python/tools/freeze_graph \--input_graph=/tmp/voice/graph.pb --input_checkpoint=/tmp/voice/model \--output_node_names=model/y_pred,inference/inference --input_binary \--output_graph=/tmp/voice/frozen.pb

最终输出/tmp/voice/frozen.pb文件,只包含得到y_pred和inference的节点,不包括用来训练的节点。freeze_graph也将权重保存进了文件,就不用再单独载入。

optimize_for_inference工具进一步简化了可计算图,它以frozen.pb作为输入,以/tmp/voice/inference.pb作为输出。这就是我们将嵌入IOS app中的文件,按如下方式运行这个工具:

bazel-bin/tensorflow/python/tools/optimize_for_inference \--input=/tmp/voice/frozen.pb --output=/tmp/voice/inference.pb \--input_names=inputs/x --output_names=model/y_pred,inference/inference \--frozen_graph=True

IOS app

你可以在VoiceTensorFlow文件夹下找到这个app。用Xcode打开这个项目,有几处需要注意:

1. App是用C++写的(源文件后缀名为.mm),因为TensorFlow没有Swift API,只有C++的;

2.inference.pb文件已经包含在项目中,如果有需要的话,你可以用你自己的inference.pb文件替换掉;

3.这个app使用了Accelerate框架;

4.这个app使用了已经编译好的静态库。

在项目设置界面打开构建参数标签页,在Other Linker Flags,你会看见如下信息:

/Users/matthijs/tensorflow/tensorflow/contrib/makefile/gen/protobuf_ios/lib/libprotobuf-lite.a /Users/matthijs/tensorflow/tensorflow/contrib/makefile/gen/protobuf_ios/lib/libprotobuf.a -force_load /Users/matthijs/tensorflow/tensorflow/contrib/makefile/gen/lib/libtensorflow-core.a

除非你的名字也是“matthijs”,否则需要用你克隆的TensorFlow存放的路径进行替换。(TensorFlow出现了两次,所以文件名为tensorflow/tensorflow/...)。

Note: 你也可以将这3个文件拷贝到项目文件夹中,就不必担心路径出错了。我之所以没有这样做,是因为libtensorflow-core.a 文件有440MB大。

再检查Header Search Paths,目前的设置是:

~/tensorflow~/tensorflow/tensorflow/contrib/makefile/downloads ~/tensorflow/tensorflow/contrib/makefile/downloads/eigen ~/tensorflow/tensorflow/contrib/makefile/downloads/protobuf/src ~/tensorflow/tensorflow/contrib/makefile/gen/proto

然后你还要将这些路径更新到您克隆仓库的位置,还有些build settings我也做了修改:

1.Enable Bitcode: No

2.Warnings / Documentation Comments: No

3.Warnings / Deprecated Functions: No

目前TensorFlow并不支持字节码,所以我禁用了这个功能。我也关闭了警告功能,否则你编译app时会遇到很多问题。(虽然你还是会遇到值转换问题的警告,禁止这个警告功能也没毛病)。

完成Other Linker Flags和 the Header Search Paths的设置之后,就可以构建并运行app了。下面看一下这个使用TensorFlow的IOS app是如何工作的。

使用Tensorflow C++ API

IOS上的TensorFlow使用C++写的,不过需要你写的C++代码有限,通常,你只需要做下面几件事:

1.从.pb文件中载入计算图和权重;

2.使用图创建会话;

3.将数据放入输入张量;

4.在图上运行一个或多个节点;

5.得到输出张量结果。

在演示的APP中,这些都是写在ViewController.mm中。首先载入图:

- (BOOL)loadGraphFromPath:(NSString *)path{    auto status = ReadBinaryProto(tensorflow::Env::Default(),                                  path.fileSystemRepresentation, &graph);    if (!status.ok()) {        NSLog(@"Error reading graph: %s", status.error_message().c_str());        return NO;    }    return YES;}

Xcode项目包含在 graph.pb上运行freeze_graph 和optimize_for_inference工具得到的inference.pb图。如果你试图载入graph.pb,会报错:

Error adding graph to session: No OpKernel was registered to support Op 'L2Loss' with these attrs.  Registered devices: [CPU], Registered kernels:  [[Node: loss-function/L2Loss = L2Loss[T=DT_FLOAT](model/W/read)]]

这个C++ API 支持的操作要比Python API少。这里他说的是损失函数节点中L2Loss操作在IOS上不支持。这就是为什么我们要使用freeze_graph简化图。

在载入图之后,创建会话:

- (BOOL)createSession{    tensorflow::SessionOptions options;    auto status = tensorflow::NewSession(options, &session);    if (!status.ok()) {        NSLog(@"Error creating session: %s",                status.error_message().c_str());        return NO;    }    status = session->Create(graph);    if (!status.ok()) {        NSLog(@"Error adding graph to session: %s",                status.error_message().c_str());        return NO;    }    return YES;}

会话创建好之后,就可以进行预测了。predict:方法需要一个包含20个浮点数的元组,代表声学特征,然后传入图中,该方法如下所示:

- (void)predict:(float *)example {    tensorflow::Tensor x(tensorflow::DT_FLOAT,                          tensorflow::TensorShape({ 1, 20 }));    auto input = x.tensor();    for (int i = 0; i < 20; ++i) {        input(0, i) = example[i];    }

首先定义张量x作为输入数据。这个张量维度为{1, 20},因为它一次接收一个样本,每个样本有20个特征。然后从float *数组将数据拷贝至张量中。

接下来运行会话:

std::vector> inputs = {        {"inputs/x-input", x}    };    std::vector nodes = {        {"model/y_pred"},        {"inference/inference"}    };    std::vector outputs;    auto status = session->Run(inputs, nodes, {}, &outputs);    if (!status.ok()) {        NSLog(@"Error running model: %s", status.error_message().c_str());        return;    }

运行如下代码:

pred, inf = sess.run([y_pred, inference], feed_dict={x: example})

这条代码看起来并没有Python版的简洁。我们创建了feed字典,运行的节点列表,以及保存结果的向量。最后,打印结果:

auto y_pred = outputs[0].tensor();    NSLog(@"Probability spoken by a male: %f%%", y_pred(0, 0));    auto isMale = outputs[1].tensor();    if (isMale(0, 0)) {        NSLog(@"Prediction: male");    } else {        NSLog(@"Prediction: female");    }}

本来只需要运行inference节点就可以得到男性/女性的预测结果,但我还想看计算出来的概率,所以后面运行了y_pred节点。

运行app

你可以在iphone模拟器或者设备上运行这个app。在模拟器上,你可能会得到诸如 “The TensorFlow library wasn’t compiled to use SSE4.1 instructions”的消息,但是在设备上则不会报错。

app会做出来两种预测:男性/女性。运行这个app,你会看到下面的输出,它先打印出图中的节点:

Node count: 9Node 0: Placeholder 'inputs/x-input'Node 1: Const 'model/W'Node 2: Const 'model/b'Node 3: MatMul 'model/MatMul'Node 4: Add 'model/add'Node 5: Sigmoid 'model/y_pred'Node 6: Const 'inference/Greater/y'Node 7: Greater 'inference/Greater'Node 8: Cast 'inference/inference'

这个图只包含进行预测的节点,并不需要训练相关的节点。然后就会输出结果:

Probability spoken by a male: 0.970405%Prediction: maleProbability spoken by a male: 0.005632%Prediction: female

如果用Python脚本测试同样的数据,会得到相同的答案。

IOS上TensorFlow的优缺点

优点:

1. 一个工具搞定所有事。你可以使用TensorFlow训练模型并进行预测。不需要将计算图移植到其他的API,如BNNS或者Metal。另一方面,你只需要将少量Python代码移植到C++代码;

2.TensorFlow有比BNNS和Metal更多的特性;

3.你可以在模拟器上运行。Metal总是要在设备上运行。

缺点:

1.目前不支持GPU。TensorFlow使用 Accelerate 框架能够发挥CPU向量指令的优势,原始速度比不上Metal;

2.TensorFlow API使用C++写的,所以你不得不写一些C++代码,并不能直接使用Swift编写。

3.相比于Python API,C++ API有限。这意味着你不能在设备上进行训练,因为不支持反向传播中用到的自动梯度计算。

4.TensorFlow静态库增加了app包大概40MB的空间。通过减少支持操作的数量,可以减少这个额外空间,不过这很麻烦。而且,这还不包括模型的大小。

目前,我个人并不提倡在IOS上使用TensorFlow。优点并没有超过缺点,作为一款有潜力的产品,谁知道未来会怎样呢?

Note: 如果决定在你的IOS app中使用TensorFlow,那你必须知道别人很容易从app安装包中拷贝图的.pb文件窃取你的模型。由于固化的图文件包含模型参数和图定义,反编译简直轻而易举。如果你的模型具有竞争优势,你可能需要做出预案防止你的机密被窃取。

使用Metal在GPU上训练

IOS app上使用TensorFlow的一个弊端是他是运行在CPU上的。对于数据和模型较小的项目,TensorFlow能够满足我们的需求。但是对于更大的数据集,特别是深度学习,你就必须要使用GPU代替CPU,在IOS上就意味着要使用Metal。

训练后,我们需要将学习到的参数w和b保存成Metal能够读取的格式。其实只要以二进制格式保存为浮点数列表就可以了。

下面的Python脚本export_weights.py和之前载入图定义和检查点的test.py很相似,如下:

W.eval().tofile("W.bin")    b.eval().tofile("b.bin")

W.eval()计算w目前的值,并以返回Numpy数组(和sess.run(W)作用是一样的)。然后使用tofile()将Numpy数组保存为二进制文件。

你可以在源码的VoiceMetal文件夹下发现Xcode项目,使用Swift编写的。

之前我们使用下面的公式计算逻辑斯蒂回归:

y_pred = sigmoid((W * x) + b)

这和神经网络中全连接层进行的计算相同,为了实现Metal版分类器,我们只需要使用MPSCNN Fully Connected 层。首先将W.bin和b.bin载入到Data对象:

let W_url = Bundle.main.url(forResource: "W", withExtension: "bin")let b_url = Bundle.main.url(forResource: "b", withExtension: "bin")let W_data = try! Data(contentsOf: W_url!)let b_data = try! Data(contentsOf: b_url!)

然后创建全连接层:

let sigmoid = MPSCNNNeuronSigmoid(device: device)let layerDesc = MPSCNNConvolutionDescriptor(                  kernelWidth: 1, kernelHeight: 1,                    inputFeatureChannels: 20, outputFeatureChannels: 1,                    neuronFilter: sigmoid)W_data.withUnsafeBytes { W in  b_data.withUnsafeBytes { b in    layer = MPSCNNFullyConnected(device: device,                convolutionDescriptor: layerDesc,                kernelWeights: W, biasTerms: b, flags: .none)  }}

因为输入是20个数字,我设计了作用于一个1x1的有20个输入信道(input channels)的全连接层。预测结果y_pred是一个数字,所以全连接层只有一个输出信道。输入和输出数据放在MPSImage 中:

let inputImgDesc = MPSImageDescriptor(channelFormat: .float16,                        width: 1, height: 1, featureChannels: 20)let outputImgDesc = MPSImageDescriptor(channelFormat: .float16,                        width: 1, height: 1, featureChannels: 1)inputImage = MPSImage(device: device, imageDescriptor: inputImgDesc)outputImage = MPSImage(device: device, imageDescriptor: outputImgDesc)

和app上的TensorFlow一样,这里也有一个predict 方法,这个方法以组成一条样本的20个浮点数作为输入。下面是完整的方法:

func predict(example: [Float]) {  convert(example: example, to: inputImage)  let commandBuffer = commandQueue.makeCommandBuffer()  layer.encode(commandBuffer: commandBuffer, sourceImage: inputImage,                destinationImage: outputImage)  commandBuffer.commit()  commandBuffer.waitUntilCompleted()  let y_pred = outputImage.toFloatArray()  print("Probability spoken by a male: \(y_pred[0])%")  if y_pred[0] > 0.5 {    print("Prediction: male")  } else {    print("Prediction: female")  }}

和运行session的结果是一样的。convert(example:to:)和toFloatArray()方法加载和输出MPSImage 对象的辅助函数。

你需要在设备上运行这个app,因为模拟器不支持Metal。输出结果如下:

Probability spoken by a male: 0.970215%Prediction: maleProbability spoken by a male: 0.00568771%Prediction: female

注意到这些概率和用TensorFlow预测到的概率不完全相同,这是因为Metal使用16位浮点数,但结果相当接近。

版权许可

本文所用的数据集是Kory Becker制作的,在Kaggle.com下载,也参考了Kory的博文源码。其他人也写过IOS上TensorFlow相关的一些东西。从这些文章和代码中我受益匪浅:

1.Getting Started with Deep MNIST and TensorFlow on iOSby Matt Rajca

2.Speeding Up TensorFlow with Metal Performance Shadersalso by Matt Rajca

3.tensorflow-cocoa-exampleby Aaron Hillegass

4.TensorFlow iOS Examplesin the TensorFlow repository

以上为译文

本文由北邮@爱可可-爱生活老师推荐,阿里云云栖社区组织翻译。

文章原标题《Getting started with TensorFlow on iOS》,由Matthijs Hollemans发布。

译者:李烽 ;审校:董昭男

文章为简译,更为详细的内容,请查看原文。中文译制文档见附件。

推荐阅读更多精彩内容