中文 | English

佈署神經網路模型更簡易——EloquentTinyML

 

翻攝自EloquentTinyML作者Simone Salerno的個人部落格,特此致謝!

(感謝原文作者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,您可以得到所需資料。(編按:請參考下圖)

感謝作者Alan Wang開放授權,特此致謝!

在總共取樣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%看起來很好,但並未計算以錯誤的方式向麥克風說話。以下是視覺化後的訓練過程:

 

感謝作者Alan Wang開放授權,特此致謝!

 

理想上,我們需要獲得大於或等於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

最後的想法

如同我先前提到的,在冗長的取樣過程中,我很難保持對麥克風說話的方式;此外,由於裝置的記憶體不足,我所使用的模型十分有限。以上兩點,大概是我沒有辦法獲得高度可靠結果的原因,未來無疑有許多改進空間。

分享到社群

This site or product includes IP2Location LITE data available from https://lite.ip2location.com.