Implementação de Reconhecimento de Comando de Voz com ESP-EYE e Machine Learning – Parte 1

Este post faz parte da série Reconhecimento de Comando de Voz com ESP-EYE

Introdução

Nos últimos anos, o uso de dispositivos com comandos de voz, como Alexa e Google Assistant, tem aumentado. Neste projeto, iremos criar uma aplicação semelhante utilizando o ESP-EYE e o componente TensorFlow Lite Micro no ESP IDF. Inicialmente, reproduziremos o  exemplo base que identifica os comandos “yes” e “no”. Em seguida, analisaremos os arquivos fonte do projeto e em um segundo artigo, modificaremos o modelo para que a aplicação reconheça os comandos “on” e “off”.

Requisitos

  1. Placa ESP-EYE
  2. ESP-IDF instalado e configurado. É necessário ter familiaridade com a utilização da plataforma. Caso não tenha antes de começar esse projeto sugiro a leitura de https://docs.franzininho.com.br/docs/franzininho-wifi/exemplos-espidf/primeiros-passos/ 

ESP-EYE

O ESP32-S3-EYE é uma pequena placa de desenvolvimento da Espressif, baseada no SoC ESP32-S3. Possui uma câmera de 2 megapixels, um display LCD e um microfone, utilizados para reconhecimento de imagem e processamento de áudio. Com 8 MB de PSRAM octal e 8 MB de flash, oferece bastante armazenamento e suporta transmissão de imagens via Wi-Fi e depuração por Micro-USB. Ela é uma placa muito utilizada para desenvolvimento de projetos AIOT – inteligência artificial e internet das coisas.

Para mais detalhes acesse: https://www.espressif.com/en/products/devkits/esp-eye/overview 

TensorFlow Lite Micro (LiteRT)

O TensorFlow Lite (ou LiteRT) para microcontroladores foi desenvolvido para executar modelos de machine learning em microcontroladores usando apenas alguns kilobytes de memória. Para mais informações acesse: https://ai.google.dev/edge/litert/microcontrollers/overview?hl=pt-br 

Projeto: Reconhecimento de Comando de Voz 

No fluxograma abaixo está representada a estrutura do projeto.

Para iniciar crie um diretório onde deseja salvar o projeto e digite o comando: 

idf.py create-project-from-example “esp-tflite-micro:micro_speech”

Ao abrir o projeto no vscode e você terá a seguinte estrutura

Análise do código fonte

Para que possamos criar e implementar nosso próprio modelo, é essencial compreender o papel de cada componente do código-fonte. Abaixo, vamos detalhar a estrutura do projeto exemplo e examinar alguns dos arquivos principais, explicando suas funcionalidades e como contribuem para o reconhecimento de comandos de voz.

Pasta managed_componentes:

Localizado os arquivos necessários para executar o TensorFlow. 

main_functions:

Esse arquivo contém as principais funções que inicializam e executam o modelo de Machine Learning. Ele inclui variáveis globais e funções centrais, como setup() e loop(), que são responsáveis pela preparação do modelo e pela execução contínua de inferências.

  • Globals e Arena:
  • O código cria variáveis globais, intérpretes e tensores para armazenar o modelo, o intérprete e os tensores de entrada/saída, respectivamente.
const tflite::Model* model = nullptr;
tflite::MicroInterpreter* interpreter = nullptr;
TfLiteTensor* model_input = nullptr;
FeatureProvider* feature_provider = nullptr;
RecognizeCommands* recognizer = nullptr;
int32_t previous_time = 0;
  • É criado um buffer de memória para armazenar os dados dos tensores. No caso, o código define o tamanho da arena em 30 * 1024 bytes (kTensorArenaSize). Esse valor normalmente é encontrado pela experimentação de “tentativa e erro”.
constexpr int kTensorArenaSize = 30 * 1024;
uint8_t tensor_arena[kTensorArenaSize];
int8_t feature_buffer[kFeatureElementCount];
int8_t* model_input_buffer = nullptr;
  • Função setup():
  • Carregamento do modelo e verificação de compatibilidade.
model = tflite::GetModel(g_model);
  if (model->version() != TFLITE_SCHEMA_VERSION) {
    MicroPrintf("Model provided is schema version %d not equal to supported "
                "version %d.", model->version(), TFLITE_SCHEMA_VERSION);
    return;
  }
  • Adiciona as operações que o modelo necessita para ser executada, esta etapa muda de acordo com o que foi feito na etapa de treinamento.
static tflite::MicroMutableOpResolver<4> micro_op_resolver;
  if (micro_op_resolver.AddDepthwiseConv2D() != kTfLiteOk) {
    return;
  }
  if (micro_op_resolver.AddFullyConnected() != kTfLiteOk) {
    return;
  }
  if (micro_op_resolver.AddSoftmax() != kTfLiteOk) {
    return;
  }
  if (micro_op_resolver.AddReshape() != kTfLiteOk) {
    return;
  }
  • Cria o intérprete para executar o modelo.
static tflite::MicroInterpreter static_interpreter(
      model, micro_op_resolver, tensor_arena, kTensorArenaSize);
  interpreter = &static_interpreter;
  • Aloca a memória para os tensores do modelo.
 TfLiteStatus allocate_status = interpreter->AllocateTensors();
  if (allocate_status != kTfLiteOk) {
    MicroPrintf("AllocateTensors() failed");
    return;
  }
  • Armazena os tensores de entrada e saída nas variáveis input e output.
model_input = interpreter->input(0);
  if ((model_input->dims->size != 2) || (model_input->dims->data[0] != 1) ||
      (model_input->dims->data[1] !=
       (kFeatureCount * kFeatureSize)) ||
      (model_input->type != kTfLiteInt8)) {
    MicroPrintf("Bad input tensor parameters in model");
    return;
  }
  model_input_buffer = tflite::GetTensorData<int8_t>(model_input);
  • Função loop():
  • Obtém o timestamp do áudio mais recente, preenche o buffer de características com novos dados de áudio usando a  função PopulateFeatureData, verifica se há erros e novos dados.
const int32_t current_time = LatestAudioTimestamp();
  int how_many_new_slices = 0;
  TfLiteStatus feature_status = feature_provider->PopulateFeatureData(
      previous_time, current_time, &how_many_new_slices);
  if (feature_status != kTfLiteOk) {
    MicroPrintf( "Feature generation failed");
    return;
  }
  previous_time = current_time;
  // If no new audio samples have been received since last time, don't bother
  // running the network model.
  if (how_many_new_slices == 0) {
    return;
  }
  • Realiza a cópia dos dados do buffer de características para o tensor de entrada do modelo.
 for (int i = 0; i < kFeatureElementCount; i++) {
    model_input_buffer[i] = feature_buffer[i];
  }
  • É realizada a inferência, executando o modelo Invoke() com o valor de entrada.
TfLiteStatus invoke_status = interpreter->Invoke();
  if (invoke_status != kTfLiteOk) {
    MicroPrintf( "Invoke failed");
    retu
  • O tensor de saída do modelo é obtido.
TfLiteTensor* output = interpreter->output(0);
  • O código tem duas abordagens para processar os resultados:
  • Usando Argmax Simples: Se você está usando o bloco #if 1, o código faz uma desquantização dos valores de saída e encontra o índice do maior valor, que representa a categoria mais provável. Se a confiança (score) for maior que 0.8, a categoria é impressa.
  • Reconhecimento de Comandos: Se você estiver no bloco #else, o código utiliza um reconhecedor para processar os resultados e verificar se um comando foi reconhecido, respondendo de acordo.

audio_provider:

O audio_provider é o que conecta o hardware do microfone de um dispositivo ao nosso código. É utilizado o  I2S (Inter-IC Sound) que  é um padrão de interface de barramento serial eletrônico usado para conectar dispositivos de áudio digital. No ESP-EYE é utilizado a seguinte porta

i2s_port_t i2s_port = I2S_NUM_0; 

e a inicialização do i2s com os pinos fica o seguinte:

static void i2s_init(void) {
  // Start listening for audio: MONO @ 16KHz
  i2s_config_t i2s_config = {
      .mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX),
      .sample_rate = 16000,
      .bits_per_sample = (i2s_bits_per_sample_t) 16,
      .channel_format = I2S_CHANNEL_FMT_ONLY_LEFT,
      .communication_format = I2S_COMM_FORMAT_I2S,
      .intr_alloc_flags = 0,
      .dma_buf_count = 3,
      .dma_buf_len = 300,
      .use_apll = false,
      .tx_desc_auto_clear = false,
      .fixed_mclk = -1,
  };
  i2s_pin_config_t pin_config = {
      .bck_io_num = 41,    // IIS_SCLK
      .ws_io_num = 42,     // IIS_LCLK
      .data_out_num = -1,  // IIS_DSIN
      .data_in_num = 2,   // IIS_DOUT
  };

feature_provider e micro_features_generator:

Nesta etapa, não entrarei em muitos detalhes, mas o feature_provider define como obter características de áudio e juntamente com micro_features_generator converter o áudio bruto em um espectrograma, conforme ilustrado no diagrama abaixo. 

micro_model_settings:

É esse arquivo que contém as configurações e parâmetros relacionados ao modelo de aprendizado de máquina que está sendo usado para o reconhecimento de comandos de voz. Essa estrutura inclui o número de classes ou comandos que o modelo pode reconhecer, especificações sobre como os dados de áudio devem ser formatados antes de serem passados para o modelo, tamanho do modelo e parâmetros de quantização. 

constexpr int kMaxAudioSampleSize = 512;
constexpr int kAudioSampleFrequency = 16000;
constexpr int kFeatureSize = 40;
constexpr int kFeatureCount = 49;
constexpr int kFeatureElementCount = (kFeatureSize * kFeatureCount);
constexpr int kFeatureStrideMs = 20;
constexpr int kFeatureDurationMs = 30;

// Variables for the model's output categories.
constexpr int kCategoryCount = 4;
constexpr const char* kCategoryLabels[kCategoryCount] = {
    "silence",
    "unknown",
    "yes",
    "no",
};

model

O arquivo model.cc é o local onde armazenamos nosso modelo no formato TensorFlow Lite (TFLite).

const unsigned char g_model[] DATA_ALIGN_ATTRIBUTE = {
  0x20, 0x00, 0x00, 0x00, 0x54, 0x46, 0x4c, 0x33, 0x00, 0x00, 0x00, 0x00,
  .
  .
  .
  .
  0xe8, 0x12, 0xf3, 0x1a, 0x05, 0xfd, 0x18, 0xe2, 0x12, 0x1f, 0xd2, 0x01,
  0x16, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x16};

const int g_model_len = 19160;

recognize_commands:

Depois do modelo gerar um conjunto de probabilidades de que uma palavra conhecida foi falada no último segundo de áudio, cabe ao código no arquivo recognize_commands determinar se isso indica uma detecção bem-sucedida. Por exemplo, se a palavra “noted” for falada, nosso modelo é treinado para detectar “no” e deve reconhecer que “noted” não é a mesma palavra. No entanto, se a janela de captura começar um pouco mais cedo, apenas a primeira sílaba de “noted” pode estar presente, o que pode levar o modelo a interpretar isso como uma alta probabilidade de “no”. Portanto a classe RecognizeCommands calcula a média das pontuações para cada palavra ao longo das últimas inferências e decide se essa média é alta o suficiente para contar como uma detecção. Para isso, alimentamos a classe com os resultados das inferências à medida que chegam.

A classe é definida com um construtor que estabelece valores padrão para algumas variáveis, como a duração da janela de média, o limiar mínimo para uma detecção, o tempo de espera após ouvir um comando e o número mínimo de inferências necessárias. Há também o método ProcessLatestResults(), que aceita um ponteiro para um tensor contendo a saída do modelo e deve ser chamado com o tempo atual. 

explicit RecognizeCommands(int32_t average_window_duration_ms = 1000,
                             float detection_threshold = .8,
                             int32_t suppression_ms = 1500,
                             int32_t minimum_count = 3);

  // Call this with the results of running a model on sample data.
  TfLiteStatus ProcessLatestResults(const TfLiteTensor* latest_results,
                                    const int32_t current_time_ms,
                                    const char** found_command, float* score,
                                    bool* is_new_command);

command_responder:

No loop principal, quando o modo de Reconhecimento de Comandos é ativado, a função RespondToCommand é chamada para processar os comandos reconhecidos. Essa função pode ser expandida para executar diversas ações, como ativar uma saída digital, enviar informações pela web, entre outras.

void RespondToCommand(int32_t current_time, const char* found_command,
                      float score, bool is_new_command) {
  if (is_new_command) {
    MicroPrintf("Dispositivo ouviu %s (%.4f) @%dms", found_command, score, current_time);
  }
  if (found_command[0] == 'y'){
      printf("Sim identificado\n");
  }
  if (found_command[0] == 'n'){      
      printf("Nao identificado\n");
  }
  
}

Implementação 

Inicialmente, vamos implementar esse projeto como está, para isso selecione a placa e a porta serial, e em seguida realize o build. No resultado, você conseguirá identificar as respostas quando falar “yes” ou “no”, apresentando um score alto. Se nenhuma dessas palavras for identificada será reconhecido como “unknown” (desconhecido) e se não houver barulho algum será identificado como “silence” (silent).

Conclusão

Neste artigo, implementamos o exemplo básico de reconhecimento de voz usando o modelo Micro Speech com TinyML no ESP32, abordando desde a configuração inicial até a execução do modelo na ESP-EYE. Essa etapa inicial é fundamental para entender como a integração de modelos de aprendizado de máquina pode ser aplicada em dispositivos embarcados.

Próximo Passo: Treinamento e Personalização do Modelo

No próximo artigo, daremos um passo adiante no desenvolvimento do sistema de reconhecimento de voz customizado. Dividiremos o conteúdo em duas fases: inicialmente, apresentaremos o treinamento e a personalização do modelo em Python, incluindo o ajuste dos parâmetros. Na segunda fase, veremos como adaptar o código no ESP IDF para integrar o modelo atualizado, personalizando o dispositivo para novas palavras ou sons.

Reconhecimento de Comando de Voz com ESP-EYE

Implementação de Reconhecimento de Comando de Voz com ESP-EYE e Machine Learning – Parte 2
Licença Creative Commons Esta obra está licenciada com uma Licença Creative Commons Atribuição-CompartilhaIgual 4.0 Internacional.
Comentários:
Notificações
Notificar
0 Comentários
recentes
antigos mais votados
Inline Feedbacks
View all comments
Home » Software » Inteligência Artificial » Implementação de Reconhecimento de Comando de Voz com ESP-EYE e Machine Learning – Parte 1

EM DESTAQUE

WEBINARS

VEJA TAMBÉM

JUNTE-SE HOJE À COMUNIDADE EMBARCADOS

Talvez você goste: