OpenVINO 黑客馬拉松:在 Raspberry Pi 上辨識聲音與情緒

30月1日-XNUMX月XNUMX日在下諾夫哥羅德舉行 OpenVINO 黑客松。 參與者被要求使用英特爾 OpenVINO 工具套件創建產品解決方案原型。 主辦單位提出了一份大致主題列表,供選擇任務時參考,但最終決定權仍由團隊決定。 此外,也鼓勵使用產品中未包含的模型。

OpenVINO 黑客馬拉松:在 Raspberry Pi 上辨識聲音與情緒

在這篇文章中,我們將向您介紹我們如何創建產品原型,並最終獲得了第一名。

超過10支隊伍參加了此次黑客馬拉松。 很高興他們中的一些人來自其他地區。 黑客馬拉鬆的場地是「Kremlinsky on Pochain」綜合體,裡面掛著下諾夫哥羅德的古老照片,還有隨行人員! (我提醒您,目前英特爾的中央辦公室位於下諾夫哥羅德)。 參與者有 26 小時的時間來編寫程式碼,最後他們必須展示他們的解決方案。 一個單獨的優勢是演示會議的存在,以確保計劃的所有內容都實際實施,並且不會在演示中留下想法。 商品、零食、食物,一應俱全!

此外,英特爾還可選提供攝影機、Raspberry PI、神經運算棒 2。

任務選擇

準備自由形式的黑客馬拉松最困難的部分之一是選擇挑戰。 我們立即決定提出一些產品中尚未包含的東西,因為公告稱這是非常受歡迎的。

經過分析 模型,它們包含在當前版本的產品中,我們得出的結論是,它們中的大多數解決了各種計算機視覺問題。 而且,在電腦視覺領域很難想出一個無法使用 OpenVINO 解決的問題,而且即使可以發明一個,也很難在公共領域找到預先訓練的模型。 我們決定向另一個方向挖掘——語音處理和分析。 讓我們考慮一個有趣的任務,從言語中辨識情緒。 必須要說的是,OpenVINO 已經有了一個根據人臉判斷人情緒的模型,但:

  • 理論上,可以創建一種同時適用於聲音和影像的組合演算法,這應該會提高準確性。
  • 攝影機通常具有較窄的視角;需要多個攝影機才能覆蓋較大的區域;而聲音則沒有這樣的限制。

讓我們發展這個想法:讓我們以零售領域的想法為基礎。 您可以在商店結帳時衡量客戶滿意度。 如果其中一位顧客對服務不滿意並開始提高語氣,您可以立即致電管理員尋求協助。
在這種情況下,我們需要添加人聲識別功能,這將使我們能夠區分商店員工和顧客,並為每個人提供分析。 嗯,此外,還可以分析商店員工本身的行為,評估團隊的氛圍,聽起來不錯!

我們制定了解決方案的要求:

  • 目標設備尺寸小
  • 即時操作
  • 低價格
  • 易於擴展

因此,我們選擇Raspberry Pi 3 c作為目標設備 英特爾網路控制系統2.

在這裡,需要注意 NCS 的一個重要功能 - 它最適合標準 CNN 架構,但如果您需要運行帶有自訂層的模型,則需要進行低階最佳化。

只需要做一件小事:您需要一個麥克風。 普通的 USB 麥克風就可以,但與 RPI 一起看起來不太好。 但即使在這裡,解決方案實際上「就在附近」。 為了錄製語音,我們決定使用套件中的 Voice Bonnet 板 谷歌AIY語音套件,其上有一個有線立體聲麥克風。

從以下位置下載 Raspbian AIY項目庫 並將其上傳到閃存驅動器,使用以下命令測試麥克風是否正常工作(它將錄製 5 秒長的音訊並將其保存到檔案中):

arecord -d 5 -r 16000 test.wav

我應該立即註意到麥克風非常靈敏並且可以很好地拾取噪音。 為了解決這個問題,我們進入 alsamixer,選擇 Capture devices 並將輸入訊號電平降低到 50-60%。

OpenVINO 黑客馬拉松:在 Raspberry Pi 上辨識聲音與情緒
我們用銼刀修改主體,一切都合適,您甚至可以用蓋子將其關閉

新增指示按鈕

拆開AIY Voice Kit,我們記得有一個RGB按鈕,其背光可以用軟體控制。 我們搜尋“Google AIY Led”並找到文件: https://aiyprojects.readthedocs.io/en/latest/aiy.leds.html
為什麼不使用這個按鈕來顯示識別的情感,我們只有7個類,而按鈕有8種顏色,就夠了!

我們透過 GPIO 將按鈕連接到 Voice Bonnet,載入必要的庫(它們已安裝在 AIY 專案的分發套件中)

from aiy.leds import Leds, Color
from aiy.leds import RgbLeds

讓我們建立一個字典,其中每種情緒都會有一個 RGB 元組形式的對應顏色和 aiy.leds.Leds 類別的對象,透過它我們將更新顏色:

led_dict = {'neutral': (255, 255, 255), 'happy': (0, 255, 0), 'sad': (0, 255, 255), 'angry': (255, 0, 0), 'fearful': (0, 0, 0), 'disgusted':  (255, 0, 255), 'surprised':  (255, 255, 0)} 
leds = Leds()

最後,在每次新的情緒預測之後,我們將根據它(按按鍵)更新按鈕的顏色。

leds.update(Leds.rgb_on(led_dict.get(classes[prediction])))

OpenVINO 黑客馬拉松:在 Raspberry Pi 上辨識聲音與情緒
按鈕,燃燒!

使用語音工作

我們將使用 pyaudio 捕獲來自麥克風的流,並使用 webrtcvad 過濾噪音並偵測語音。 此外,我們將建立一個佇列,我們將非同步新增和刪除語音摘錄。

由於 webrtcvad 對所提供片段的大小有限制 - 它必須等於 10/20/30ms,並且用於識別情緒的模型的訓練(稍後我們將了解到)是在 48kHz 資料集上進行的,因此我們將捕獲大小為48000×20ms/1000×1(單聲道)=960 位元組的區塊。 Webrtcvad 將為每個區塊傳回 True/False,這對應於區塊中是否存在投票。

讓我們實作以下邏輯:

  • 我們將把那些有投票的區塊加到清單中;如果沒有投票,那麼我們將增加空區塊的計數器。
  • 如果空區塊的計數器> = 30(600毫秒),那麼我們查看累積區塊清單的大小;如果它> 250,那麼我們將其新增至佇列;如果不是,我們認為長度記錄的內容不足以將其提供給模型來辨識說話者。
  • 如果空區塊的計數器仍然< 30,且累積區塊清單的大小超過300,那麼我們會將片段新增至佇列以進行更準確的預測。 (因為情緒會隨著時間而改變)

 def to_queue(frames):
    d = np.frombuffer(b''.join(frames), dtype=np.int16)
    return d

framesQueue = queue.Queue()
def framesThreadBody():
    CHUNK = 960
    FORMAT = pyaudio.paInt16
    CHANNELS = 1
    RATE = 48000

    p = pyaudio.PyAudio()
    vad = webrtcvad.Vad()
    vad.set_mode(2)
    stream = p.open(format=FORMAT,
                channels=CHANNELS,
                rate=RATE,
                input=True,
                frames_per_buffer=CHUNK)
    false_counter = 0
    audio_frame = []
    while process:
        data = stream.read(CHUNK)
        if not vad.is_speech(data, RATE):
            false_counter += 1
            if false_counter >= 30:
                if len(audio_frame) > 250:              
                    framesQueue.put(to_queue(audio_frame,timestamp_start))
                    audio_frame = []
                    false_counter = 0

        if vad.is_speech(data, RATE):
            false_counter = 0
            audio_frame.append(data)
            if len(audio_frame) > 300:                
                    framesQueue.put(to_queue(audio_frame,timestamp_start))
                    audio_frame = []

是時候在公共領域尋找預先訓練的模型了,去 github、Google,但請記住,我們對所使用的架構有限制。 這是一個相當困難的部分,因為您必須在輸入資料上測試模型,此外,還將它們轉換為 OpenVINO 的內部格式 - IR(中間表示)。 我們嘗試了來自 github 的大約 5-7 種不同的解決方案,如果用於識別情緒的模型立即起作用,那麼對於語音識別我們必須等待更長時間 - 他們使用更複雜的架構。

我們將重點放在以下幾個方面:

接下來我們將從理論開始討論模型轉換。 OpenVINO 包含幾個模組:

  • 打開模型動物園,可以使用其中的模型並將其包含在您的產品中
  • Model Optimzer,借助它,您可以將模型從各種框架格式(Tensorflow、ONNX 等)轉換為中間表示格式,我們將使用該格式進一步工作
  • 推理引擎可讓您在 Intel 處理器、Myriad 晶片和神經運算棒加速器上運行 IR 格式的模型
  • 最高效的 OpenCV 版本(支援推理引擎)
    IR 格式的每個模型由兩個檔案描述:.xml 和.bin。
    模型透過模型優化器轉換為 IR 格式,如下所示:

    python /opt/intel/openvino/deployment_tools/model_optimizer/mo_tf.py --input_model speaker.hdf5.pb --data_type=FP16 --input_shape [1,512,1000,1]

    --data_type 允許您選擇模型將使用的資料格式。 支援 FP32、FP16、INT8。 選擇最佳的資料類型可以帶來良好的效能提升。
    --input_shape 表示輸入資料的維度。 動態更改它的能力似乎存在於 C++ API 中,但我們沒有深入研究,只是針對其中一個模型修復了它。
    接下來,我們嘗試透過 DNN 模組將已轉換的 IR 格式的模型載入到 OpenCV 中並將其轉發給它。

    import cv2 as cv
    emotionsNet = cv.dnn.readNet('emotions_model.bin',
                              'emotions_model.xml')
    emotionsNet.setPreferableTarget(cv.dnn.DNN_TARGET_MYRIAD)

    在這種情況下,最後一行允許您將計算重定向到神經計算棒,基本計算是在處理器上執行的,但在 Raspberry Pi 的情況下,這不起作用,您將需要一個神經計算棒。

    接下來,邏輯如下:我們將音訊劃分為一定大小的視窗(對我們來說是 0.4 秒),我們將每個視窗轉換為 MFCC,然後將其饋送到網格:

    emotionsNet.setInput(MFCC_from_window)
    result = emotionsNet.forward()

    接下來,我們來看所有視窗最常見的類別。 這是一個簡單的解決方案,但對於黑客馬拉松來說,只要你有時間,你就不需要想出太深奧的東西。 我們還有很多工作要做,所以讓我們繼續下去——我們將處理語音辨識問題。 有必要建立某種資料庫來儲存預先錄製的語音頻譜圖。 由於時間所剩無幾,我們將盡力解決這個問題。

    也就是說,我們建立一個用於錄製語音摘錄的腳本(其工作方式與上述相同,只有當鍵盤中斷時,它才會將語音儲存到檔案中)。

    咱們試試:

    python3 voice_db/record_voice.py test.wav

    我們錄製幾個人的聲音(在我們的例子中,是三個團隊成員)
    接下來,對於每個錄製的聲音,我們執行快速傅立葉變換,獲得頻譜圖並將其儲存為 numpy 陣列 (.npy):

    for file in glob.glob("voice_db/*.wav"):
            spec = get_fft_spectrum(file)
            np.save(file[:-4] + '.npy', spec)

    文件中的更多詳細信息 create_base.py
    因此,當我們運行主腳本時,我們將從一開始就從這些頻譜圖中獲得嵌入:

    for file in glob.glob("voice_db/*.npy"):
        spec = np.load(file)
        spec = spec.astype('float32')
        spec_reshaped = spec.reshape(1, 1, spec.shape[0], spec.shape[1])
        srNet.setInput(spec_reshaped)
        pred = srNet.forward()
        emb = np.squeeze(pred)

    收到發聲片段的嵌入後,我們將能夠透過從段落到資料庫中所有聲音的餘弦距離來確定它屬於誰(越小,可能性越大) - 對於演示,我們設定了閾值至 0.3):

            dist_list = cdist(emb, enroll_embs, metric="cosine")
            distances = pd.DataFrame(dist_list, columns = df.speaker)

    最後,我想指出的是,推理速度很快,可以添加 1-2 個模型(對於 7 秒長的範例,推理需要 2.5 秒)。 我們不再有時間添加新模型,而是專注於編寫 Web 應用程式的原型。

    Web應用程序

    重要的一點:我們從家裡攜帶路由器並設置我們的本地網絡,它有助於透過網路連接設備和筆記型電腦。

    後端是前端和Raspberry Pi之間的端對端訊息通道,基於websocket技術(http over tcp協定)。

    第一階段是接收樹莓派處理後的訊息,即以json封裝的預測變量,中途保存在資料庫中,以便統計使用者在這段時間內的情緒背景。 然後,該資料包被發送到前端,前端使用訂閱並從 websocket 端點接收資料包。 整個後端機制是用 golang 語言建構的;選擇它是因為它非常適合非同步任務,而 goroutine 可以很好地處理非同步任務。
    當存取端點時,使用者被註冊並輸入到結構中,然後接收他的訊息。 用戶和訊息都輸入到一個公共集線器中,訊息已經從該集線器進一步發送(到訂閱的前端),如果用戶關閉連接(樹莓派或前端),則他的訂閱將被取消,並且他將從中刪除樞紐。

    OpenVINO 黑客馬拉松:在 Raspberry Pi 上辨識聲音與情緒
    我們正在等待後面的連接

    前端是一個使用 JavaScript 編寫的 Web 應用程序,使用 React 函式庫來加速和簡化開發過程。 該應用程式的目的是可視化使用在後端運行的演算法和直接在 Raspberry Pi 上運行的演算法獲得的數據。 該頁面具有使用react-router實現的分段路由,但我們感興趣的主頁是主頁面,其中使用WebSocket技術從伺服器即時接收連續的資料流。 Raspberry Pi 偵測到聲音,從註冊資料庫中確定該聲音是否屬於特定人,並將機率清單傳送給客戶端。 客戶端顯示最新的相關數據,顯示最有可能對著麥克風說話的人的頭像,以及他發音時的情緒。

    OpenVINO 黑客馬拉松:在 Raspberry Pi 上辨識聲音與情緒
    包含更新預測的主頁

    結論

    按計劃完成所有事情是不可能的,我們根本沒有時間,所以主要希望是在演示中,一切都能正常進行。 在演講中,他們談論了一切是如何運作的,他們採用了什麼模型,他們遇到了什麼問題。 接下來是演示部分 - 專家以隨機順序在房間裡走動,並接近每個團隊查看工作原型。 他們也向我們提出了問題,每個人都回答了自己的部分,他們將網路留在了筆記型電腦上,一切都按預期進行。

    請注意,我們解決方案的總成本為 150 美元:

    • 樹莓派 3 ~ 35 美元
    • Google AIY Voice Bonnet(您可以收取接聽費)~ 15$
    • 英特爾NCS 2 ~ 100$

    如何提高:

    • 使用客戶端註冊 - 要求閱讀隨機產生的文本
    • 增加更多模型:可以透過聲音確定性別和年齡
    • 分離同時發聲的聲音(分類)

    存儲庫: https://github.com/vladimirwest/OpenEMO

    OpenVINO 黑客馬拉松:在 Raspberry Pi 上辨識聲音與情緒
    我們很累但很快樂

    最後,我想對主辦單位和參與者表示感謝。 在其他團隊的專案中,我們個人很喜歡監控免費停車位的解決方案。 對我們來說,這是一次非常酷的沉浸在產品和開發中的體驗。 我希望這些地區能舉辦越來越多有趣的活動,包括人工智慧主題的活動。

來源: www.habr.com

添加評論