Hackathon OpenVINO: reconhecendo voz e emoções no Raspberry Pi

30 de novembro a 1º de dezembro em Nizhny Novgorod foi realizado Hackathon OpenVINO. Os participantes foram convidados a criar um protótipo de uma solução de produto usando o kit de ferramentas Intel OpenVINO. Os organizadores propuseram uma lista de temas aproximados que poderiam ser orientados na escolha de uma tarefa, mas a decisão final ficou com as equipes. Além disso, foi incentivado o uso de modelos que não acompanham o produto.

Hackathon OpenVINO: reconhecendo voz e emoções no Raspberry Pi

Neste artigo contaremos como criamos nosso protótipo do produto, com o qual acabamos conquistando o primeiro lugar.

Mais de 10 equipes participaram do hackathon. É bom que alguns deles tenham vindo de outras regiões. O local do hackathon foi o complexo “Kremlinsky on Pochain”, onde fotos antigas de Nizhny Novgorod foram penduradas em uma comitiva! (Lembro que no momento o escritório central da Intel está localizado em Nizhny Novgorod). Os participantes tiveram 26 horas para escrever o código e ao final tiveram que apresentar sua solução. Uma outra vantagem foi a presença de uma sessão de demonstração para garantir que tudo o que foi planejado foi realmente implementado e não restaram ideias na apresentação. Merch, lanches, comida, estava tudo lá também!

Além disso, a Intel forneceu opcionalmente câmeras, Raspberry PI, Neural Compute Stick 2.

Seleção de tarefas

Uma das partes mais difíceis da preparação para um hackathon de formato livre é escolher um desafio. Decidimos imediatamente criar algo que ainda não estava no produto, pois o anúncio dizia que isso era muito bem-vindo.

Tendo analisado modelos, que estão incluídos no produto na versão atual, chegamos à conclusão de que a maioria deles resolve vários problemas de visão computacional. Além disso, é muito difícil encontrar um problema no campo da visão computacional que não possa ser resolvido com o OpenVINO e, mesmo que um possa ser inventado, é difícil encontrar modelos pré-treinados de domínio público. Decidimos ir em outra direção - em direção ao processamento e análise de fala. Consideremos uma tarefa interessante de reconhecimento de emoções na fala. É preciso dizer que o OpenVINO já possui um modelo que determina as emoções de uma pessoa com base em seu rosto, mas:

  • Em teoria, é possível criar um algoritmo combinado que funcione tanto no som quanto na imagem, o que deve proporcionar um aumento na precisão.
  • As câmeras geralmente têm um ângulo de visão estreito; é necessária mais de uma câmera para cobrir uma área grande; o som não tem essa limitação.

Vamos desenvolver a ideia: vamos tomar como base a ideia do segmento varejista. Você pode medir a satisfação do cliente nas caixas da loja. Se um dos clientes estiver insatisfeito com o serviço e começar a aumentar o tom, você pode ligar imediatamente para o administrador para obter ajuda.
Neste caso, precisamos adicionar o reconhecimento de voz humana, o que nos permitirá distinguir os funcionários da loja dos clientes e fornecer análises para cada indivíduo. Pois bem, além disso, será possível analisar o comportamento dos próprios funcionários da loja, avaliar o clima da equipe, parece bom!

Formulamos os requisitos para nossa solução:

  • Tamanho pequeno do dispositivo de destino
  • Operação em tempo real
  • Baixo preço
  • Escalabilidade fácil

Como resultado, selecionamos Raspberry Pi 3 c como dispositivo de destino Intel NCS2.

Aqui é importante observar um recurso importante do NCS: ele funciona melhor com arquiteturas CNN padrão, mas se você precisar executar um modelo com camadas personalizadas, espere uma otimização de baixo nível.

Só há uma pequena coisa a fazer: você precisa de um microfone. Um microfone USB normal serve, mas não ficará bem junto com o RPI. Mas mesmo aqui a solução literalmente “está próxima”. Para gravar voz, decidimos usar a placa Voice Bonnet do kit Kit de voz do Google AIY, no qual há um microfone estéreo com fio.

Baixe o Raspbian em Repositório de projetos AIY e carregue-o em uma unidade flash, teste se o microfone funciona usando o seguinte comando (ele gravará áudio de 5 segundos e salvará em um arquivo):

arecord -d 5 -r 16000 test.wav

Devo observar imediatamente que o microfone é muito sensível e capta bem o ruído. Para corrigir isso, vamos ao alsamixer, selecione Dispositivos de captura e reduza o nível do sinal de entrada para 50-60%.

Hackathon OpenVINO: reconhecendo voz e emoções no Raspberry Pi
Modificamos o corpo com uma lima e cabe tudo, você pode até fechar com tampa

Adicionando um botão indicador

Ao desmontar o AIY Voice Kit, lembramos que existe um botão RGB cuja luz de fundo pode ser controlada por software. Procuramos “Google AIY Led” e encontramos a documentação: https://aiyprojects.readthedocs.io/en/latest/aiy.leds.html
Por que não usar este botão para exibir a emoção reconhecida, temos apenas 7 classes, e o botão tem 8 cores, basta!

Conectamos o botão via GPIO ao Voice Bonnet, carregamos as bibliotecas necessárias (elas já estão instaladas no kit de distribuição dos projetos AIY)

from aiy.leds import Leds, Color
from aiy.leds import RgbLeds

Vamos criar um dict no qual cada emoção terá uma cor correspondente na forma de uma Tupla RGB e um objeto da classe aiy.leds.Leds, através do qual atualizaremos a cor:

led_dict = {'neutral': (255, 255, 255), 'happy': (0, 255, 0), 'sad': (0, 255, 255), 'angry': (255, 0, 0), 'fearful': (0, 0, 0), 'disgusted':  (255, 0, 255), 'surprised':  (255, 255, 0)} 
leds = Leds()

E por fim, a cada nova previsão de uma emoção, atualizaremos a cor do botão de acordo com ela (por tecla).

leds.update(Leds.rgb_on(led_dict.get(classes[prediction])))

Hackathon OpenVINO: reconhecendo voz e emoções no Raspberry Pi
Botão, queime!

Trabalhando com voz

Usaremos pyaudio para capturar o stream do microfone e webrtcvad para filtrar ruído e detectar voz. Além disso, criaremos uma fila à qual adicionaremos e removeremos trechos de voz de forma assíncrona.

Como o webrtcvad tem uma limitação no tamanho do fragmento fornecido - deve ser igual a 10/20/30ms, e o treinamento do modelo de reconhecimento de emoções (como aprenderemos mais tarde) foi realizado em um conjunto de dados de 48kHz, iremos capture pedaços de tamanho 48000×20ms/1000×1(mono)=960 bytes. Webrtcvad retornará Verdadeiro/Falso para cada um desses pedaços, o que corresponde à presença ou ausência de voto no pedaço.

Vamos implementar a seguinte lógica:

  • Adicionaremos à lista aqueles pedaços onde há votação; se não houver votação, incrementaremos o contador de pedaços vazios.
  • Se o contador de pedaços vazios for >=30 (600 ms), então olhamos o tamanho da lista de pedaços acumulados; se for >250, então o adicionamos à fila; caso contrário, consideramos que o comprimento do registro não é suficiente para alimentá-lo ao modelo para identificar o orador.
  • Se o contador de pedaços vazios ainda for <30 e o tamanho da lista de pedaços acumulados exceder 300, adicionaremos o fragmento à fila para uma previsão mais precisa. (porque as emoções tendem a mudar com o tempo)

 def to_queue(frames):
    d = np.frombuffer(b''.join(frames), dtype=np.int16)
    return d

framesQueue = queue.Queue()
def framesThreadBody():
    CHUNK = 960
    FORMAT = pyaudio.paInt16
    CHANNELS = 1
    RATE = 48000

    p = pyaudio.PyAudio()
    vad = webrtcvad.Vad()
    vad.set_mode(2)
    stream = p.open(format=FORMAT,
                channels=CHANNELS,
                rate=RATE,
                input=True,
                frames_per_buffer=CHUNK)
    false_counter = 0
    audio_frame = []
    while process:
        data = stream.read(CHUNK)
        if not vad.is_speech(data, RATE):
            false_counter += 1
            if false_counter >= 30:
                if len(audio_frame) > 250:              
                    framesQueue.put(to_queue(audio_frame,timestamp_start))
                    audio_frame = []
                    false_counter = 0

        if vad.is_speech(data, RATE):
            false_counter = 0
            audio_frame.append(data)
            if len(audio_frame) > 300:                
                    framesQueue.put(to_queue(audio_frame,timestamp_start))
                    audio_frame = []

É hora de procurar modelos pré-treinados de domínio público, ir ao github, Google, mas lembre-se que temos uma limitação na arquitetura utilizada. Esta é uma parte bastante difícil, pois você tem que testar os modelos nos seus dados de entrada e, além disso, convertê-los para o formato interno do OpenVINO - IR (Intermediate Representation). Tentamos cerca de 5 a 7 soluções diferentes do github, e se o modelo de reconhecimento de emoções funcionasse imediatamente, então com o reconhecimento de voz teríamos que esperar mais - eles usam arquiteturas mais complexas.

Nós nos concentramos no seguinte:

A seguir falaremos sobre conversão de modelos, começando pela teoria. OpenVINO inclui vários módulos:

  • Open Model Zoo, modelos que podem ser usados ​​e incluídos em seu produto
  • Model Optimzer, graças ao qual você pode converter um modelo de vários formatos de framework (Tensorflow, ONNX etc) para o formato de Representação Intermediária, com o qual trabalharemos mais adiante
  • O Inference Engine permite executar modelos em formato IR em processadores Intel, chips Myriad e aceleradores Neural Compute Stick
  • A versão mais eficiente do OpenCV (com suporte para Inference Engine)
    Cada modelo no formato IR é descrito por dois arquivos: .xml e .bin.
    Os modelos são convertidos para o formato IR por meio do Model Optimizer da seguinte forma:

    python /opt/intel/openvino/deployment_tools/model_optimizer/mo_tf.py --input_model speaker.hdf5.pb --data_type=FP16 --input_shape [1,512,1000,1]

    --data_type permite selecionar o formato de dados com o qual o modelo funcionará. FP32, FP16, INT8 são suportados. Escolher o tipo de dados ideal pode proporcionar um bom aumento de desempenho.
    --input_shape indica a dimensão dos dados de entrada. A capacidade de alterá-lo dinamicamente parece estar presente na API C++, mas não fomos tão longe e simplesmente corrigimos isso para um dos modelos.
    A seguir, vamos tentar carregar o modelo já convertido em formato IR através do módulo DNN para OpenCV e encaminhá-lo para ele.

    import cv2 as cv
    emotionsNet = cv.dnn.readNet('emotions_model.bin',
                              'emotions_model.xml')
    emotionsNet.setPreferableTarget(cv.dnn.DNN_TARGET_MYRIAD)

    A última linha neste caso permite redirecionar os cálculos para o Neural Compute Stick, os cálculos básicos são realizados no processador, mas no caso do Raspberry Pi isso não funcionará, você precisará de um stick.

    A seguir, a lógica é a seguinte: dividimos nosso áudio em janelas de um determinado tamanho (para nós são 0.4 s), convertemos cada uma dessas janelas em MFCC, que então alimentamos na grade:

    emotionsNet.setInput(MFCC_from_window)
    result = emotionsNet.forward()

    A seguir, vamos pegar a classe mais comum para todas as janelas. Uma solução simples, mas para um hackathon você não precisa inventar algo muito obscuro, apenas se tiver tempo. Ainda temos muito trabalho pela frente, então vamos em frente - trataremos do reconhecimento de voz. É necessário fazer algum tipo de banco de dados onde seriam armazenados espectrogramas de vozes pré-gravadas. Como resta pouco tempo, resolveremos esse problema da melhor maneira possível.

    Ou seja, criamos um script para gravar um trecho de voz (funciona da mesma forma que descrito acima, somente quando interrompido pelo teclado salvará a voz em um arquivo).

    Vamos tentar:

    python3 voice_db/record_voice.py test.wav

    Gravamos as vozes de várias pessoas (no nosso caso, três membros da equipe)
    A seguir, para cada voz gravada realizamos uma transformação rápida de Fourier, obtemos um espectrograma e o salvamos como um array numpy (.npy):

    for file in glob.glob("voice_db/*.wav"):
            spec = get_fft_spectrum(file)
            np.save(file[:-4] + '.npy', spec)

    Mais detalhes no arquivo create_base.py
    Como resultado, quando executarmos o script principal, obteremos embeddings desses espectrogramas logo no início:

    for file in glob.glob("voice_db/*.npy"):
        spec = np.load(file)
        spec = spec.astype('float32')
        spec_reshaped = spec.reshape(1, 1, spec.shape[0], spec.shape[1])
        srNet.setInput(spec_reshaped)
        pred = srNet.forward()
        emb = np.squeeze(pred)

    Depois de receber a incorporação do segmento sonoro, poderemos determinar a quem ele pertence calculando a distância do cosseno da passagem para todas as vozes no banco de dados (quanto menor, mais provável) - para a demonstração definimos o limite para 0.3):

            dist_list = cdist(emb, enroll_embs, metric="cosine")
            distances = pd.DataFrame(dist_list, columns = df.speaker)

    No final, gostaria de observar que a velocidade de inferência foi rápida e possibilitou adicionar mais 1 a 2 modelos (para uma amostra de 7 segundos foram necessários 2.5 para inferência). Não tivemos mais tempo para adicionar novos modelos e nos concentramos em escrever um protótipo da aplicação web.

    Aplicativo da Web

    Um ponto importante: levamos um roteador de casa e configuramos nossa rede local, isso ajuda a conectar o aparelho e os laptops pela rede.

    O backend é um canal de mensagens ponta a ponta entre o front e o Raspberry Pi, baseado na tecnologia websocket (protocolo http sobre tcp).

    A primeira etapa é receber as informações processadas do framboesa, ou seja, preditores embalados em json, que são salvos no banco de dados na metade da jornada para que sejam geradas estatísticas sobre o histórico emocional do usuário no período. Este pacote é então enviado para o frontend, que usa assinatura e recebe pacotes do endpoint websocket. Todo o mecanismo de backend é construído na linguagem golang; ele foi escolhido porque é adequado para tarefas assíncronas, que as goroutines lidam bem.
    Ao acessar o endpoint, o usuário é cadastrado e inserido na estrutura, em seguida sua mensagem é recebida. Tanto o usuário quanto a mensagem são inseridos em um hub comum, a partir do qual as mensagens já são enviadas posteriormente (para a frente inscrita), e se o usuário fechar a conexão (framboesa ou frente), sua assinatura é cancelada e ele é removido de o centro.

    Hackathon OpenVINO: reconhecendo voz e emoções no Raspberry Pi
    Estamos aguardando uma conexão por trás

    Front-end é uma aplicação web escrita em JavaScript que utiliza a biblioteca React para acelerar e simplificar o processo de desenvolvimento. O objetivo deste aplicativo é visualizar dados obtidos por meio de algoritmos executados no back-end e diretamente no Raspberry Pi. A página possui roteamento seccional implementado usando react-router, mas a página principal de interesse é a página principal, onde um fluxo contínuo de dados é recebido em tempo real do servidor usando a tecnologia WebSocket. Raspberry Pi detecta uma voz, determina se ela pertence a uma pessoa específica do banco de dados cadastrado e envia uma lista de probabilidades ao cliente. O cliente exibe os últimos dados relevantes, mostra o avatar da pessoa que provavelmente falou ao microfone, bem como a emoção com que pronuncia as palavras.

    Hackathon OpenVINO: reconhecendo voz e emoções no Raspberry Pi
    Página inicial com previsões atualizadas

    Conclusão

    Não foi possível concluir tudo conforme planejado, simplesmente não tivemos tempo, então a principal esperança estava na demo, que tudo funcionasse. Na apresentação eles falaram sobre como tudo funciona, quais modelos adotaram, quais problemas encontraram. Em seguida veio a parte de demonstração: especialistas andaram pela sala em ordem aleatória e abordaram cada equipe para ver o protótipo funcional. Eles também nos fizeram perguntas, cada um respondeu a sua parte, deixaram a web no laptop e tudo realmente funcionou como esperado.

    Deixe-me observar que o custo total da nossa solução foi de US$ 150:

    • Framboesa Pi 3 ~ $ 35
    • Google AIY Voice Bonnet (você pode cobrar uma taxa de novo palestrante) ~ 15$
    • Intel NCS2 ~ 100$

    Como melhorar:

    • Utilize o cadastro do cliente - peça para ler o texto que é gerado aleatoriamente
    • Adicione mais alguns modelos: você pode determinar sexo e idade por voz
    • Vozes separadas que soam simultaneamente (diarização)

    Repositório: https://github.com/vladimirwest/OpenEMO

    Hackathon OpenVINO: reconhecendo voz e emoções no Raspberry Pi
    Cansados, mas felizes estamos

    Para concluir, gostaria de agradecer aos organizadores e participantes. Entre os projetos de outras equipes, gostamos pessoalmente da solução de monitoramento de vagas gratuitas. Para nós, foi uma experiência muito legal de imersão no produto e no desenvolvimento. Espero que cada vez mais eventos interessantes sejam realizados nas regiões, inclusive sobre temas de IA.

Fonte: habr.com

Adicionar um comentário