Pull to refresh

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

Reading time8 min
Views11K

Введение

Алиса, 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.html
https://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-03
https://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

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

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

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

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

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

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


Первый проход сети:
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")

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

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

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

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

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

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

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

  • Создай файл

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

CTC loss

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

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

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

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

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

Модель

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)

Результат

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

А с другой...

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

Планы

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

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

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

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

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

Tags:
Hubs:
Total votes 11: ↑11 and ↓0+11
Comments13

Articles