(感謝原文作者Alan Wang開放授權,使我們得以摘譯本篇文章,特此致謝!因本文篇幅較長,且內有程式碼,如果讀者覺得程式碼不夠清楚,還請進一步對照原文中的程式碼,請點擊:https://www.hackster.io/alankrantas/eloquenttinyml-easier-voice-classifier-on-nano-33-ble-sense-ebb81e?fbclid=IwAR2kfIfUAy03aEFHL2VjpYdUw9j7kdapi1x24MzTZiDJCe7MJVTxvMnhFK0 謝謝。)
感謝歐萊禮媒體(O’Reilly Media)出版的書籍(編按:歐萊禮媒體於2019年12月出版了《TinyML》一書,作者為Pete Warden與Daniel Situnayake),Tensorflow Lite也被稱作TinyML,並且於書籍出版後受到眾多關注。因為使用者可在一個有限處理能力和記憶體的微控制器上,佈署一個神經網路預測模型。聽起來很棒,對吧?
然而,書籍中的步驟和工具十分複雜,只要從Arduino_TensorflowLite函式庫開啟任何一個範例,您就知道原因了。並且,TF Lite C++ API也沒有很好的記錄。
感恩的是,有一位聰明的大大名叫Simone Salerno(@EloquentArduino),已針對Arduino IDE撰寫了一個函式庫,名叫EloquentTinyML(TF Lite的打包版本),以及一個名為TinyML gen的Python工具包。有了這兩者您便能以簡單許多的方式,建構並上載一個TF Lite模型至您的開發板。
事實上我已發現EloquentTinyML(至少簡單的程式碼)可被上載到我的一些開發板,例如ESP32、ESP8266、Adafruit Metro M4 Express及Seeeduino XIAO。奇怪的是,Seeeduino XIAO是唯一一塊我擁有的SAMD21開發板,可以運作EloquentTinyML且沒有編譯錯誤。
所以,根據Simone撰寫的函式庫EloquentTinyML,以及前面所提到《TinyML》一書,以下是我在這篇文章中,嘗試達成的基本目標:
- 能運用使用者所說出的任何單字訓練模型,而單字也包括非英文的單字。(《TinyML》這本書,使用Google的語音命令資料集作為輸入。)
- 使用愈少的函式庫、檔案及開發工具愈好。
- 為其他同好提供前期工作,以於未來在邊緣裝置上,產生更好、甚至更容易的聲音/語音辨識。
請先閱讀以下的免責聲明:
首先,我(編按:在此指作者Alan Wang)不是神經網路及Tensoeflow框架的專家,所以,歡迎您指出這篇文章內任何關於我的錯誤。在此,我假設您已經有一些關於Arduino、C++、Python及機器學習分類的基本了解。
此外,我並不保證,未來任何關於Tensorflow Lite或EloquentTinyML的修改,仍將相容於以下的程式碼。
第三,使用神經網路不意味一定比較好,因為神經網路基本上是處於黑盒子的狀態,您只能嘗試,並看哪一個設定會產生比較好的結果。並且訓練神經網路模型是一個漫長且困難的過程,很容易充滿挫折,更別提您對著麥克風說話的方式,也對模型如何表現有很大的影響。
在本篇文章中,我將展示一個三單字(Yes、No及OK)的分類器。經過訓練後,模型達成了良好的測試準確度,但真正在裝置上的成功預測率明顯比較低,部分原因可能是我的神經網路模型不夠好,更可能的因素是在整個訓練及預測階段,難以維持同樣的說話方式。既然我自己的聲音比較低沉且有點嘶啞,也可能對模型預測造成額外困難。
設定
Arduino IDE:
- 從您的「開發板管理員」新增「Arduino nRF528x開發板」,也同時會一併安裝PDM函式庫。
- 安裝EloquentTinyML函式庫
Python:
- Tensorflow 2.x (需要64位元的Python。我在Window 10作業系統上,使用一套稱為Thonny的IDE,並且選擇Python 3.8.5作為編譯器。我使用pip3.8,在上述環境內安裝軟體包。)
- Tinymlgen(https://github.com/eloquentarduino/tinymlgen,您也可以在PyPI上找到。)
- NumPy、matplotlib、scikit-learn
設定說明:
針對這篇文章,我使用Arduino IDE 1.8.13、Tensorflow 2.3.1、NumPy 1.18.5 (1.19.x不被Tensorflow支援)、matplotlib 3.3.2及scikit-learn 0.23.2.。我已試過在Tensorflow 2.3.1、2.5.2及2.8.0上訓練腳本都沒有問題。如果您的Tensorflow無辦運作,請進行升級。
硬體
一個Arduino Nano 33 BLE Sense開發板。
第一部份:聲音樣本
首先,我們需要取樣聲音或說出來的單字作為訓練資料。每一項單字樣本或範例,都是一項有32個浮點數的Numpy陣列。
以下是腳本如何從PDM麥克風,「錄下」聲音樣本:
- 上傳腳本。完成後,開啟序列監控視窗,將波特率(Baud rate)設定至115200。
- 在其回呼函式內,麥克風連續錄下256段讀數。之後,這256個值被讀取為128個脈衝密度調變(Pulse Density Modulation,PDM)資料。
- 之後,這些PDM資料將被計算成一個單一的RMS(平方平均數,Root Mean Square,簡稱RMS)值,換言之,即這個採樣的摘要。
- 若目前的RMS值高於閾值,意即使用者說出的內容夠大聲,也會觸發錄音流程。接著,板上的LED燈會亮起來且開始錄音。(所以,最開頭的單字不會被錄下,但我們仍可錄下剩餘的內容。)
- 每20毫秒,Arduino Nano 33 BLE Sense開發板會產生一個RMS值,共32次。(所以,總共的時間將涵蓋640毫秒或0.64秒,足夠您說出一個單字。)
- 以上這個32個值的資料,代表一個說出的單字(一個範例),您將看到它顯示在前面提到的序列監控視窗內。
- 等待下一次板上LED燈閃爍時,再說一次這個單字。
我的確嘗試使用快速傅立葉變換(Fast Fourier Transform,簡稱FFT),但大概因為採樣流程的本質,我嘗試使用的2到3個函式庫,總是「卡住」程式,所以便沒有作用了。
[Nano33ble_voice_sampler.iso]
/*
* Voice sampler for Arduino Nano 33 BLE Sense by Alan Wang
*/
#include <math.h>
#include <PDM.h>
#define SERIAL_PLOT_MODE false // set to true to test sampler in serial plotter
#define PDM_SOUND_GAIN 255 // sound gain of PDM mic
#define PDM_BUFFER_SIZE 256 // buffer size of PDM mic
#define SAMPLE_THRESHOLD 900 // RMS threshold to trigger sampling
#define FEATURE_SIZE 32 // sampling size of one voice instance
#define SAMPLE_DELAY 20 // delay time (ms) between sampling
#define TOTAL_SAMPLE 50 // total number of voice instance
double feature_data[FEATURE_SIZE];
volatile double rms;
unsigned int total_counter = 0;
// callback function for PDM mic
void onPDMdata() {
rms = -1;
short sample_buffer[PDM_BUFFER_SIZE];
int bytes_available = PDM.available();
PDM.read(sample_buffer, bytes_available);
// calculate RMS (root mean square) from sample_buffer
unsigned int sum = 0;
for (unsigned short i = 0; i < (bytes_available / 2); i++) sum += pow(sample_buffer[i], 2);
rms = sqrt(double(sum) / (double(bytes_available) / 2.0));
}
void setup() {
Serial.begin(115200);
while (!Serial);
PDM.onReceive(onPDMdata);
PDM.setBufferSize(PDM_BUFFER_SIZE);
PDM.setGain(PDM_SOUND_GAIN);
if (!PDM.begin(1, 16000)) { // start PDM mic and sampling at 16 KHz
Serial.println("Failed to start PDM!");
while (1);
}
pinMode(LED_BUILTIN, OUTPUT);
// wait 1 second to avoid initial PDM reading
delay(900);
digitalWrite(LED_BUILTIN, HIGH);
delay(100);
digitalWrite(LED_BUILTIN, LOW);
if (!SERIAL_PLOT_MODE) Serial.println("# === Voice data start ===");
}
void loop() {
// waiting until sampling triggered
while (rms < SAMPLE_THRESHOLD);
digitalWrite(LED_BUILTIN, HIGH);
for (unsigned short i = 0; i < FEATURE_SIZE; i++) { // sampling
while (rms < 0);
feature_data[i] = rms;
delay(SAMPLE_DELAY);
}
digitalWrite(LED_BUILTIN, LOW);
// pring out sampling data
if (!SERIAL_PLOT_MODE) Serial.print("[");
for (unsigned short i = 0; i < FEATURE_SIZE; i++) {
if (!SERIAL_PLOT_MODE) {
Serial.print(feature_data[i]);
Serial.print(", ");
} else {
Serial.println(feature_data[i]);
}
}
if (!SERIAL_PLOT_MODE) {
Serial.println("],");
} else {
for (unsigned short i = 0; i < (FEATURE_SIZE / 2); i++) Serial.println(0);
}
// stop sampling when enough samples are collected
if (!SERIAL_PLOT_MODE) {
total_counter++;
if (total_counter >= TOTAL_SAMPLE) {
Serial.println("# === Voice data end ===");
PDM.end();
while (1) {
delay(100);
digitalWrite(LED_BUILTIN, HIGH);
delay(100);
digitalWrite(LED_BUILTIN, LOW);
}
}
}
// wait for 1 second after one sampling
delay(900);
digitalWrite(LED_BUILTIN, HIGH);
delay(100);
digitalWrite(LED_BUILTIN, LOW);
}
請將腳本上傳至您的Nano 33 BLE Sense。您可改變一些參數,我將RMS閾值設定比較高,否則隨機的噪音和您自己的呼吸聲,將時常觸發麥克風。既然PDM麥克風只有在非常近的距離內具備足夠敏銳度,我決定讓我的嘴巴非常靠近麥克風,並且在說出單字後立刻將開發板移開,以免呼吸聲觸動麥克風。
測試/取樣語音資料
您可將「SERIAL_PLOT_MODE」更改為true,以在Arduino IDE serial plotter視窗進行測試(波特率:115200)。Plot模式不計算樣本數,並且在樣本與樣本間,增加一些0以分開他們。我推薦您使用這個模式進行練習,並且找出您將如何及在哪裡錄製可靠的資料。
當將「SERIAL_PLOT_MODE」更改為false,您可以得到所需資料。(編按:請參考下圖)
在總共取樣50個樣本後,開發板會進入無窮迴圈的狀態,並且持續閃爍其LED燈。
現在,請將資料複製、貼上至Python腳本的資料集內。如您所見,資料呈現Python列表的格式。您可依照意願,將# comment刪除。之後重新啟動Nano 33 BLE Sense,並針對下一個單字再採取50個樣本。
語音資料集
現在,讓我們將不同的語音資料集整合起來,一起放入一個Python腳本內。
[voice_dataset.py]
import numpy as np
NUMBER_OF_LABELS = 3
DATA_SIZE_OF_LABEL = 50 # number of instances for each label
data = np.array([
[976.44, 809.81, 852.16, 795.61, 733.75, 743.48, 766.01, 643.91, 815.27, 541.93, 388.19, 466.88, 455.32, 410.88, 1723.84, 651.68, 1066.49, 1552.68, 1886.37, 1434.68, 700.44, 450.38, 136.17, 73.71, 220.99, 276.30, 421.08, 341.11, 306.07, 250.11, 317.13, 319.75, ],
[900.02, 1324.65, 1553.57, 1300.46, 768.41, 1315.89, 1572.04, 1284.38, 898.83, 725.21, 566.74, 449.95, 230.06, 97.65, 64.58, 171.64, 341.67, 407.33, 516.53, 607.64, 717.49, 753.78, 779.85, 760.53, 711.42, 669.78, 6
...(omitted)
[2247.54, 731.01, 225.72, 2644.80, 3746.85, 415.67, 712.12, 765.10, 769.43, 806.61, 683.78, 518.41, 161.55, 130.77, 120.98, 314.71, 476.08, 528.22, 561.84, 522.31, 189.19, 124.10, 88.45, 280.16, 348.51, 452.32, 348.11, 272.22, 153.52, 90.54, 22.94, 37.59, ],
])
target = np.array(
[label for label in range(NUMBER_OF_LABELS) for _ in range(DATA_SIZE_OF_LABEL)]
)
請將data = np.array之間的數值,替換成您的資料,並且設定正確的label數,以及每個label的資料大小。
請記得設定正確label數,我同時也假定,在資料集內,每個label擁有同樣的範例數:目標資料將自動生成。所以:
- label 0 = “Yes”
- label 1 = “No”
- label 2 = “OK”
第二部分:訓練模型
現在,我們進入最困難也是最神秘的部分:嘗試訓練一個夠強大進行預測的神經網路。這個部分將會花費我們許多時間和精力。
在下方程式碼中,我使用像這樣的模型:
model = Sequential()
model.add(layers.Dense(data.shape[1], activation='relu', input_shape=(data.shape[1],)))
model.add(layers.Dropout(0.25))
model.add(layers.Dense(np.unique(target).size * 4, activation='relu'))
model.add(layers.Dropout(0.25))
model.add(layers.Dense(np.unique(target).size, activation='softmax'))
model.compile(optimizer='adam', loss='sparse_categorical_crossentropy', metrics=['accuracy'])
model.summary()
這個模型定義了一個五層的簡易神經網路,其中三層為全連接層,而其它兩層為Dropout層。每層皆具備會將資料傳遞至下一層的神經元,並且在最後一層獲得結果。
第一層如同資料範例長度一樣大(具備32個神經元);第三層label的數量乘以四(具備12個神經元);最後一層具備3個神經元,而也正是在這一層,我們將獲得預測結果。Dropout層被用來避免過凝合(Over-fitting),它們會在四項輸入資料中,隨機屏除一項,以迫使其餘神經元適應。
激活函數(Activation functions)的作用正如過濾器,目的在於控制一個神經元如何傳送資料給下一個神經元。
當訓練模型時,Tensorflow會根據來自先前迭代的預測正確率和損失,嘗試在每個神經元內最佳化最好的權重,這個過程恰似藉由盲目亂走,試圖找到下山路徑。因此在訓練模型的過程中,您可能會卡在同一個地方非常久,而無法進一步改進模型。
邏輯回歸的多類版本「Softmax」及損失函數sparse_categorical_crossentropy的作用是分類。在模型的最後一層,它們將產生三個浮點數作為每個label的機率。有最高機率的label便是最終預測的單字。
若您想得到更好的預測結果,您需要改變一些參數,例如神經元的數目、Dropout的比率、訓練的速度,以及訓練迭代的數目。
Model: "sequential"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
dense (Dense) (None, 32) 1056
_________________________________________________________________
dropout (Dropout) (None, 32) 0
_________________________________________________________________
dense_1 (Dense) (None, 12) 396
_________________________________________________________________
dropout_1 (Dropout) (None, 12) 0
_________________________________________________________________
dense_2 (Dense) (None, 3) 39
=================================================================
Total params: 1,491
Trainable params: 1,491
Non-trainable params: 0
[Nano33ble_voice_trainer.py]
'''
Voice trainer for Arduino Nano 33 BLE Sense and Tensorflow Lite by Alan Wang
Required packages:
Tensorflow 2.x
tinymlgen (https://github.com/eloquentarduino/tinymlgen)
NumPy
matplotlib
scikit-learn
'''
import os
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3' # only print out fatal log
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score, classification_report
import tensorflow as tf
from tensorflow.keras import layers, Sequential
# force computer to use CPU if there are no GPUs present
tf.config.list_physical_devices('GPU')
tf.test.is_gpu_available()
# set random seed
RANDOM_SEED = 42
np.random.seed(RANDOM_SEED)
tf.random.set_seed(RANDOM_SEED)
# import dataset and transform target (label data) to categorical arrays
from voice_dataset import data, target
# create training data (60%), validation data (20%) and testing data (20%)
data_train, data_test, target_train, target_test = train_test_split(
data, target, test_size=0.2, random_state=RANDOM_SEED)
data_train, data_validate, target_train, target_validate = train_test_split(
data_train, target_train, test_size=0.25, random_state=RANDOM_SEED)
# create a TF model
model = Sequential()
model.add(layers.Dense(data.shape[1], activation='relu', input_shape=(data.shape[1],)))
model.add(layers.Dropout(0.25))
model.add(layers.Dense(np.unique(target).size * 4, activation='relu'))
model.add(layers.Dropout(0.25))
model.add(layers.Dense(np.unique(target).size, activation='softmax'))
model.compile(optimizer='adam', loss='sparse_categorical_crossentropy',
metrics=['accuracy'])
model.summary()
# training TF model
ITERATION = 2000
BATCH_SIZE = 16
history = model.fit(data_train, target_train, epochs=ITERATION, batch_size=BATCH_SIZE,
validation_data=(data_validate, target_validate))
predictions = model.predict(data_test)
test_score = model.evaluate(data_test, target_test)
# get the predicted label based on probability
predictions_categorical = np.argmax(predictions, axis=1)
# display prediction performance on validation data and test data
print('Prediction Accuracy:', accuracy_score(target_test, predictions_categorical).round(3))
print('Test accuracy:', round(test_score[1], 3))
print('Test loss:', round(test_score[0], 3))
print('')
print(classification_report(target_test, predictions_categorical))
# convert TF model to TF Lite model as a C header file (for the classifier)
from tinymlgen import port
with open('tf_lite_model.h', 'w') as f: # change path if needed
f.write(port(model, optimize=False))
# visualize prediction performance
DISPLAY_SKIP = 100
import matplotlib.pyplot as plt
accuracy = history.history['accuracy']
loss = history.history['loss']
val_loss = history.history['val_loss']
val_accuracy = history.history['val_accuracy']
epochs = np.arange(len(accuracy)) + 1
plt.rcParams['font.size'] = 12
plt.figure(figsize=(14, 8))
plt.subplot(211)
plt.title(f'Test accuracy: {round(test_score[1], 3)}')
plt.plot(epochs[DISPLAY_SKIP:], accuracy[DISPLAY_SKIP:], label='Accuracy')
plt.plot(epochs[DISPLAY_SKIP:], val_accuracy[DISPLAY_SKIP:], label='Validate accuracy')
plt.grid(True)
plt.legend()
plt.subplot(212)
plt.title(f'Test loss: {round(test_score[0], 3)}')
plt.plot(epochs[DISPLAY_SKIP:], loss[DISPLAY_SKIP:], label='Loss', color='green')
plt.plot(epochs[DISPLAY_SKIP:], val_loss[DISPLAY_SKIP:], label='Validate loss', color='red')
plt.grid(True)
plt.legend()
plt.tight_layout()
plt.show()
接下來,就是運作腳本且耐心等候結果。照預設,會在Python腳本的同一個資料夾內,產生tf_lite_model.h檔案。請注意,當開始運作腳本時,您可能會看見一些警告訊息:
WARNING:tensorflow:From C:\xxx\Nano33ble_voice_trainer.py:28: is_gpu_available (from tensorflow.python.framework.test_util) is deprecated and will be removed in a future version.
Instructions for updating:
Use `tf.config.list_physical_devices('GPU')` instead.
此外,當使用tinymlgen,您可能會看到以下訊息:
WARNING:tensorflow:From C:\Users\xxx\AppData\Roaming\Python\Python38\site-packages\tensorflow\python\training\tracking\tracking.py:111: Model.state_updates (from tensorflow.python.keras.engine.training) is deprecated and will be removed in a future version.
Instructions for updating:
This property should not be used in TensorFlow 2.0, as updates are applied automatically.
WARNING:tensorflow:From C:\Users\xxx\AppData\Roaming\Python\Python38\site-packages\tensorflow\python\training\tracking\tracking.py:111: Layer.updates (from tensorflow.python.keras.engine.base_layer) is deprecated and will be removed in a future version.
Instructions for updating:
This property should not be used in TensorFlow 2.0, as updates are applied automatically.
以上兩則警告訊息皆屬正常,且不會影響輸出的模型。
訓練結果
以下大概是我所能獲得最好的結果:
Prediction Accuracy: 0.9
Test accuracy: 0.9
Test loss: 0.95
precision recall f1-score support
0 0.89 0.80 0.84 10
1 0.80 0.89 0.84 9
2 1.00 1.00 1.00 11
accuracy 0.90 30
macro avg 0.90 0.90 0.89 30
weighted avg 0.90 0.90 0.90 30
請相信我,總體準確率90%看起來很好,但並未計算以錯誤的方式向麥克風說話。以下是視覺化後的訓練過程:
理想上,我們需要獲得大於或等於0.8至0.9的準確率,損失愈低愈好;而驗證準確率應盡量接近準確率,驗證損失應盡量接近損失,以確保模型沒有過凝合。
產生Tensorflow Lite模型
基本上,tinymlgen工具包會自動將Tensorflow模型轉換成Tensorflow Lite版本,然後轉換成C++版本。而我僅將C++字符串格式的結果,寫入一個.h的檔案,除非您改變了腳本內的輸出路徑,否則您可以在Nano33ble_voice_trainer.py同樣的目錄內,找到這個檔案。
[tf_lite_model.h]
#ifdef __has_attribute
#define HAVE_ATTRIBUTE(x) __has_attribute(x)
#else
#define HAVE_ATTRIBUTE(x) 0
#endif
#if HAVE_ATTRIBUTE(aligned) || (defined(__GNUC__) && !defined(__clang__))
#define DATA_ALIGN_ATTRIBUTE __attribute__((aligned(4)))
#else
#define DATA_ALIGN_ATTRIBUTE
#endif
const unsigned char model_data[] DATA_ALIGN_ATTRIBUTE = {0x1c, 0x00, 0x00, 0x00, 0x54, 0x46, 0x4c, 0x33, 0x00, 0x00,
...(omitted) 0x00, 0x00, 0x00};
const int model_data_len = 7644;
第三部分:聲音分類器
將以下的腳本存入Arduino IDE,關閉腳本,並且將tf_lite_model.h檔案,複製到 Nano33ble_voice_classifier.iso同樣的目錄內,關閉Arduino IDE,再次開啟Arduino腳本。接下來,您應可看到.h的檔案已經匯入了。若您需要,這也是您更新模型的方式。
[Nano33ble_voice_classifier.ino]
/*
* Voice classifier for Arduino Nano 33 BLE Sense by Alan Wang
*/
#include <math.h>
#include <PDM.h>
#include <EloquentTinyML.h> // https://github.com/eloquentarduino/EloquentTinyML
#include "tf_lite_model.h" // TF Lite model file
#define PDM_SOUND_GAIN 255 // sound gain of PDM mic
#define PDM_BUFFER_SIZE 256 // buffer size of PDM mic
#define SAMPLE_THRESHOLD 900 // RMS threshold to trigger sampling
#define FEATURE_SIZE 32 // sampling size of one voice instance
#define SAMPLE_DELAY 20 // delay time (ms) between sampling
#define NUMBER_OF_LABELS 3 // number of voice labels
const String words[NUMBER_OF_LABELS] = {"Yes", "No", "OK"}; // words for each label
#define PREDIC_THRESHOLD 0.6 // prediction probability threshold for labels
#define RAW_OUTPUT true // output prediction probability of each label
#define NUMBER_OF_INPUTS FEATURE_SIZE
#define NUMBER_OF_OUTPUTS NUMBER_OF_LABELS
#define TENSOR_ARENA_SIZE 4 * 1024
Eloquent::TinyML::TfLite<NUMBER_OF_INPUTS, NUMBER_OF_OUTPUTS, TENSOR_ARENA_SIZE> tf_model;
float feature_data[FEATURE_SIZE];
volatile float rms;
bool voice_detected;
// callback function for PDM mic
void onPDMdata() {
rms = -1;
short sample_buffer[PDM_BUFFER_SIZE];
int bytes_available = PDM.available();
PDM.read(sample_buffer, bytes_available);
// calculate RMS (root mean square) from sample_buffer
unsigned int sum = 0;
for (unsigned short i = 0; i < (bytes_available / 2); i++) sum += pow(sample_buffer[i], 2);
rms = sqrt(float(sum) / (float(bytes_available) / 2.0));
}
void setup() {
Serial.begin(115200);
while (!Serial);
PDM.onReceive(onPDMdata);
PDM.setBufferSize(PDM_BUFFER_SIZE);
PDM.setGain(PDM_SOUND_GAIN);
if (!PDM.begin(1, 16000)) { // start PDM mic and sampling at 16 KHz
Serial.println("Failed to start PDM!");
while (1);
}
pinMode(LED_BUILTIN, OUTPUT);
// wait 1 second to avoid initial PDM reading
delay(900);
digitalWrite(LED_BUILTIN, HIGH);
delay(100);
digitalWrite(LED_BUILTIN, LOW);
// start TF Lite model
tf_model.begin((unsigned char*) model_data);
Serial.println("=== Classifier start ===\n");
}
void loop() {
// waiting until sampling triggered
while (rms < SAMPLE_THRESHOLD);
digitalWrite(LED_BUILTIN, HIGH);
for (int i = 0; i < FEATURE_SIZE; i++) { // sampling
while (rms < 0);
feature_data[i] = rms;
delay(SAMPLE_DELAY);
}
digitalWrite(LED_BUILTIN, LOW);
// predict voice and put results (probability) for each label in the array
float prediction[NUMBER_OF_LABELS];
tf_model.predict(feature_data, prediction);
// print out prediction results;
// in theory, you need to find the highest probability in the array,
// but only one of them would be high enough over 0.5~0.6
Serial.println("Predicting the word:");
if (RAW_OUTPUT) {
for (int i = 0; i < NUMBER_OF_LABELS; i++) {
Serial.print("Label ");
Serial.print(i);
Serial.print(" = ");
Serial.println(prediction[i]);
}
}
voice_detected = false;
for (int i = 0; i < NUMBER_OF_LABELS; i++) {
if (prediction[i] >= PREDIC_THRESHOLD) {
Serial.print("Word detected: ");
Serial.println(words[i]);
Serial.println("");
voice_detected = true;
}
}
if (!voice_detected && !RAW_OUTPUT) Serial.println("Word not recognized\n");
// wait for 1 second after one sampling/prediction
delay(900);
digitalWrite(LED_BUILTIN, HIGH);
delay(100);
digitalWrite(LED_BUILTIN, LOW);
}
現在,請上傳腳本至您的開發板。這個過程會花費一段時間,以編譯新的Tensorflow模型。
模型預測進行中
您可看見分類器腳本,以取樣腳本一模一樣的方式收集聲音資料。兩者的不同點是,分類器會將資料提供給模型,並獲得預測結果。
以下是一些預測結果的範例:
Start
GetModel done
Version check done
AllocateTensors done
Begin done
=== Classifier start ===
Predicting the word:
Label 0 = 0.99
Label 1 = 0.00
Label 2 = 0.01
Word detected: Yes
Predicting the word:
Label 0 = 0.20
Label 1 = 0.80
Label 2 = 0.00
Word detected: No
Predicting the word:
Label 0 = 0.12
Label 1 = 0.00
Label 2 = 0.88
Word detected: OK
Predicting the word:
Label 0 = 1.00
Label 1 = 0.00
Label 2 = 0.00
Word detected: Yes
Predicting the word:
Label 0 = 0.20
Label 1 = 0.80
Label 2 = 0.00
Word detected: No
Predicting the word:
Label 0 = 0.00
Label 1 = 0.00
Label 2 = 1.00
Word detected: OK
最後的想法
如同我先前提到的,在冗長的取樣過程中,我很難保持對麥克風說話的方式;此外,由於裝置的記憶體不足,我所使用的模型十分有限。以上兩點,大概是我沒有辦法獲得高度可靠結果的原因,未來無疑有許多改進空間。