OpenVINO хакатон: розпізнаємо голос та емоції на Raspberry Pi

30 листопада - 1 грудня в Нижньому Новгороді пройшов OpenVINO хакатон. Учасникам пропонувалося створити прототип продуктового рішення із використанням Intel OpenVINO toolkit. Організаторами було запропоновано список приблизних тем, куди можна було орієнтуватися під час виборів завдання, але фінальне рішення залишалося за командами. Крім того, заохочувалося використання моделей, які не входять у продукт.

OpenVINO хакатон: розпізнаємо голос та емоції на Raspberry Pi

У статті розповімо про те, як ми створювали свій прототип продукту, з яким у підсумку посіли перше місце.

У хакатоні взяло участь понад 10 команд. Приємно, деякі з них приїхали з інших регіонів. Місцем проведення хакатону було обрано комплекс "Кремлівський на Почаїні", де всередині були розвішані старовинні фотографії Нижнього Новгорода, антуражно! (Нагадую, що на даний момент центральний офіс компанії Intel розташований саме в Нижньому Новгороді). На написання коду учасникам відводилося 26 годин, наприкінці потрібно було презентувати своє рішення. Окремим плюсом була наявність демо-сесії, щоби переконатися, що все задумане правда реалізовано, а не залишилося ідеями у презентації. Мерч, снеки, їжа, все також було!

Крім цього, компанія Intel за бажанням надавала камери, Raspberry PI, Neural Compute Stick 2.

вибір завдання

Однією з найскладніших частин підготовки до хакатона із вільною тематикою є вибір завдання. Відразу вирішили вигадувати щось, чого у продукті ще немає, тому що в анонсі було сказано, що це всіляко вітається.

Проаналізувавши моделі, які входять у продукт у поточному релізі, приходимо до висновку, що більшість їх вирішують різні завдання комп'ютерного зору. Причому дуже складно придумати завдання з області комп'ютерного зору, яке не можна вирішити з використанням OpenVINO, а якщо таке і можна придумати, то у відкритому доступі складно знайти моделі, що передбачаються. Вирішуємо копати ще й у іншому напрямку — у бік обробки та аналітики мови. Розглядаємо цікаве завдання щодо розпізнавання емоцій з мови. Потрібно сказати, що в OpenVINO вже є модель, що визначає емоції людини по обличчю, але:

  • Теоретично, можна зробити суміщений алгоритм, який працюватиме як за звуком, так і за зображенням, що має дати приріст у точності.
  • Камери зазвичай мають вузький кут огляду, щоб покрити велику зону, потрібна не одна камера, звук не має такого обмеження.

Розвиваємо ідею: візьмемо за основу ідею для retail сегменту. Можна визначати задоволеність покупця на касах магазинів. Якщо хтось із покупців незадоволений обслуговуванням та починає підвищувати тон — можна одразу звати адміністратора на допомогу.
В цьому випадку потрібно додати розпізнавання людини за голосом, це дозволить нам відрізняти співробітників магазину від покупців, видавати аналітику по кожному індивідууму. Та й крім того, можна буде аналізувати поведінку самих співробітників магазину, оцінювати атмосферу в колективі, звучить непогано!

Формуємо вимоги до нашого рішення:

  • Маленький розмір цільового девайсу
  • Робота в реальному часі
  • Низька ціна
  • Легка масштабованість

У результаті як цільовий девайс вибираємо Raspberry Pi 3 c Intel NCS 2.

Тут важливо відзначити одну важливу особливість NCS - найкраще він працює зі стандартними CNN архітектурами, якщо ж потрібно запустити на ньому модель з кастомними шарами, то чекайте на ̶т̶а̶н̶ц̶е̶в̶ ̶с̶ ̶б̶у̶б̶н̶о̶

Справа за малим: треба роздобути мікрофон. Підійде і звичайний USB-мікрофон, правда він не виглядатиме добре разом з RPI. Але й тут рішення буквально "лежать під боком". Для запису голосу вирішуємо використовувати плату Voice Bonnet із набору Голосовий комплект Google AIY, де є розпаяний стерео мікрофон.

Завантажуємо Raspbian з репозиторія AIY projects і заливаємо на флешку, тестуємо, що мікрофон працює за допомогою наступної команди (вона запише аудіо завдовжки 5 секунд і збереже у файлик):

arecord -d 5 -r 16000 test.wav

Відразу наголошу, що мікрофон дуже чутливий і добре ловить шуми. Щоб виправити це, зайдемо в alsamixer, виберемо Capture devices і знизимо рівень вхідного сигналу до 50-60%.

OpenVINO хакатон: розпізнаємо голос та емоції на Raspberry Pi
Допрацьовуємо корпус напилком і все влазить, можна навіть закрити кришкою

Додаємо кнопку-індикатор

Під час аналізу AIY Voice Kit на частини згадуємо, що там є RGB-кнопка, підсвічуванням якої можна керувати програмно. Шукаємо "Google AIY Led" і знаходимо документацію: https://aiyprojects.readthedocs.io/en/latest/aiy.leds.html
Чому б не використовувати цю кнопку для відображення розпізнаної емоції, у нас всього 7 класів, а в кнопці 8 кольорів якраз вистачає!

Підключаємо кнопку GPIO до Voice Bonnet, підвантажуємо потрібні бібліотеки (вони вже встановлені в диструбутиві від AIY projects)

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

Створимо dict, в якому кожній емоції відповідатиме колір у вигляді RGB Tuple та об'єкт класу aiy.leds.Leds, через який будемо оновлювати колір:

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

І, нарешті, після кожного нового прогнозу емоції будемо оновлювати колір кнопки відповідно до неї (за ключом).

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

OpenVINO хакатон: розпізнаємо голос та емоції на Raspberry Pi
Кнопочка, гори!

Працюємо з голосом

Будемо використовувати pyaudio для захоплення потоку з мікрофону та webrtcvad для фільтрації шуму та детектування голосу. Крім цього, створимо чергу, в яку асинхронно будемо додавати і забирати уривки з голосом.

Так як у webrtcvad є обмеження на розмір фрагмента, що подається — він повинен дорівнювати 10/20/30мс, а навчання моделі для розпізнавання емоцій (як ми далі дізнаємося) проводилося на датасеті 48кГц, захоплюватимемо чанки розміру 48000×20мс/1000 моно) = 1 байт. Webrtcvad повертатиме True/False для кожного з таких чанків, що відповідає наявності або відсутності голосу у чанці.

Реалізуємо наступну логіку:

  • Додаватимемо в list ті чанки, де є голос, якщо голосу немає, то інкрементуємо лічильник порожніх чанків.
  • Якщо лічильник порожніх чанків> = 30 (600 мс), то дивимося на розмір аркуша накопичених чанків, якщо він >250, то додаємо в чергу, якщо ж ні, вважаємо, що довжини запису недостатньо, щоб подати її на модель для ідентифікації того, хто говорить.
  • Якщо ж лічильник порожніх чанків все ще < 30, а розмір листа чанків, що накопичилися, перевищив 300, то додамо уривок в чергу для більш точного передбачення. (бо згодом емоціям властиво змінюватися)

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

Настав час пошукати у відкритому доступі передбачені моделі, йдемо на github, гуглим, але пам'ятаємо, що ми маємо обмеження на використовувану архітектуру. Це досить складна частина, тому що доводиться тестувати моделі на своїх вхідних даних, а також конвертувати у внутрішній формат OpenVINO — IR (Intermediate Representation). Ми пробували близько 5-7 різних рішень з github, і якщо модель для розпізнавання емоцій запрацювала відразу, то з розпізнаванням по голосу довелося посидіти довше - там використовуються складніші архітектури.

Зупиняємось на наступних:

  • Емоції з голосу https://github.com/alexmuhr/Voice_Emotion
    Працює вона за таким принципом: аудіо нарізається на уривки певного розміру, для кожного з цих уривків виділяємо MFCC і далі подаємо їх на вхід до CNN
  • Розпізнавання за голосом https://github.com/linhdvu14/vggvox-speaker-identification
    Тут замість MFCC працюємо зі спектрограмою, після FFT подаємо сигнал CNN, де на виході отримуємо векторне подання голосу.

Далі йтиметься про конвертацію моделей, почнемо з теорії. OpenVINO включає кілька модулів:

  • Open Model Zoo, моделі з якого можна було використовувати та включати у свій продукт
  • Model Optimzer, завдяки якому можна переконвертувати модель з різних форматів фреймворків (Tensorflow, ONNX etc) у формат Intermediate Representation, з яким далі ми і працюватимемо
  • Inference Engine дозволяє запускати моделі в IR форматі на процесорах Intel, чіпах Myriad та прискорювачах Neural Compute Stick
  • Найбільш ефективна версія OpenCV (з підтримкою Inference Engine)
    Кожна модель у форматі IR описується двома файлами: .xml та .bin.
    Моделі конвертуються у формат IR через Model Optimizer таким чином:

    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 дозволяє вибрати формат даних, з яким працюватиме модель. Підтримуються FP32, FP16, INT8. Вибір оптимального типу даних може дати добрий приріст до продуктивності.
    --input_shape вказує на розмірність вхідних даних. Можливість динамічно її міняти начебто є у C++ API, але ми так далеко копати не стали і для однієї з моделей просто зафіксували її.
    Далі спробуємо завантажити вже сконвертовану модель в IR форматі через DNN модуль OpenCV і зробити forward на неї.

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

    Останній рядок в даному випадку дозволяє перенаправити обчислення на Neural Compute Stick, базові обчислення виконуються на процесорі, але у випадку з Raspberry Pi це не прокотить, знадобиться стик.

    Далі логіка наступна: розділимо наше аудіо на вікна певного розміру (у нас це 0.4с), кожне з цих вікон перетворимо на MFCC, які потім подамо на сітку:

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

    Після візьмемо найчастіше зустрічається клас всім вікон. Просте рішення, але для хакатона і не потрібно вигадувати щось занадто химерне, тільки якщо є час. У нас роботи ще багато, тому йдемо далі — розуміємо розпізнавання по голосу. Потрібно створити якусь базу, в якій зберігалися б спектрограми заздалегідь записаних голосів. Оскільки часу залишилося небагато, вирішуємо це питання як можемо.

    А саме, створюємо скрипт для запису уривка голосу (працює він так само, як описано вище, тільки при перериванні з клавіатури він зберігатиме голос у файлик).

    Пробуємо:

    python3 voice_db/record_voice.py test.wav

    Записуємо голоси кількох людей (у нашому випадку трьох членів команди)
    Для кожного записаного голосу виконуємо fast fourier transform, отримуємо спектрограму і зберігаємо у вигляді numpy array (.npy):

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

    Докладніше у файлі create_base.py
    У результаті при запуску основного скрипту ми на самому початку отримаємо ембедінги з цих спектрограм:

    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)

    Після отримання ембеддингу з відрізка, що пролунав, зможемо визначити, кому він належить, взявши cosine distance від уривка до всіх голосів в базі (чим менше, тим ймовірніше) — для демо ми виставили поріг 0.3):

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

    Наприкінці зазначу, що швидкість інференсу була швидкою і дозволяла додати ще 1-2 моделі (на семпл довжиною 7 секунд на інференс йшло 2.5). Додати нові моделі ми вже не встигали і сфокусувалися на написанні веб-прототипу.

    Веб-додаток

    Важливий пункт: беремо із собою роутер із дому та налаштовуємо свою локалку, допомагає з'єднати девайс та ноути по сітці.

    Бекенд представляє собою наскрізний канал повідомлень між фронтом і Raspberry Pi, заснований на технології websocket (http over tcp protocol).

    Першим етапом є отримання обробленої інформації з розпберрі, тобто упаковані в json предикти, які на середині свого шляху зберігаються в базі даних, щоб можна було формувати статистику про емоційний фон користувача за період. Далі цей пакет відправляється на фронтенд, який використовує передплату та отримання пакетів з ендпоінту вебсокету. Весь механізм бекенд побудований мовою golang, вибір на нього упав тим, що він добре підходить для асинхронних завдань, з якими горутини добре справляються.
    При доступі до ендпоїнт користувач реєструється і заноситься в структуру, потім відбувається отримання його повідомлення. І користувач, і повідомлення заносяться до загального hub, з якого повідомлення вже відправляються далі (на підписаний фронт), а якщо користувач закриває з'єднання (распберрі або фронт), то його підписка анулюється, і він видаляється з hub.

    OpenVINO хакатон: розпізнаємо голос та емоції на Raspberry Pi
    Очікуємо коннект з бека

    Front-end є web-додатком, написаним на JavaScript з використанням бібліотеки React для прискорення та спрощення процесу розробки. Метою цієї програми є візуалізація даних, отриманих за допомогою алгоритмів, запущених на back-end стороні і безпосередньо Raspberry Pi. На сторінці є роутинг по розділах, реалізований за допомогою react-router, але основний інтерес представляє головна сторінка, де в режимі реального часу надходить безперервний потік даних із сервера за технологією WebSocket. Raspberry Pi детектує голос, визначає приналежність до певної людини із зареєстрованої бази та надсилає список probability клієнту. Клієнт відображає останні актуальні дані, виводить аватарку людини, яка з найбільшою ймовірністю розмовляла в мікрофон, а також емоцію, з якою вона вимовляє слова.

    OpenVINO хакатон: розпізнаємо голос та емоції на Raspberry Pi
    Головна сторінка з оновлюваними предиктами

    Висновок

    Не вдалося доробити все до задуманого, банально не встигли, тому головна надія була на демо, те, що все запрацює. У презентації розповіли про те, як все влаштовано, які моделі взяли, з якими проблемами зіткнулися. Далі була частина демо — експерти ходили по залі довільно і підходили до кожної команди, щоб подивитися на працюючий прототип. Задавали питання і нам, кожен відповідав по своїй частині, на ноуті залишили веб, і все справді працювало, як і очікувалося.

    Зазначу, що загальна вартість нашого рішення становила 150$:

    • Raspberry Pi 3 ~ 35$
    • Google AIY Voice Bonnet (можна взяти плату respeaker) ~ 15 $
    • Intel NCS 2 ~ 100 $

    Як покращити:

    • Використовувати реєстрацію з клієнта - просити прочитати текст, який генеруємо випадково
    • Додати ще кілька моделей: за голосом можна визначати стать та вік
    • Розділяти голоси, що одночасно звучать (діаризація)

    Репозиторій: https://github.com/vladimirwest/OpenEMO

    OpenVINO хакатон: розпізнаємо голос та емоції на Raspberry Pi
    Втомлені, але щасливі ми

    На закінчення хочеться сказати спасибі організаторам та учасникам. З проектів інших команд особисто нам сподобалося рішення для моніторингу вільних місць для паркування. Для нас це був дико крутий досвід занурення у продукт та розробки. Сподіваюся, що в регіонах буде проводитися все більше цікавих заходів, у тому числі і з AI тематики.

Джерело: habr.com

Додати коментар або відгук