Google Teachable Machine 聲音分類結合 Linkit 7697,拍手彈指都聽得懂

前言

已過我們寫了非常多關於 Google Teachable Machine 的影像分類應用,本文將說明如何使用 Google Teachable Machine 的聲音分類 (audio classification) 訓練工具,讓你的電腦能夠辨識不同的聲音,並藉此觸發 LinkIt 7697 開發板執行對應的動作。

相同的架構搬到 Raspberry Pi, Jetson Nano 上也可直接執行,歡迎玩玩看喔!也可以改用 Wio Terminal 搭配 Edge Impulse 來進行聲音分類,但後者與本篇所說的做法不一樣。

本文

操作 Google Teachable Machine 聲音分類

作為 coding-free tool,Google Teachable Machine (本文後簡稱 TM) 這幾年的能見度相當高,讓您輕鬆完成神經網路視覺辨識中最麻煩的幾件事:環境建置、訓練與匯出模型檔。您只要對自己所要分類的資料有一定的掌握度,都可以做出不錯的專案。當然,免費工具一定有其限制,因此 TM 或Microsoft LOBE.ai(相關文章請點我) 目前都只提供了影像分類,如果要進一步到物件偵測或影像分割的話,就需要另外找合適的工具了。

TM 目前提供了三種不同的AI應用,分別是影像分類專案(Image Project)、聲音辨識專案(Audio Project) 與身體姿態辨識專案(Pose Project),因此屬於「監督式學習Supervised learning」,針對不同的情境提供了Tensorflow、Tensorflow.js與Tensorflow Lite三種模型格式,可供後續製作各種離線推論,或放到 Raspberry Pi / Jetson nano / Arduino Sense 33 等裝置來實現邊緣運算。

每個專案都被設計成三個步驟,分別是 收集(Gather)、訓練(Train) 與匯出(Export)

收集資料

進入 TM 網站之後,點選 Get started 開始專案,並選擇 Audio Project。根據其說明,會使用長度為1秒鐘的聲音檔來訓練。

 

主要操作頁面分成三大區塊,分別就是上面所說 收集(Gather)、訓練(Train)、即時預覽(Preview)功能和匯出模型(Export Model)。可想而知,您會在收集資料這一段花費相當的時間,但模型成效與資料集品質有直接相關,所以在此多花一點時間整理出很好的資料集是絕對值得的!

以收集背景雜音(Background Noise)來示範,TM 要求至少要有 20 筆資料 (也就是 20 秒),請點選 Record 之後開始錄音 (下圖左),完成之後再點選 Extract Sample 會自動分割成 20 筆長度為1秒鐘的資料。其餘各聲音類別則是至少 8 筆資料,否則無法訓練。為避免錄音上的操作失誤,TM 讓你一次錄 2 秒但之後還是會切成長度 1 秒的聲音檔,這與神經網路的設定有關。

既然都是 Google 的服務,您可以把進行中的專案存檔到 Google Drive,或載入先前未完成的專案來繼續操作,非常方便

訓練

按下畫面中央的 Train 即可訓練,這也是 TM 最貼心的地方吧,馬上就可以看到成果了。完成之後,就會立刻透過麥克風來連續辨識聲音,讓您看看訓練的成果,可以調整 overlap factor (概念類似 sliding window)。如果有某個類別的表現不如預期,請回頭加入更多資料來重新訓練,也請注意各類別中樣本數須保持平衡,也就是數量不可相差太多。

辨識為 “sorry”
辨識為 “snap”,但信心不高

匯出 tensoflow lite 模型檔

如果您滿意本次的訓練結果的話,就可以匯出模型檔了。點選右上角的 Export 就會看到以下畫面,點選 Tensorflow Lite 再點選 Download my model 就會下載模型 .zip 檔,解壓縮之後會有一個 model.tflte  與 labels.txt,後續執行 python 檔時就會用到

如果不滿意,請加入更多聲音樣本,或由朋友來幫你錄製來增加資料的多元性。

使用 Netron 來檢視神經網路架構

在此可用 Netron 來檢視神經網路架構,請開啟 Netron 網站之後上傳 model.tflite 即可看到下圖,

由此可知,即便是免費的神經網路工具,Google 還是用很優秀的架構來處理。有興趣的進階使用者可以試試看修改神經網路架構讓效能更好。

點選某一層可在畫面右側看到該層的細節參數

 


執行

請根據本文設定好環境之後,請開啟 anaconda prompt 執行以下指令,並確定所有檔案都位於同一個資料夾下,否則會找不到檔案。

python TM2_tflite_audio.py --model model.tflite --labels labels.txt

執行畫面如下,可以看到每次辨識的結果與信心指數,例如 1 Clapping 0.99667513

本程式使用 tensorflow 2.7.0版,如果您執行上出現問題,請使用以下指令來安裝 tensorflow:

pip install tensorflow==2.7.0

python程式碼

python 程式碼會根據  TM 所匯出的聲音分類模型(model.tflite)與標籤 (labels.txt) 來對麥克風所收到的聲音進行推論,並把推論結果顯示在 console。

from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

# Imports
# from tflite_support.task import audio
# from tflite_support.task import core
# from tflite_support.task import processor

import argparse
import serial
import io
import time
import numpy as np
import cv2
import tensorflow as tf
import pyaudio
import wave

# from tflite_support.task import audio
# from tflite_support.task import core
# from tflite_support.task import processor
#import tensorflow as tf

from PIL import Image
# from tf.lite.interpreter import Interpreter

def load_labels(path):
with open(path, 'r') as f:
return {i: line.strip() for i, line in enumerate(f.readlines())}

def set_input_tensor(interpreter, image):
tensor_index = interpreter.get_input_details()[0]['index']
input_tensor = interpreter.tensor(tensor_index)()[0]
input_tensor[:] = image

def main():
BAUD_RATES = 9600
# 修改實際的 com port
ser = serial.Serial('com7', BAUD_RATES)
while (True):
parser = argparse.ArgumentParser(
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument(
'--model', help='File path of .tflite file.', required=True)
parser.add_argument(
'--labels', help='File path of labels file.', required=True)
args = parser.parse_args()

labels = load_labels(args.labels)


interpreter = tf.lite.Interpreter(args.model)
# interpreter = Interpreter(args.model)

interpreter.allocate_tensors()
a = interpreter.get_input_details()[0]['shape']
print(a)
getaudio()
x = np.fromfile(open('output.wav'),np.int32)[11:]

# x[0, 0] = np.fromfile(open('output.wav'),np.int16)[22:]

# for i in range(44032):
# x[0, i] = np.fromfile(open('output.wav'),np.int16)[22:][i]

print(x.shape)
results = classify_audio(interpreter, x)

label_id, prob = results[0]
print(labels[label_id],prob)

if (labels[label_id]=='0 Background Noise') :
ser.write(b'command_1\n') # 訊息必須是位元組類型
# time.sleep(0.5) # 暫停0.5秒,再執行底下接收回應訊息的迴圈

elif (labels[label_id]=='1 Clapping') :
ser.write(b'command_2\n') # 訊息必須是位元組類型
# time.sleep(0.5) # 暫停0.5秒,再執行底下接收回應訊息的迴圈

elif (labels[label_id]=='2 Snapping') :
ser.write(b'command_3\n') # 訊息必須是位元組類型
# time.sleep(0.5) # 暫停0.5秒,再執行底下接收回應訊息的迴圈
elif (labels[label_id]=='3 Sorry') :
ser.write(b'command_4\n') # 訊息必須是位元組類型
# time.sleep(0.5) # 暫停0.5秒,再執行底下接收回應訊息的迴圈
time.sleep(2)

def classify_audio(interpreter, image, top_k=1):
"""Returns a sorted array of classification results."""
set_input_tensor(interpreter, image)
interpreter.invoke()
output_details = interpreter.get_output_details()[0]
output = np.squeeze(interpreter.get_tensor(output_details['index']))

# If the model is quantized (uint8 data), then dequantize the results
if output_details['dtype'] == np.uint8:
scale, zero_point = output_details['quantization']
output = scale * (output - zero_point)

ordered = np.argpartition(-output, top_k)
return [(i, output[i]) for i in ordered[:top_k]]

def getaudio():
p = pyaudio.PyAudio()

CHANNELS = 1
FORMAT = pyaudio.paInt16
CHUNK = 1024
RATE = 44100
p = pyaudio.PyAudio()

stream = p.open(format=FORMAT,
channels=CHANNELS,
rate=RATE,
input=True,
frames_per_buffer=CHUNK)

#print("Start recording!")

frames = []
seconds = 2
for i in range (0, int(RATE/CHUNK*seconds)):
data = stream.read(CHUNK)
frames.append(data)

#print("Stop recording.")
stream.stop_stream()
stream.close()

p.terminate()

wf = wave.open("output.wav", 'wb')
wf.setnchannels(CHANNELS)
wf.setsampwidth(p.get_sample_size(FORMAT))
wf.setframerate(RATE)
wf.writeframes(b''.join(frames))
wf.close()

main()

Arduino 程式碼

在此使用 LinkIt 7697 來執行,透過序列埠接收到訊息之後執行對應的動作。您可以自由修改要做哪些事情,完成您專屬的聲音辨識系統!

Linkit 7697 code
#define LED_1_pin 2
#define LED_2_pin 3
#define LED_3_pin 4

String str;

void setup(void)
{
Serial.begin(9600);

// init pin states
pinMode(LED_1_pin, OUTPUT);
digitalWrite(LED_1_pin,LOW);
pinMode(LED_2_pin, OUTPUT);
digitalWrite(LED_2_pin,LOW);
pinMode(LED_3_pin, OUTPUT);
digitalWrite(LED_3_pin,LOW);

}

void loop(void)
{
int i;

if (Serial.available()) {
// 讀取傳入的字串直到"\n"結尾
str = Serial.readStringUntil('\n');

if (str == "command_1") { // 若字串值是 "command_1" 燈號OX
digitalWrite(LED_1_pin,HIGH);
digitalWrite(LED_2_pin,LOW);
digitalWrite(LED_3_pin,LOW);

Serial.println("command_1");
}
else if (str == "command_2") { // 若字串值是 "command_2" 燈號XO
digitalWrite(LED_1_pin,LOW);
digitalWrite(LED_2_pin,HIGH);
digitalWrite(LED_3_pin,LOW);

Serial.println("command_2");
}
else if (str == "command_3" ) { // 若字串值是 "command_3" 燈號XX
digitalWrite(LED_1_pin,LOW);
digitalWrite(LED_2_pin,LOW);
digitalWrite(LED_3_pin,HIGH);

Serial.println("command_3");
}
else if (str == "command_4" ) { // 若字串值是 "command_3" 燈號XX
digitalWrite(LED_1_pin,HIGH);
digitalWrite(LED_2_pin,HIGH);
digitalWrite(LED_3_pin,HIGH);

Serial.println("command_4");
}
}
}

 

執行

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *