TinyML - Personal Trainer usando inteligência artificial na borda
Participantes:
João Vitor Yukio Bordin YamashitaResumo do projeto:
Foi utilizado dados de um acelerômetro e inteligência artificial (IA) embarcada para classificar se um exercício feito usando um haltere foi feito de forma correta ou incorreta. Obteve-se uma precisão de 92,6% para o movimento "rosca direta".Descrição do projeto:
Coleta de dados
Os dados foram coletados usando o acelerômetro interno do MPU6050 e o acelerômetro LSM9DS1 do Arduino Nano 33 BLE Sense, os sensores foram fixados em halteres de 10 kg, como pode ser visto na Figura 1, e foram realizados os movimentos de uma rosca direta feita de forma correta e movimentos de uma rosca direta feita de forma incorreta, como podemos observar na Figura 2. Esses dados foram enviados para plataforma de aquisição de dados da Edge Impulse para serem processados.
Para coletar os dados usando a Franzininho ou qualquer outra placa de desenvolvimento não suportada diretamente para aquisição de dados podemos utilizar a ferramenta de Data Forwarder disponível nos tutoriais da Edge Impulse (na seção de software foi disponibilizado um código utilizado para aquisição dos dados com a Franzininho).
Processamento dos dados
Podemos ver um exemplo de dado adquirido na etapa anterior nas Figuras 3 e 4.
Esses dados são filtrados por um filtro passa-baixas para eliminar possíveis ruídos de alta frequência presente nos dados, como podemos ver na Figura 5.
Após a filtragem do sinal são feitas duas análises espectrais, a primeira é a Fast Fourier Transform (FFT) que “quebra” o sinal em uma soma de várias frequências e a segunda análise é a Spectral Power Distribution (SPD) que consiste em encontrar a potência distribuída em cada faixa de frequência do sinal. Podemos ver um exemplo de saída do sinal usando a FFT e SPD na Figura 6
Cada coeficiente dessas análises vão ser usados como feature para a rede neural que irá classificar o movimento.
Arquitetura da rede e treinamento
Como foi mencionado anteriormente os coeficientes obtidos das análises são os inputs da nossa rede neural. Foram testados algumas arquiteturas de rede, mas a que obteve um bom equilibro entre tamanho (em termos de parâmetros) e performance foi a rede com três camadas densas com 20 neurônios em cada. A rede pode ser observada na Figura 7.
Com a arquitetura definida foi realizado o treinamento com 30 epochs e os resultados se mostraram muito satisfatórios, com precisão de 92,6%. Além disso conseguimos observar na Figura 8, que não tivemos overfitting já que a curva da precisão e loss dos dados de validação seguiram as curvas dos dados de treinamento.
Além da rede densa, foi utilizado um detector de anomalias, utilizando o método k-means. Esse método agrupa, no nosso caso, os valores RMS dos eixos em clusters, como podemos ver na Figura 9, caso algum valor esteja fora desses clusters ele é classificado como uma anomalia.
A matriz de confusão dos dados de validação pode ser observada abaixo.
Sistema embarcado
O conjunto do sensor com a Franzinho pode ser observado na Figura 10.
Foi feito o download do modelo na plataforma Edge Impulse para a plataforma Arduino, esse arquivo pode ser usado como uma biblioteca genérica, apenas é necessário garantir que os dados que vão ser utilizados no modelo estão “formatados” da mesma forma que no treinamento. Após o programa ser gravado na placa é necessário fixar a placa com o sensor no haltere, como pode ser visto na Figura 11, e realizar os testes. Para visualização do usuário foi utilizada a luz verde para um movimento correto, vermelha para o incorreto e azul caso seja detectado que o haltere está parado.
Os resultados podem ser vistos no vídeo abaixo, nesse vídeo realizei o teste das três classes, idle, correto e incorreto.
Foi possível implementar um sistema para classificar se o movimento feito em um exercício usando haltere foi feito de forma correta ou não, se for desenvolvido de forma mais aprofundada pode ser feito algum tipo de adaptador que pode ser impresso usando uma impressora 3d para ser utilizado em uma escala maior, o procedimento para outros tipos de exercício é o mesmo, um GitHub foi feito com todos os códigos utilizados e também do trabalho desenvolvido usando o Arduino Nano 33 BLE Sense, onde é discutido um pouco mais a fundo o tradeoff de se utilizar vários exercícios em um mesmo modelo, mas é possível ter dois ou mais exercícios em um mesmo modelo se a redução da precisão for aceitável.
Histórico do desenvolvimento:
03/jul/2021 – Início do planejamento do projeto final do curso da Unifei.
12/jul/2021 – Ideia do projeto finalizada e conversa com academia local para uso dos halteres e opinião dos professores.
21/jul/2021 – Aquisição de dados na academia
26/jul/2021 – Processamento dos dados
08/ago/2021 – Treinamento do modelo
09/ago/2021 – Embarcar o modelo na placa Arduino Nano 33 BLE Sense
11/ago/2021 – Avaliação da performance embarcada
18/ago/2021 – Elaboração do artigo
09/nov/2021 – Inicio planejamento adaptações para Franzininho WiFi
18/nov/2021 – Finalização do planejamento e compra dos componentes necessários
25/nov/2021 – Finalização das adaptações na Frazininho WiFi, tanto em relação à software quanto ao hardware
17/dez/2021 – Inicio escrita do conteúdo para o projeto
27/dez/2021 – Teste preliminar do código e funcionamento do sistema
27/dez/2021 – Finalização do protótipo com halter
Hardware:
Conexão MPU6050 – Franzininho WiFi
MPU6050 SCL -> Pino 9 Franzininho WiFi
MPU6050 SDA -> Pino 8 Franzininho WiFi
MPU6050 GND -> GND
MPU6050 VCC -> 3V3
Conexão Power Bank – Franzininho WiFi
5v Power Bank -> 5v Franzininho WiFi
GND Power Bank -> GND Franzininho WiFi
Software/Firmware:
Código referente ao uso da ferramenta Data Forwarder da Edge Impulse
#include <Adafruit_MPU6050.h>
#include <Adafruit_Sensor.h>
#include <Wire.h>
#define FREQUENCY_HZ 60
#define INTERVAL_MS (1000 / (FREQUENCY_HZ + 1))
// objeto da classe Adafruit_MPU6050
Adafruit_MPU6050 mpu;
static unsigned long last_interval_ms = 0;
void setup() {
Serial.begin(115200);
Serial.println(“Classificador de gestos com TinyML”);
// Try to initialize!
if (!mpu.begin()) {
Serial.println(“Failed to find MPU6050 chip”);
while (1) {
delay(10);
}
}
Serial.println(“MPU6050 Found!”);
mpu.setAccelerometerRange(MPU6050_RANGE_8_G);
Serial.print(“Accelerometer range set to: “);
switch (mpu.getAccelerometerRange()) {
case MPU6050_RANGE_2_G:
Serial.println(“+-2G”);
break;
case MPU6050_RANGE_4_G:
Serial.println(“+-4G”);
break;
case MPU6050_RANGE_8_G:
Serial.println(“+-8G”);
break;
case MPU6050_RANGE_16_G:
Serial.println(“+-16G”);
break;
}
mpu.setGyroRange(MPU6050_RANGE_500_DEG);
Serial.print(“Gyro range set to: “);
switch (mpu.getGyroRange()) {
case MPU6050_RANGE_250_DEG:
Serial.println(“+- 250 deg/s”);
break;
case MPU6050_RANGE_500_DEG:
Serial.println(“+- 500 deg/s”);
break;
case MPU6050_RANGE_1000_DEG:
Serial.println(“+- 1000 deg/s”);
break;
case MPU6050_RANGE_2000_DEG:
Serial.println(“+- 2000 deg/s”);
break;
}
mpu.setFilterBandwidth(MPU6050_BAND_21_HZ);
Serial.print(“Filter bandwidth set to: “);
switch (mpu.getFilterBandwidth()) {
case MPU6050_BAND_260_HZ:
Serial.println(“260 Hz”);
break;
case MPU6050_BAND_184_HZ:
Serial.println(“184 Hz”);
break;
case MPU6050_BAND_94_HZ:
Serial.println(“94 Hz”);
break;
case MPU6050_BAND_44_HZ:
Serial.println(“44 Hz”);
break;
case MPU6050_BAND_21_HZ:
Serial.println(“21 Hz”);
break;
case MPU6050_BAND_10_HZ:
Serial.println(“10 Hz”);
break;
case MPU6050_BAND_5_HZ:
Serial.println(“5 Hz”);
break;
}
Serial.println(“”);
delay(100);
}
void loop() {
sensors_event_t a, g, temp;
if (millis() > last_interval_ms + INTERVAL_MS) {
last_interval_ms = millis();
mpu.getEvent(&a, &g, &temp);
Serial.print(a.acceleration.x);
Serial.print(“,”);
Serial.print(a.acceleration.y);
Serial.print(“,”);
Serial.println(a.acceleration.z);
}
}
Código referente a inferência do modelo na Franzininho WiFi
#include <Adafruit_MPU6050.h>
#include <Adafruit_Sensor.h>
#include <Wire.h>
#include <dumbell_class_inferencing.h>
#include <Adafruit_NeoPixel.h>
#define FREQUENCY_HZ 100
#define INTERVAL_MS (1000 / (FREQUENCY_HZ + 1))
// objeto da classe Adafruit_MPU6050
Adafruit_MPU6050 mpu;
float features[EI_CLASSIFIER_DSP_INPUT_FRAME_SIZE];
size_t feature_ix = 0;
static unsigned long last_interval_ms = 0;
#define PIN 18
#define NUMPIXELS 1
#define RED pixels.Color(255, 0, 0) // vermelho
#define GREEN pixels.Color(0, 255, 0) // verde
#define BLUE pixels.Color(0, 0, 255) // azul
#define WHITE pixels.Color(255, 255, 255) // branco
Adafruit_NeoPixel pixels(NUMPIXELS, PIN, NEO_GRB + NEO_KHZ800);
void setup() {
Serial.begin(115200);
pixels.begin();
if (!mpu.begin()) {
Serial.println(“Failed to find MPU6050 chip”);
while (1) {
delay(10);
}
}
Serial.println(“MPU6050 Found!”);
mpu.setAccelerometerRange(MPU6050_RANGE_8_G);
Serial.print(“Accelerometer range set to: “);
switch (mpu.getAccelerometerRange()) {
case MPU6050_RANGE_2_G:
Serial.println(“+-2G”);
break;
case MPU6050_RANGE_4_G:
Serial.println(“+-4G”);
break;
case MPU6050_RANGE_8_G:
Serial.println(“+-8G”);
break;
case MPU6050_RANGE_16_G:
Serial.println(“+-16G”);
break;
}
mpu.setGyroRange(MPU6050_RANGE_500_DEG);
Serial.print(“Gyro range set to: “);
switch (mpu.getGyroRange()) {
case MPU6050_RANGE_250_DEG:
Serial.println(“+- 250 deg/s”);
break;
case MPU6050_RANGE_500_DEG:
Serial.println(“+- 500 deg/s”);
break;
case MPU6050_RANGE_1000_DEG:
Serial.println(“+- 1000 deg/s”);
break;
case MPU6050_RANGE_2000_DEG:
Serial.println(“+- 2000 deg/s”);
break;
}
mpu.setFilterBandwidth(MPU6050_BAND_21_HZ);
Serial.print(“Filter bandwidth set to: “);
switch (mpu.getFilterBandwidth()) {
case MPU6050_BAND_260_HZ:
Serial.println(“260 Hz”);
break;
case MPU6050_BAND_184_HZ:
Serial.println(“184 Hz”);
break;
case MPU6050_BAND_94_HZ:
Serial.println(“94 Hz”);
break;
case MPU6050_BAND_44_HZ:
Serial.println(“44 Hz”);
break;
case MPU6050_BAND_21_HZ:
Serial.println(“21 Hz”);
break;
case MPU6050_BAND_10_HZ:
Serial.println(“10 Hz”);
break;
case MPU6050_BAND_5_HZ:
Serial.println(“5 Hz”);
break;
}
Serial.println(“”);
delay(100);
Serial.print(“Features: “);
Serial.println(EI_CLASSIFIER_DSP_INPUT_FRAME_SIZE);
Serial.print(“Label count: “);
Serial.println(EI_CLASSIFIER_LABEL_COUNT);
}
void loop() {
sensors_event_t a, g, temp;
if (millis() > last_interval_ms + INTERVAL_MS) {
last_interval_ms = millis();
mpu.getEvent(&a, &g, &temp);
features[feature_ix++] = a.acceleration.x;
features[feature_ix++] = a.acceleration.y;
features[feature_ix++] = a.acceleration.z;
if (feature_ix == EI_CLASSIFIER_DSP_INPUT_FRAME_SIZE) {
Serial.println(“Running the inference…”);
signal_t signal;
ei_impulse_result_t result;
int err = numpy::signal_from_buffer(features, EI_CLASSIFIER_DSP_INPUT_FRAME_SIZE, &signal);
if (err != 0) {
ei_printf(“Failed to create signal from buffer (%d)\n”, err);
return;
}
EI_IMPULSE_ERROR res = run_classifier(&signal, &result, true);
if (res != 0) return;
ei_printf(“Predictions “);
ei_printf(“(DSP: %d ms., Classification: %d ms.)”,
result.timing.dsp, result.timing.classification);
ei_printf(“: \n”);
for (size_t ix = 0; ix < EI_CLASSIFIER_LABEL_COUNT; ix++) {
ei_printf(” %s: %.5f\n”, result.classification[ix].label, result.classification[ix].value);
if (result.classification[ix].value > 0.7) { //caso o modelo tenha mais de 70% do movimento
if (result.classification[ix].label == “correto”) { // caso seja um movimento correto
pixels.setPixelColor(0, GREEN);
pixels.show(); // envia o pixel atualizado para o hardware
} else if (result.classification[ix].label == “idle”) { //caso classifique como idle
pixels.setPixelColor(0, BLUE);
pixels.show(); // envia o pixel atualizado para o hardware
} else { // caso seja incorreto
pixels.setPixelColor(0, RED);
pixels.show(); // envia o pixel atualizado para o hardware
}
}
}
feature_ix = 0;
}
}
}
void ei_printf(const char *format, …) {
static char print_buf[1024] = { 0 };
va_list args;
va_start(args, format);
int r = vsnprintf(print_buf, sizeof(print_buf), format, args);
va_end(args);
if (r > 0) {
Serial.write(print_buf);
}
}
Referências:
Guias Franzininho WiFi:
https://docs.franzininho.com.br/docs/franzininho-wifi/exemplos-arduino/primeiros-passos
https://docs.franzininho.com.br/docs/franzininho-wifi/exemplos-arduino/neopixel-onboard
Guias Edge Impulse:
https://docs.edgeimpulse.com/docs/cli-data-forwarder
Uso Esp32 com Edge Impulse:
https://www.hackster.io/Yukio/gesture-classification-with-esp32-and-tinyml-dab252
GitHub do projeto:
https://github.com/JoaoYukio/PersonalTrainer-FranzininhoWiFi