IOS平台TensorFlow實踐

作者簡介:

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 python3n

然後使用Python的包管理器pip來安裝所需要的包,在終端輸入如下命令:

pip3 install numpynpip3 install scipynpip3 install scikit-learnnpip3 install pandasnpip3 install tensorflown

除了TensorFlow,我們還安裝了Numpy, Scipy, pandas和scikit-learn庫,這些包會安裝在/usr/local/lib/python3.6/site-packages目錄下,你可以隨時查看。pip可以自動安裝最適合你系統的TensorFlow版本。如果你想安裝其它版本,請參照離線安裝指南。

下面測試一下所有的東西是否都已被正確安裝。創建tryit.py,如下:

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

然後從終端運行這個腳本,就會輸出關於設備的一些調試信息,最可能是關於CPU的,如果你的Mac裝有NVIDIA GPU,就會輸出GPU相關的情況。最後會輸出:

[5 7 9]n

這是兩個向量a和b的和。亦有可能出現如下信息:

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

如果出現上述調試信息,這意味著你的系統上安裝的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. nt ntimport numpy as np ntimport pandas as pd nt nt# Read the CSV file. ntdf = pd.read_csv("voice.csv", header=0) nt nt# Extract the labels into a numpy array. The original labels are text but we convert nt# this to numbers: 1 = male, 0 = female. ntlabels = (df["label"] == "male").values * 1 nt nt# labels is a row vector but TensorFlow expects a column vector, so reshape it. ntlabels = labels.reshape(-1, 1) nt nt# Remove the column with the labels. ntdel df["label"] nt nt# OPTIONAL: Do additional preprocessing, such as scaling the features. nt# for column in df.columns: nt# mean = df[column].mean() nt# std = df[column].std() nt# df[column] = (df[column] - mean) / std nt nt# Convert the training data to a numpy array. ntdata = df.values ntprint("Full dataset size:", data.shape) nt nt# Split into a random training set and a test set. ntfrom sklearn.model_selection import train_test_split ntX_train, X_test, y_train, y_test = train_test_split(data, labels, test_size=0.3, random_state=123456) nt ntprint("Training set size:", X_train.shape) ntprint("Test set size:", X_test.shape) nt nt# Save the matrices using numpys native format. ntnp.save("X_train.npy", X_train) ntnp.save("X_test.npy", X_test) ntnp.save("y_train.npy", y_train) ntnp.save("y_test.npy", y_test) n

在本例的二分類器中,我們用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"):n W = tf.Variable(tf.zeros([num_inputs, num_classes]), name="W")n b = tf.Variable(tf.zeros([num_classes]), name="b")n

張量w是權重矩陣(一個20×1的矩陣),b是偏置。W和b被聲明為TensorFlow的變數(variables),會在反向傳播的過程中被更新。

下面聲明邏輯斯蒂回歸分類器的核心公式:

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

這裡將x和w相乘再加上b,然後輸入sigmod函數中,得到預測值y_pred,表示x中音頻數據是男性聲音的概率。

Note:實際上,這行代碼現在還沒有計算任何東西,目前只是在構建計算圖。這行代碼將矩陣乘法和加法的節點,以及sigmod函數(tf.sigmoid)加入圖中。當計算圖構建完成時,創建一個TensorFlow會話(session),就可以測試真實數據了。

為了訓練模型還需要定義一個損失函數(loss function),對於二值邏輯斯蒂回歸分類器,TensorFlow已經內置了log_loss函數:

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

log_loss節點接收樣本數據的真實標籤y作為輸入,與預測值y_pred比較,比較的結果代表損失值(loss)。第一次訓練時,在所有的樣本上預測值y_pred都會是0.5,因為分類器現在並不知道真實答案。初始損失值為-ln(0.5),即0.693146,。隨著不斷訓練,損失值會變得越來越小。

上面第三行代碼加入了L2正則化項防止過擬合。正則項係數regularization 定義在另一個placeholder中:

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

前面我們使用了placeholder來定義輸入x和y,這裡又定義了超參(hyperparameters)。這些參數不像權重w和偏置b能夠通過模型學習得到,你只能根據經驗來設置。另一超參learning-rate定義了步長。

optimizer 進行反向傳播運算:以loss作為輸入,決定如何更新權重和偏置。TensorFlow中各種優化類提供了為損失函數計算梯度的方法,這裡我們選用AdamOptimizer:

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

這裡添加了操作節點train_op,用於最小化loss,後面會運行這個節點來訓練分類器。在訓練過程中,我們使用快照技術與準確率確定分類器效果。定義一個計算預測結果準確率的圖節點accuracy:

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

之前有說過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"):n inference = tf.to_float(y_pred > 0.5, name="inference")n

訓練分類器

這個簡單的邏輯斯蒂分類器可能很快就能訓練好,但一個深度神經網路可能就需要數小時甚至幾天才能達到足夠好的準確率。下面是train.py的第一部分:

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

我們創建了一個session對象來運行圖。調用sess.run(init)將w和b置為0。同時,將圖保存在/tmp/voice/graph.pb文件。後面測試分類器在測試集上的效果以及將分類器用在IOS app上都需要用到這個圖。

在while True:循環內,操作如下:

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

在每次進行訓練時,將訓練集中的數據隨機打亂,避免讓分類器根據樣本的順序來進行預測。下面session將會運行train_op節點,進行一次訓練:

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

通過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:n train_accuracy, loss_value = sess.run([accuracy, loss], n feed_dict=feed)n print("step: %4d, loss: %.4f, training accuracy: %.4f" % n (step, loss_value, train_accuracy))n

注意的是,在訓練集上高準確率並不意味著在測試集上也能表現良好,但是這個值應該隨著訓練過程逐漸上升,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 npnimport tensorflow as tfnfrom sklearn import metricsnnX_test = np.load("X_test.npy")ny_test = np.load("y_test.npy")n

由於現在只是驗證分類器的效果,所以並不需要整個圖,只需要train_op 和 loss節點。之前已經將計算圖保存到graph.pb文件,所以這裡只需要載入就可以了:

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

TensorFlow推薦使用*.Pb保存數據,所以這裡只需要一些輔助代碼就可以載入這個文件,並導入會話(session)中。再從檢查點文件中載入w和b的值:

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

我們將節點都放在域(scope)中並命名,就可以使用get_tensor_by_name()輕易找到。如果你沒有給他們一個明確的命名,那麼你只能在整個圖中尋找TensorFlow默認名稱,這將會很麻煩。還需要引用其他的節點,尤其是輸入x和y以及進行預測的節點:

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

現在就可以對測試集中的數據進行預測:

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

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

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

在終端運行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/Developern

我們將使用名為bazel的工具來安裝TensorFlow。先使用Homebrew安裝所需要的包:

brew cask install javanbrew install bazelnbrew install automakenbrew install libtooln

完成之後,你需要克隆TensorFlow GitHub倉庫。注意,一定要保存在沒有空格的路徑下,否則bazel會拒絕構建。我是克隆到我的主目錄下:

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

-b r1.0表明克隆的是r1.0分支。當然你也可以隨時獲取最新的分支或者主分支。

Note:在MacOS Sierra 上,運行下面的配置腳本報錯了,我只能克隆主分支來代替。在OS X EI Caption 上使用r1.0分支就不會有任何問題。

一旦GitHub倉庫克隆完畢,你就需要運行配置腳本(configure script):

cd tensorflown./configuren

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

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

我寫的是/usr/local/bin/python3,因為我使用的是Python 3.6。如果你選擇默認選項,就會使用Python 2.7來創建TensorFlow。

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

這裡只需要按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.shn

這個腳本首先會下載一些依賴項,然後開始構建。一切順利的話,它會創建三個鏈入你的app的靜態庫:libtensorflow-core.a, libprotobuf.a, libprotobuf-lite.a。

還有另外兩個工具需要構建,在終端運行如下兩行命令:

bazel build tensorflow/python/tools:freeze_graphnbazel build tensorflow/python/tools:optimize_for_inferencen

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_packagennbazel-bin/tensorflow/tools/pip_package/build_pip_package /tmp/tensorflow_pkgn

-march=native選項添加了對SSE,AVX,AVX2,FMA等指令的支持(如果這些指令能夠在你的CPU上運行)。然後安裝包:

pip3 uninstall tensorflownsudo -H pip3 install /tmp/tensorflow_pkg/tensorflow-1.0.0-XXXXXX.whln

更多詳細指令請參考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 n--input_graph=/tmp/voice/graph.pb --input_checkpoint=/tmp/voice/model n--output_node_names=model/y_pred,inference/inference --input_binary n--output_graph=/tmp/voice/frozen.pbn

最終輸出/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 n--input=/tmp/voice/frozen.pb --output=/tmp/voice/inference.pb n--input_names=inputs/x --output_names=model/y_pred,inference/inference n--frozen_graph=Truen

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/nlibprotobuf-lite.a nn/Users/matthijs/tensorflow/tensorflow/contrib/makefile/gen/protobuf_ios/lib/nlibprotobuf.a nn-force_load /Users/matthijs/tensorflow/tensorflow/contrib/makefile/gen/lib/nlibtensorflow-core.an

除非你的名字也是「matthijs」,否則需要用你克隆的TensorFlow存放的路徑進行替換。(TensorFlow出現了兩次,所以文件名為tensorflow/tensorflow/...)。

Note: 你也可以將這3個文件拷貝到項目文件夾中,就不必擔心路徑出錯了。我之所以沒有這樣做,是因為libtensorflow-core.a 文件有440MB大。

再檢查Header Search Paths,目前的設置是:

~/tensorflown~/tensorflow/tensorflow/contrib/makefile/downloads n~/tensorflow/tensorflow/contrib/makefile/downloads/eigen n~/tensorflow/tensorflow/contrib/makefile/downloads/protobuf/src n~/tensorflow/tensorflow/contrib/makefile/gen/proton

然後你還要將這些路徑更新到您克隆倉庫的位置,還有些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 *)pathn{n auto status = ReadBinaryProto(tensorflow::Env::Default(), n path.fileSystemRepresentation, &graph);n if (!status.ok()) {n NSLog(@"Error reading graph: %s", status.error_message().c_str());n return NO;n }n return YES;n}n

Xcode項目包含在 graph.pb上運行freeze_graph 和optimize_for_inference工具得到的inference.pb圖。如果你試圖載入graph.pb,會報錯:

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

這個C++ API 支持的操作要比Python API少。這裡他說的是損失函數節點中L2Loss操作在IOS上不支持。這就是為什麼我們要使用freeze_graph簡化圖。

在載入圖之後,創建會話:

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

會話創建好之後,就可以進行預測了。predict:方法需要一個包含20個浮點數的元組,代表聲學特徵,然後傳入圖中,該方法如下所示:

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

首先定義張量x作為輸入數據。這個張量維度為{1, 20},因為它一次接收一個樣本,每個樣本有20個特徵。然後從float *數組將數據拷貝至張量中。

接下來運行會話:

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

運行如下代碼:

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

這條代碼看起來並沒有Python版的簡潔。我們創建了feed字典,運行的節點列表,以及保存結果的向量。最後,列印結果:

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

本來只需要運行inference節點就可以得到男性/女性的預測結果,但我還想看計算出來的概率,所以後面運行了y_pred節點。

運行app

你可以在iphone模擬器或者設備上運行這個app。在模擬器上,你可能會得到諸如 「The TensorFlow library wasn』t compiled to use SSE4.1 instructions」的消息,但是在設備上則不會報錯。

app會做出來兩種預測:男性/女性。運行這個app,你會看到下面的輸出,它先列印出圖中的節點:

Node count: 9nNode 0: Placeholder inputs/x-inputnNode 1: Const model/WnNode 2: Const model/bnNode 3: MatMul model/MatMulnNode 4: Add model/addnNode 5: Sigmoid model/y_prednNode 6: Const inference/Greater/ynNode 7: Greater inference/GreaternNode 8: Cast inference/inferencen

這個圖只包含進行預測的節點,並不需要訓練相關的節點。然後就會輸出結果:

Probability spoken by a male: 0.970405%nPrediction: malennProbability spoken by a male: 0.005632%nPrediction: femalen

如果用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")n b.eval().tofile("b.bin")n

W.eval()計算w目前的值,並以返回Numpy數組(和sess.run(W)作用是一樣的)。然後使用tofile()將Numpy數組保存為二進位文件。

你可以在源碼的VoiceMetal文件夾下發現Xcode項目,使用Swift編寫的。

之前我們使用下面的公式計算邏輯斯蒂回歸:

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

這和神經網路中全連接層進行的計算相同,為了實現Metal版分類器,我們只需要使用MPSCNN Fully Connected 層。首先將W.bin和b.bin載入到Data對象:

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

然後創建全連接層:

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

因為輸入是20個數字,我設計了作用於一個1x1的有20個輸入信道(input channels)的全連接層。預測結果y_pred是一個數字,所以全連接層只有一個輸出信道。輸入和輸出數據放在MPSImage 中:

let inputImgDesc = MPSImageDescriptor(channelFormat: .float16, n width: 1, height: 1, featureChannels: 20)nlet outputImgDesc = MPSImageDescriptor(channelFormat: .float16, n width: 1, height: 1, featureChannels: 1)nninputImage = MPSImage(device: device, imageDescriptor: inputImgDesc)noutputImage = MPSImage(device: device, imageDescriptor: outputImgDesc)n

和app上的TensorFlow一樣,這裡也有一個predict 方法,這個方法以組成一條樣本的20個浮點數作為輸入。下面是完整的方法:

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

和運行session的結果是一樣的。convert(example:to:)和toFloatArray()方法載入和輸出MPSImage 對象的輔助函數。

你需要在設備上運行這個app,因為模擬器不支持Metal。輸出結果如下:

Probability spoken by a male: 0.970215%nPrediction: malennProbability spoken by a male: 0.00568771%nPrediction: femalen

注意到這些概率和用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發布。

譯者:李烽 ;審校:董昭男

文章為簡譯,更為詳細的內容,請查看原文。中文譯制文檔見附件。

更多技術乾貨請點擊關註:阿里云云棲社區 - 知乎


推薦閱讀:

TensorFlow會話的配置項
Github|如何用TensorFlow實現DenseNet和DenseNet-BC(附源代碼)
TF Boys (TensorFlow Boys ) 養成記(五):CIFAR10 Model 和 TensorFlow 的四種交叉熵介紹
Google 開發技術周刊 088 期

TAG:iOS开发 | TensorFlow | 深度学习DeepLearning |