版权声明
作者:Matthijs Hollemans
译者:运和凭
原文链接:
http://machinethink.net/blog/tensorflow-on-ios/
本文由作者授权翻译并发布,未经允许禁止转载。
在
上篇文章
中,我们了解了TensorFlow的特性和具体如何使用,并且实际创建了一个分类器,那么它的实际效果如何?
在本文里我们还将看到
如何在iOS上使用TensorFlow,最后讨论一下iOS上使用TensorFlow的优劣。
在完成对分类器的训练之后,接下来就是对其进行测试以了解它在实践当中的运行效果。大家需要使用未在训练中涉及过的数据完成这项测试。正因为如此,我们才在此前将数据集拆分为训练集与测试集。
我们将创建一套新的test.py脚本,负责加载计算图定义以及测试集,并计算其正确预测的测试示例数量。这里我将只提供重要的部分,大家可以点击此处查看完整脚本内容。
备注:测试集的结果精确度将始终低于训练集的结果精确度(后者为97%)。不过如果前者远低于后者,则大家应对分类器进行检查并对训练流程进行调整。我们预计测试集的实际结果应该在95%左右。任何低于90%的精度结果都应引起重视。
与之前一样,这套脚本会首先导入必要软件包,包括来自scikit-learn的指标包以输出各类其它报告。当然,这一次我们选择加载测试集而不再是训练集。
import numpy as np
import tensorflow as tf from sklearn
import metrics
X_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),因此我们可以使用部分helper代码以加载此文件并将其作为计算图导入至会话当中。
接下来,我们需要从checkpoint文件处加载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)
正因为如此,我们需要将节点引入范围并为其命名,从而利用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")
好的,到这里我们已经将计算图重新加载至内存当中。我们还需要再次将分类器学习到的内容加载至W与b当中。现在我们终于可以测试分类器在处理其之前从未见过的数据时表现出的精确度了:
feed = {x: X_test, y: y_test}
print("Test set accuracy:", sess.run(accuracy, feed_dict=feed))
上述代码会运行accuracy节点并利用来自X_test数组的声学特征作为输入内容,同时使用来自y_test的标签进行结果验证。
备注:这一次,馈送词典不再需要为learning_rate与regularization占位符指定任何值。我们只需要在accuracy节点上运行计算图的一部分,且此部分中并不包括这些占位符。
我们还可以借助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))
这一次,我们使用inference节点以获取预测结果。由于inference只会计算预测结果而不会检查其精确度,因此馈送词典中仅需要包含输入内容x而不再需要y。
运行此脚本之后,大家应该会看到类似于以下内容的输出结果:
$ python3 test.py
Test set accuracy: 0.958991
Classification report:
precision recall f1-score support
0 0.98 0.94 0.96 474
1 0.94 0.98 0.96 477
avg / total 0.96 0.96 0.96 951
Confusion matrix:
[[446 28]
[ 11 466]]
测试集的预测精确度接近96%——与预期一样,略低于训练集的精确度,但也已经相当接近。这意味着我们的训练已经获得成功,且我们也证明了这套分类器能够有效处理其从未见过的数据。其当然还不够完美——每25次尝试中即有1次属于分类错误,但对于本教程来说这一结果已经完全令人满意。
分类报告与混淆矩阵显示了与错误预测相关的示例统计信息。通过混淆矩阵,我们可以看到共有446项得到正确预测的女声示例,而另外28项女声示例则被错误地判断为男声。在466项男声示例中分类器给出了正确结论,但有11项则被错误判断为女声。
这样看来,我们的分类器似乎不太擅长分辨女性的语音,因为其女声识别错误率更高。分类报告/回调数字亦给出了相同的结论。
现在我们已经拥有了一套经过训练的模型,其拥有比较理想的测试集预测精确度。下面我们将构建一款简单的iOS应用,并利用这套模型在其中实现预测能力。首先,我们利用TensorFlow C++库构建一款应用。在下一章节中,我们将把模型引入Metal以进行比较。
这里我们既有好消息也有坏消息。坏消息是大家需要利用源代码自行构建TensorFlow。事实上,情况相当糟糕:大家需要安装Java方可实现这项目标。而好消息是整个流程其实并不复杂。感兴趣的朋友可以点击此处查看完整指南,但以下步骤也基本能够帮助大家解决问题(在TensorFlow 1.0上实测有效)。
这里需要注意的是,大家应当安装Xcode 8,并确保活动开发者目录指向您Xcode的安装位置(如果大家先安装了Homebrew,随后才安装Xcode,则其可能指向错误位置,意味着TensorFlow将无法完成构建):
sudo xcode-select -s /Applications/Xcode.app/Contents/Developer
TensorFlow利用一款名为bazel的工具进行构建,bazel则要求配合Java JDK 8。大家可以利用Homebrew轻松安装必要的软件包:
brew cask install java brew install bazel brew install automake brew install libtool
在完成之后,大家需要克隆TensorFlow GitHub库。需要注意的是:请确保您指定的路径不具备充足的空间,否则bazel会拒绝进行构建(没错,这是真的!)。我直接将其克隆到了自己的主目录当中:
cd /Users/matthijs git clone https://github.com/tensorflow/tensorflow -b r1.0
其中的-b r1.0标记告知git克隆r1.0分支。大家可以随意使用其它更新的分支,或者选择使用master分支。
备注:在MacOS Sierra上,接下来即将运行的configure脚本会提示多项错误。为了解决问题,我不得不选择克隆master分支。在OS X El Capitan上,使用r1.0分支则不会引发任何错误。
在代码库克隆完成后,大家需要运行configure脚本。
cd tensorflow ./configure
其会提出几个问题,以下为我给出的回答:
Please specify the location of python. [Default is /usr/bin/python]:
我的回答是/usr/local/bin/python3,因为我希望使用Python 3.6配合TensorFlow。如果大家选择默认选项,则TensorFlow将利用Python 2.7进行构建。
Please specify optimization flags to use during compilation [Default is -march=native]:
在这里直接按下回车键,接下来的几个问题则全部按n选择否。
在其问及要使用哪套Python库时,按下回车以选择默认选项(即Python 3.6库)。
接下来的问题全部按n选择否。现在该脚本将下载几项依赖性项目并为构建TensorFlow做好准备。
我们可以通过以下两种方式构建TensorFlow:
在Mac系统上,使用bazel构建工具。
在iOS上使用Makefile。
由于我们需要面向iOS进行构建,因此选择选项二。然而,我们还需要构建其它一些工具,因此也得涉及选项一的内容。
在tensorflow目录下执行以下脚本:
tensorflow/contrib/makefile/build_all_ios.sh
其会首先下载一些依赖性选项,而后开始进行构建流程。如果一切顺利,其将创建出三套接入应用所必需的静态库,分别为: libtensorflow-core.a、libprotobuf.a、libprotobuf-lite.a。
警告:构建这些库需要一段时间——我的iMac需要25分钟,机型较旧的MacBook Pro则需要3个小时,而且整个过程中风扇一直在全力运转!大家可能会在过程中看到一些编译器警告甚至错误提示信息一闲而过。当作没看见就好,时间一到工作自然就绪!
到这里工作还没结束。我们还需要构建其它两款helper工具。在终端当中运行以下两条命令:
bazel build tensorflow/python/tools:freeze_graph bazel build tensorflow/python/tools:optimize_for_inference
注意:这一过程大约需要20分钟左右,因为其需要再次从零开始构建TensorFLow(这一次使用bazel)。
备注:如果大家在过程中遇到了问题,请参阅官方指南。
这部分内容为可选项目,但由于大家已经安装了全部必要软件包,因此为Mac系统构建TensorFlow并不困难。其会创建一个新的pip软件包,大家可进行安装以取代官方TensorFlow软件包。
为什么不使用官方软件包?因为这样我们才能创建一套包含自定义选项的TensorFlow版本。举例来说,如果大家在运行train.py脚本时遇到了“此TensorFlow库无法利用SSE4.1指令进行编译”的警告提示,则可编译一套特殊的TensorFLow版本以启用这些指令。
要为Mac系统构建TensorFlow,请在终端中运行以下命令:
bazel build --copt=-march=native -c opt //tensorflow/tools/pip_package:build_pip_package bazel-bin/tensorflow/tools/pip_package/build_pip_package /tmp/tensorflow_pkg
其中的-march=native选项用于在您的CPU能够支持的前提下,添加对SSE、AVX、AVX2以及FMA等的支持。
随后安装该软件包:
pip3 uninstall tensorflow sudo -H pip3 install /tmp/tensorflow_pkg/tensorflow-1.0.0-XXXXXX.whl
欲了解更多细节信息,请参阅TensorFlow官方网站。
我们将要创建的iOS应用将利用Python脚本加载之前训练完成的模型,并利用其作出一系列预测。
大家应该还记得,train.py将计算图定义保存在了/tmp/voice/graph.pb文件当中。遗憾的是,大家无法直接将该计算图加载至iOS应用当中。完整的计算图中包含的操作目前还不受TensorFlow C++ API的支持。正因为如此,我们才需要使用刚刚构建完成的其它两款工具。其中freeze_graph负责获取graph.pb以及包含有W与b训练结果值的checkpoint文件。其还会移除一切在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的好处在于,其还将固定该文件中的权重值,这样大家就无需分别进行权重值加载了:frozen.pb中已经包含我们需要的一切。而optimize_for_inference工具则负责对计算图进行进一步简化。其将作为grozen.pb文件的输入内容,并写入/tmp/voice/inference.pb作为输出结果。我们随后会将此文件嵌入至iOS应用当中。使用以下命令运行该工具:
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
大家可以在github.com/hollance/TensorFlow-iOS-Example中的VoiceTensorFlow文件夹内找到我们此次使用的iOS应用。
在Xcode当中打开该项目,其中包含以下几条注意事项:
-
此应用利用Objective-C++编写(其源文件扩展名为.mm)。在编写时尚不存在面向TensorFlow的Swift API,因此只能使用C++。
-
其中的inference.pb文件已经包含在项目当中。如果需要,大家也可以直接将自有版本的inference.pb复制到此项目的文件夹之内。
-
此应用与Accelerate.framework相链接。
-
此应用与我们此前已经编译完成的几套静态库相链接。
前往Project Settings(项目设置)屏幕并切换至Build Settings(构建设置)标签。在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/...)
备注:大家也可以将这三个.a文件复制到项目文件夹之内,如此即不必担心路径可能出现问题。我个人并不打算在这一示例项目中采取这种方式,因为libtensorflow-core.a文件是一套体积达440 MB的库。
另外请注意检查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
另外,大家还需要将其更新至您的克隆目录当中。
以下为我在构建设置当中进行了修改的其它条目:
Bitcode目前尚不受TensorFLow的支持,所以我决定将其禁用。我还关闭了警告选项,否则在编译应用时会出现一大票问题提示。(禁用之后,大家仍然会遇到几项关于值转换问题的警告。大家当然也可以将其一并禁用,但我个人还是希望多少了解一点其中的错误。)
在完成了对Other Linker Flags与Header Search Paths的变更之后,大家即可构建并运行我们的iOS应用。
很好,现在大家已经拥有了一款能够使用TensorFlow的iOS应用了!下面让我们看看它的实际运行效果。
TensorFlow for iOS由C++编写而成,但其中需要编写的C++代码量其实——幸运的是——并不多。一般来讲,大家只需要完成以下工作:
-
从.pb文件中加载计算图与权重值。
-
利用此计算图创建一项会话。
-
将您的数据放置在一个输入张量内。
-
在一个或者多个节点上运行计算图。
-
从输出结果张量中获取结果。
在本示例应用当中,这一切皆发生在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。这里提到我们在loss函数节点中所使用的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:method会提供一个包含20项浮点数值的数组——即声学特征——并将这些数字馈送至计算得意洋洋发中。
下面我们一起来看此方法的工作方式:
- (void)predict:(float *)example {
tensorflow::Tensor x(tensorflow::DT_FLOAT,
tensorflow::TensorShape({ 1, 20 }));
auto input = x.tensor();
for (int i = 0; i
其首先将张量x定义为我们需要使用的输入数据。此张量为{1,20},因为其一次提取一项示例且该示例中包含20项特征。在此之后,我们将数据由float *数组复制至该张量当中。
接下来,我们运行该项会话:
std::vector<:pair tensorflow::tensor="">> inputs = {
{"inputs/x-input", x}
};
std::vector<:string> nodes = {
{"model/y_pred"},
{"inference/inference"}
};
std::vector<:tensor> outputs;
auto status = session->Run(inputs, nodes, {}, &outputs);
if (!status.ok()) {
NSLog(@"Error running model: %s", status.error_message().c_str());
return;
}
这里得出了类似于Python代码的内容:
pred, inf = sess.run([y_pred, inference], feed_dict={x: example})
只是不那么简洁。我们需要创建馈送词典、用于列出需要运行的全部节点的向量,外加一个负责容纳对应结果的向量。最后,我们告知该会话完成上述任务。
在会话运行了全部必要节点后,我们即可输出以下结果:
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节点。
大家可以在iPhone模拟器或者实机之上运行这款应用。在模拟器上,大家仍然会看到“此TensorFlow库无法利用SSE4.1指令进行编译”的提示,但在实机上则不会出现这样的问题。
出于测试的目的,这款应用只会进行两项预测:一次为男声示例预测,一次为女声示例预测。(我直接从测试集中提取了对应示例。大家也可以配合其它示例并修改maleExample或者emaleExample数组当中的数字。)
运行这款应用,大家应该会看到以下输出结果。该应用首先给出了计算图当中的各节点:
Node count: 9
Node 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: male Probability spoken by a male: 0.005632% Prediction: female
如果大家利用Python脚本尝试使用同样的示例,那么结果也将完全一致。任务完成!
备注:这里要提醒大家,此项演示项目中我们对数据进行了“伪造”(即使用了提取自测试集中的示例)。如果大家希望利用这套模型处理真正的音频,则首先需要将对应音频转化为20项声学特征。
TensorFlow是一款出色的机器学习模型训练工具,特别是对于那些不畏数学计算并乐于创建新型算法的朋友。要对规模更大的模型进行训练,大家甚至可以在云环境下使用TensorFLow。
除了训练之外,本篇博文还介绍了如何将TensorFLow添加至您的iOS应用当中。对于这一部分,我希望概括这种作法的优势与缺点。
在iOS之上使用TensorFlow的优势:
-
使用一款工具即可实现全部预期。大家可以同时利用TensorFlow训练模型并将其引用于设备之上。我们不再需要将自己的计算图移植至BNNS或者Metal等其它API处。在另一方面,大家则必须至少将部分Python代码“移植”为C++形式。