IOS平台構建TensorFlow應用教程詳解(附源碼)(二)
更多深度文章,請關注雲計算頻道:https://yq.aliyun.com/cloud
推特地址:https://twitter.com/mhollemans
郵件地址:mailto:matt@machinethink.net
github地址:https://github.com/hollance
個人博客:https://machinethink.net/
前麵已經訓練好模型,下麵創建一個利用TensorFlow C++ 庫和這個模型的app。壞消息是你不得不從源構建TensorFlow,還需要使用Java環境;好消息是這個過程相對簡單。完整的指導在這裏,但是下麵幾步很重要(測試環境為TensorFlow 1.0)。
sudo xcode-select -s /Applications/Xcode.app/Contents/Developer
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表明克隆的是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]:
tensorflow/contrib/makefile/build_all_ios.sh
這個腳本首先會下載一些依賴項,然後開始構建。一切順利的話,它會創建三個鏈入你的app的靜態庫:libtensorflow-core.a, libprotobuf.a, libprotobuf-lite.a。
bazel build tensorflow/python/tools:freeze_graph
bazel build tensorflow/python/tools:optimize_for_inference
Note: 這個過程至少需要20分鍾,因為它會從頭開始構建TensorFlow(本次使用的是bazel)。如果遇到問題,請參考官方指導。
現在你就可以創建一個自定義的TensorFlow版本。例如,當運行train.py腳本時,如果出現“The TensorFlow library wasn’t compiled to use SSE4.1 instructions”提醒,你可以編譯一個允許這些指令的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
pip3 uninstall tensorflow
sudo -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
你可以在VoiceTensorFlow 文件夾下找到這個app。用Xcode打開這個項目,有幾處需要注意:
/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
~/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
1.從.pb文件中載入計算圖和權重;
- (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:
<no 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<float, 2>();
for (int i = 0; i < 20; ++i) {
input(0, i) = example[i];
}
首先定義張量x作為輸入數據。這個張量維度為{1, 20},因為它一次接收一個樣本,每個樣本有20個特征。然後從float *數組將數據拷貝至張量中。
std::vector<std::pair<std::string, tensorflow::Tensor>> inputs = {
{"inputs/x-input", x}
};
std::vector<std::string> nodes = {
{"model/y_pred"},
{"inference/inference"}
};
std::vector<tensorflow::Tensor> 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})
auto y_pred = outputs[0].tensor<float, 2>();
NSLog(@"Probability spoken by a male: %f%%", y_pred(0, 0));
auto isMale = outputs[1].tensor<float, 2>();
if (isMale(0, 0)) {
NSLog(@"Prediction: male");
} else {
NSLog(@"Prediction: female");
}
}
本來隻需要運行inference節點就可以得到男性/女性的預測結果,但我還想看計算出來的概率,所以後麵運行了y_pred節點。
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
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的空間。通過減少支持操作的數量,可以減少這個額外空間,不過這很麻煩。而且,這還不包括模型的大小。
Note: 如果決定在你的IOS app中使用TensorFlow,那你必須知道別人很容易從app安裝包中拷貝圖的.pb文件竊取你的模型。由於固化的圖文件包含模型參數和圖定義,反編譯簡直輕而易舉。如果你的模型具有競爭優勢,你可能需要做出預案防止你的機密被竊取。
訓練後,我們需要將學習到的參數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: male
Probability 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 iOS by Matt Rajca
2.Speeding Up TensorFlow with Metal Performance Shaders also by Matt Rajca
3.tensorflow-cocoa-example by Aaron Hillegass
4.TensorFlow iOS Examples in the TensorFlow repository
以上為譯文
本文由北郵@愛可可-愛生活 老師推薦,阿裏雲雲棲社區組織翻譯。
文章原標題《Getting started with TensorFlow on iOS》,由Matthijs Hollemans發布。
譯者:李烽 ;審校:
文章為簡譯,更為詳細的內容,請查看原文。中文譯製文檔見附件。
最後更新:2017-04-15 00:02:21