Hackathon OpenVINO: recoñecendo voz e emocións en Raspberry Pi

30 de novembro - 1 de decembro celebrouse en Nizhny Novgorod Hackathon OpenVINO. Pedíuselles aos participantes que creasen un prototipo dunha solución de produto utilizando o kit de ferramentas Intel OpenVINO. Os organizadores propuxeron unha lista de temas aproximados polos que se podían orientar á hora de elixir unha tarefa, pero a decisión final quedaba nos equipos. Ademais, fomentouse o uso de modelos que non están incluídos no produto.

Hackathon OpenVINO: recoñecendo voz e emocións en Raspberry Pi

Neste artigo falarémosche de como creamos o noso prototipo de produto, co que finalmente acadamos o primeiro lugar.

Máis de 10 equipos participaron no hackathon. Dá gusto que algúns deles chegasen doutras rexións. O lugar do hackathon foi o complexo "Kremlinsky on Pochain", onde se colgaban no seu interior fotografías antigas de Nizhny Novgorod, nunha comitiva. (Lembro que neste momento a oficina central de Intel está situada en Nizhny Novgorod). Os participantes tiñan 26 horas para escribir código, e ao final debían presentar a súa solución. Unha vantaxe aparte foi a presenza dunha sesión de demostración para asegurarse de que todo o planeado se implementou realmente e non quedaron ideas na presentación. Merch, petiscos, comida, todo estaba alí tamén!

Ademais, Intel proporcionou opcionalmente cámaras, Raspberry PI, Neural Compute Stick 2.

Selección de tarefas

Unha das partes máis difíciles de prepararse para un hackathon de forma libre é escoller un desafío. Inmediatamente decidimos crear algo que aínda non estaba no produto, xa que o anuncio dicía que era moi benvido.

Tendo analizado modelos, que se inclúen no produto na versión actual, chegamos á conclusión de que a maioría deles solucionan varios problemas de visión por ordenador. Ademais, é moi difícil dar con un problema no campo da visión por ordenador que non se poida resolver con OpenVINO, e aínda que se poida inventar, é difícil atopar modelos previamente adestrados no dominio público. Decidimos cavar noutra dirección: cara ao procesamento e análise da fala. Consideremos unha tarefa interesante de recoñecer emocións a partir da fala. Hai que dicir que OpenVINO xa ten un modelo que determina as emocións dunha persoa en función do seu rostro, pero:

  • En teoría, é posible crear un algoritmo combinado que funcione tanto no son como na imaxe, o que debería dar un aumento da precisión.
  • As cámaras adoitan ter un ángulo de visión estreito; é necesaria máis dunha cámara para cubrir unha gran área; o son non ten esa limitación.

Desenvolvemos a idea: tomemos como base a idea do segmento de venda polo miúdo. Podes medir a satisfacción do cliente nas caixas da tenda. Se un dos clientes non está satisfeito co servizo e comeza a subir o ton, pode chamar inmediatamente ao administrador para pedir axuda.
Neste caso, necesitamos engadir o recoñecemento de voz humana, isto permitiranos distinguir aos empregados da tenda dos clientes e proporcionar análises para cada individuo. Ben, ademais, será posible analizar o comportamento dos propios empregados da tenda, avaliar o ambiente no equipo, soa ben!

Formulamos os requisitos para a nosa solución:

  • Pequeno tamaño do dispositivo de destino
  • Operación en tempo real
  • Prezo baixo
  • Fácil escalabilidade

Como resultado, seleccionamos Raspberry Pi 3 c como dispositivo de destino Intel NCS 2.

Aquí é importante ter en conta unha característica importante de NCS: funciona mellor con arquitecturas estándar de CNN, pero se necesitas executar un modelo con capas personalizadas, espera unha optimización de baixo nivel.

Só hai unha pequena cousa que facer: necesitas un micrófono. Un micrófono USB normal funcionará, pero non se verá ben xunto co RPI. Pero incluso aquí a solución literalmente "está preto". Para gravar voz, decidimos utilizar a placa Voice Bonnet do kit Kit de voz Google AIY, no que hai un micrófono estéreo con cable.

Descargar Raspbian desde Repositorio de proxectos AIY e cárgueo nunha unidade flash, proba que o micrófono funciona usando o seguinte comando (gravará o audio de 5 segundos de duración e gardalo nun ficheiro):

arecord -d 5 -r 16000 test.wav

Debo notar inmediatamente que o micrófono é moi sensible e capta ben o ruído. Para solucionar isto, imos a alsamixer, seleccione Capturar dispositivos e reduza o nivel do sinal de entrada ao 50-60%.

Hackathon OpenVINO: recoñecendo voz e emocións en Raspberry Pi
Modificamos o corpo cunha lima e todo cabe, incluso podes pechalo cunha tapa

Engadindo un botón indicador

Mentres desmontamos o kit de voz AIY, lembramos que hai un botón RGB, cuxa luz de fondo pode controlarse mediante software. Buscamos "Google AIY Led" e atopamos documentación: https://aiyprojects.readthedocs.io/en/latest/aiy.leds.html
Por que non usa este botón para mostrar a emoción recoñecida, só temos 7 clases, e o botón ten 8 cores, o suficiente!

Conectamos o botón a través de GPIO a Voice Bonnet, cargamos as bibliotecas necesarias (xa están instaladas no kit de distribución dos proxectos AIY)

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

Imos crear un dictado no que cada emoción terá unha cor correspondente en forma de tupla RGB e un obxecto da clase aiy.leds.Leds, a través do cal actualizaremos 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 finalmente, despois de cada nova predición dunha emoción, actualizaremos a cor do botón de acordo con ela (por tecla).

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

Hackathon OpenVINO: recoñecendo voz e emocións en Raspberry Pi
Botón, queima!

Traballar coa voz

Usaremos pyaudio para capturar o fluxo desde o micrófono e webrtcvad para filtrar o ruído e detectar a voz. Ademais, crearemos unha cola á que engadiremos e eliminaremos fragmentos de voz de forma asíncrona.

Dado que webrtcvad ten unha limitación no tamaño do fragmento subministrado: debe ser igual a 10/20/30 ms, e o adestramento do modelo para recoñecer emocións (como aprenderemos máis adiante) levouse a cabo nun conxunto de datos de 48 kHz, captura anacos de tamaño 48000x20ms/1000x1(mono)=960 bytes. Webrtcvad devolverá Verdadeiro/Falso para cada un destes anacos, o que corresponde á presenza ou ausencia dun voto no fragmento.

Imos implementar a seguinte lóxica:

  • Engadiremos á lista aqueles anacos onde hai voto; se non hai voto, incrementaremos o contador de anacos baleiros.
  • Se o contador de anacos baleiros é >=30 (600 ms), entón miramos o tamaño da lista de anacos acumulados; se é >250, engádelle á cola; se non, consideramos que a lonxitude do rexistro non é suficiente para alimentalo ao modelo para identificar o falante.
  • Se o contador de anacos baleiros aínda é < 30 e o tamaño da lista de anacos acumulados supera os 300, entón engadiremos o fragmento á cola para unha predición máis precisa. (porque as emocións tenden a cambiar co paso do 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 = []

É o momento de buscar modelos previamente adestrados no dominio público, vai a github, Google, pero lembra que temos unha limitación na arquitectura empregada. Esta é unha parte bastante difícil, porque tes que probar os modelos nos teus datos de entrada e, ademais, convertelos ao formato interno de OpenVINO: IR (Representación intermedia). Probamos unhas 5-7 solucións diferentes de github, e se o modelo para recoñecer emocións funcionaba inmediatamente, entón co recoñecemento de voz tivemos que esperar máis tempo: usan arquitecturas máis complexas.

Centrámonos no seguinte:

A continuación falaremos da conversión de modelos, comezando pola teoría. OpenVINO inclúe varios módulos:

  • Open Model Zoo, modelos dos que se poderían utilizar e incluír no seu produto
  • Model Optimzer, grazas ao cal pode converter un modelo de varios formatos de framework (Tensorflow, ONNX, etc.) ao formato de representación intermedia, co que seguiremos traballando
  • Inference Engine permítelle executar modelos en formato IR en procesadores Intel, chips Myriad e aceleradores Neural Compute Stick
  • A versión máis eficiente de OpenCV (con compatibilidade con Inference Engine)
    Cada modelo en formato IR descríbese mediante dous ficheiros: .xml e .bin.
    Os modelos convértense ao formato IR mediante Model Optimizer do seguinte xeito:

    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 seleccionar o formato de datos co que funcionará o modelo. FP32, FP16, INT8 son compatibles. Escoller o tipo de datos óptimo pode aumentar o rendemento.
    --input_shape indica a dimensión dos datos de entrada. A capacidade de cambialo dinámicamente parece estar presente na API de C++, pero non escavamos tan lonxe e simplemente solucionalo para un dos modelos.
    A continuación, imos tentar cargar o modelo xa convertido en formato IR a través do módulo DNN en OpenCV e reenviar a el.

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

    A última liña neste caso permítelle redirixir os cálculos ao Neural Compute Stick, os cálculos básicos realízanse no procesador, pero no caso do Raspberry Pi isto non funcionará, necesitarás un stick.

    A continuación, a lóxica é a seguinte: dividimos o noso audio en fiestras dun determinado tamaño (para nós é de 0.4 s), convertemos cada unha destas fiestras en MFCC, que despois alimentamos á grella:

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

    A continuación, imos tomar a clase máis común para todas as fiestras. Unha solución sinxela, pero para un hackathon non é preciso que che dea algo demasiado abstruso, só se tes tempo. Aínda nos queda moito traballo por facer, así que sigamos: ocuparémonos do recoñecemento de voz. É necesario facer algún tipo de base de datos na que se almacenen espectrogramas de voces pregravadas. Xa que queda pouco tempo, resolveremos este problema o mellor posible.

    É dicir, creamos un script para gravar un fragmento de voz (funciona do mesmo xeito que se describe anteriormente, só cando se interrompe desde o teclado gardará a voz nun ficheiro).

    Imos probar:

    python3 voice_db/record_voice.py test.wav

    Gravamos as voces de varias persoas (no noso caso, tres membros do equipo)
    A continuación, para cada voz gravada realizamos unha transformada de Fourier rápida, obtemos un espectrograma e gárdao como matriz numpy (.npy):

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

    Máis detalles no arquivo create_base.py
    Como resultado, cando executamos o script principal, obteremos incorporacións destes espectrogramas ao principio:

    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)

    Despois de recibir a incorporación do segmento soado, poderemos determinar a quen pertence tomando a distancia coseno desde o paso a todas as voces da base de datos (canto máis pequenas, máis probables) - para a demostración establecemos o limiar ata 0.3):

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

    Ao final, gustaríame sinalar que a velocidade de inferencia foi rápida e permitiu engadir 1-2 modelos máis (para unha mostra de 7 segundos, necesitou 2.5 para a inferencia). Xa non tivemos tempo de engadir novos modelos e centrámonos en escribir un prototipo da aplicación web.

    Aplicación web

    Un punto importante: levamos un router connosco da casa e configuramos a nosa rede local, axuda a conectar o dispositivo e os portátiles pola rede.

    O backend é unha canle de mensaxes de extremo a extremo entre a parte frontal e Raspberry Pi, baseada na tecnoloxía websocket (protocolo http sobre tcp).

    A primeira etapa é recibir información procesada de raspberry, é dicir, predictores empaquetados en json, que se gardan na base de datos a metade do seu percorrido para que se poidan xerar estatísticas sobre o trasfondo emocional do usuario para o período. Este paquete envíase entón ao frontend, que usa a subscrición e recibe paquetes do punto final do websocket. Todo o mecanismo de backend está construído na linguaxe golang; escolleuse porque é moi adecuado para tarefas asíncronas, que as goroutines manexan ben.
    Ao acceder ao punto final, o usuario rexístrase e introdúcese na estrutura, despois recíbese a súa mensaxe. Tanto o usuario como a mensaxe introdúcense nun concentrador común, desde o cal as mensaxes xa se envían máis adiante (á fronte subscrita) e se o usuario pecha a conexión (framboesa ou fronte), a súa subscrición cancelarase e elimínase de o hub.

    Hackathon OpenVINO: recoñecendo voz e emocións en Raspberry Pi
    Estamos agardando unha conexión desde atrás

    Front-end é unha aplicación web escrita en JavaScript que utiliza a biblioteca React para acelerar e simplificar o proceso de desenvolvemento. O propósito desta aplicación é visualizar os datos obtidos mediante algoritmos que se executan no lado de fondo e directamente no Raspberry Pi. A páxina ten enrutamento seccional implementado mediante react-router, pero a páxina principal de interese é a páxina principal, onde se recibe un fluxo continuo de datos en tempo real do servidor mediante a tecnoloxía WebSocket. Raspberry Pi detecta unha voz, determina se pertence a unha persoa específica desde a base de datos rexistrada e envía unha lista de probabilidades ao cliente. O cliente mostra os últimos datos relevantes, mostra o avatar da persoa que probablemente falou polo micrófono, así como a emoción coa que pronuncia as palabras.

    Hackathon OpenVINO: recoñecendo voz e emocións en Raspberry Pi
    Páxina de inicio con predicións actualizadas

    Conclusión

    Non foi posible completar todo como estaba previsto, simplemente non tiñamos tempo, polo que a principal esperanza estaba na demo, que todo funcionase. Na presentación falaron de como funciona todo, que modelos tomaron, que problemas atoparon. A continuación foi a parte de demostración: os expertos percorreron a sala en orde aleatoria e achegáronse a cada equipo para mirar o prototipo de traballo. Tamén nos fixeron preguntas, cada quen respondeu da súa parte, deixaron a web no portátil, e todo funcionou como se esperaba.

    Déixeme notar que o custo total da nosa solución foi de $ 150:

    • Raspberry Pi 3 ~ $35
    • Google AIY Voice Bonnet (podes pagar unha tarifa por repetidores) ~ 15 $
    • Intel NCS 2 ~ 100 $

    Como mellorar:

    • Use o rexistro do cliente: solicite ler o texto que se xera aleatoriamente
    • Engade algúns modelos máis: podes determinar o sexo e a idade pola voz
    • Separar voces que soan simultáneamente (diarización)

    Repositorio: https://github.com/vladimirwest/OpenEMO

    Hackathon OpenVINO: recoñecendo voz e emocións en Raspberry Pi
    Estamos cansos pero felices

    Para rematar, quero agradecer aos organizadores e participantes. Entre os proxectos doutros equipos gustounos persoalmente a solución de vixilancia de prazas de aparcamento gratuíto. Para nós, foi unha experiencia moi interesante de inmersión no produto e desenvolvemento. Espero que cada vez se celebren máis eventos interesantes nas rexións, incluso sobre temas de IA.

Fonte: www.habr.com

Engadir un comentario