端到端智慧音箱

2022-05-25 12:02:14

背景

前段時間買了個小米的空調伴侶,想用來檢視空調的功率,以確認空調到底會用掉多少電。
買的時候發現空調伴侶也支援紅外控制,這就得好好利用一下了。但是有個尷尬的問題,就是空調插座在很偏的地方,導致了無法控制空調,也沒法控制投影儀。

當一個東西你擁有了以後又失去的時候,你會很難受,所以我就想著要再買它一個紅外控制裝置來做這個事,看了一圈發現,小米萬能遙控沒得買了,變成了理財產品,所以就找了一款替代產品。歐瑞博的一款萬能遙控。買完之後才發現,雖然能控制空調和投影儀了,但是有兩個很嚴重的問題:

  1. 是沒法使用小愛來通過歐瑞博的萬能遙控來控制投影儀,目前兩邊還沒打通,所以該功能還沒法支援。
  2. 是使用小愛通過歐瑞博控制空調的時候常常無響應,估計就是小愛呼叫歐瑞博的過程出問題了,因為直接使用歐瑞博的app是可以生效的。

決定自制紅外控制裝置

到此,我決定,要不自己做一個吧,貌似成本也挺便宜的,還能玩玩IoT,多有意義。

所以我就花了大量的時間來做這個事,先去調研了一下用什麼微控制器,後面瞭解到用ESP32還挺好的,整合了WIFI和藍芽,之後還可以實現一些比較fancy的功能。

除了硬體,當然還需要給刷上韌體了,所以,還需要快速自學一下程式該咋寫,於是乎就快速過了一遍《Arduino從基礎到實踐》,通過控制LED燈來練了一下手。接下來就是開發紅外功能了,淘寶上9塊錢買了紅外收發的模組,藉助PlatformIO開發了第一版紅外韌體,成功地點亮了投影儀,但是同時還有兩個大坑沒填:

  1. 是紅外發射模組太弱了,只能在差不多半米的距離來控制裝置;
  2. 是死活控制不了空調。

後續通過另購了一款16元的大功率發射模組解決了問題1。

對於問題2,著實折磨了我好幾天的時間,到處檢查到底是哪裡的程式碼寫出問題了,因此還去研究了一下紅外協定,按位元對比了一下我發射出去的紅外和空調遙控發射出去的紅外到底有什麼不同,後來發現,格力空調的紅外有點特殊,但只需要修改一下設定,就能輕鬆解決。

終於搞定了控制問題,順手再擼一個WebServer來接受請求。

到此,微控制器端的部分就告一段落了。

決定自制智慧音箱

我想的是接下來就是如何通過小愛音箱來控制我的微控制器發射紅外訊號了,所以搜尋了一下該怎麼做,網上有人實現的做法是通過註冊小愛開發者賬號,通過類似"小愛同學" -> "讓XXX開啟空調"的方式來控制。具體就是在小米那邊設定一類意圖,只要是"讓XXX幹什麼"這樣的命令,都轉發到我們自己的伺服器上,然後在伺服器端解析小米給你傳送的意圖和實體,自行實現邏輯來進行控制(在我們的場景下就是傳送一個http請求)。所以,這還要求我們能有個公網地址,或者通過別的服務來做內網穿透。
然後我就去申請了小愛開發者平臺,但是他們的服務效率真的是太低了,由於我是周4申請的,竟然兩天都沒申請下來,到了週末,我就又動了"自己整一個小愛音箱的想法"。

說幹就幹,要實現這功能,首先得找找思路,所以就在github上先找了一下這類智慧音箱的開原始碼。

對比了一圈,發現wukong-robot 是一款挺好的產品,支援中文,還可以自定義外掛,所以就衝了一波。

然後就又碰到問題了,這個庫使用了各種雲服務來支援實現智慧音箱的功能,所以,我又又需要申請這些雲服務的賬號來試用,這我就不樂意了,咋又要申請呢?

所以,最後我決定,所有用到的雲服務有功能,通通本地搭建。

好處很明顯:

  1. 延遲降低,本地呼叫微妙級別的延遲。
  2. 又又可以學到了。

所以就先將智慧音箱涉及到的技術部分都先研究一下是些什麼。

一款智慧音箱會涉及到:

  1. 喚醒:通過一個關鍵詞來喚醒音箱聽取你的命令
  2. ASR(automatic speech recognition)或STT(speech to text):也就是語音識別,將語音轉換成文字
  3. NLU(natural language understanding):也就是讓機器理解這句話的意思。這本身是一個極其龐大的主題,但是在智慧音箱領域,就簡化了很多,我們只需要識別出來我們的命令想要做什麼就行了。這裡面就包含了兩部分:
    1. 意圖識別:也就是一句話是幹什麼的,比如【開啟空調】就可以識別成【操縱裝置】的意圖。
    2. NER(named entity recognition)命名實體識別:有的地方會稱之為slot(槽),就是識別出你關心的物件,比如【開啟空調】中,【開啟】就屬於【操作】實體,【空調】就屬於【裝置】實體。
  4. 處理意圖和實體:通過識別的意圖和實體來實現自己的控制邏輯,比如當出現【操縱裝置】的意圖時,去檢查一下具體的操作和裝置是什麼實體,來達到控制裝置的效果。
  5. TTS(text to speech):也就是語音合成,將文字轉成語音,一般用於回覆使用者的命令。

ASR 部分

知道了各種技術後,要做的無非就是上github上找各種實現了。

最先打算入手的就是ASR,語音識別算是被研究得比較多的,最先想到的就是去看看百度出的飛槳上是不是有類似的模型,還真讓我找到一個,有一個PaddleSpeech的庫,上面有一些訓練好的模型可以用。
其中碰到的一個坑就是,PaddleSpeech涉及到很多設定,而且在設定中還用了相對路徑,導致一開始程式以執行路徑為起始來找相對路徑,以至於怎麼都執行不起來,後來只能稍微改造了一下程式碼,先指定work path,然後用work path來拼接相對路徑,最終才能成功執行。
執行起來後測試了一下語音識別的效果也還行,不能說百分百識別,但是起碼音都算是對的,只是可能對應的字有時候不太對。
想著,應該也沒有啥大影響,所以也就沒咋處理了。

ASR部分就暫時告一段落了。

NLU 部分

下面就是NLU部分,NLU的話選用了RASA來構建,之前其實也聽說過RASA,主要是用來構建智慧聊天機器人的,也就是少掉語音轉文字的部分,單使用文字來聊天。
它本身帶有很多NLU相關的模型,我選用了
jieba(分詞)+bert-base-chinese LanguageModelFeaturizer(詞向量提取)+DIETClassifier(實體提取+意圖識別)的組合來訓練我自己的NLU。
在使用的過程中也碰到了一個巨坑,訓練報錯,而且報錯資訊及其簡陋:

tensorflow.python.framework.errors_impl.InvalidArgumentError:  Incompatible shapes: [57,10] vs. [57,7]
	 [[{{node cond/PartitionedCall/cond_11/else/_298/cond/add_1}}]] [Op:__inference_train_function_52346]

鬼知道這是啥錯啊,然後google一下,竟然還發現了某個github的issue上有類似的症狀,但是迄今仍未解決。

那我就只能自己動手了,debug之,花了一個星期的時間來理解它的程式碼,終於找到了原因,jieba分詞和bert LanguageModelFeaturizer的詞向量提取不相容,jieba分詞會把【空格】也識別為一個token,而bert LanguageModelFeaturizer預設會把【空格】去除。所以最簡單的做法就是刪除我訓練樣本中的空格,之後還可以研究一下,怎麼改RASA的原始碼,增加一個選項來選擇是否移除空格。

優化ASR

終於可以開始組裝了,RASA因為本身就是一個很成熟的庫,提供了以Web Server來服務的方式,所以只需要自己在wukong_robot中新增一個nlu的類,實現一下各種方法就可以。
而PaddleSpeech就自己寫一個Python的flask Web Server來提供一下服務了,並且在wukong_robot中新增上對應的類。

至此,終於可以來試一下效果了,不試不知道,一試發現了各種問題:

  1. 效果稀爛,,出現了下面這種尷尬的情況:
    2. 【開啟電視】識別成了【打個電視】
    3. 【關閉電視】識別成了【光閉電視】

    之前以為ASR識別不算特別準應該沒啥大的影響,但是NLU十分依賴ASR的結果,【打個電視】和【關閉電視】直接導致NLU無法識別出操作型別,到底是開啟還是關閉就無從得知了。

  2. 喚醒很費力,wukong_robot使用的喚醒庫是snowboy,這個庫其實挺火的,但是可能是我本身英語發音不是很標準吧,就很難喚醒它,最終通過調高了靈敏度來解決,代價就是,有時候啥也沒說,突然它就被喚醒了,挺嚇人的。

  3. 錄音部分有點問題,有時候說命令的時間不夠,就我說著說著,它就停止接受命令了,原因是錄音使用的定時,只要時間到了,就沒法繼續說了;相應地,時間如果不到,即使命令說完了,也會一直尬等著。

這幾部分很明顯都是屬於語音相關的,所以只好再去研究研究語音方面的內容,後來瞭解到,語音識別其實也分得很細(但是也慢慢地被深度學習給端到端替代了):

  1. 聲波編碼:將聲波編碼成計算機能識別的二進位制格式
  2. 特徵提取:提取聲音中的特徵,通常會使用mfcc特徵提取,最近的端到端深度學習會更多使用mfcc特徵的中間結果,fbank特徵
  3. 聲學模型:將聲音特徵識別成token,在最早的語音識別中,token一般是"狀態"這是比音素更小的單位。音素大致可以理解為英文中的音標。現在端到端機器學習中,會直接識別出字,甚至是片語的
  4. 語言模型:就是找出比較符合常識的結果,就比如上面的【開啟電視】和【打個電視】,【開啟電視】的出現概率會明顯高於【開啟】電視。而端到端深度學習的方法中,有時候不會專門分出語言模型來,但是訓練完一個模型後,再附加一個語言模型,往往能提升準確率。

在研究語音相關內容的時候,意外發現了一個庫,wenet ,然後就試用了一下它預訓練的一個multi_cn的模型,發現,這才是真的香呀,引入該模型後直接解決了音箱的兩個問題:

  1. 識別效果:效果賊棒,它在識別的時候會列印出中間過程,從中間過程可以看到,雖然中間過程產出的文字是不太對的,但是你一句話說完後,它能給你把前面不對的部分給糾正過來。
  2. 錄音部分:它支援識別說話結束,也就是說,如果你有一句話特別特別長的話,它會一直等你說完它才會認為這句話說完了,所以只要把ASR替換成這個模型,我的音箱就基本可用了。

wenet提供了CPU部署和GPU部署兩種方式,CPU部署非常簡單,一行命令啟動docker容器就行了,GPU的話需要使用它提供的DockerFile自己build一個docker image,目前multi_cn這個模型沒有支援,我也去嘗試了一下,發現build完後,在推理時會報錯,原因是到了某一層後,所有的引數都變成-1了,目前沒有太多的時間研究(趕緊完成音箱才是重點),所以就暫時擱置了。

直接使用了wenet提供的image進行CPU部署,目前它只提供了websocket的服務方式,所以需要我再花些時間適配一下。

終於,除了偶爾會誤喚醒外,聽力非常正常的智慧音箱完工了,

總結

效果演示

下面放上一段效果視訊

總體的架構圖:

想持續瞭解後續內容,請關注公眾號