888
技術社區[雲棲]
從零到一:IOS平台TensorFlow入門及應用詳解(附源碼)(一)
更多深度文章,請關注雲計算頻道:https://yq.aliyun.com/cloud
推特地址:https://twitter.com/mhollemans
郵件地址:mailto:matt@machinethink.net
github地址:https://github.com/hollance
個人博客:https://machinethink.net/
本文中,作者詳細介紹了如何使用TensorFlow訓練一個簡單的分類器並應用在IOS app上。本文將會使用Gender Recognition by Voice and Speech Analysis dataset數據集,項目源碼已托管至GitHub。
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。
Note: 二值分類器雖然是最簡單的分類器,但是其思想和那些能夠區分成百上千類的分類器一樣。雖然這篇文章中並沒有進行深度學習,但某些理論基礎是共同的。
每條輸入數據由代表用戶聲音的聲學特征的20個數字組成,後麵會詳細說明,現在你將其看作是聲頻和其他信息就可以了。如圖所示,20個數字和一個sum塊連接,這些連接有不同的權重(weights),對應著這20個代表特征的數字的重要程度。
圖中,x0 – x19表示輸入特征,w0 - w19表示連接的權重,在sum 塊中,按如下方式進行運算(就是普通的點乘):
如果sum是一個大的正數,sigmod函數將返回1或者概率100%。如果sum是一個大的負數,sigmod函數會返回0。所以對於大的正數和負數,我們就能得到確定的“是”和“否”的預測結果。然而,如果sum接近0,sigmod函數就會返回一個接近50%的概率。當我們開始訓練分類器的時候,初始預測會是50/50,這是因為分類器還沒有學到任何東西,對所有的輸出並不確定。但是隨著訓練次數的增加,概率就會越接近1和0,分類結果就會變得更加明確。
y_pred即語音來自男性的概率。如果這個概率大於0.5,我們就認為這是男性的聲音,否則,就認為是女性的聲音。
使用邏輯斯蒂回歸的二值分類器的原理:分類器的輸入數據由描述音頻記錄聲學特征的20個數字組成,加權求和再使用sigmod函數,最後輸出是男性語音的概率。
上圖中數據從左邊流向右邊,從輸入流向輸出。這就是TensorFlow中“flow”的來源。圖中的數據都是以張量(tensor)的形式流動的。張量其實就是n維數組(n-dimensional array)。前麵提到w是權重矩陣,TensorFlow認為它是一個二階張量(second-order tensor),其實也就是二維數組(two-dimensional array)。如:
2.向量是一階張量;
深度學習中,比如卷積神經網絡(convolutional neural networks, CNN)經常需要處理四維張量,但本文中的邏輯斯蒂分類器比較簡單,不會超過二階張量,即矩陣。之前提到x是一個向量,現在把x和y都當作一個矩陣。如此一來,損失值就可以一次性計算出來。單個樣本有20個數據,如果載入全部3168個樣本,那麼x將變成一個3168 X 20的矩陣。在x和w相乘之後,輸出y_pred是一個3168 X 1的矩陣。即為數據集中的每一個樣本都進行了預測。總之,用矩陣/張量表示計算圖,就可以一次性為多個樣本進行預測。
brew install python3
pip3 install numpy
pip3 install scipy
pip3 install scikit-learn
pip3 install pandas
pip3 install tensorflow
pip可以自動安裝最適合你係統的TensorFlow版本。如果你想安裝其它版本,請參照離線安裝指南。
import tensorflow as tf
a = 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))
[5 7 9]
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教程中常用的MNIST手寫數字是被數據集,而是使用了根據語音識別性別的數據集,voice.csv文件如下所示。這些數字代表語音記錄不同的聲學特征(acoustic properties)。通過腳本從錄音中抽取出這些特征,然後轉換為這個CSV文件。如果感興趣的話可以參照R語言源碼。
雖然不清楚這些特征代表的含義,但這並不重要,我們關心的僅是從這些數據中訓練出一個能夠區分男性和女性聲音的分類器。如果要在你的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條樣本。
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),就可以測試真實數據了。
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定義了步長。
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/目錄下:
當你發現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 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保存數據,所以這裏隻需要一些輔助代碼就可以載入這個文件,並導入會話(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))
以上為譯文
本文由北郵@愛可可-愛生活 老師推薦,阿裏雲雲棲社區組織翻譯。
文章原標題《Getting started with TensorFlow on iOS》,由Matthijs Hollemans發布。
譯者:李烽 ;審校:
文章為簡譯,更為詳細的內容,請查看原文。中文譯製文檔見附件。
最後更新:2017-04-14 20:30:34