Hackathon OpenVINO: reconocimiento de voz y emociones en Raspberry Pi

Del 30 de noviembre al 1 de diciembre se celebró en Nizhny Novgorod. Hackatón OpenVINO. Se pidió a los participantes que crearan un prototipo de una solución de producto utilizando el kit de herramientas Intel OpenVINO. Los organizadores propusieron una lista de temas aproximados por los que podrían guiarse a la hora de elegir una tarea, pero la decisión final quedó en manos de los equipos. Además, se fomentó el uso de modelos que no estén incluidos en el producto.

Hackathon OpenVINO: reconocimiento de voz y emociones en Raspberry Pi

En este artículo le contaremos cómo creamos nuestro prototipo del producto, con el que finalmente obtuvimos el primer lugar.

Más de 10 equipos participaron en el hackathon. Es bueno que algunos de ellos vinieran de otras regiones. El lugar del hackathon fue el complejo “Kremlinsky on Pochain”, donde en su interior se colgaron fotografías antiguas de Nizhny Novgorod, ¡en un séquito! (Les recuerdo que actualmente la oficina central de Intel se encuentra en Nizhny Novgorod). Los participantes tuvieron 26 horas para escribir código y al final debían presentar su solución. Otra ventaja fue la presencia de una sesión de demostración para asegurarse de que todo lo planificado se implementara realmente y no quedaran ideas en la presentación. Mercancía, snacks, comida, ¡todo estaba ahí también!

Además, Intel proporcionó opcionalmente cámaras, Raspberry PI y Neural Compute Stick 2.

Selección de tareas

Una de las partes más difíciles de prepararse para un hackathon de formato libre es elegir un desafío. Inmediatamente decidimos idear algo que aún no estaba en el producto, ya que en el anuncio decía que era muy bienvenido.

haber analizado modelos, que se incluyen en el producto en la versión actual, llegamos a la conclusión de que la mayoría de ellos resuelven diversos problemas de visión por computadora. Además, es muy difícil encontrar un problema en el campo de la visión por computadora que no pueda resolverse usando OpenVINO, e incluso si se pudiera inventar uno, es difícil encontrar modelos previamente entrenados en el dominio público. Decidimos profundizar en otra dirección: hacia el procesamiento y el análisis del habla. Consideremos una tarea interesante de reconocer emociones a partir del habla. Hay que decir que OpenVINO ya cuenta con un modelo que determina las emociones de una persona en función de su rostro, pero:

  • En teoría, es posible crear un algoritmo combinado que funcione tanto con el sonido como con la imagen, lo que debería aumentar la precisión.
  • Las cámaras suelen tener un ángulo de visión estrecho; se requiere más de una cámara para cubrir un área grande; el sonido no tiene tal limitación.

Desarrollamos la idea: tomemos como base la idea para el segmento minorista. Puede medir la satisfacción del cliente en las cajas de las tiendas. Si uno de los clientes no está satisfecho con el servicio y comienza a elevar el tono, puede llamar inmediatamente al administrador para pedir ayuda.
En este caso, necesitamos agregar reconocimiento de voz humana, esto nos permitirá distinguir a los empleados de la tienda de los clientes y proporcionar análisis para cada individuo. Bueno, además, será posible analizar el comportamiento de los propios empleados de la tienda, evaluar el ambiente en el equipo, ¡suena bien!

Formulamos los requisitos para nuestra solución:

  • Tamaño pequeño del dispositivo de destino.
  • Operación en tiempo real
  • Precio bajo
  • Fácil escalabilidad

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

Aquí es importante tener en cuenta una característica importante de NCS: funciona mejor con arquitecturas CNN estándar, pero si necesita ejecutar un modelo con capas personalizadas, espere una optimización de bajo nivel.

Sólo hay una pequeña cosa que hacer: necesitas un micrófono. Un micrófono USB normal servirá, pero no quedará bien junto con el RPI. Pero incluso en este caso la solución literalmente “está cerca”. Para grabar voz, decidimos utilizar la placa Voice Bonnet del kit. Kit de voz AIY de Google, en el que hay un micrófono estéreo con cable.

Descargar Raspbian desde Repositorio de proyectos AIY y cárguelo en una unidad flash, pruebe que el micrófono funcione usando el siguiente comando (grabará audio de 5 segundos de duración y lo guardará en un archivo):

arecord -d 5 -r 16000 test.wav

Debo señalar inmediatamente que el micrófono es muy sensible y capta bien el ruido. Para solucionar este problema, vayamos a alsamixer, seleccionemos Capturar dispositivos y reduzcamos el nivel de la señal de entrada al 50-60%.

Hackathon OpenVINO: reconocimiento de voz y emociones en Raspberry Pi
Modificamos el cuerpo con una lima y todo encaja, incluso puedes cerrarlo con una tapa.

Agregar un botón indicador

Al desmontar el AIY Voice Kit, recordamos que hay un botón RGB, cuya retroiluminación se puede controlar mediante software. Buscamos “Google AIY Led” y encontramos documentación: https://aiyprojects.readthedocs.io/en/latest/aiy.leds.html
¿Por qué no usar este botón para mostrar la emoción reconocida? Tenemos solo 7 clases y el botón tiene 8 colores, ¡suficiente!

Conectamos el botón vía GPIO a Voice Bonnet, cargamos las bibliotecas necesarias (ya están instaladas en la distribución de proyectos AIY)

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

Creemos un dict en el que cada emoción tendrá un color correspondiente en forma de Tupla RGB y un objeto de la clase aiy.leds.Leds, a través del cual actualizaremos el color:

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()

Y finalmente, después de cada nueva predicción de una emoción, actualizaremos el color del botón de acuerdo con ella (por clave).

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

Hackathon OpenVINO: reconocimiento de voz y emociones en Raspberry Pi
¡Botón, quema!

trabajando con voz

Usaremos pyaudio para capturar la transmisión desde el micrófono y webrtcvad para filtrar el ruido y detectar la voz. Además, crearemos una cola a la que agregaremos y eliminaremos extractos de voz de forma asincrónica.

Dado que webrtcvad tiene una limitación en el tamaño del fragmento suministrado: debe ser igual a 10/20/30 ms, y el entrenamiento del modelo para reconocer emociones (como aprenderemos más adelante) se llevó a cabo en un conjunto de datos de 48 kHz, capturar fragmentos de tamaño 48000 × 20 ms/1000 × 1 (mono) = 960 bytes. Webrtcvad devolverá Verdadero/Falso para cada uno de estos fragmentos, lo que corresponde a la presencia o ausencia de un voto en el fragmento.

Implementemos la siguiente lógica:

  • Agregaremos a la lista aquellos fragmentos donde haya un voto; si no hay voto, entonces incrementaremos el contador de fragmentos vacíos.
  • Si el contador de fragmentos vacíos es >=30 (600 ms), entonces miramos el tamaño de la lista de fragmentos acumulados; si es >250, entonces lo agregamos a la cola; si no, consideramos que la longitud del registro no es suficiente para alimentarlo al modelo para identificar al hablante.
  • Si el contador de fragmentos vacíos sigue siendo <30 y el tamaño de la lista de fragmentos acumulados supera los 300, agregaremos el fragmento a la cola para una predicción más precisa. (porque las emociones tienden a cambiar con el tiempo)

 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 = []

Es hora de buscar modelos previamente entrenados en el dominio público, vaya a github, Google, pero recuerde que tenemos una limitación en la arquitectura utilizada. Esta es una parte bastante difícil, porque debe probar los modelos en sus datos de entrada y, además, convertirlos al formato interno de OpenVINO: IR (Representación Intermedia). Probamos entre 5 y 7 soluciones diferentes de Github, y si el modelo para reconocer emociones funcionó de inmediato, entonces con el reconocimiento de voz tuvimos que esperar más: utilizan arquitecturas más complejas.

Nos centramos en lo siguiente:

A continuación hablaremos sobre la conversión de modelos, comenzando con la teoría. OpenVINO incluye varios módulos:

  • Open Model Zoo, modelos que podrían usarse e incluirse en su producto
  • Model Optimzer, gracias al cual puede convertir un modelo de varios formatos de marco (Tensorflow, ONNX, etc.) al formato de representación intermedia, con el que trabajaremos más adelante.
  • Inference Engine le permite ejecutar modelos en formato IR en procesadores Intel, chips Myriad y aceleradores Neural Compute Stick
  • La versión más eficiente de OpenCV (con soporte para Inference Engine)
    Cada modelo en formato IR se describe mediante dos archivos: .xml y .bin.
    Los modelos se convierten al formato IR mediante Model Optimizer de la siguiente manera:

    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 le permite seleccionar el formato de datos con el que funcionará el modelo. Se admiten FP32, FP16, INT8. Elegir el tipo de datos óptimo puede aumentar el rendimiento.
    --input_shape indica la dimensión de los datos de entrada. La capacidad de cambiarla dinámicamente parece estar presente en la API de C++, pero no profundizamos tanto y simplemente la arreglamos para uno de los modelos.
    A continuación, intentemos cargar el modelo ya convertido en formato IR a través del módulo DNN en OpenCV y reenviárselo.

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

    La última línea en este caso le permite redirigir los cálculos al Neural Compute Stick, los cálculos básicos se realizan en el procesador, pero en el caso de Raspberry Pi esto no funcionará, necesitará un dispositivo.

    A continuación, la lógica es la siguiente: dividimos nuestro audio en ventanas de un cierto tamaño (para nosotros es 0.4 s), convertimos cada una de estas ventanas en MFCC, que luego alimentamos a la cuadrícula:

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

    A continuación, tomemos la clase más común para todas las ventanas. Una solución simple, pero para un hackathon no es necesario que se te ocurra algo demasiado abstruso, solo si tienes tiempo. Todavía tenemos mucho trabajo por hacer, así que sigamos adelante: nos ocuparemos del reconocimiento de voz. Es necesario crear algún tipo de base de datos en la que se almacenen espectrogramas de voces pregrabadas. Como queda poco tiempo, resolveremos este problema lo mejor que podamos.

    Es decir, creamos un script para grabar un extracto de voz (funciona de la misma manera que se describe arriba, solo que cuando se interrumpe desde el teclado guardará la voz en un archivo).

    Intentemos:

    python3 voice_db/record_voice.py test.wav

    Grabamos las voces de varias personas (en nuestro caso, tres miembros del equipo)
    A continuación, para cada voz grabada realizamos una transformada rápida de Fourier, obtenemos un espectrograma y lo guardamos como una matriz numpy (.npy):

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

    Más detalles en el archivo create_base.py
    Como resultado, cuando ejecutamos el script principal, obtendremos incrustaciones de estos espectrogramas desde el 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)

    Después de recibir la incrustación del segmento sonado, podremos determinar a quién pertenece tomando la distancia del coseno desde el pasaje a todas las voces en la base de datos (cuanto más pequeñas, más probable) - para la demostración establecemos el umbral a 0.3):

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

    Al final, me gustaría señalar que la velocidad de inferencia fue rápida y permitió agregar 1-2 modelos más (para una muestra de 7 segundos de duración, se necesitaron 2.5 para la inferencia). Ya no tuvimos tiempo de agregar nuevos modelos y nos concentramos en escribir un prototipo de la aplicación web.

    Aplicación web

    Un punto importante: nos llevamos un enrutador desde casa y configuramos nuestra red local, esto ayuda a conectar el dispositivo y las computadoras portátiles a través de la red.

    El backend es un canal de mensajes de un extremo a otro entre el front y Raspberry Pi, basado en la tecnología websocket (protocolo http sobre tcp).

    La primera etapa consiste en recibir información procesada de raspberry, es decir, predictores empaquetados en json, que se guardan en la base de datos a mitad de su recorrido para que se puedan generar estadísticas sobre el trasfondo emocional del usuario durante ese período. Luego, este paquete se envía al frontend, que utiliza una suscripción y recibe paquetes desde el punto final websocket. Todo el mecanismo de backend está construido en el lenguaje golang; se eligió porque es muy adecuado para tareas asincrónicas, que las gorutinas manejan bien.
    Al acceder al endpoint, el usuario se registra y se ingresa a la estructura, luego se recibe su mensaje. Tanto el usuario como el mensaje se ingresan en un centro común, desde donde los mensajes ya se envían más (al frente suscrito), y si el usuario cierra la conexión (frambuesa o frente), entonces su suscripción se cancela y se elimina de el centro.

    Hackathon OpenVINO: reconocimiento de voz y emociones en Raspberry Pi
    Estamos esperando una conexión desde atrás.

    El front-end es una aplicación web escrita en JavaScript que utiliza la biblioteca React para acelerar y simplificar el proceso de desarrollo. El propósito de esta aplicación es visualizar datos obtenidos utilizando algoritmos que se ejecutan en el back-end y directamente en la Raspberry Pi. La página tiene enrutamiento seccional implementado usando reaccionar-router, pero la página principal de interés es la página principal, donde se recibe un flujo continuo de datos en tiempo real desde el servidor usando la tecnología WebSocket. Raspberry Pi detecta una voz, determina si pertenece a una persona específica de la base de datos registrada y envía una lista de probabilidades al cliente. El cliente muestra los últimos datos relevantes, muestra el avatar de la persona que probablemente habló por el micrófono, así como la emoción con la que pronuncia las palabras.

    Hackathon OpenVINO: reconocimiento de voz y emociones en Raspberry Pi
    Página de inicio con predicciones actualizadas

    Conclusión

    No fue posible completar todo según lo planeado, simplemente no teníamos tiempo, por lo que la principal esperanza estaba en la demostración, que todo funcionara. En la presentación hablaron de cómo funciona todo, qué modelos tomaron, qué problemas encontraron. La siguiente fue la parte de demostración: los expertos caminaron por la sala en orden aleatorio y se acercaron a cada equipo para observar el prototipo funcional. Nos hicieron preguntas también, cada uno respondió su parte, dejaron la web en el portátil y realmente todo funcionó como se esperaba.

    Permítanme señalar que el costo total de nuestra solución fue de $150:

    • Frambuesa Pi 3 ~ $35
    • Google AIY Voice Bonnet (puedes cobrar una tarifa de re-altavoz) ~ 15$
    • Intel NCS 2 ~ 100$

    Cómo mejorar:

    • Utilice el registro del cliente: solicite leer el texto que se genera aleatoriamente
    • Agregue algunos modelos más: puede determinar el sexo y la edad por voz
    • Separar voces que suenan simultáneamente (diarización)

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

    Hackathon OpenVINO: reconocimiento de voz y emociones en Raspberry Pi
    Cansados ​​pero felices estamos

    Para concluir, me gustaría dar las gracias a los organizadores y participantes. Entre los proyectos de otros equipos, a nosotros personalmente nos gustó la solución para monitorear las plazas de aparcamiento libres. Para nosotros fue una experiencia tremendamente genial de inmersión en el producto y el desarrollo. Espero que en las regiones se celebren cada vez más eventos interesantes, también sobre temas relacionados con la inteligencia artificial.

Fuente: habr.com

Añadir un comentario