前面的部落格裡說過最近幾個月我從傳統語音(語音通訊)切到了智慧語音(語音識別)。剛開始是學語音識別領域的基礎知識,學了後把自己學到的寫了PPT給組內同學做了presentation(語音識別傳統方法(GMM+HMM+NGRAM)概述)。一段時間後老闆就佈置了具體任務:在我們公司自己的ARM晶片上基於kaldi搭建一個線上語音識別系統,三個人花三個月左右的時間完成。由於我們都是語音識別領域的小白,要求可以低些,就用傳統的GMM-HMM來實現。說實話接到這個任務我們心裡是有點沒底的,不知道能不能按時完成,畢竟我們對語音識別不熟,對kaldi不熟。既然任務下達了,硬著頭皮也要上,並盡最大努力完成。我本能的先在網上用百度/google搜了搜,看有沒有一些經驗可供參考,好讓我們少走彎路。遺憾的是沒搜到有價值的東西。沒辦法,我們只能根據自己以前的經驗摸索著前進。最終我們按計劃花了不到三個月的時間完成了嵌入式平臺上線上語音識別系統的搭建。雖然只是demo,但是為後面真正做商用的產品打下了良好的基礎,累積了不少的經驗。今天我就把我們怎麼做的分享出來,給也想做類似產品的朋友做個參考。

 

既然作為一個專案來做,就要有計劃,分幾個階段完成這個專案。我在學習語音識別基礎知識時對kaldi有一個簡單的瞭解(在做語音識別前就已知kaldi的大名,沒辦法這幾年人工智慧(AI)太熱了。智慧語音作為人工智慧的主要落地點之一,好多都是基於kaldi來實現的。我是做語音的,自然會關注這個熱門領域的動態)。根據對kaldi的簡單瞭解,我把專案分成了三個階段,第一階段是學習kaldi,對kaldi有一個更深的認識,同時搞清楚基於kaldi做方案後面有哪些事情要做,計劃花一個月左右的時間完成。第二階段是設計軟體架構、寫程式碼、訓練模型等,也是花一個月左右的時間完成。第三階段是除錯,提升識別率,還是花一個月左右的時間完成。計劃的時間會根據實際情況做微調。

 

1,第一階段

第一階段就是學習kaldi。由於我們是三個人做這個專案,我就把學習任務分成三塊:資料準備和MFCC、GMM-HMM模型訓練、解碼網路建立和解碼。在其他兩位同學挑好模組後剩下的解碼網路建立和解碼就有我來學習了。學習過程就是看網上文章、看部落格和看kaldi程式碼、指令碼的過程。學完後大家搞清楚了後面有哪些事情要做,同時做了PPT給組內同學講,讓大家共同提高。解碼相關的見我前面的文章(基於WFST的語音識別解碼器 )。Kaldi中解碼有兩種型別:offline(多用於模型除錯等)和online(多用於線上識別等),其中online也有兩種方式,一種是通過PortAudio從MIC採集語音資料做線上語音識別,另一種是通過讀音訊WAV檔案的方式做線上語音識別。我們要做的是線上語音識別,這兩個就是很好的參考,尤其是通過PortAudio從MIC採集方式的,很有必要弄明白執行機制。於是我根據網上的部落格基於thchs30搭建了PC上的線上識別來除錯,基本上搞清楚了程式碼的執行機制。Kaldi中設定取樣率為16kHZ,每幀25ms(其中幀移10ms),每27幀為一組集中做MFCC特徵提取和解碼,這樣處理一組的語音時長是285ms(25+(27-1)*10=285),共4560(16*285=4560)個取樣點。每次處理完一組後就從buffer中再取出一組做MFCC和解碼,解碼後看有沒有識別的字出來,有的話就打印出來。

 

2,第二階段 

第一階段主要是學習,第二階段就要真正幹活了。我們在Linux上開發,先制定系統搭建完成後的目標:裝置用資料線連在PC上,能線上實時識別英文數字0—9(選識別這些是因為網上有現成的英國人說的音訊源,我們可以省去錄音頻源的工作,好節約時間),即人對著裝置說出英文數字0—9後PC螢幕上能實時打印出來,識別率接近GMM-HMM模型下的較好值。大家的任務還是沿襲第一階段的。學習資料準備和MFCC的同學先資料準備相關的工作,如標註等,好給模型訓練的同學用,然後移植kaldi中MFCC相關的程式碼。學習模型訓練的同學先開始模型訓練的準備工作,等要準備的資料好了後就開始訓練。我負責整個軟體架構的設計,同時還要把kaldi中的絕大部分(除了MFCC)移植進我們系統中。通過對kaldi的學習,使我對怎麼設計這個線上語音識別的軟體架構有了更深的認識。語音識別分兩個階段,即訓練階段和識別階段。訓練階段就是得到模型給識別階段用。它相對獨立,我們就基於kaldi來訓練模型,最終得到final.mdl等檔案給識別階段的軟體用(在初始化時讀取這些檔案得到解碼網路)。識別階段的軟體主要分兩部分,聲音採集和識別(包括特徵提取和解碼)。這樣系統就有兩個thread,一個是聲音採集thread(audio capture thread),它基於ALSA來做,負責聲音的採集和前處理(如噪聲抑制),另一個是識別thread(kaldi process thread),負責MFCC和解碼。兩個thread通過ring buffer互動資料,同時要注意資料的保護。這樣系統的軟體架構框圖如下:

 

大家對軟體架構討論覺得沒什麼問題後我就開始寫程式碼搭建軟體框架了。在 Linux中建立thread等都是一些套路活。Audio capture thread裡先做初始化,包括ALSA的配置以及前處理模組的初始化等。然後就每隔一定時間通過ALSA_LIB的API完成一次音訊資料的採集工作,讀完資料後就做前處理,處理好後把音訊資料放進ring buffer中,同時啟用kaldi process thread,讓kaldi process thread開始幹活。Kaldi thread也是先做一些初始化的工作,然後睡下去等待啟用。啟用後先從ring buffer裡取語音資料,然後做MFCC和decoder。完成後又睡下去等待下次再被啟用。搭建軟體框架時kaldi相關的程式碼還沒被移植進去,kaldi process thread裡僅僅把從ring  buffer裡拿到的語音資料寫進PCM檔案,然後用CoolEdit聽,聲音正常就說明軟體框架基本成型了。剛開始時audio capture thread裡也沒加前處理模組,除錯時把從ALSA裡獲取的資料寫進PCM檔案聽後發現有噪聲,就加了噪聲抑制(ANS)模組。這個模組用的是webRTC裡的。webRTC裡的三大前處理模組(AEC/ANS/AGC)幾年前我就用過,這次拿過來簡單處理一下就用好了,去噪效果也挺好的。ANS一個loop是10ms,而前面說過kaldi裡線上識別解碼一次處理一組27幀是285ms,我就取兩者的最小公倍數570ms作為audio capture thread的loop時間。從ALSA取到語音資料後分57(570/10 = 57)次做噪聲抑制,再把抑制後的語音資料寫進ring buffer。Kaldi thread啟用後還是每次取出285ms語音資料做處理,只不過要取兩次(570/285 = 2)。

 

軟體架構搭好後就開始移植kaldi程式碼了。Kaldi程式碼量大,不可能也沒必要全部移植到我們系統裡,只需要移植我們需要的就可以了。怎樣才能移植我們需要的程式碼呢?考慮後我用瞭如下的方法:先把線上解碼相關的程式碼移植進去,然後開始不停的編譯,報什麼錯提示缺什麼就加什麼,直到編譯通過。這種方法保證了把需要的檔案都移植進系統了,但有可能某些檔案中的函式沒用到,即到檔案級還沒到函式級。由於時間緊,這個問題就暫時不管了。移植過程更多的是一個體力活,需要小心細緻。在移植過程中遇到問題就去網上搜,最後都圓滿解決了。Kaldi主要用到了三個開源庫:openfst、BLAS、LAPACK。BLAS和LAPACK我用的常規方法,即到官網上下載編譯後生成庫,然後把庫和標頭檔案放到系統的”/usr/lib”和“/use/include”下,讓其他程式碼用。kaldi支援的有BALS庫有 ATLAS / CLAPACK / openBLAS / MKL等。在X86的Ubuntu PC上跑kaldi時就用的Intel的MKL,在ARM上就不能用了,需要用其他的幾種之一。我評估下來用了openBLAS,主要因為三點:1)它是BSD的;2)它支援多種架構(ARM/X86/MIPS/….),是開源庫裡效能最好的(各種架構裡都嵌了很多的彙編程式碼),被多家著名公司使用,如IBM/ARM/nvidia/huawei等;3)它有多個編譯選項可供選擇,比如單執行緒/多執行緒選擇、設定執行緒數等。BLAS的早期程式碼都是用fortran寫的,後來用C對其進行了封裝,所以系統還要加上對fortran的支援。對openFST,我發現用到的程式碼並不多,也就沒用常規的方法,而是直接把用到的程式碼移植進系統。我移植好編譯沒問題後另一個同學把剩下的MFCC以及和ALSA介面(用ALSA介面替代kaldi裡的PortAudio介面)相關的也移植進去了。這樣移植工作就算結束了。對比了下移植進系統的kaldi程式碼和kaldi裡SRC下的程式碼,應該是隻用了其中一小部分。下圖顯示了移植進系統的kaldi檔案(沒列出相關的標頭檔案)。同時負責模型訓練的同學也有了一個初步的模型生成的檔案,把這些檔案放進系統裡就可以跑起來了,人說話後PC螢幕上就有詞打印出來,不過不正確。這也正常呀,因為還沒除錯呢!

 

 

3,第三階段

第三階段就是除錯。第二階段結束後說話就有詞出來,但都是錯的,需要排查定位問題。線上語音識別系統從大的角度可以分兩塊:模型和程式碼實現。首先我們需要定位是模型的問題還是程式碼實現的問題,先從模型排查。在第一階段時利用thchs30大致搞清楚了線上解碼的機制,是用模型tri1調的,當時識別率很差。現在要關注識別率了,把模型換成了tri2b,識別率有所提高。這說明kaldi裡的線上解碼的程式碼是沒有問題的,識別率差問題出在模型。況且全球這麼多人在用kaldi,如果線上解碼有問題應該早就fix了。所以我們決定把我們生成的模型檔案放進thchs30裡來驗證模型是否有問題。為了排除從MIC輸入的音訊資料有噪聲等的干擾,先用讀檔案的方式驗證。把我們的模型檔案放進去後發現基本識別不正確,這說明模型是有問題的。負責模型的同學去調查,發現用於訓練的音源都是8K取樣的,但是線上解碼用的都是16K取樣的,這是我們自己挖的坑,用重取樣程式把8K的全部轉成16K的,這個坑也就填好了,但是識別率依舊不好。又發現訓練集全是英國人的發音,而測試集是我們中國人的發音,有一定口音的,最好用我們中國人自己的發音作為訓練集。於是我們自己又錄了用於訓練的音源,為了加大訓練的資料,又請好多其他人錄了音源。訓練後得到了新的模型,再放到thchs30裡面驗證,識別率有六七成了,這說明模型的大方向對了,為了提高識別率,模型還需要繼續除錯。

 

接下來就要看程式碼部分是否有問題了。把新生產的模型放進我們自己的系統,並且用從音訊檔案都資料的方式(我們的系統既可以從MIC採集資料也可以從音訊檔案讀資料,從音訊檔案讀資料是為了debug)來替代從MIC採集到的資料(這樣做是為了排除噪聲等因素的干擾)來看程式碼是否有問題。執行下來發現識別率依舊很差,這說明我們的程式碼也是有問題的。在第二階段我已經除錯過部分程式碼,確保了在kaldi process thread裡從PCM ring buffer裡拿到的音訊資料是沒有問題的。還有兩方面需要除錯,一是送進MFCC的PCM資料要是OK的,二是我們的線上解碼機制要跟kaldi裡的線上解碼機制完全一樣。一很快就除錯好了。二是先再深入研究吃透kaldi裡的線上解碼機制,改正我們與它不一樣的地方,經過兩三天除錯後識別率跟thchs30裡的差不多了,這說明我們的程式碼經過除錯後也有一個好的base了,後面就要開始調效能了。

 

前面是通過從音訊檔案中讀取資料來做線上識別的,資料相對乾淨些。現在要從MIC讀取音訊資料做真正線上識別了,試下來後識別率明顯偏低,這說明我們的前處理還沒完全做好(前面除錯時只加了ANS模組)。我把前處理後的音訊資料dump出來用CoolEdit聽,的確有時候音質不好,於是我又把webRTC中的AGC模組加上去,再次dump出前處理後的音訊資料聽,多次聽後都感覺音質正常。再來執行加了AGC後的從MIC採集音訊資料的線上識別,識別率果然有了明顯的提升。前處理能做的都做了,要想再提高識別率,就要靠模型發力了。做模型的同學一邊請更多的人錄音源來訓練,一邊嘗試各種模型,最終用的是tri4b,有了一個相對不錯的識別率。由於我們用的是GMM-HMM,如今主流的語音識別中已不再使用,老闆就覺得沒有必要再調了,後面肯定會用主流的模型的,但是整個嵌入式上的線上語音識別軟體程式碼尤其軟體架構和音訊採集還是有用的,後面就要基於這些程式碼做真正的產品。

 

對語音識別領域的資深人士來說,這個嵌入式線上語音識別系統還很稚嫩。但通過搭這個系統,讓我們對語音識別領域有了多一點的感性認識,也有了一個良好的開端,給老闆以信心,並且可以繼續做下去。這次工程上的事情偏多,後面希望更深入的做下去,累積更多的語音識別領域的經驗。搭這個系統沒有任何可供參考的資料,純粹是根據我們以往的經驗摸索著搭出來的。做的產品可能不一樣,但很多解決問題的思路都是一樣的。如果有朋友也搭過嵌入式上的線上語音識別系統,歡迎探討,搭出一個更好的線上語音識別系