Как мы обучили сфинкса для голосового помощника

    В процессе разработки проекта «голосовой помощник» одним из требований была возможность распознавания управляющих команд в оффлайн режиме. Это было нужно, так как в противном случае пришлось бы постоянно слушать и посылать поток с аудиоданными на распознавание, получать ответ и анализировать его.

    Это весьма накладное решение, которое сложно реализовать из-за постоянной нагрузки на сервер,  большого объема трафика и увеличенного время отклика всей системы.  Для распознавания управляющих команд в режиме оффлайн мы выбрали Pocketsphinx.



    Что случилось


    Изначально Pocketsphinx работал у нас на железе OrangePi, у которого были: RAM 512Mb, несколько ГБ свободного дискового пространства, а на борту крутилась Ubuntu. При этом мы использовали python интерфейс (в виде python пакета) для работы с Pocketsphinx.

    Мы установили Pocketsphinx командой

    `pip install pocketsphinx`

    Python позволил использовать нам имеющиеся библиотеки и повысить скорость разработки кода. Однако, даже один только процесс потреблял порядка 30-40 Мб оперативной памяти.

    Это могло стать проблемой, потому что выяснилось, что доступное по цене железо заметно скромнее: для операционной системы доступно 123 из 128 Мб RAM, часть которой отводится под нужды железа. Из них часть памяти уже использовалась ОС Linux. Для нашего ПО оставалось только 62 Мб RAM и несколько десятков Мб свободной флеш памяти для хранения кода и файлов. Нужно было искать альтернативу python.

    Решили запустить часть с Pocketsphinx на Си.

    Сборка Pocketsphinx на Си


    Исходники для Pocketsphinx были взяты с сайта: cmusphinx.github.io

    Для работы Pocketsphinx требуются обе части — Sphinxbase и Pocketsphinx. Далее нужно было понять как кросс-компилировать имеющиеся исходники компилятором для нашей целевой аппаратной платформы.

    Сборку кода решено было делать без поддержки python, для чего использовали ключ

    `--without-python`

    В результате мы получили инструментарий, в котором есть библиотеки для работы с Pocketsphinx. Он находятся по пути

    `sphinx/lib`

    Для дальнейшей работы с Си кодом требуются библиотеки:

    `libpocketsphinx.so libsphinxbase.so libsphinxad.so`

    Нюансы


    После запуска Pocketsphinx с использованием библиотек оказалось, что Си вариант гораздо менее требователен к ресурсам. Использование RAM сводилось буквально к нескольким Mb.

    Приложение после запуска занимало не более 5Mb RAM, но в нем при этом использовались еще с десяток других модулей и ряд библиотек. Если в варианте с python загрузка ядра CPU была весьма заметной (50% или даже более), то тут загрузка ядра была 7-9% (и это на все приложение, а не только на Pocketsphinx).

    В процессе работы нами было замечено, что Pocketsphinx начинает определять ключевое слово не сразу, а через некоторое время (от нескольких десятков секунд до пары минут). Начался новый этап работы.

    Работа с Pocketsphinx


    Работа с Pocketsphinx состоит из этапов:

    1. Подготовка записей в соответствии с текущей задачей.
    2. Подготовка базы данных для модели.
    3. Построение языковой модели: обучение модели на полученных записях. Получение Pocketsphinx model and language model.
    4. Реализация алгоритма использования API Pocketsphinx.
    5. Поиск оптимальных параметров Pocketsphinx эмпирическим методом.

    Остановимся на каждом этапе подробнее.

    1. Подготовка записей в соответствии с текущей задачей


    Подготовить базу записей в формате PCM.

    Мы использовали следующие настройки:

    Format settings, Endianness: Little
    Bit rate: 256 Kbps
    Channel(s): 1 channel
    Sampling rate: 16.0 KHz
    Bit depth: 16 bits

    В специальном помещении для записи голоса они произносили ряд слов, каждый в нескольких интонациях (удивление, злость, грусть) и частотах (громко, тихо, нормально). Было сделано 100 записей: 60 мужских и 40 женских.

    Примечание: для изменения частоты дискретизации используйте инструмент sox: sox original.wav -b 16 sample.wav каналов 1 скорость 16.

    2. Подготовка базы данных для модели


    ├─ etc
    │ ├─ your_db.dic (Phonetic dictionary)
    │ ├─ your_db.phone (Phoneset file)
    │ ├─ your_db.jsgf (Language grammer)
    │ ├─ your_db.filler (List of fillers)
    │ ├─ your_db_train.fileids (List of files for training)
    │ ├─ your_db_train.transcription (Transcription for training)
    │ ├─ your_db_test.fileids (List of files for testing)
    │ └─ your_db_test.transcription (Transcription for testing)
    └─ wav
    ├─ speaker_1
    │ └─ file_1.wav (Recording of speech utterance)
    └─ speaker_2
    └─ file_2.wav

    3. Построение языковой модели


    1. Подготовить базу данных;
    2. Создать учебные файлы и полный словарь и список слов;
    3. Создать словарь из ru.dic и wordlist;
    4. Создайте файл из ru.dic;
    5. Извлечь текст из файла транскрипции;
    6. Построить языковую модель. Скачать инструмент SRILM и создать пакет для построения модели.
    7. Конвертировать файлы lm в файлы dmp.

      sphinx_lm_convert -i model.lm -o model.dmp
      sphinx_lm_convert -i model.dmp -ifmt dmp -o model.lm -ofmt arpa
      
      sphinx_lm_convert -i model.lm -o model.lm.bin
      sphinx_lm_convert -i model.lm.bin -ifmt bin -o model.lm -ofmt arpa
    8. И, наконец, обучить модель на полученных записях.

    Шаги обучения


    После подготовки файлов мы должны сгенерировать файл конфигурации: из корневой папки модели мы запускаем:
    sphinxtrain -t setup "имя модели"

    в конфигурационном файле sphinx_train.cfg:

    1- set the value current to the variable CFG_CMN         $CFG_CMN = 'current';
    2 -$CFG_INITIAL_NUM_DENSITIES = 256; change this value to 1
    3- $CFG_FINAL_NUM_DENSITIES = 256; change this value to 8
    4- $CFG_N_TIED_STATES = 200;  set this value to 200
    $CFG_CD_TRAIN = 'yes'; set this value to no if the data set is small

    Примечание: если мы изменим

    $ CFG_CD_TRAIN

    на «нет», то нам нужно изменить

    $ DEC_CFG_MODEL_NAME

    на

    $ CFG_EXPTNAME.ci_cont

    из вашего sphinx_train.cfg. Изменить можно так:

    $ DEC_CFG_MODEL_NAME = "$ CFG_EXPTNAME.ci_cont";

    или

    $ DEC_CFG_MODEL_NAME = "$ CFG_EXPTNAME.ci _ $ {CFG_DIRLABEL}";

    Последний шаг — начать тренировки нашей модели: sphinxtrain run.

    Языковую модель возможно создать с нуля при помощи Sphinxtrain или можно использовать готовую из источника.

    Инструкция по создании модули с нуля можно найти на github.

    4. Реализация алгоритма использования API Pocketsphinx


    В репозитории Pocketsphinx имеется исходный код для сборки программы: pocketsphinx_continuous. В программе демонстрируются различные способы взаимодействия с Pocketsphinx, их можно разделить по источнику входных данных на:

    1. Распознавание речи в записаном аудио файле;
    2. Распознавание речи в потоке данных от микрофона.

    Обобщенный алгоритм взаимодействия с Pocketsphinx после инициализации необходимых ресурсов:

    1. Начать распознавание речи:

    `int ps_start_utt(ps_decoder_t *ps)`

    2. Передать данные из входного потока в Pocketsphinx:

    `int ps_process_raw(ps_decoder_t *ps, int16 const *data, size_t n_samples, int no_search, int full_utt)`

    3. Закончить распознавание речи:

    `int ps_end_utt(ps_decoder_t *ps)`

    4. Получить результат распознавания в виде строки:

    `char const *ps_get_hyp(ps_decoder_t *ps, int32 *out_best_score)`

    Подробный пример взаимодействия с API Pocketsphinx, при чтении данных из микрофона:

    ```C
    for (;;) {
        if ((k = ad_read(ad, adbuf, 2048)) < 0)
            E_FATAL("Failed to read audio\n");
        ps_process_raw(ps, adbuf, k, FALSE, FALSE);
        in_speech = ps_get_in_speech(ps);
        if (in_speech && !utt_started) {
            utt_started = TRUE;
            E_INFO("Listening...\n");
        }
        if (!in_speech && utt_started) {
            /* speech -> silence transition, time to start new utterance  */
            ps_end_utt(ps);
            hyp = ps_get_hyp(ps, NULL );
            if (hyp != NULL) {
                printf("%s\n", hyp);
                fflush(stdout);
            }
            if (ps_start_utt(ps) < 0)
                E_FATAL("Failed to start utterance\n");
            utt_started = FALSE;
            E_INFO("Ready....\n");
        }
        sleep_msec(100);
    }
    ```

    Подробный пример взаимодействия с API Pocketsphinx, при чтении данных из файла:

    ```C
    while ((k = fread(adbuf, sizeof(int16), 2048, rawfd)) > 0) {
        ps_process_raw(ps, adbuf, k, FALSE, FALSE);
        in_speech = ps_get_in_speech(ps);
        if (in_speech && !utt_started) {
            utt_started = TRUE;
        } 
        if (!in_speech && utt_started) {
            ps_end_utt(ps);
            hyp = ps_get_hyp(ps, NULL);
            if (hyp != NULL)
        	printf("%s\n", hyp);
            fflush(stdout);
            ps_start_utt(ps);
            utt_started = FALSE;
        }
    }
    ```

    5. Поиск оптимальных параметров Pocketsphinx эмпирическим методом


    После создания или скачивая языковой модели в файлах модели возможно найти файл feat.params. Данный файл необходим для задания параметров работы Pocketshinx. Если параметра нет в данном файле, то используется значение по умолчанию, список всех параметров и их описание возможно найти в исходном коде проекта или на сайте mankier.

    Пример вывода программы pocketsphinx_continuous при использовании языковой модели.

    ```
    $ pocketsphinx_continuous -samprate 8000 -lm ru.lm -dict ru.dic -hmm zero_ru.cd_cont_4000 -remove_noise no -infile decoder-test.wav
    
    INFO: pocketsphinx.c(152): Parsed model-specific feature parameters from zero_ru.cd_cont_4000/feat.params
    Current configuration:



    ```

    Распознавание из аудио файла или из данных микрофона определяется ключами:

    `-infile <file-name>` и `-inmic <yes/no>`

    соответственно.

    Ключ файл языковой модели:

    `-lm ru.lm`

    Файл словаря:

    `-dict ru.dic`

    Путь, где хранятся файлы внутренней кухни Pocketsphinx:

    `-hmm zero_ru.cd_cont_4000`

    также файл feat.params.

    Существует возможность распознавать в речи только часть слов языковой модели. Функцию включает ключ:

    `-kws <file-name.txt>`

    Например, наш файл с ключевой фразой выглядит так:

    ```
    окей скай /1e-15/
    ```

    Осталось собрать все вместе и получить устройство, которое может услышать и распознать ваш голос, чтобы помочь с приготовлением чая.

    Итог


    Краткая версия для спешащих: мы использовали Pocketsphinx на python, но потом эту стратегию пришлось изменить из-за ограничений памяти на железе. Мы выбрали язык Си. Столкнулись с некоторыми нюансами:

    Сборка не проходила с первого раза, а чаще всего со второго;
    Ключевое слово определялось не сразу: от нескольких долей секунд до нескольких минут.

    Работа с Pocketsphinx состоит из этапов:

    1. Подготовка записей в соответствии с текущей задачей.
    2. Подготовка базы данных для модели.
    3. Построение языковой модели: обучение модели на полученных записях. Получение Pocketsphinx model and language model.
    4. Реализация алгоритма использования API Pocketsphinx.
    5. Поиск оптимальных параметров Pocketsphinx эмпирическим методом.

    Сейчас мы собираем все вместе.
    Ready for Sky
    Компания

    Комментарии 7

      0
      У вас VAD используется? И, если да, то где: микрофон слушает непрерывно и передает участки голосовой активности или уже внутри PS?
        0
        Да, используется VAD, который встроен в сам PS. Мы только его настроили под свои микрофоны. Параметры, которые настраивают VAD, описаны в таблице. Они начинаются с vad_.
        0
        Ключевое слово определялось не сразу: от нескольких долей секунд до нескольких минут.

        Решили ли вы эту проблему? Никто же не будет ждать две минуты ответа от устройства.
          0
          Да, конечно, решили.
            0
            Можете поделиться какими-то подробностями?
              0
              Если кратко: мы постоянно отправляем звуковые данные порциями по 250 ms в PS функцией:
              ps_process_raw(ps, adbuf, k, FALSE, FALSE);
              А VAD сообщает когда стоит получить гипотезу или другими словами распознать ключевое слово при помощи вызова функци:
              ps_get_hyp(ps, NULL );
                +1
                Спасибо!

        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

        Самое читаемое