в голову, применить к этому преобразованию снова преобразование Фурье и оставить лишь несколько коэффициентов. Особенностью преобразования Фурье является то, что в результате мы снова получим исходный отрезок. Чтобы избавиться от такого результата, сначала к преобразованию Фурье применяется логарифм, а уже после этого — обратное преобразование Фурье. Идея оказалась очень плодотворной, поскольку попутно удалось решить и другие задачи. Мел-кепстральные коэффициенты получаются после того, как кепстр применяется не к самому отрезку файла, а к результатам фильтрации исходного отрезка с помощью специальной гребенки фильтров. Для вычисления этих коэффициентов имеется функция в пакете librosa. Эти коэффициенты можно использовать для получения характеристик фрагмента, однако, следует учесть, что это весьма затратная с точки зрения времени операция.
До сих пор мы имели дело с отдельными значениями параметров. Ниже будут рассмотрены параметры, представленные в виде кривых. Возникает вопрос, каким образом сравнивать две кривые, чтобы определить их взаимную близость.
Разложение по ортогональному базису
Пусть заданы две кривые, определенные функциями F(t),G(t). Выбираем ортонормированный базис и оставляем заданное число M коэффициентов. В качестве примера такого разложения можно взять преобразование Фурье или дискретное косинус преобразование. Этот подход хорош, когда совпадают области определения обеих функций. Если же они разнятся (имеют разные длины), то для сравнения понадобятся дополнительные манипуляции с коэффициентами.
Существует другой подход, основанный на квантилях, свободный от указанного выше недостатка и требующий минимальных вычислений. С этой целью исходные функции нормируются таким образом, чтобы их область значений заполняла интервал [0,1]. После этого выбирается набор квантилей в качестве вектора характеристик функций. Квантили зависят только от области значений, поэтому зависимость от длины интервалов, на которых определены функции, сводится к минимуму.
def getQuant(Wave,Intervs):
'''Standard quaniles normalized by the maximum of wave
'''
Wave = np.float32(Wave)
Wave /= max(Wave)
Quant = np.quantile(Wave,Intervs)
return np.float32(Quant)
Применим квантили для сравнения формы двух тригонометрических кривых
X = np.arange(0,1,0.01)
F = np.sin(2*np.pi*X)
G = np.cos(2*np.pi*X)
Intervs = np.arange(1,5)/5
print(getQuant(F,Intervs))
print(getQuant(G,Inters))
Ответы
[-0.809017 -0.309017 0.309017 0.809017]
[-0.809017 -0.309017 0.309017 0.809017]
Здесь мы имеем полное совпадение квантилей. Если растянуть одну из кривых, то абсолютного совпадения не получится, но квантили примут близкие значение. Очень важное свойство этого подхода — число выданных параметров будет одним и тем же для любой функции.
Кривые, применяемые для описания фрагмента.
Первым примером служит абсолютный спектр, полученный с помощью fft. Эта информация дополняет частоту F0, рассмотренную выше. Кроме параметров, заданных одним значением, важную роль играют характеристики, описывающие динамику сигнала. Этим свойством обладают кривые, вычисляющие, значения, зависящие от времени
Согласно теории (Teager), энергия сигнала X в точке n определяется формулой X[n}*X[n]-X[n-1]*X[n+1]. Построенная согласно этой формуле кривая описывает изменение энергии со временем, поэтому служит очень важной характеристикой фрагмента. Используя квантили, получаем сжатое описание этой кривой. Обычно применяют квантили равномерно распределенные по длине так, как это сделано в примере с тригонометрическими функциями.
Огибающая кривая сигнала Dat строится с помощью функции, зависящей от параметра WinLen. В данном случае выбрано значение 20, но это эмпирическая величина. Визуально огибающая мало зависит от этого параметра, если он берется из интервала [20,50], однако, увеличение WinLen ведет росту времени на вычисление.
WinLen = 20
Beg = 0
End = WinLen
Env1 = []
while End <= len(Dat1):
Fragm = Dat1[Beg: End]
Beg += 1
End += 1
Env1.append(F(Fragm,WinLen))
Рассмотренные выше кривые слабо зависят от сдвига сигнала. В противоположность им, функция np.cumsum(Dat) зависит существенно. Вычислив значения этой функции и подсчитав квантили, получим важную характеристику фрагмента.
Идея выделения фрагментов, отвечающих эмоциональному возбуждению, основана на предположении, что в найденных параметрах, отвечающих этому фрагменту, наблюдается отклонение от средних значений. Обучение нейронной сети, обнаруживающей такое отклонение, является трудной задачей, поскольку такие отклонения весьма индивидуальны. По этой причине мы отдаем предпочтение простым алгоритмам, не требующим такого обучения. Это известная задача об обнаружении отклонений, которая рассмотрена в литературе.
Процедура на основе PCA
Идея алгоритма представлена в книге: Анкур Пател — «Прикладное машинное обучение без учителя» ", Диалектика — 2020.
Предположим, что выбраны K параметров для каждого из M «слов» во входном потоке. Для каждого слова получаем вектор длины K, содержащий все значения параметров, найденных для этого слова. В результате всех вычислений находим матрицу размера M x K. Выбираем количество N компонент с помощью функций класса PCA из модуля from scikit-learn.decomposition import PCA и находим приближение каждой строки матрицы с помощью найденных компонент. Затем вычисляем разницу между оригиналом и приближением. Те строки (слова), для которых эти отклонения оказались большими, объявляются зонами возбуждения. Все подробности указанной процедуры можно найти в книге, отмеченной выше.
Решение на основе процедуры GaussianMixture
В этом пункте изложим процедуру выделения отклонения от нормы, принятую на упомянутом выше сайте http://5.23.55.2. Снова имеем M «слов», для каждого из которых имеется K измеряемых параметров. Измеренные параметры помещаем в матрицу D размера K x M. Таким образом, в каждой строке находится одна и та же характеристика, например, длина слова, вычисленная для каждого из имеющихся слов. Теперь в каждой строке надо выделить отклонение от нормы. Процедура GaussianMixture предназначена для построения модели смеси гауссовских распределений. Пользователь указывает число смесей, а функция относит то или иное измерение к нужному классу.
from sklearn import mixture
def gaussDistr(In):
Clf = mixture.GaussianMixture(n_components=2,
covariance_type="full")
Clf.fit(In)
return Clf.predict(In)
На вход этой функции подаем строку матрицы D, а на выходе получаем последовательность из 0 и 1. Это означает, что каждый элемент строки отнесен к тому или иному классу (элементу смеси). Мы используем гипотезу, согласно которой элементы возбуждения встречаются реже, чем обычные фрагменты в речи. Для этого подсчитываем число 1 в выходной последовательности и сравниваем это число с длиной последовательности. Если оно превышает половину длины последовательности, то последовательность инвертируется. Обработав все строки матрицы D, получаем новую матрицу F, состоящую из 0 и 1. Окончательное решение о принадлежности слова моменту возбуждения решается с помощью процедуры голосования. Для этого суммируются элементы в каждом столбце матрицы F, и если эта сумма превышает K/2, то принимается решение о принадлежности слова, определяющего столбец матриц, моменту возбуждения.