Голосовое управление

Введение

Алиса, Siri, Маруся — это далеко не весь список проектов в области голосовых помощников. С каждым днем проектов становиться больше, а функционал шире и кажется настал тот момент, когда всерьез можно подумать о переводе компьютера на голосовое управление.

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

Распознавание речи

Такая популярная тема не могла остаться без огромного количества статей, но с появлением API Яндекса и Google большое количество статей начинается и заканчивается так:

import speech_recognition

Это имеет место быть, но у меня натура пытливая, да и опыт в машинном обучении у меня имеется, так почему бы не сделать распознавание самому? Потому что это огромная гора, потратив на подъем на нее кучу времени ты лишь осознаешь, что вершина очень далеко.

«И что не так с import speech_recognition?» спросили меня когда я вывел первую версию статьи на суд людской.

  1. Конфиденциальность — Яндекс и Google могут упорно заявлять, что наши данные не куда не утекут и не будут ни где использоваться, но готовы ли вы поставить карьеру на их заявление? Вот и система безопасности любой крупной компании тоже не готова, так что при работе с гос контрактами или при доступе к секретности использование такого решения будет запрещено.

  2. Языки — Давно вы говорили на керекском? Думаю, что вы даже не слышали как звучит этот язык, все потому, что носителей этого языка всего 2 человека в России. А теперь представим, что один из них захочет себе «Джарвиса». Конечно это крайний случай, но открытые API не всегда справляются с заявленными языками, что говорить о других?

  3. Интернет — Недавно заезжал в прекрасное место около Рязани, птички, да поля бескрайние. Так вдохновляющие! Но Алиса не сильно оценила отсутствие интернета. Такая любовь к городской жизни объяснима, хоть она и может распознать голос любого человека говорящего на русском языке, но развернуть такую махину (Сбер недавно заявлял о Нейросети на 23 млрд параметров) на компьютере, а тем более на своем смартфоне задача не выполнимая.

Определившись со значимостью начнем по порядку.

Звук — это волна

Компьютер не дружит с волнами, но обожает цифры.

Возьмем какое-то время t (шаг дискретизации), например 1 секунда. И начнем каждое время t записывать уровень шума на микрофоне (Точки на графике ниже). После чего возьмем число A = 256. Это число будет характеризовать в сколько бит мы хотим записать точку.

Уровень максимального шума (УМШ) — максимальное значение которое может выдать микрофон
Уровень тишины (УТ) — значение которое выдает микрофон при тишине
Тогда УМШ после записи должен быть равен (А-1), то есть 255, а УТ = 0

Отсюда число ШК = (УМШ — УТ) / А 
ШК — шаг квантования

https://vossta.ru/referat-obzor-i-analiz-funkcionalenosti-programmnogo-obespeche.htmlhttps://vossta.ru/referat-obzor-i-analiz-funkcionalenosti-programmnogo-obespeche.html

Теперь каждое t секунд, мы будем брать значение с микрофона делить его ШК и полученное число записывать в файл. Записанный файл назовем «Запись 1.wav» и попробуем послушать. Ничего осознанного там мы не услышим так как мы взяли очень большой шаг дискретизации (t). Здесь появляется еще одна характеристика записи — частота дискретизации, из физики помним, что:

V(частота) = {1 \over T(период)}

Возьмем часто используемую частоту 44 кГц и теперь голос на записи начал звучать. Сохраним запись в папочке Data, чтобы удобнее было с ней работать.

FFT

Мы записали 5 секунд с частотой дискретизации 44 кГц и получили 200 000 чисел, как можно заставить компьютер понять, что там сказано?

Так как звук это волна, значит, то что мы записали есть сумма разно частотных колебаний, а как доказано до меня именно в частоте скрыта информация передаваемая звуком. Здесь то мы и приходим к преобразованию Фурье (FT), а точнее его модификации Быстрое преобразование Фурье (FFT).

Здесь подробно рассказано, как это работает.

После преобразования Фурье мы получаем набор частот, характеризующий нашу дорожку.

https://proglib.io/p/preobrazovaniya-fure-dlya-obrabotki-signalov-s-pomoshchyu-python-2020-11-03https://proglib.io/p/preobrazovaniya-fure-dlya-obrabotki-signalov-s-pomoshchyu-python-2020–11–03

На этом этапе мы можем сделать отсеивание информации. Так как мы слышим в диапазоне от 20 Гц до 20 кГц, все что выше этого диапазона нас не интересует. Мы же используем речь, чтобы общаться друг с другом, а значит кодированная информация должна лежать в слышимом диапазоне.

Мы хотели бы посимвольно распознавать речь, ведь это даст нам более гибкий инструмент. Для этого используем «окна». Возьмем первые n наносекунд и сделаем для них преобразование Фурье. Потом следующие n и так далее. Теперь у нас есть данные основываясь на которых мы можем попробовать предсказать какой символ из нашего словаря произносится в каждом «окне».

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

Проведя преобразование Фурье для всех «окон», мы получим спектрограмму .

https://keras.io/examples/audio/ctc_asr/https://keras.io/examples/audio/ctc_asr/

Теперь мы можем работать с ней как с картинкой и применить алгоритмы помогающие компьютерам видеть собак или объезжать препятствия, но такой подход говорит о том, что нейронная сеть будет просто прогнозировать вероятность соответствия преобразования Фурье символу из словаря. В сказанных словах еще есть иногда и смысл, чтобы его могла использовать наша нейронная сеть используем LSTM слой.

LSTM

Чтобы не расширять статью, здесь не буду рассказывать, что такое нейронная сеть.
Вот на этом канале можно послушать про основы.

Когда мы говорим о нейронных сетях, то возникает такое представление:

image-loader.svg

Да, это крутая визуализация простой нейросети, но когда мы хотим работать со смыслом текста, то нам нужен контекст, а следовательно, нейросеть должна помнить, что было до этого. Для такой памяти разработали рекуррентные нейронные сети.

Хоть и избитая, зато понятнаяХоть и избитая, зато понятная

RNN слой имеет как и обычный слой вход X и выход Y, но при этом еще есть вход h (t-1) и выход h. Когда нейронная сеть такого типа просчитывает себя, она формирует массив Y, который идет не только на выход слоя, но и на вход следующему просчету сети.

image-loader.svg

Пример:
Хотим перевести «Привет» на английский язык.


Первый проход сети:
x = «п» в категориальном представлении x.shape = (1, 34)
h (t-1) = нулевой вектор h (t-1).shape = (1, 22)
y = w * (h & x), здесь x и h дополняют друг друга (h & x).shape = (1, 56), w.shape = (1, 56)


Второй проход сети:
x = «р» в категориальном представлении x.shape = (1, 34)
h (t-1) = y из прошлого прохода h (t-1).shape = (1, 22)
y = w * (h & x), здесь x и h дополняют друг друга (h & x).shape = (1, 56), w.shape = (1, 56)

Словарь

«В категориальном представлении», давайте теперь разберемся с тем, что я имел ввиду.

Как с волнами — компьютер, так и машинное обучение с буквами не очень дружат. Следовательно нам нужно превратить буквы в цифры. Самое простое, что можно придумать это пронумеровать символы получив словарь:

{«а»: 0, «б»: 1, «в»: 2, «г»: 3, «д»: 4 …» »: 37}

В данном режиме на выходе нейронной сети мы будем получать одно число от 0 до 37, которое не будет иметь правильного смысла так как если нейронная сеть будет думать между «а» и «я», то в ответе она вообще выдаст какое-нибудь «п». Чтобы этого не произошло давайте попросим нейросеть выдавать нам вероятность того или иного символа на этом месте. Чтобы это реализовать наш словарь должен иметь такой вид:

{
«а»: [1, 0, 0, 0 …],
«б»: [0, 1, 0, 0 …],
«в»: [0, 0, 1, 0 …],
«г»: [0, 0, 0, 1 …]

» »: [… 0, 0, 0, 1]
}

Здесь каждый символ закодирован массивом из нулей, где на месте порядкового номера стоит 1. Получив такой словарь мы можем перейти к подготовке данных для обучения.

Данные

Теперь перейдем к одному из самых интересных вопросов: «Где взять данные?».
Вообще есть два варианта:

  1. Создать

  2. Скачать

Со «скачать» все просто, например для начального обучения я использовал этот датасет (Habr/Git)
Преобразование данных, с которым я столкнулся в этой статье, принимает на вход WAV файлы, так что преобразуем OPUS в WAV:

import pandas as pd
import soundfile as sf
import os


def convert_opus_to_wav(data):
    for index in data.index:  # Пробегаем по встроенному манифесту датасета
        file = "Data/" + data.loc[index, "Файлы"]  # Запоминаем путь к opus файлу
        if os.path.exists(file):  # Если файл есть, то преобразовываем
            audio, sample_rate = sf.read(file, dtype='int16')  # Читаем opus
            sf.write(file.replace(".opus", ".wav"), audio, sample_rate)  # Сохраняем wav
            os.remove(file)  # Заметаем следы (Удаляем преобразованный файл)


manifest = pd.read_csv("Data/public_series_1.csv", header=None)  # Считываем манифест
manifest.columns = ["Файлы", "Текст", "Длительность"]  # Чтоб по красоте 
del manifest["Длительность"]  # Удаляю все что не планирую использовать
convert_opus_to_wav(manifest)

На данный момент обучение проходило на модулях:

  • asr_public_stories_1 — аудиокниги

  • public_series_1 — YouTube

  • public_youtube700_val — YouTube

Так же нам надо подправить еще немного манифест и сохранить исправления

for i in manifest.index:
    # Удаляем расширение и добавляем нужную директорию
    manifest.loc[i, "Файлы"] = "Data/" + manifest.loc[i, "Файлы"].replace(".wav", "").replace(".opus", "")
    # Меняем путь к текстовому файлу на сам текст
    with open("Data/" + manifest.loc[i, "Текст"], "r") as file:
        manifest.loc[i, "Текст"] = file.read().replace("\n", "")
print(manifest.head())
manifest.to_csv("Data/public_series_1_e.csv")

Теперь наш манифест имеет такой вид:

image-loader.svg

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

Создать же свой датасет тоже не сильно сложно, если вас не интересует конечно объемы Open SST. Чуть позже я выпущу статью как быстро справился с этой задачей с помощью Telegram и 150 строк кода.
В общих словах вам нужно взять текст, разбить его на фразы, а после озвучить эти фразы записав 1000 WAV файлов (у меня это получилось примерно 1,5 часа данных). В своих экспериментах я взял для озвучивания «Преступление и наказание», но в ходе озвучки понял, что там попадаются слова, которые в повседневной жизни не встречаются (Спасибо, Кэп), что немного обесценивает знание контекста, к которому мы стремились выбирая LSTM. Так что думаю третьим шагом обучения будут заготовленные команды, по типу:

  • Алиса, как погодка?

  • Алиса, посмотри в Яндексе…

  • Открой первую ссылку

  • Включи музыку

  • Создай файл

  • Напомни поесть!!!

CTC loss

Ну вот мы и дошли к самому главным вопросам:

  1. Как провести обучение без сложной разметки?

  2. Как понять, что «орвлыарлов» не похожа на «Привет, как дела?» и как оценить степень похожести?

В 2006 году вышла статья Алекса Грейвса «Connectionist temporal classification», которая рассказывает как это можно сделать и доказывает это математикой. Так как математика точная наука и не любит приблизительных пересказов, я оставлю ее за скобками своей статьи.

Общий смысл подхода сводиться к тому, чтобы подсчитать вероятность каждого символа в каждом «окне», после чего преобразовать это в строку выбрав более вероятные символы (» » — тоже символ), а дальше подсчитать расстояние Левенштейна выдав его метрикой похожести.

Модель

from tensorflow import keras
from tensorflow.keras import layers
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense, Dropout, LSTM

def build_model(input_dim, output_dim, rnn_layers=2, rnn_units=32, load=False):
 		model = Sequential()
    model.add(layers.Input((None, input_dim), name="input"))
    model.add(layers.Reshape((-1, input_dim), name="expand_dim"))
    model.add(LSTM(512, return_sequences=True))
    model.add(Dropout(0.4))
    for i in range(rnn_layers):
        model.add(LSTM(rnn_units, return_sequences=True))
        model.add(Dropout(0.4))
    model.add(Dense(output_dim + 1, activation='softmax'))
    if load:
        model.load_weights(dir_+"model/my_model_1.hdf5")
    opt = keras.optimizers.Adam(learning_rate=1e-4)
    model.compile(optimizer=opt, loss=CTCLoss)
    model.summary()
    return model
  
 model = build_model(input_dim=fft_length // 2 + 1, output_dim=char_to_num.vocabulary_size(), rnn_units=128, load=True)

Результат

Тут не все так однозначно, с одной стороны:

image-loader.svg

А с другой…

image-loader.svg

Такой результат я получил при обучении на своем компьютере, через 2 дня обучения.

Планы

Тут наткнулся на идею поверх прикрутить лингвистическую модель, которая бы удаляла огрехи по типу отсутствия пробелов между словами.

Так же скоро закончу кастомный датасет и отполирую им мелкие дефекты.

Выбрать файлы на которых нейронка спотыкается и проанализировать. Есть два варианта:

  1. файл дефектный — решение: удаляем его из датасета, благо Open SST огромный

  2. нейронка мало с ним работала — решение: добавляем его в кастомный датасет

© Habrahabr.ru