深度学习指南:在iOS平台上使用TensorFlow
在利用深度學(xué)習(xí)網(wǎng)絡(luò)進(jìn)行預(yù)測性分析之前,我們首先需要對其加以訓(xùn)練。目前市面上存在著大量能夠用于神經(jīng)網(wǎng)絡(luò)訓(xùn)練的工具,但TensorFlow無疑是其中極為重要的首選方案之一。
大家可以利用TensorFlow訓(xùn)練自己的機(jī)器學(xué)習(xí)模型,并利用這些模型完成預(yù)測性分析。訓(xùn)練通常由一臺極為強(qiáng)大的設(shè)備或者云端資源完成,但您可能想象不到的是,TensorFlow亦可以在iOS之上順利起效——只是存在一定局限性。
相關(guān)廠商內(nèi)容
一手實(shí)踐,摩拜產(chǎn)品研發(fā)負(fù)責(zé)人談?wù)効战祱F(tuán)隊那些事
業(yè)務(wù)與產(chǎn)品面面觀,Mobvista CTO談?wù)劶夹g(shù)與業(yè)務(wù)融合之道
技術(shù)領(lǐng)導(dǎo)者,我是如何打造自己的影響力?
如何抓住技術(shù)浪潮變革的紅利
數(shù)字化經(jīng)濟(jì)下技術(shù)領(lǐng)導(dǎo)者的洞察創(chuàng)新之路
相關(guān)贊助商
全球技術(shù)領(lǐng)導(dǎo)力峰會2017,6月30日-7月1日,上海·寶華萬豪酒店,精彩內(nèi)容搶先看
在今天的博文中,我們將共同了解TensorFlow背后的設(shè)計思路、如何利用其訓(xùn)練一套簡單的分類器,以及如何將上述成果引入您的iOS應(yīng)用。
在本示例中,我們將使用“根據(jù)語音與對話分析判斷性別”數(shù)據(jù)集以了解如何根據(jù)音頻記錄判斷語音為男聲抑或女聲。
獲取相關(guān)代碼:大家可以通過GitHub上的對應(yīng)項(xiàng)目獲取本示例的源代碼。
TensorFlow是什么,我們?yōu)楹涡枰右允褂?#xff1f;
TensorFlow是一套用于構(gòu)建計算性圖形,從而實(shí)現(xiàn)機(jī)器學(xué)習(xí)的軟件資源庫。
其它一些工具往往作用于更高級別的抽象層級。以Caffe為例,大家需要將不同類型的“層”進(jìn)行彼此互連,從而設(shè)計出一套神經(jīng)網(wǎng)絡(luò)。而iOS平臺上的BNNS與MPSCNN亦可實(shí)現(xiàn)類似的功能。
在TensorFlow當(dāng)中,大家亦可處理這些層,但具體處理深度將更為深入——甚至直達(dá)您算法中的各項(xiàng)計算流程。
大家可以將TensorFlow視為一套用于實(shí)現(xiàn)新型機(jī)器學(xué)習(xí)算法的工具集,而其它深度學(xué)習(xí)工具則用于幫助用戶使用這些算法。
當(dāng)然,這并不是說用戶需要在TensorFlow當(dāng)中從零開始構(gòu)建一切。TensorFlow擁有一整套可復(fù)用的構(gòu)建組件,同時囊括了Keras等負(fù)責(zé)為TensorFlow用戶提供大量便捷模塊的資源庫。
因此TensorFlow在使用當(dāng)中并不強(qiáng)制要求大家精通相關(guān)數(shù)學(xué)專業(yè)知識,當(dāng)然如果各位愿意自行構(gòu)建,TensorFlow也能夠提供相應(yīng)的工具。
利用邏輯回歸實(shí)現(xiàn)二元分類
在今天的博文當(dāng)中,我們將利用邏輯回歸(logistic regression)算法創(chuàng)建一套分類器。沒錯,我們將從零開始進(jìn)行構(gòu)建,因此請大家做好準(zhǔn)備——這可是項(xiàng)有點(diǎn)復(fù)雜的任務(wù)。所謂分類器,其基本工作原理是獲取輸入數(shù)據(jù),而后告知用戶該數(shù)據(jù)所歸屬的類別——或者種類。在本項(xiàng)目當(dāng)中,我們只設(shè)定兩個種類:男聲與女聲——也就是說,我們需要構(gòu)建的是一套二元分類器(binary classifier)。
備注:二元分類器屬于最簡單的一種分類器,但其基本概念與設(shè)計思路同用于區(qū)分成百上千種不同類別的分類器完全一致。因此,盡管我們在本份教程中不會太過深入,但相信大家仍然能夠從中一窺分類器設(shè)計的門徑。
在輸入數(shù)據(jù)方面,我們將使用包含20個數(shù)字朗讀語音、囊括多種聲學(xué)特性的給定錄音。我將在后文中對此進(jìn)行詳盡解釋,包括音頻頻率及其它相關(guān)信息。
在以下示意圖當(dāng)中,大家可以看到這20個數(shù)字全部接入一個名為sum的小框。這些連接擁有不同的weights(權(quán)重),對于分類器而言代表著這20個數(shù)字各自不同的重要程度。
以下框圖展示了這套邏輯分類器的起效原理:
在sum框當(dāng)中,輸入數(shù)據(jù)區(qū)間為x0到x19,且其對應(yīng)連接的權(quán)重w0到w19進(jìn)行直接相加。以下為一項(xiàng)常見的點(diǎn)積:
sum = x[0]*w[0] + x[1]*w[1] + x[2]*w[2] + ... + x[19]*w[19] + b我們還在所謂bias(偏離)項(xiàng)的末尾加上了b。其僅僅代表另一個數(shù)字。
數(shù)組w中的權(quán)重與值b代表著此分類器所學(xué)習(xí)到的經(jīng)驗(yàn)。對該分類器進(jìn)行訓(xùn)練的過程,實(shí)際上是為了幫助其找到與w及b正確匹配的數(shù)字。最初,我們將首先將全部w與b設(shè)置為0。在數(shù)輪訓(xùn)練之后,w與b則將包含一組數(shù)字,分類器將利用這些數(shù)字將輸入語音中的男聲與女聲區(qū)分開來。為了能夠?qū)um轉(zhuǎn)化為一條概率值——其取值在0與1之間——我們在這里使用logistic sigmoid函數(shù):
y_pred = 1 / (1 + exp(-sum))這條方程式看起來很可怕,但做法卻非常簡單:如果sum是一個較大正數(shù),則sigmoid函數(shù)返回1或者概率為100%; 如果sum是一個較大負(fù)數(shù),則sigmoid函數(shù)返回0。因此對于較大的正或者負(fù)數(shù),我們即可得出較為肯定的“是”或者“否”預(yù)測結(jié)論。
然而,如果sum趨近于0,則sigmoid函數(shù)會給出一個接近于50%的概率,因?yàn)槠錈o法確定預(yù)測結(jié)果。當(dāng)我們最初對分類器進(jìn)行訓(xùn)練時,其初始預(yù)期結(jié)果會因分類器本身訓(xùn)練尚不充分而顯示為50%,即對判斷結(jié)果并無信心。但隨著訓(xùn)練工作的深入,其給出的概率開始更趨近于1及0,即分類器對于結(jié)果更為肯定。
現(xiàn)在y_pred中包含的預(yù)測結(jié)果顯示,該語音為男聲的可能性更高。如果其概率高于0.5(或者50%),則我們認(rèn)為語音為男聲; 相反則為女聲。
這即是我們這套利用邏輯回歸實(shí)現(xiàn)的二元分類器的基本設(shè)計原理。輸入至該分類器的數(shù)據(jù)為一段對20個數(shù)字進(jìn)行朗讀的音頻記錄,我們會計算出一條權(quán)重sum并應(yīng)用sigmoid函數(shù),而我們獲得的輸出概率指示朗讀者應(yīng)為男性。
然而,我們?nèi)匀恍枰⒂糜谟?xùn)練該分類器的機(jī)制,而這時就需要請出今天的主角——TensorFlow了。
在TensorFlow中實(shí)現(xiàn)此分類器
要在TensorFlow當(dāng)中使用此分類器,我們需要首先將其設(shè)計轉(zhuǎn)化為一套計算圖(computational graph)。一項(xiàng)計算圖由多個負(fù)責(zé)執(zhí)行計算的節(jié)點(diǎn)組成,且輸入數(shù)據(jù)會在各節(jié)點(diǎn)之間往來流通。
我們這套邏輯回歸算法的計算圖如下所示:
看起來與之前給出的示意圖存在一定區(qū)別,但這主要是由于此處的輸入內(nèi)容x不再是20個獨(dú)立的數(shù)字,而是一個包含有20個元素的向量。在這里,權(quán)重由矩陣W表示。因此,之前得出的點(diǎn)積也在這里被替換成了一項(xiàng)矩陣乘法。
另外,本示意圖中還包含一項(xiàng)輸入內(nèi)容y。其用于對分類器進(jìn)行訓(xùn)練并驗(yàn)證其運(yùn)行效果。我們在這里使用的數(shù)據(jù)集為一套包含3168條example語音記錄的集合,其中每條示例記錄皆被明確標(biāo)記為男聲或女聲。這些已知男聲或女聲結(jié)果亦被稱為該數(shù)據(jù)集的label(標(biāo)簽),并作為我們交付至y的輸入內(nèi)容。
為了訓(xùn)練我們的分類器,這里需要將一條示例加載至x當(dāng)中并允許該計算圖進(jìn)行預(yù)測:即語音到底為男聲抑或是女聲?由于初始權(quán)重值全部為0,因此該分類器很可能給出錯誤的預(yù)測。我們需要一種方法以計算其錯誤的“具體程度”,而這一目標(biāo)需要通過loss函數(shù)實(shí)現(xiàn)。Loss函數(shù)會將預(yù)測結(jié)果y_pred與正確輸出結(jié)果y進(jìn)行比較。
在將loss函數(shù)提供給訓(xùn)練示例后,我們利用一項(xiàng)被稱為backpropagation(反向傳播)的技術(shù)通過該計算圖進(jìn)行回溯,旨在根據(jù)正確方向?qū)與b的權(quán)重進(jìn)行小幅調(diào)整。如果預(yù)測結(jié)果為男聲但實(shí)際結(jié)果為女聲,則權(quán)重值即會稍微進(jìn)行上調(diào)或者下調(diào),從而在下一次面對同樣的輸入內(nèi)容時增加將其判斷為“女聲”的概率。
這一訓(xùn)練規(guī)程會利用該數(shù)據(jù)集中的全部示例進(jìn)行不斷重復(fù)再重復(fù),直到計算圖本身已經(jīng)獲得了最優(yōu)權(quán)重集合。而負(fù)責(zé)衡量預(yù)測結(jié)果錯誤程度的loss函數(shù)則因此隨時間推移而變低。
反向傳播在計算圖的訓(xùn)練當(dāng)中扮演著極為重要的角色,但我們還需要加入一點(diǎn)數(shù)學(xué)手段讓結(jié)果更為準(zhǔn)確。而這也正是TensorFlow的專長所在:我們只要將全部“前進(jìn)”操作表達(dá)為計算圖當(dāng)中的節(jié)點(diǎn),其即可自動意識到“后退”操作代表的是反向傳播——我們完全無需親自進(jìn)行任何數(shù)學(xué)運(yùn)算。太棒了!
Tensorflow到底是什么?
在以上計算圖當(dāng)中,數(shù)據(jù)流向?yàn)閺淖笾劣?#xff0c;即代表由輸入到輸出。而這正是TensorFlow中“流(flow)”的由來。不過Tensor又是什么?
Tensor一詞本義為張量,而此計算圖中全部數(shù)據(jù)流皆以張量形式存在。所謂張量,其實(shí)際代表的就是一個n維數(shù)組。我曾經(jīng)提到W是一項(xiàng)權(quán)重矩陣,但從TensorFlow的角度來看,其實(shí)際上屬于一項(xiàng)二階張量——換言之,一個二組數(shù)組。
一個標(biāo)量代表一個零階張量。
- 一個向量代表一個一階張量。
- 一個矩陣代表一個二階張量。
- 一個三維數(shù)組代表一個三階張量。
- 之后以此類推……
這就是Tensor的全部含義。在卷積神經(jīng)網(wǎng)絡(luò)等深度學(xué)習(xí)方案當(dāng)中,大家會需要與四維張量打交道。但本示例中提到的邏輯分類器要更為簡單,因此我們在這里最多只涉及到二階張量——即矩陣。
我之前還提到過,x代表一個向量——或者說一個一階張量——但接下來我們同樣將其視為一個矩陣。y亦采用這樣的處理方式。如此一來,我們即可將數(shù)據(jù)庫組視為整體對其loss進(jìn)行計算。
一條簡單的示例(example)語音內(nèi)包含20個數(shù)據(jù)元素。如果大家將全部3168條示例加載至x當(dāng)中,則x會成為一個3168 x 20的矩陣。再將x與W相乘,則得出的結(jié)果y_pred為一個3168 x 1的矩陣。具體來講,y_pred代表的是為數(shù)據(jù)集中的每條語音示例提供一項(xiàng)預(yù)測結(jié)論。
通過將我們的計算圖以矩陣/張量的形式進(jìn)行表達(dá),我們可以一次性對多個示例進(jìn)行預(yù)測。
安裝TensorFlow
好的,以上是本次教程的理論基礎(chǔ),接下來進(jìn)入實(shí)際操作階段。
我們將通過Python使用TensorFlow。大家的Mac設(shè)備可能已經(jīng)安裝有某一Python版本,但其版本可能較為陳舊。在本教程中,我使用的是Python 3.6,因此大家最好也能安裝同一版本。
安裝Python 3.6非常簡單,大家只需要使用Homebrew軟件包管理器即可。如果大家還沒有安裝homebrew,請點(diǎn)擊此處參閱相關(guān)指南。
接下來打開終端并輸入以下命令,以安裝Python的最新版本:
brew install python3Python也擁有自己的軟件包管理器,即pip,我們將利用它安裝我們所需要的其它軟件包。在終端中輸入以下命令:
pip3 install numpy pip3 install scipy pip3 install scikit-learn pip3 install pandas pip3 install tensorflow除了TensorFlow之外,我們還需要安裝NumPy、SciPy、pandas以及scikit-learn:
NumPy是一套用于同n級數(shù)組協(xié)作的庫。聽起來耳熟嗎?NumPy并非將其稱為張量,但之前提到了數(shù)組本身就是一種張量。TensorFlow Python API就建立在NumPy基礎(chǔ)之上。- SciPy是一套用于數(shù)值計算的庫。其它一些軟件包的起效需要以之為基礎(chǔ)。
- pandas負(fù)責(zé)數(shù)據(jù)集的加載與清理工作。
- scikit-learn在某種意義上可以算作TensorFlow的競爭對手,因?yàn)槠渫瑯邮且惶子糜跈C(jī)器學(xué)習(xí)的庫。我們之所以在本項(xiàng)目中加以使用,是因?yàn)樗邆涠囗?xiàng)便利的功能。由于TensorFlow與scikit-learn皆使用NumPy數(shù)組,因?yàn)槎吣軌蝽槙硨?shí)現(xiàn)協(xié)作。
實(shí)際上,大家無需pandas與scikit-learn也能夠使用TensorFlow,但二者確實(shí)能夠提供便捷功能,而且每一位數(shù)據(jù)科學(xué)家也都樂于加以使用。
如大家所知,這些軟件包將被安裝在/usr/local/lib/python3.6/site-packages當(dāng)中。如果大家需要查看部分未被公布在其官方網(wǎng)站當(dāng)中的TensorFlow源代碼,則可以在這里找到。
備注:pip應(yīng)會為您的系統(tǒng)自動安裝TensorFlow的最佳版本。如果大家希望安裝其它版本,則請點(diǎn)擊此處參閱官方安全指南。另外,大家也可以利用源代碼自行構(gòu)建TensorFlow,這一點(diǎn)我們稍后會在面向iOS構(gòu)建TensorFlow部分中進(jìn)行說明。
下面我們進(jìn)行一項(xiàng)快速測試,旨在確保一切要素都已經(jīng)安裝就緒。利用以下內(nèi)容創(chuàng)建一個新的tryit.py文件:
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))而后通過終端運(yùn)行這套腳本:
python3 tryit.py其會顯示一些與TensorFlow運(yùn)行所在設(shè)備相關(guān)的調(diào)試信息(大多為CPU信息,但如果您所使用的Mac設(shè)備配備有英偉達(dá)GPU,亦可能提供GPU信息)。最終結(jié)果顯示為:
[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.如果出現(xiàn)上述內(nèi)容,則代表您在系統(tǒng)當(dāng)中安裝的TensorFlow并非當(dāng)前CPU的最優(yōu)適配版本。修復(fù)方法之一是利用源代碼自行構(gòu)建TensorFlow,因?yàn)檫@允許大家對全部選項(xiàng)加以配置。但在本示例當(dāng)中,由于其不會造成什么影響,因此直接忽略即可。
深入觀察訓(xùn)練數(shù)據(jù)集
要訓(xùn)練分類器,我們自然需要數(shù)據(jù)。
在本項(xiàng)目當(dāng)中,我們使用來自Kory Becker的“根據(jù)語音判斷性別”數(shù)據(jù)集。為了能夠讓這份教程能夠與TensorFlow指南上的MNIST數(shù)字化識別有所不同,這里我決定在Kaggle.com上尋找數(shù)據(jù)集,并最終選定了這一套。
那么我們到底該如何立足音頻實(shí)現(xiàn)性別判斷?下載該數(shù)據(jù)集并打開voice.csv文件之后,大家會看到其中包含著一排排數(shù)字:
我們首先需要強(qiáng)調(diào)這一點(diǎn),這里列出的并非實(shí)際音頻數(shù)據(jù)!相反,這些數(shù)字代表著語音記錄當(dāng)中的不同聲學(xué)特征。這些屬性或者特征由一套腳本自音頻記錄中提取得出,并被轉(zhuǎn)化為這個CSV文件。具體提取方式并不屬于本篇文章希望討論的范疇,但如果大家感興趣,則可點(diǎn)擊此處查閱其原始R源代碼。
這套數(shù)據(jù)集中包含3168項(xiàng)示例(每項(xiàng)示例在以上表格中作為一行),且基本半數(shù)為男聲錄制、半數(shù)為女聲錄制。每一項(xiàng)示例中存在20項(xiàng)聲學(xué)特征,例如:
以kHz為單位的平均頻率
- 頻率的標(biāo)準(zhǔn)差
- 頻譜平坦度
- 頻譜熵
- 峰度
- 聲學(xué)信號中測得的最大基頻
- 調(diào)制指數(shù)
- 等等……
別擔(dān)心,雖然我們并不了解其中大多數(shù)條目的實(shí)際意義,但這不會影響到本次教程。我們真正需要 關(guān)心的是如何利用這些數(shù)據(jù)訓(xùn)練自己的分類器,從而立足于上述特征確保其有能力區(qū)分男性與女性的語音。
如果大家希望在一款應(yīng)用程序當(dāng)中使用此分類器,從而通過錄音或者來自麥克風(fēng)的音頻信息檢測語音性別,則首先需要從此類音頻數(shù)據(jù)中提取聲學(xué)特征。在擁有了這20個數(shù)字之后,大家即可對其分類器加以訓(xùn)練,并利用其判斷語音內(nèi)容為男聲還是女聲。
因此,我們的分類器并不會直接處理音頻記錄,而是處理從記錄中提取到的聲學(xué)特征。
備注:我們可以以此為起點(diǎn)了解深度學(xué)習(xí)與邏輯回歸等傳統(tǒng)算法之間的差異。我們所訓(xùn)練的分類器無法學(xué)習(xí)非常復(fù)雜的內(nèi)容,大家需要在預(yù)處理階段提取更多數(shù)據(jù)特征對其進(jìn)行幫助。在本示例的特定數(shù)據(jù)集當(dāng)中,我們只需要考慮提取音頻記錄中的音頻數(shù)據(jù)。
深度學(xué)習(xí)最酷的能力在于,大家完全可以訓(xùn)練一套神經(jīng)網(wǎng)絡(luò)來學(xué)習(xí)如何自行提取這些聲學(xué)特征。如此一來,大家不必進(jìn)行任何預(yù)處理即可利用深度學(xué)習(xí)系統(tǒng)采取原始音頻作為輸入內(nèi)容,并從中提取任何其認(rèn)為重要的聲學(xué)特征,而后加以分類。
這當(dāng)然也是一種有趣的深度學(xué)習(xí)探索方向,但并不屬于我們今天討論的范疇,因此也許日后我們將另開一篇文章單獨(dú)介紹。
建立一套訓(xùn)練集與測試集
在前文當(dāng)中,我提到過我們需要以如下步驟對分類器進(jìn)行訓(xùn)練:
向其交付來自數(shù)據(jù)集的全部示例。
- 衡量預(yù)測結(jié)果的錯誤程度。
- 根據(jù)loss調(diào)整權(quán)重。
事實(shí)證明,我們不應(yīng)利用全部數(shù)據(jù)進(jìn)行訓(xùn)練。我們只需要其中的特定一部分?jǐn)?shù)據(jù)——即測試集——從而評估分類器的實(shí)際工作效果。因此,我們將把整體數(shù)據(jù)集拆分為兩大部分:訓(xùn)練集,用于對分類器進(jìn)行訓(xùn)練; 測試集,用于了解該分類器的預(yù)測準(zhǔn)確度。
為了將數(shù)據(jù)拆分為訓(xùn)練集與測試集,我創(chuàng)建了一套名為split_data.py的Python腳本,其內(nèi)容如下所示:
import numpy as np # 1 import pandas as pd df = pd.read_csv("voice.csv", header=0) # 2 labels = (df["label"] == "male").values * 1 # 3 labels = labels.reshape(-1, 1) # 4 del df["label"] # 5 data = df.values # 6 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) np.save("X_train.npy", X_train) # 7 np.save("X_test.npy", X_test) np.save("y_train.npy", y_train) np.save("y_test.npy", y_test)下面我們將分步驟了解這套腳本的工作方式:
大家也可以進(jìn)行額外的一些預(yù)處理對腳本中的數(shù)據(jù)進(jìn)行調(diào)整,例如對特征進(jìn)行擴(kuò)展,從而使其擁有0均值及相等的方差,但由于本次示例項(xiàng)目比較簡單,所以并無深入調(diào)整的必要。
利用以下命令在終端中運(yùn)行這套腳本:
python3 split_data.py這將給我們帶來4個新文件,其中包含有訓(xùn)練救命(X_train.npy)、這些示例的對應(yīng)標(biāo)簽(y_train.npy)、測試示例(X_test.npy)及其對應(yīng)標(biāo)簽(y_test.npy)。
備注:大家可能想了解為什么這些變量名稱為何有些是大寫,有些是小寫。在數(shù)學(xué)層面來看,矩陣通常以大寫表示而向量則以小寫表示。在我們的腳本中,X代表一個矩陣,y代表一個向量。這是一種慣例,大部分機(jī)器學(xué)習(xí)代碼中皆照此辦理。
建立計算圖
現(xiàn)在我們已經(jīng)對數(shù)據(jù)進(jìn)行了梳理,而后即可編寫一套腳本以利用TensorFlow對這套邏輯分類器進(jìn)行訓(xùn)練。這套腳本名為train.py。為了節(jié)省篇幅,這里就不再列出腳本的具體內(nèi)容了,大家可以點(diǎn)擊此處在GitHub上進(jìn)行查看。
與往常一樣,我們首先需要導(dǎo)入需要的軟件包。在此之后,我們將訓(xùn)練數(shù)據(jù)加載至兩個NumPy數(shù)組當(dāng)中,即X_train與y_train。(我們在本腳本中不會使用測試數(shù)據(jù)。)
import numpy as np import tensorflow as tf X_train = np.load("X_train.npy") y_train = np.load("y_train.npy")現(xiàn)在我們可以建立自己的計算圖。首先,我們?yōu)槲覀兊妮斎雰?nèi)容x與y定義所謂placeholders(占位符):
num_inputs = 20 num_classes = 1 with tf.name_scope("inputs"): x = tf.placeholder(tf.float32, [None, num_inputs], name="x-input") y = tf.placeholder(tf.float32, [None, num_classes], name="y-input")其中tf.name_scope("...")可用于對該計算圖中的不同部分按不同范圍進(jìn)行分組,從而簡化對計算圖內(nèi)容的理解。我們將x與y添加至“inputs”范圍之內(nèi)。我們還將為其命名,分別為“x-input”與“y-input”,這樣即可在隨后輕松加以引用。
大家應(yīng)該還記得,每條輸入示例都是一個包含20項(xiàng)元素的向量。每條示例亦擁有一個標(biāo)簽(1代表男聲,0代表女聲)。我之前還提到過,我們可以將全部示例整合為一個矩陣,從而一次性對其進(jìn)行全面計算。正因?yàn)槿绱?#xff0c;我們這里將x與y定義為二維張量:x擁有[None, 20]維度,而y擁有[None, 1]維度。
其中的None代表第一項(xiàng)維度為靈活可變且目前未知。在訓(xùn)練集當(dāng)中,我們將2217條示例導(dǎo)入x與y; 而在測試集中,我們引入951條示例。現(xiàn)在,TensorFlow已經(jīng)了解了我們的輸入內(nèi)容,接下來對分類器的parameters(參數(shù))進(jìn)行定義:
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包含有分類器將要學(xué)習(xí)的權(quán)重(這是一個20 x 1矩陣,因?yàn)槠渲邪?0條輸入特征與1條輸出結(jié)果),而b則包含偏離值。這二者被聲明為TensorFlow變量,意味著二者可在反向傳播過程當(dāng)中實(shí)現(xiàn)更新。
在一切準(zhǔn)備就緒之后,我們可以對作為邏輯回歸分類器核心的計算流程進(jìn)行聲明了:
y_pred = tf.sigmoid(tf.matmul(x, W) + b)這里將x與W進(jìn)行相乘,同時加上偏離值b,而后取其邏輯型成長曲線(logistic sigmoid)。如此一來,y_pred中的結(jié)果即根據(jù)x內(nèi)音頻數(shù)據(jù)的描述特性而被判斷為男聲的概率。
備注:以上代碼目前實(shí)際還不會做出任何計算——截至目前,我們還只是構(gòu)建起了必要的計算圖。這一行單純是將各節(jié)點(diǎn)添加至計算圖當(dāng)中以作為矩陣乘法(tf.matmul)、加法(+)以及sigmoid函數(shù)(tf.sigmoid)。在完成整體計算圖的構(gòu)建之后,我們方可創(chuàng)建TensorFlow會話并利用實(shí)際數(shù)據(jù)加以運(yùn)行。
到這里任務(wù)還未完成。為了訓(xùn)練這套模型,我們需要定義一項(xiàng)loss函數(shù)。對于一套二元邏輯回歸分類器,我們需要使用log loss,幸運(yùn)的是TensorFlow本身內(nèi)置有一項(xiàng)log_loss()函數(shù)可供直接使用:
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計算圖節(jié)點(diǎn)作為輸入內(nèi)容y,我們會獲取與之相關(guān)的示例標(biāo)簽并將其與我們的預(yù)測結(jié)果y_pred進(jìn)行比較。以數(shù)字顯示的結(jié)果即為loss值。
在剛開始進(jìn)行訓(xùn)練時,所有示例的預(yù)測結(jié)果y_pred皆將為0.5(或者50%男聲),這是因?yàn)榉诸惼鞅旧砩胁磺宄绾潍@得正確答案。其初始loss在經(jīng)-1n(0.5)計算后得出為0.693146。而在訓(xùn)練的推進(jìn)當(dāng)中,其loss值將變得越來越小。
第二行用于計算loss值與所謂L2 regularization(正則化)的加值。這是為了防止過度擬合阻礙分類器對訓(xùn)練數(shù)據(jù)的準(zhǔn)確記憶。這一過程比較簡單,因?yàn)槲覀兊姆诸惼鳌皟?nèi)存”只包含20項(xiàng)權(quán)重值與偏離值。不過正則化本身是一種常見的機(jī)器學(xué)習(xí)技術(shù),因此在這里必須一提。
這里的regularization值為另一項(xiàng)占位符:
with tf.name_scope("hyperparameters"):???? regularization = tf.placeholder(tf.float32, name="regularization")???? learning_rate = tf.placeholder(tf.float32, name="learning-rate")我們還將利用占位符定義我們的輸入內(nèi)容x與y,不過二者的作用是定義hyperparameters。
Hyperparameters允許大家對這套模型及其具體訓(xùn)練方式進(jìn)行配置。其之所以被稱為“超”參數(shù),是因?yàn)榕c常見的W與b參數(shù)不同,其并非由模型自身所學(xué)習(xí)——大家需要自行將其設(shè)置為適當(dāng)?shù)闹怠?/p>
其中的learning_rate超參數(shù)負(fù)責(zé)告知優(yōu)化器所應(yīng)采取的調(diào)整幅度。該優(yōu)化器(optimizer)負(fù)責(zé)執(zhí)行反向傳播:其會提取loss值并將其傳遞回計算圖以確定需要對權(quán)重值與偏離值進(jìn)行怎樣的調(diào)整。這里可以選擇的優(yōu)化器方案多種多樣,而我們使用的為ADAM:
with tf.name_scope("train"):???? optimizer = tf.train.AdamOptimizer(learning_rate)???? train_op = optimizer.minimize(loss)其能夠在計算圖當(dāng)中創(chuàng)建一個名為train_op的節(jié)點(diǎn)。我們稍后將運(yùn)行此節(jié)點(diǎn)以訓(xùn)練分類器。為了確定該分類器的運(yùn)行效果,我們還需要在訓(xùn)練當(dāng)中偶爾捕捉快照并計算其已經(jīng)能夠在訓(xùn)練集當(dāng)中正確預(yù)測多少項(xiàng)示例。訓(xùn)練集的準(zhǔn)確性并非分類器運(yùn)行效果的最終檢驗(yàn)標(biāo)準(zhǔn),但對其進(jìn)行追蹤能夠幫助我們在一定程度上把握訓(xùn)練過程與預(yù)測準(zhǔn)確性趨勢。具體來講,如果越是訓(xùn)練結(jié)果越差,那么一定是出了什么問題!
下面我們?yōu)橐粋€計算圖節(jié)點(diǎn)定義計算精度:
with tf.name_scope("inference"): inference = tf.to_float(y_pred > 0.5, name="inference")我們可以運(yùn)行其中的accuracy節(jié)點(diǎn)以查看有多少個示例得到了正確預(yù)測。大家應(yīng)該還記得,y_pred中包含一項(xiàng)介于0到1之間的概率。通過進(jìn)行tf.to_float(y_pred > 0.5),若預(yù)測結(jié)果為女聲則返回值為0,若預(yù)測結(jié)果為男聲則返回值為1。我們可以將其與y進(jìn)行比較,y當(dāng)中包含有正確值。而精度值則代表著正確預(yù)測數(shù)量除以預(yù)測總數(shù)。
在此之后,我們將利用同樣的accuracy節(jié)點(diǎn)處理測試集,從而了解這套分類器的實(shí)際工作效果。
另外,我們還需要定義另外一個節(jié)點(diǎn)。此節(jié)點(diǎn)用于對我們尚無對應(yīng)標(biāo)簽的數(shù)據(jù)進(jìn)行預(yù)測:
with tf.name_scope("inference"):???? inference = tf.to_float(y_pred > 0.5, name="inference")為了將這套分類器引入應(yīng)用,我們還需要記錄幾個語音文本詞匯,對其進(jìn)行分析以提取20項(xiàng)聲學(xué)特征,而后再將其交付至分類器。由于處理內(nèi)容屬于全新數(shù)據(jù),而非來自訓(xùn)練或者測試集的數(shù)據(jù),因此我們顯然不具備與之相關(guān)的標(biāo)簽。大家只能將數(shù)據(jù)直接交付至分類器,并希望其能夠給出正確的預(yù)測結(jié)果。而inference節(jié)點(diǎn)的作用也正在于此。
好的,我們已經(jīng)投入了大量精力來構(gòu)建這套計算圖。現(xiàn)在我們希望利用訓(xùn)練集對其進(jìn)行實(shí)際訓(xùn)練。
訓(xùn)練分類器
訓(xùn)練通常以無限循環(huán)的方式進(jìn)行。不過對于這套簡單的邏輯分類器,這種作法顯然有點(diǎn)夸張——因?yàn)槠洳坏揭环昼娂纯赏瓿捎?xùn)練。但對于深層神經(jīng)網(wǎng)絡(luò),我們往往需要數(shù)小時甚至數(shù)天時間進(jìn)行腳本運(yùn)行——直到其獲得令人滿意的精度或者您開始失去耐心。
以下為train.py當(dāng)中訓(xùn)練循環(huán)的第一部分:
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/pre>我們首先創(chuàng)建一個新的TensorFlow session(會話)對象。要運(yùn)行該計算圖,大家需要建立一套會話。調(diào)用sess.run(init)會將W與b全部重設(shè)為0。
我們還需要將該計算圖寫入一個文件。我們將之前創(chuàng)建的全部節(jié)點(diǎn)序列至/tmp/voice/graph.pb文件當(dāng)中。我們之后需要利用此計算圖定義以立足測試集進(jìn)行分類器運(yùn)行,并嘗試將該訓(xùn)練后的分類器引入iOS應(yīng)用。
在while True:循環(huán)當(dāng)中,我們使用以下內(nèi)容:
perm = np.arange(len(X_train)) np.random.shuffle(perm) X_train = X_train[perm] y_train = y_train[perm]首先,我們對訓(xùn)練示例進(jìn)行隨機(jī)洗牌。這一點(diǎn)非常重要,因?yàn)榇蠹耶?dāng)然不希望分類器根據(jù)示例的具體順序進(jìn)行判斷——而非根據(jù)其聲學(xué)特征進(jìn)行判斷。
接下來是最重要的環(huán)節(jié):我們要求該會話運(yùn)行train_op節(jié)點(diǎn)。其將在計算圖之上運(yùn)行一次訓(xùn)練:
feed = {x: X_train, y: y_train, learning_rate: 1e-2, regularization: 1e-5} sess.run(train_op, feed_dict=feed)在運(yùn)行sess.run()時,大家還需要提供一套饋送詞典。其將負(fù)責(zé)告知TensorFlow當(dāng)前占位符節(jié)點(diǎn)的實(shí)際值。
由于這只是一套非常簡單的分類器,因此我們將始終一次性對全部訓(xùn)練集進(jìn)行訓(xùn)練,所以這里我們將X_train數(shù)組引入占位符x并將y_train數(shù)組引入占位符y。(對于規(guī)模更大的數(shù)據(jù)集,大家可以先從小批數(shù)據(jù)內(nèi)容著手,例如將示例數(shù)量設(shè)定為100到1000之間。)
到這里,我們的操作就階段性結(jié)束了。由于我們使用了無限循環(huán),因此train_op節(jié)點(diǎn)會反復(fù)再反復(fù)加以執(zhí)行。而在每一次迭代時,其中的反向傳播機(jī)制都會對權(quán)重值W與b作出小幅調(diào)整。隨著時間推移,這將令權(quán)重值逐步趨近于最優(yōu)值。
我們當(dāng)然有必要了解訓(xùn)練進(jìn)度,因此我們需要經(jīng)常性地輸出進(jìn)度報告(在本示例項(xiàng)目中,每進(jìn)行1000次訓(xùn)練即輸出一次結(jié)果):
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))這一次我們不再運(yùn)行train_op節(jié)點(diǎn),而是運(yùn)行accuracy與loss兩個節(jié)點(diǎn)。我們使用同樣的饋送詞典,這樣accuracy與loss都會根據(jù)訓(xùn)練集進(jìn)行計算。正如之前所提到,訓(xùn)練集中的較高預(yù)測精度并不代表分類器能夠在處理測試集時同樣擁有良好表現(xiàn),但大家當(dāng)然希望隨著訓(xùn)練的進(jìn)行其精度值不斷提升。與此同時,loss值則應(yīng)不斷下降。
另外,我們還需要時不時保存一份checkpoint:
if step % save_every == 0: checkpoint_file = os.path.join(checkpoint_dir, "model") saver.save(sess, checkpoint_file) print("*** SAVED MODEL ***")其會獲取分類器當(dāng)前已經(jīng)學(xué)習(xí)到的W與b值,并將其保存為一個checkpoint文件。此checkpoint可供我們參閱,并判斷分類器是否已經(jīng)可以轉(zhuǎn)而處理測試集。該checkpoinit文件同樣被保存在/tmp/voice/目錄當(dāng)中。
使用以下命令在終端中運(yùn)行該訓(xùn)練腳本:
python3 train.py輸出結(jié)果應(yīng)如下所示:
Training set size: (2217, 20) Initial loss: 0.693146 step:??? 0, loss: 0.7432, training accuracy: 0.4754 step: 1000, loss: 0.4160, training accuracy: 0.8904 step: 2000, loss: 0.3259, training accuracy: 0.9170 step: 3000, loss: 0.2750, training accuracy: 0.9229 step: 4000, loss: 0.2408, training accuracy: 0.9337 step: 5000, loss: 0.2152, training accuracy: 0.9405 step: 6000, loss: 0.1957, training accuracy: 0.9553 step: 7000, loss: 0.1819, training accuracy: 0.9594 step: 8000, loss: 0.1717, training accuracy: 0.9635 step: 9000, loss: 0.1652, training accuracy: 0.9666 *** SAVED MODEL *** step: 10000, loss: 0.1611, training accuracy: 0.9702 step: 11000, loss: 0.1589, training accuracy: 0.9707 . . .當(dāng)發(fā)現(xiàn)loss值不再下降時,就稍等一下直到看到下一條*** SAVED MODEL ***信息,這時按下Ctrl+C以停止訓(xùn)練。
在超參數(shù)設(shè)置當(dāng)中,我選擇了正規(guī)化與學(xué)習(xí)速率,大家應(yīng)該會看到其訓(xùn)練集的準(zhǔn)確率已經(jīng)達(dá)到約97%,而loss值則約為0.157。(如果大家在饋送詞典中將regularization設(shè)置為0,則loss甚至還能夠進(jìn)一步降低。)
實(shí)際效果如何?
在完成對分類器的訓(xùn)練之后,接下來就是對其進(jìn)行測試以了解它在實(shí)踐當(dāng)中的運(yùn)行效果。大家需要使用未在訓(xùn)練中涉及過的數(shù)據(jù)完成這項(xiàng)測試。正因?yàn)槿绱?#xff0c;我們才在此前將數(shù)據(jù)集拆分為訓(xùn)練集與測試集。
我們將創(chuàng)建一套新的test.py腳本,負(fù)責(zé)加載計算圖定義以及測試集,并計算其正確預(yù)測的測試示例數(shù)量。這里我將只提供重要的部分,大家可以點(diǎn)擊此處查看完整腳本內(nèi)容。
備注:測試集的結(jié)果精確度將始終低于訓(xùn)練集的結(jié)果精確度(后者為97%)。不過如果前者遠(yuǎn)低于后者,則大家應(yīng)對分類器進(jìn)行檢查并對訓(xùn)練流程進(jìn)行調(diào)整。我們預(yù)計測試集的實(shí)際結(jié)果應(yīng)該在95%左右。任何低于90%的精度結(jié)果都應(yīng)引起重視。
與之前一樣,這套腳本會首先導(dǎo)入必要軟件包,包括來自scikit-learn的指標(biāo)包以輸出各類其它報告。當(dāng)然,這一次我們選擇加載測試集而不再是訓(xùn)練集。
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")為了計算測試集的結(jié)果精確度,我們?nèi)匀恍枰嬎銏D。不過這一次不再需要完整的計算圖,因?yàn)閠rain_op與loss兩個用于訓(xùn)練的節(jié)點(diǎn)這里不會被用到。大家當(dāng)然可以再次手動建立計算圖,但由于此前我們已經(jīng)將其保存在graph.pb文件當(dāng)中,因此這里直接加載即可。以下為相關(guān)代碼:
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可能會將其數(shù)據(jù)保存為協(xié)議緩沖文件(擴(kuò)展名為.pb),因此我們可以使用部分helper代碼以加載此文件并將其作為計算圖導(dǎo)入至?xí)挳?dāng)中。
接下來,我們需要從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)正因?yàn)槿绱?#xff0c;我們需要將節(jié)點(diǎn)引入范圍并為其命名,從而利用get_tensor_by_name()輕松再次將其找到。如果大家沒有為節(jié)點(diǎn)提供明確的名稱,則可能需要認(rèn)真查閱計算圖定義才能找到TensorFlow為其默認(rèn)分配的名稱。
我們還需要引用其它幾個節(jié)點(diǎn),特別是作為輸入內(nèi)容的x與y以及其它負(fù)責(zé)進(jìn)行預(yù)測的節(jié)點(diǎn):
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")好的,到這里我們已經(jīng)將計算圖重新加載至內(nèi)存當(dāng)中。我們還需要再次將分類器學(xué)習(xí)到的內(nèi)容加載至W與b當(dāng)中。現(xiàn)在我們終于可以測試分類器在處理其之前從未見過的數(shù)據(jù)時表現(xiàn)出的精確度了:
feed = {x: X_test, y: y_test}???? print("Test set accuracy:", sess.run(accuracy, feed_dict=feed))上述代碼會運(yùn)行accuracy節(jié)點(diǎn)并利用來自X_test數(shù)組的聲學(xué)特征作為輸入內(nèi)容,同時使用來自y_test的標(biāo)簽進(jìn)行結(jié)果驗(yàn)證。
備注:這一次,饋送詞典不再需要為learning_rate與regularization占位符指定任何值。我們只需要在accuracy節(jié)點(diǎn)上運(yùn)行計算圖的一部分,且此部分中并不包括這些占位符。
我們還可以借助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節(jié)點(diǎn)以獲取預(yù)測結(jié)果。由于inference只會計算預(yù)測結(jié)果而不會檢查其精確度,因此饋送詞典中僅需要包含輸入內(nèi)容x而不再需要y。
運(yùn)行此腳本之后,大家應(yīng)該會看到類似于以下內(nèi)容的輸出結(jié)果:
$ 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]]測試集的預(yù)測精確度接近96%——與預(yù)期一樣,略低于訓(xùn)練集的精確度,但也已經(jīng)相當(dāng)接近。這意味著我們的訓(xùn)練已經(jīng)獲得成功,且我們也證明了這套分類器能夠有效處理其從未見過的數(shù)據(jù)。其當(dāng)然還不夠完美——每25次嘗試中即有1次屬于分類錯誤,但對于本教程來說這一結(jié)果已經(jīng)完全令人滿意。
分類報告與混淆矩陣顯示了與錯誤預(yù)測相關(guān)的示例統(tǒng)計信息。通過混淆矩陣,我們可以看到共有446項(xiàng)得到正確預(yù)測的女聲示例,而另外28項(xiàng)女聲示例則被錯誤地判斷為男聲。在466項(xiàng)男聲示例中分類器給出了正確結(jié)論,但有11項(xiàng)則被錯誤判斷為女聲。
這樣看來,我們的分類器似乎不太擅長分辨女性的語音,因?yàn)槠渑曌R別錯誤率更高。分類報告/回調(diào)數(shù)字亦給出了相同的結(jié)論。
在iOS上使用TensorFlow
現(xiàn)在我們已經(jīng)擁有了一套經(jīng)過訓(xùn)練的模型,其擁有比較理想的測試集預(yù)測精確度。下面我們將構(gòu)建一款簡單的iOS應(yīng)用,并利用這套模型在其中實(shí)現(xiàn)預(yù)測能力。首先,我們利用TensorFlow C++庫構(gòu)建一款應(yīng)用。在下一章節(jié)中,我們將把模型引入Metal以進(jìn)行比較。
這里我們既有好消息也有壞消息。壞消息是大家需要利用源代碼自行構(gòu)建TensorFlow。事實(shí)上,情況相當(dāng)糟糕:大家需要安裝Java方可實(shí)現(xiàn)這項(xiàng)目標(biāo)。而好消息是整個流程其實(shí)并不復(fù)雜。感興趣的朋友可以點(diǎn)擊此處查看完整指南,但以下步驟也基本能夠幫助大家解決問題(在TensorFlow 1.0上實(shí)測有效)。
這里需要注意的是,大家應(yīng)當(dāng)安裝Xcode 8,并確保活動開發(fā)者目錄指向您Xcode的安裝位置(如果大家先安裝了Homebrew,隨后才安裝Xcode,則其可能指向錯誤位置,意味著TensorFlow將無法完成構(gòu)建):
sudo xcode-select -s /Applications/Xcode.app/Contents/DeveloperTensorFlow利用一款名為bazel的工具進(jìn)行構(gòu)建,bazel則要求配合Java JDK 8。大家可以利用Homebrew輕松安裝必要的軟件包:
brew cask install java brew install bazel brew install automake brew install libtool在完成之后,大家需要克隆TensorFlow GitHub庫。需要注意的是:請確保您指定的路徑不具備充足的空間,否則bazel會拒絕進(jìn)行構(gòu)建(沒錯,這是真的!)。我直接將其克隆到了自己的主目錄當(dāng)中:
cd /Users/matthijs git clone https://github.com/tensorflow/tensorflow -b r1.0其中的-b r1.0標(biāo)記告知git克隆r1.0分支。大家可以隨意使用其它更新的分支,或者選擇使用master分支。
備注:在MacOS Sierra上,接下來即將運(yùn)行的configure腳本會提示多項(xiàng)錯誤。為了解決問題,我不得不選擇克隆master分支。在OS X El Capitan上,使用r1.0分支則不會引發(fā)任何錯誤。
在代碼庫克隆完成后,大家需要運(yùn)行configure腳本。
cd tensorflow ./configure其會提出幾個問題,以下為我給出的回答:
Please specify the location of python. [Default is /usr/bin/python]:????????我的回答是/usr/local/bin/python3,因?yàn)槲蚁M褂肞ython 3.6配合TensorFlow。如果大家選擇默認(rèn)選項(xiàng),則TensorFlow將利用Python 2.7進(jìn)行構(gòu)建。
Please specify optimization flags to use during compilation [Default is? -march=native]:?
在這里直接按下回車鍵,接下來的幾個問題則全部按n選擇否。
在其問及要使用哪套Python庫時,按下回車以選擇默認(rèn)選項(xiàng)(即Python 3.6庫)。
接下來的問題全部按n選擇否。現(xiàn)在該腳本將下載幾項(xiàng)依賴性項(xiàng)目并為構(gòu)建TensorFlow做好準(zhǔn)備。
構(gòu)建靜態(tài)庫
我們可以通過以下兩種方式構(gòu)建TensorFlow:
在tensorflow目錄下執(zhí)行以下腳本:
tensorflow/contrib/makefile/build_all_ios.sh其會首先下載一些依賴性選項(xiàng),而后開始進(jìn)行構(gòu)建流程。如果一切順利,其將創(chuàng)建出三套接入應(yīng)用所必需的靜態(tài)庫,分別為: libtensorflow-core.a、libprotobuf.a、libprotobuf-lite.a。
警告:構(gòu)建這些庫需要一段時間——我的iMac需要25分鐘,機(jī)型較舊的MacBook Pro則需要3個小時,而且整個過程中風(fēng)扇一直在全力運(yùn)轉(zhuǎn)!大家可能會在過程中看到一些編譯器警告甚至錯誤提示信息一閑而過。當(dāng)作沒看見就好,時間一到工作自然就緒!
到這里工作還沒結(jié)束。我們還需要構(gòu)建其它兩款helper工具。在終端當(dāng)中運(yùn)行以下兩條命令:
bazel build tensorflow/python/tools:freeze_graph bazel build tensorflow/python/tools:optimize_for_inference注意:這一過程大約需要20分鐘左右,因?yàn)槠湫枰俅螐牧汩_始構(gòu)建TensorFLow(這一次使用bazel)。
備注:如果大家在過程中遇到了問題,請點(diǎn)擊此處參閱官方指南。
為Mac設(shè)備構(gòu)建TensorFlow
這部分內(nèi)容為可選項(xiàng)目,但由于大家已經(jīng)安裝了全部必要軟件包,因此為Mac系統(tǒng)構(gòu)建TensorFlow并不困難。其會創(chuàng)建一個新的pip軟件包,大家可進(jìn)行安裝以取代官方TensorFlow軟件包。
為什么不使用官方軟件包?因?yàn)檫@樣我們才能創(chuàng)建一套包含自定義選項(xiàng)的TensorFlow版本。舉例來說,如果大家在運(yùn)行train.py腳本時遇到了“此TensorFlow庫無法利用SSE4.1指令進(jìn)行編譯”的警告提示,則可編譯一套特殊的TensorFLow版本以啟用這些指令。
要為Mac系統(tǒng)構(gòu)建TensorFlow,請在終端中運(yùn)行以下命令:
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選項(xiàng)用于在您的CPU能夠支持的前提下,添加對SSE、AVX、AVX2以及FMA等的支持。
隨后安裝該軟件包:
pip3 uninstall tensorflow sudo -H pip3 install /tmp/tensorflow_pkg/tensorflow-1.0.0-XXXXXX.whl欲了解更多細(xì)節(jié)信息,請點(diǎn)擊此處參閱TensorFlow官方網(wǎng)站。
“凍結(jié)”計算圖
我們將要創(chuàng)建的iOS應(yīng)用將利用Python腳本加載之前訓(xùn)練完成的模型,并利用其作出一系列預(yù)測。
大家應(yīng)該還記得,train.py將計算圖定義保存在了/tmp/voice/graph.pb文件當(dāng)中。遺憾的是,大家無法直接將該計算圖加載至iOS應(yīng)用當(dāng)中。完整的計算圖中包含的操作目前還不受TensorFlow C++ API的支持。正因?yàn)槿绱?#xff0c;我們才需要使用剛剛構(gòu)建完成的其它兩款工具。其中freeze_graph負(fù)責(zé)獲取graph.pb以及包含有W與b訓(xùn)練結(jié)果值的checkpoint文件。其還會移除一切在iOS之上不受支持的操作。
在終端當(dāng)中立足tensorflow目錄運(yùn)行該工具:
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當(dāng)中創(chuàng)建一套經(jīng)過簡化的計算圖,其僅具備y_pred與inference兩個節(jié)點(diǎn)。其并不包含任何用于訓(xùn)練的計算圖節(jié)點(diǎn)。
使用freeze_graph的好處在于,其還將固定該文件中的權(quán)重值,這樣大家就無需分別進(jìn)行權(quán)重值加載了:frozen.pb中已經(jīng)包含我們需要的一切。而optimize_for_inference工具則負(fù)責(zé)對計算圖進(jìn)行進(jìn)一步簡化。其將作為grozen.pb文件的輸入內(nèi)容,并寫入/tmp/voice/inference.pb作為輸出結(jié)果。我們隨后會將此文件嵌入至iOS應(yīng)用當(dāng)中。使用以下命令運(yùn)行該工具:
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=TrueiOS應(yīng)用
大家可以在github.com/hollance/TensorFlow-iOS-Example中的VoiceTensorFlow文件夾內(nèi)找到我們此次使用的iOS應(yīng)用。
在Xcode當(dāng)中打開該項(xiàng)目,其中包含以下幾條注意事項(xiàng):
前往Project Settings(項(xiàng)目設(shè)置)屏幕并切換至Build Settings(構(gòu)建設(shè)置)標(biāo)簽。在Other Linker Flags(其它鏈接標(biāo)記)下,大家會看到以下內(nèi)容:
/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庫的實(shí)際克隆路徑。(請注意,這里tensorflow出現(xiàn)了兩次,所以文件夾名稱應(yīng)為tensorflow/tensorflow/...)
備注:大家也可以將這三個.a文件復(fù)制到項(xiàng)目文件夾之內(nèi),如此即不必?fù)?dān)心路徑可能出現(xiàn)問題。我個人并不打算在這一示例項(xiàng)目中采取這種方式,因?yàn)閘ibtensorflow-core.a文件是一套體積達(dá)440 MB的庫。
另外請注意檢查Header Search Paths(標(biāo)題搜索路徑)。以下為目前的設(shè)置:
~/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另外,大家還需要將其更新至您的克隆目錄當(dāng)中。
以下為我在構(gòu)建設(shè)置當(dāng)中進(jìn)行了修改的其它條目:
Enable Bitcode: No Warnings / Documentation Comments: No Warnings / Deprecated Functions: NoBitcode目前尚不受TensorFLow的支持,所以我決定將其禁用。我還關(guān)閉了警告選項(xiàng),否則在編譯應(yīng)用時會出現(xiàn)一大票問題提示。(禁用之后,大家仍然會遇到幾項(xiàng)關(guān)于值轉(zhuǎn)換問題的警告。大家當(dāng)然也可以將其一并禁用,但我個人還是希望多少了解一點(diǎn)其中的錯誤。)
在完成了對Other Linker Flags與Header Search Paths的變更之后,大家即可構(gòu)建并運(yùn)行我們的iOS應(yīng)用。
很好,現(xiàn)在大家已經(jīng)擁有了一款能夠使用TensorFlow的iOS應(yīng)用了!下面讓我們看看它的實(shí)際運(yùn)行效果。
使用 TensorFlow C++ API
TensorFlow for iOS由C++編寫而成,但其中需要編寫的C++代碼量其實(shí)——幸運(yùn)的是——并不多。一般來講,大家只需要完成以下工作:
在本示例應(yīng)用當(dāng)中,這一切皆發(fā)生在ViewController.mm之內(nèi)。首先,我們加載計算圖:
- (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項(xiàng)目當(dāng)中已經(jīng)包含我們通過在graph.pb上運(yùn)行freeze_graph與optimize_for_inference工具所構(gòu)建的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)]]這是因?yàn)镃++ API所能支持的操作要遠(yuǎn)少于Python API。這里提到我們在loss函數(shù)節(jié)點(diǎn)中所使用的L2Loss操作在iOS上并不適用。正因?yàn)槿绱?#xff0c;我們才需要利用freeze_graph以簡化自己的計算圖。
在計算圖加載完成之后,我們使用以下命令創(chuàng)建一項(xiàng)會話:
- (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; }會話創(chuàng)建完成后,我們可以利用其執(zhí)行預(yù)測操作。其中的predict:method會提供一個包含20項(xiàng)浮點(diǎn)數(shù)值的數(shù)組——即聲學(xué)特征——并將這些數(shù)字饋送至計算得意洋洋發(fā)中。
下面我們一起來看此方法的工作方式:
- (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定義為我們需要使用的輸入數(shù)據(jù)。此張量為{1,20},因?yàn)槠湟淮翁崛∫豁?xiàng)示例且該示例中包含20項(xiàng)特征。在此之后,我們將數(shù)據(jù)由float *數(shù)組復(fù)制至該張量當(dāng)中。
接下來,我們運(yùn)行該項(xiàng)會話:
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;}這里得出了類似于Python代碼的內(nèi)容:
??? pred, inf = sess.run([y_pred, inference], feed_dict={x: example})只是不那么簡潔。我們需要創(chuàng)建饋送詞典、用于列出需要運(yùn)行的全部節(jié)點(diǎn)的向量,外加一個負(fù)責(zé)容納對應(yīng)結(jié)果的向量。最后,我們告知該會話完成上述任務(wù)。
在會話運(yùn)行了全部必要節(jié)點(diǎn)后,我們即可輸出以下結(jié)果:
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");} }出于演示需求,只需要運(yùn)行inference節(jié)點(diǎn)即可完成對音頻數(shù)據(jù)的男聲/女聲判斷。不過我還希望查看計算得出的概率,因此這里我也運(yùn)行了y_pred節(jié)點(diǎn)。
運(yùn)行iOS應(yīng)用
大家可以在iPhone模擬器或者實(shí)機(jī)之上運(yùn)行這款應(yīng)用。在模擬器上,大家仍然會看到“此TensorFlow庫無法利用SSE4.1指令進(jìn)行編譯”的提示,但在實(shí)機(jī)上則不會出現(xiàn)這樣的問題。
出于測試的目的,這款應(yīng)用只會進(jìn)行兩項(xiàng)預(yù)測:一次為男聲示例預(yù)測,一次為女聲示例預(yù)測。(我直接從測試集中提取了對應(yīng)示例。大家也可以配合其它示例并修改maleExample或者emaleExample數(shù)組當(dāng)中的數(shù)字。)
運(yùn)行這款應(yīng)用,大家應(yīng)該會看到以下輸出結(jié)果。該應(yīng)用首先給出了計算圖當(dāng)中的各節(jié)點(diǎn):
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'需要注意的是,此計算圖中僅包含實(shí)施預(yù)測所必需的操作,而不包括任何與訓(xùn)練相關(guān)的內(nèi)容。
此后,其會輸出預(yù)測結(jié)果:
Probability spoken by a male: 0.970405% Prediction: male? Probability spoken by a male: 0.005632% Prediction: female如果大家利用Python腳本嘗試使用同樣的示例,那么結(jié)果也將完全一致。任務(wù)完成!
備注:這里要提醒大家,此項(xiàng)演示項(xiàng)目中我們對數(shù)據(jù)進(jìn)行了“偽造”(即使用了提取自測試集中的示例)。如果大家希望利用這套模型處理真正的音頻,則首先需要將對應(yīng)音頻轉(zhuǎn)化為20項(xiàng)聲學(xué)特征。
iOS平臺上TensorFlow的優(yōu)勢與缺點(diǎn)
TensorFlow是一款出色的機(jī)器學(xué)習(xí)模型訓(xùn)練工具,特別是對于那些不畏數(shù)學(xué)計算并樂于創(chuàng)建新型算法的朋友。要對規(guī)模更大的模型進(jìn)行訓(xùn)練,大家甚至可以在云環(huán)境下使用TensorFLow。
除了訓(xùn)練之外,本篇博文還介紹了如何將TensorFLow添加至您的iOS應(yīng)用當(dāng)中。對于這一部分,我希望概括這種作法的優(yōu)勢與缺點(diǎn)。
在iOS之上使用TensorFlow的優(yōu)勢:
在iOS上使用TensorFLow的缺點(diǎn):
就個人來講,我認(rèn)為在iOS上使用TensorFlow并沒有什么性價比可言——至少就目前而言是如此。其優(yōu)勢根本無法抵消致命的缺點(diǎn)。不過作為一款年輕的產(chǎn)品,我相信TensorFLow未來會得到進(jìn)一步改善……
備注:如果大家決定在自己的iOS應(yīng)用當(dāng)中使用TensorFlow,則應(yīng)意識到人們完全可以直接從應(yīng)用包中復(fù)制計算圖的.pb文件以竊取您的模型。雖然這個問題不僅存在于TensorFlow當(dāng)中,但由于“凍結(jié)”計算圖文件中同時包含模型參數(shù)與計算圖定義,因此對方能夠輕松完成逆向工程。如果您的模型將作為應(yīng)用本身的核心競爭優(yōu)勢存在,那么請務(wù)必想辦法對其加以保護(hù)以避免受到惡意窺探。
在GPU上運(yùn)行:使用Metal
在iOS之上使用TensorFLow的一大缺點(diǎn)在于,其運(yùn)行在CPU之上。雖然對于數(shù)據(jù)與模型規(guī)模較小的TensorFlow項(xiàng)目而言,CPU的處理能力已經(jīng)完全足夠,但對于較大的模型、特別是深度學(xué)習(xí)項(xiàng)目而言,大家無疑需要利用GPU進(jìn)行相關(guān)運(yùn)算。而在iOS系統(tǒng)上,這意味著我們必須選擇Metal。
大家仍然需要在自己的Mac設(shè)備上利用TensorFlow進(jìn)行訓(xùn)練——或者使用其它擁有強(qiáng)大GPU的Linux設(shè)備甚至云資源——不過運(yùn)行在iOS上的引用代碼則可使用Metal而非TensorFlow庫。
在對必需的學(xué)習(xí)參數(shù)進(jìn)行訓(xùn)練之后——即W與b值——我們需要將其導(dǎo)出為Metal可以讀取的格式。幸運(yùn)的是,我們只需要將其作為二進(jìn)制格式保存為一份浮點(diǎn)數(shù)值列表即可。
現(xiàn)在我們需要編寫另一套Python腳本:export_weights.py(點(diǎn)擊此處查看完整版本)。其內(nèi)容與我們之前用于加載計算圖定義及checkpoint文件的test.py非常相似。不過這一次,我們使用以下內(nèi)容:
??? W.eval().tofile("W.bin")???? b.eval().tofile("b.bin")W.eval()負(fù)責(zé)計算W的當(dāng)前值并將其返回為一個NumPy數(shù)組(過程與執(zhí)行sess.run(W)完全一致)。此后,我們使用tofile()將該NumPy數(shù)據(jù)保存為一個二進(jìn)制文件。好了,就是這么簡單:-)
備注:對于我們的示例分類器,W是一個20 x 1的矩陣,即一份簡單的20項(xiàng)浮點(diǎn)數(shù)值列表。對于更為復(fù)雜的模型,大家的學(xué)習(xí)參數(shù)可能屬于四維張量。在這種情況下,大家可能需要對其中的部分維度進(jìn)行順序調(diào)整,因?yàn)門ensorFlow存儲數(shù)據(jù)的順序與Metal的預(yù)期存在差異。大家可以直接使用tf.transpose()命令實(shí)現(xiàn)這一目標(biāo),但再次重申,我們的這一示例項(xiàng)目并不需要這些過程。
下面來看我們這套邏輯分類器的Metal版本。大家可以點(diǎn)擊此處在其源代碼的VoiceMetal文件夾中找到對應(yīng)的Xcode項(xiàng)目。此項(xiàng)目以Swift語言編寫而成。
大家應(yīng)該還記得,這里的邏輯回歸算法采用了以下方程式進(jìn)行計算:
y_pred = sigmoid((W * x) + b)其計算過程與神經(jīng)網(wǎng)絡(luò)當(dāng)中完全連接層的執(zhí)行過程相同。因此為了利用Metal實(shí)現(xiàn)我們的分類器,只需要使用一個MPSCNNFullyConnected層。首先,我們將W.bin與b.bin加載至Data對象當(dāng)中:
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!)此后,我們創(chuàng)建該完全連接層:
let sigmoid = MPSCNNNeuronSigmoid(device: device) let layerDesc = MPSCNNConvolutionDescriptor(kernelWidth: 1, kernelHeight: 1, inputFeatureChannels: 20, outputFeatureChannels: 1, neuronFilter: sigmoid)W_data.withUnsafeBytes { W inb_data.withUnsafeBytes { b inlayer = MPSCNNFullyConnected(device: device, convolutionDescriptor: layerDesc, kernelWeights: W, biasTerms: b, flags: .none)} }由于輸入內(nèi)容為20個數(shù)字,我決定將完全連接層的設(shè)定為一套1 x 1且包含20條輸入通道的維度“圖像”。而結(jié)果y_pred僅為單一數(shù)字,這樣該完全連接層將僅擁有一條輸出通道。作為輸入與輸出數(shù)據(jù)駐留所在的對象,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)由于使用的是應(yīng)用中的TensorFlow版本,因此其中的predict方法將獲取用以構(gòu)建單一示例的20條浮點(diǎn)數(shù)值。以下為完整的方法內(nèi)容:
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")} }這即為Metal當(dāng)中的運(yùn)行會話版本。其中convert(example:to:)與toFloatArray()方法屬于helper,負(fù)責(zé)將數(shù)據(jù)加載進(jìn)/出MPSImage對象。就是這么簡單,我們已經(jīng)成功完成了Metal版本的應(yīng)用成果!大家需要在實(shí)機(jī)之上運(yùn)行此應(yīng)用,因?yàn)镸etal并不支持模擬器運(yùn)行機(jī)制。
Probability spoken by a male: 0.970215% Prediction: male? Probability spoken by a male: 0.00568771% Prediction: female需要注意的是,這些概率與TensorFlow提供的預(yù)測結(jié)果并不完全一致。這是因?yàn)镸etal會在內(nèi)部使用16位浮點(diǎn)數(shù)值,但二者的結(jié)果仍然相當(dāng)接近!
鳴謝
本示例當(dāng)中使用的數(shù)據(jù)集由Kory Becker構(gòu)建并下載自Kaggle.com。感興趣的朋友亦可參閱Kory的博文與源代碼。
其他作者亦發(fā)布了與在iOS系統(tǒng)之上使用TensorFlow相關(guān)的文章,我本人也從中得到了大量啟發(fā)并引用了部分代碼示例,具體包括:
《在iOS上使用Deep MNIST與TensorFlow:上手指南》,由 Matt Rajca撰寫。
《利用Metal Performance Shaders實(shí)現(xiàn)TensorFlow加速》,同樣來自 Matt Rajca。《Tensorflow Cocoa示例》,由Aaron Hillegass撰寫。
TensorFlow資源庫中的《TensorFlow iOS示例》。
原文鏈接:http://machinethink.net/blog/tensorflow-on-ios/
總結(jié)
以上是生活随笔為你收集整理的深度学习指南:在iOS平台上使用TensorFlow的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Github 优秀开源项目 Best O
- 下一篇: 干货 | 算法工程师入门第一期——罗恒讲