Wednesday, June 25, 2014

Speech recognition for dummies

This article is devoted to understanding such interesting area of software development as Speech Recognition. Unfortunately, I'm not an expert in this stuff, so my clause will have a lot of inaccuracies, mistakes and disappointments. Nonetheless, the chief objective of this paper (as you can see from its topic) is not a professional analysis of the problem, but analysis of the basic concepts, issues and theirs solutions.


Let's start from the point that our speech is a sequence of sounds. A sound, in turn, is a superposition (overlay) of acoustic oscillations (waves) of different frequencies. Finally, a wave, as we all know from physics, has two major characteristics - amplitude and frequency.

Storing a sound signal on a digital device (i.e. your computer) requires splitting the signal into a set of intervals and calculating average amplitude value for each interval.

That is how mechanical vibrations can be transformed to a set of numbers, which can be processed on our phones/computers.

Hence, the speech recognition task is reduced to "matching" a set of numerical values and words from a dictionary (i.e. Russian dictionary :).

Let's see in more details on the way of implementation of such "matching".

Input data

Suppose we have a file/stream with an audio data. First of all we need to understand its internal structure and how it can be read. Let's consider the most simple case - a WAV file.

This format implies the presence of two blocks in the file. The first one is a header with the audio stream information: bitrate, frequency, amount of channels, length of the file, etc. The second one contains the "raw" data - the digital signal, the set of signal's amplitudes values.

The reading logic in this case is quite simple. Read the header, check some restrictions (e.g. no compression), store the data into a special array.



Theoretically, we already know all that we need to compare (element-wise) our sample (which we've read in the previous section) with another one, which text equivalent is known. In other words, we can try to recognize... But we shouldn't do this :)

Our approach must be stable (at least, a little bit) to any changes in the voice (of a man who uttering the word), volume and speed of pronunciation. Of course, we can't guarantee that with element-wise comparison of two audio signals.

That's why we choose the another way.


First of all let's split our data into a set of little time intervals – frames. Notice that frames shouldn't go one by one, instead they should “overlap” each other. Thus the end of the n-frame must intersect with the beginning of n+1-frame.

Frames are more appropriate units for analysis rather than values of singnal's amplitudes just because we are dealing with waves, and waves are supposed to be analyzed on intervals, not in particular points :) Meanwhile overlapping of frames allows us to smooth analysis results by using a frame as a some kind of “window”, which moves along the original signal (its amplitudes values).

Experiments show us that the optimal frame length should correspond to the interval of 10ms, "overlap" - to 50%. Given that the average word length is 500ms (at least in my experiments) - this length will give us approximately 500 / (10 * 0.5) = 100 frames per word.

Splitting words

The first problem, which must be solved in the process of speech recognition is splitting the speech into different words. Let's simplify our task and put that the speech contains some pauses (intervals of silence), that can be used as the words splitter.

In this case we need to find the threshold. All values above it will be words, below – silence. Here we can use several ways:

  • use a constant (the original signal must be always generated under the same circumstances, in the same way);
  • cluster signal's values into two sets: one with words values and another one with silence (works only if silence takes a big part in the original signal);
  • analyze the entropy;

As you can guess, we are going to discuss the last point :) Let's start from the entropy definition - “a measure of disorder” (c). In our case, the entropy shows how much our signal "varies" within a given frame.

In order to calculate the entropy of a given frame, perform the following steps:

  • assume that our signal is normalized and all its values ​​range in [-1;1];
  • build the histogram of the signal's values:
    • split the range into i=0..N intervals (N is a preselected value, recommended from 50 to 100);
    • compute P[i], amount of signal values, matched to the i-th interval;
    • normalize P[i] (divide on the frame's size);
  • calculate the entropy as ;


So, we got the value of the entropy. But this is just an another frame's characteristic. We still need to compare it with something if we want to separate the sound and the silence.
Fortunately, entropy (in contrast to Root Mean Square) is a relatively independent measure. This fact allowed us to put its threashold value as a constant (e.g. 0.1).

Nevertheless, the problems don't end here :( The entropy may sag in the middle of a word (in vowels) or may suddenly jump (in case of a little noise). In order to fix the first problem we need to use the “minimal distance between two words” and “glue” nearby frame sets, which were divided because of the sag. The second problem can be resolved by using “minimal word length” and removing all the candidates which are not as long as we need (and aren't used in the first point).

If the speech doesn't have any pauses between words (or the speaker speaks too fast), we can try to create/extract sets of various subsequences from the original frame set. Then all these subsequences can be used as a source for the recognition process. But that's absolutely another story :)


So, we have the set of frames, which matches with a particular word. We can take the path of least resistance and use average square (root mean square) of all the frame's elements as theirs numerical characteristic. However, such metric gives us a quite little information for further analysis.

That's why it's time to introduce Mel-Frequency Cepstral Coefficients. According to Wikipedia (which, as we know, never lies) MFCC are a kind of signal's energy representation. Pros of their usage are as follows:

  • using of the spectrum of the signal (i.e. expansion by the orthogonal basis of [co]sine functions), which takes into account the “wave” nature of the signal;
  • the spectrum is projected onto a special mel-scale, which allows us to extract the most valuable for human perception frequencies;
  • amount of calculated coefficients may be limited by any value (e.g. 12), that allows us to “squeeze” the frame and, as a consequence, amount of information to further processing;

It's time to consider the process of MFCC computing for a particular frame.

Let's think about our frame as a vector , where N is its size.

Fourier transformation

First of all let's calculate the spectrum of the signal by computing the discreet Fourier transformation (preferably its “fast” FFT implementation).

It's also recommended to apply so-called “window” Hamming function to “smooth” values on the frame's bounds.

As a result we will have the following vector:

It is important to understand that after this conversion, we have X-axis for the frequency (hz) signal, and the Y-axis for the magnitude (as a way to escape from the complex values​​):

Mel-coefficients calculation

Let's start from the “mel” definition. According to Wikipedia (again), mel is a "psychophysical unit of sound pitch, based on the subjective perception of an average human. It primarily depends on the frequency of the sound (as well as on volume and tone). In other words, this value shows us how much the sound of a certain frequency is "valuable" for us.

Frequency can be converted into mel-values by the following formula (remind it as "formula-1"):

Backward transformation looks in the following way (remind it as "formula-2"):

Graph of mel/frequency scale:

But let's get back to our task. Assume that we have a frame which size is N = 256 elements. We know (from the audio format data) that the frequency of the frame is 16000hz. Let's put that the human speech belongs to the range [300; 8000]hz. Amount of mel-coefficients that we want to find is M = 10.

In order to expand the spectrum (which we've calculated above) by the mel-scale, we need to create the “comb” of mel-filters. In fact, each mel-filter is a triangular window function, which summarizes amount of energy at the certain frequency range (thus calculates the mel-coefficient). Hence, since we know the amount of mel-coefficients and the frequency range, we can construct the following set of filters:

Notice, the greater the index number of the mel-coefficient is, the wider the base of the filter is. This is because the process of splitting frequency bands into the filters intervals happens on the mel-scale (not on the frequency-scale).

But I digress. For our case the frequency range is [300, 8000]. According to the “formula-1” on the mel-scale this interval transforms into [401.25; 2834.99].

Next, we need 12 reference points to build 10 triangular filters:

m[i] = [401.25, 622.50, 843.75, 1065.00, 1286.25, 1507.50, 1728.74, 1949.99, 2171.24, 2392.49, 2613.74, 2834.99]

Notice, on the mel-scale the points are located uniformly. Let's transform the values back to frequency using the “formula-2”:

h[i] = [300, 517.33, 781.90, 1103.97, 1496.04, 1973.32, 2554.33, 3261.62, 4122.63, 5170.76, 6446.70, 8000]

As you can see, our values start to stretch, thus aligning the growth dynamics of the "significance" of the sound on the low and high frequencies.

Now let's impose the scale (which we got above) on the spectrum of our frame. As we remember, X-axis is for frequency. Length of the spectrum is 256 elements, which refers to 16000Hz diapason. Solving the simple proportion we obtain the following formula:

f(i) = floor( (frameSize+1) * h(i) / sampleRate)

in our case this is equivalent to

f(i) = 4, 8, 12, 17, 23, 31, 40, 52, 66, 82, 103, 128

That is all! Since we know the reference points on the X-axis of our spectrum we can easily construct the necessary filters by the following formula:

Applying the mel-filters

Applying the filter to the spectrum mean pairwise multiplying the filter values ​​with the spectrum values. The sum of the elements in the result vector is a mel-coefficient. Since we have M-filters, the number of mel-coefficients will be the same.

However, we need to apply mel-filters not to the spectrum, but to its energy. Then we must logarithm the result. It's believed that the trick above decreases the sensitivity to noise ratios.

Cosine transformation

Discrete Cosine Transform (DCT) is used in order to get the "cepstral" coefficients. It allows to “squeeze” the results, increasing the importance of the first coefficients and reducing the importance of the last ones.

In current case we use DCT-II without additional multiplication on the scale factor .

Now we have the set of M mfcc-coefficients for each frame. These coefficients can (and will) be used for the further analysis!

Examples of the methods above can be found here.

Recognition algorithm

Here, dear reader, you'll face the major disappointment. In the internets I've seen a lot of highly (and not very) intelligent debates about what is the best way for speech recognizing. Somebody stands up for Hidden Markov Models, someone - for neural networks... someone's thoughts can't be understood at all :)

In any case a lot of people choose HMM, and this is the way which I'm going to implement… in the future :)

At the moment I suggest to stop on much less effective, but much more simple way.

So, let's remember that our task is about recognition words from a dictionary. For simplicity's sake, we will recognize first ten numbers from Russian dictionary: “odin”, “dva”, “tri”, “chetyre”, “pyat”, “shest”, “sem”, “vosem”, “devyat”, “desyat” :)

Now, take your iPhone/Android and walk through your colleagues asking them to dictate these words for recording. Save the audio files on your computer and convert them into Wav format. Next, calculate mfcc-coefficients for each audio file (a set/vector of concatenated mfcc-coefficients of all frames from the file) and store matches between words and related sets of mfcc-coefficients into a local storage (file or database);

Let's call each [word; sets of mfcc-coefficients] match “Model”, and the process of creating these matches - “Machine Learning”! In fact, the simple adding of new samples in the local storage has a very tenuous connection with machine learning ... But it's a painfully trendy term :)

Now our task is reduced to the selection of the most "close" model for the set of mfcc-coefficients (the word which we want to recognize). At first glance, the problem can be solved quite simply:

  • calculate distances between each model and the word we want to recognize (distance between the model and the word is average euclid distance between word's mfcc-coefficients vector and model's mfcc-coefficients vectors);
  • choose as a correct the model with the shortest distance;

However, two different speakers can pronounce the same word with different speed. That means size of mfcc-vector can be different for two sample of the same word.

Fortunately, the problem of comparing sequences of different length has been solved in the form of Dynamic Time Warping algorithm. This dynamic programming algorithm is perfectly described on Wiki.

The only thing that we need to do with that algorithm is changing of the distance calculation logic. We must remember that the mfcc-vector of the model is a sequence of mfcc-subvectors (M-size) obtained from the frames. So, DTW algorithm must calculate distances between these subvectors, not their elements. Hence, elements of distance matrix are Euclid distances between frame based mfcc-subvectors.



I haven't been able to verify the approach on a big amount of “training” samples. As for test results based on 3 samples education... They are quite bad – only 65% correct recognitions.

However, my task was implementation of the simplest speech recognition application. So called “proof of concept” :)


The attentive reader has noticed that the article contains a lot of references on the GitHub project. It's worth to say that it's my first C++ project since the university times. Also it's my first attempt to calculate something more complicated than the arithmetic mean since the same times... In other words “it comes with absolutely no warranty” (с) :)


Friday, June 13, 2014

Распознование речи для чайников

В этой статье я хочу рассмотреть основы такой интереснейшей области разработки ПО как Распознавание Речи. Экспертом в данной теме я, естественно, не являюсь, поэтому мой рассказ будет изобиловать неточностями, ошибками и разочарованиями. Тем не менее, главной целью моего "труда", как можно понять из названия, является не профессиональный разбор проблемы, а описание базовых понятий, проблем и их решений. В общем, прошу всех заинтересовавшихся пожаловать под кат!


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

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

Таким вот образом механические колебания превращаются в набор чисел, пригодный для обработки на современных ЭВМ.

Отсюда следует, что задача распознавания речи сводится к "сопоставлению" множества численных значений (цифрового сигнала) и слов из некоторого словаря (русского языка, например).

Давайте разберемся, как, собственно, это самое "сопоставление" может быть реализовано.

Входные данные

Допустим у нас есть некоторый файл/поток с аудиоданными. Прежде всего нам нужно понять, как он устроен и как его прочесть. Давайте рассмотрим самый простой вариант - WAV файл.

Формат подразумевает наличие в файле двух блоков. Первый блок - это заголовка с информацией об аудиопотоке: битрейте, частоте, количестве каналов, длине файла и т.д. Второй блок состоит из "сырых" данных - того самого цифрового сигнала, набора значений амплитуд.

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



Чисто теоретически, теперь мы можем сравнить (поэлементно) имеющийся у нас образец с каким-нибудь другим, текст которого нам уже известен. То есть попробовать "распознать" речь... Но лучше этого не делать :)

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

Поэтому мы пойдем несколько иным путём.


Первым делом разобьём наши данные по небольшим временным промежуткам - фреймам. Причём фреймы должны идти не строго друг за другом, а “внахлёст”. Т.е. конец одного фрейма должен пересекаться с началом другого.

Фреймы являются более подходящей единицей анализа данных, чем конкретные значения сигнала, так как анализировать волны намного удобней на некотором промежутке, чем в конкретных точках. Расположение же фреймов “внахлёст” позволяет сгладить результаты анализа фреймов, превращая идею фреймов в некоторое “окно”, движущееся вдоль исходной функции (значений сигнала).

Опытным путём установлено, что оптимальная длина фрейма должна соответствовать промежутку в 10мс, "нахлёст" - 50%. С учётом того, что средняя длина слова (по крайней мере в моих экспериментах) составляет 500мс - такой шаг даст нам примерно 500 / (10 * 0.5) = 100 фреймов на слово.

Разбиение слов

Первой задачей, которую приходится решать при распознавании речи, является разбиение этой самой речи на отдельные слова. Для простоты предположим, что в нашем случае речь содержит в себе некоторые паузы (промежутки тишины), которые можно считать “разделителями” слов.

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

  • задать константой (сработает, если исходный сигнал всегда генерируется при одних и тех же условиях, одним и тем же способом);
  • кластеризовать значения сигнала, явно выделив множество значений соответствующих тишине (сработает только если тишина занимает значительную часть исходного сигнала);
  • проанализировать энтропию;

Как вы уже догадались, речь сейчас пойдёт о последнем пункте :) Начнём с того, что энтропия - это мера беспорядка, “мера неопределённости какого-либо опыта” (с). В нашем случае энтропия означает то, как сильно “колеблется” наш сигнал в рамках заданного фрейма.

Для того, что бы подсчитать энтропию конкретного фрейма выполним следующие действия:

  • предположим, что наш сигнал пронормирован и все его значения лежат в диапазоне [-1;1];
  • построим гистограмму (плотность распределения) значений сигнала фрейма:
    • разобьём этот диапазон на i=0..N промежутков (N-выбирается заранее, рекомендуется от 50 до 100);
    • посчитаем P[i]-ые, количество значений сигнала, входящих в i-ый промежуток;
    • пронормируем P[i] (разделим на размер фрейма);
  • рассчитаем энтропию, как ;


И так, мы получили значение энтропии. Но это всего лишь ещё одна характеристика фрейма, и для того, что бы отделить звук от тишины, нам по прежнему нужно её с чем-то сравнивать. В некоторых статьях рекомендуют брать порог энтропии равным среднему между её максимальным и минимальным значениями (среди всех фреймов). Однако, в моём случае такой подход не дал сколь либо хороших результатов.
К счастью, энтропия (в отличие от того же среднего квадрата значений) - величина относительно самостоятельная. Что позволило мне подобрать значение её порога в виде константы (0.1).

Тем не менее проблемы на этом не заканчиваются :( Энтропия может проседать по середине слова (на гласных), а может внезапно вскакивать из-за небольшого шума. Для того, что бы бороться с первой проблемой, приходится вводить понятие “минимально расстояния между словами” и “склеивать” близ лежачие наборы фреймов, разделённые из-за проседания. Вторая проблема решается использованием “минимальной длины слова” и отсечением всех кандидатов, не прошедших отбор (и не использованных в первом пункте).

Если же речь в принципе не является “членораздельной”, можно попробовать разбить исходный набор фреймов на определённым образом подготовленные подпоследовательности, каждая из которых будет подвергнута процедуре распознавания. Но это уже совсем другая история :)


И так, мы у нас есть набор фреймов, соответствующих определённому слову. Мы можем пойти по пути наименьшего сопротивления и в качестве численной характеристики фрейма использовать средний квадрат всех его значений (Root Mean Square). Однако, такая метрика несёт в себе крайне мало пригодной для дальнейшего анализа информации.

Вот тут в игру и вступают Мел-частотные кепстральные коэффициенты (Mel-frequency cepstral coefficients). Согласно Википедии (которая, как известно, не врёт) MFCC - это своеобразное представление энергии спектра сигнала. Плюсы его использования заключаются в следующем:

  • Используется спектр сигнала (то есть разложение по базису ортогональных [ко]синусоидальных функций), что позволяет учитывать волновую “природу” сигнала при дальнейшем анализе;
  • Спектр проецируется на специальную mel-шкалу, позволяя выделить наиболее значимые для восприятия человеком частоты;
  • Количество вычисляемых коэффициентов может быть ограничено любым значением (например, 12), что позволяет “сжать” фрейм и, как следствие, количество обрабатываемой информации;

Давайте рассмотрим процесс вычисления MFCC коэффициентов для некоторого фрейма.

Представим наш фрейм в виде вектора , где N - размер фрейма.

Разложение в ряд Фурье

Первым делом рассчитываем спектр сигнала с помощью дискретного преобразования Фурье (желательно его “быстрой” FFT реализацией).

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

То есть результатом будет вектор следующего вида:

Важно понимать, что после этого преобразования по оси Х мы имеем частоту (hz) сигнала, а по оси Y - магнитуду (как способ уйти от комплексных значений):

Расчёт mel-фильтров

Начнём с того, что такое mel. Опять же согласно Википедии, mel - это “психофизическая единица высоты звука”, основанная на субъективном восприятии среднестатистическими людьми. Зависит в первую очередь от частоты звука (а так же от громкости и тембра). Другими словами, эта величина, показывающая, на сколько звук определённой частоты “значим” для нас.

Преобразовать частоту в мел можно по следующей формуле (запомним её как "формула-1"):

Обратное преобразование выглядит так (запомним её как "формула-2"):

График зависимости mel / частота:

Но вернёмся к нашей задаче. Допустим у нас есть фрейм размером 256 элементов. Мы знаем (из данных об аудиоформате), что частота звука в данной фрейме 16000hz. Предположим, что человеческая речь лежит в диапазоне от [300; 8000]hz. Количество искомых мел-коэффициентов положим M = 10 (рекомендуемое значение).

Для того, что бы разложить полученный выше спектр по mel-шкале, нам потребуется создать “гребёнку” фильтров. По сути, каждый mel-фильтр это треугольная оконная функция, которая позволяет просуммировать количество энергии на определённом диапазоне частот и тем самым получить mel-коэффициент. Зная количество мел-коэффициентов и анализируемый диапазон частот мы можем построить набор таких вот фильтров:

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

Но мы опять отвлеклись. И так для нашего случая диапазон интересующих нас частот равен [300, 8000]. Согласно формуле-1 в на мел-шкале этот диапазон превращается в [401.25; 2834.99].

Далее, для того, что бы построить 10 треугольных фильтров нам потребуется 12 опорных точек:

m[i] = [401.25, 622.50, 843.75, 1065.00, 1286.25, 1507.50, 1728.74, 1949.99, 2171.24, 2392.49, 2613.74, 2834.99]

Обратите внимание, что на мел-шкале точки расположены равномерно. Переведём шкалу обратно в герцы с помощью формулы-2:

h[i] = [300, 517.33, 781.90, 1103.97, 1496.04, 1973.32, 2554.33, 3261.62, 4122.63, 5170.76, 6446.70, 8000]

Как видите теперь шкала стала постепенно растягиваться, выравнивая тем самым динамику роста “значимости” на низких и высоких частотах.

Теперь нам нужно наложить полученную шкалу на спектр нашего фрейма. Как мы помним, по оси Х у нас находится частота. Длина спектра 256 - элементов, при этом в него умещается 16000hz. Решив нехитрую пропорцию можно получить следующую формулу:

f(i) = floor( (frameSize+1) * h(i) / sampleRate)

что в нашем случае эквивалентно

f(i) = 4, 8, 12, 17, 23, 31, 40, 52, 66, 82, 103, 128

Вот и всё! Зная опорные точки на оси Х нашего спектра, легко построить необходимые нам фильтры по следующей формуле:

Применение фильтров, логарифмирование энергии спектра

Применение фильтра заключается в попарном перемножении его значений со значениями спектра. Результатом этой операции является mel-коэффициент. Поскольку фильтров у нас M, коэффициентов будет столько же.

Однако, нам нужно применить mel-фильтры не к значениям спектра, а к его энергии. После чего прологарифмировать полученные результаты. Считается, что таким образом понижается чувствительность коэффициентов к шумам.

Косинусное преобразование

Дискретное косинусное преобразование (DCT) используется для того, что бы получить те самые “кепстральные” коэффициенты. Смысл его в том, что бы “сжать” полученные результаты, повысив значимость первых коэффициентов и уменьшив значимость последних.

В данном случае используется DCTII без каких-либо домножений на (scale factor).

Теперь для каждого фрейма мы имеем набор из M mfcc-коэффициентов, которые могут быть использованы для дальнейшего анализа.

Примеры код для вышележащих методов можно найти тут.

Алгоритм распознавания

Вот тут, дорогой читатель, тебя и ждёт главное разочарование. В интернетах мне довелось увидеть множество высокоинтеллектуальных (и не очень) споров о том, какой же способ распознавания лучше. Кто-то ратует за Скрытые Марковские Модели, кто-то - за нейронные сети, чьи-то мысли в принципе невозможно понять :)

В любом случае немало предпочтений отдаётся именно СММ, и именно их реализацию я собираюсь добавить в свой код… в будущем :)

На данный момент, предлагаю остановится на гораздо менее эффективном, но в разы более простом способе.

И так, вспомним, что наша задача заключается в распознавании слова из некоторого словаря. Для простоты, будем распознавать называния первых десять цифр: “один“, “два“, “три“, “четыре“, “пять“, “шесть“, “семь“, “восемь“, “девять“, “десять“.

Теперь возьмем в руки айфон/андроид и пройдёмся по L коллегам с просьбой продиктовать эти слова под запись. Далее поставим в соответствие (в какой-нибудь локальной БД или простом файле) каждому слову L наборов mfcc-коэффициентов соответствующих записей.

Это соответствие мы назовём “Модель”, а сам процесс - Machine Learning! На самом деле простое добавление новых образцов в базу имеет крайне слабую связь с машинным обучением… Но уж больно термин модный :)

Теперь наша задача сводится к подбору наиболее “близкой” модели для некоторого набора mfcc-коэффициентов (распознаваемого слова). На первый взгляд задачу можно решить довольно просто:

  • для каждой модели находим среднее (евклидово) расстояние между идентифицируемым mfcc-вектором и векторами модели;
  • выбираем в качестве верной ту модель, среднее расстояние до которой будет наименьшим;

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

К счастью, задача сравнения последовательностей разной длины уже решена в виде Dynamic Time Warping алгоритма. Этот алгоритм динамическо программирования прекрасно расписан как в буржуйской Wiki, так и на православном Хабре.

Единственное изменение, которое в него стоит внести - это способ нахождения дистанции. Мы должны помнить, что mfcc-вектор модели - на самом деле последовательность mfcc-“подвекторов” размерности M, полученных из фреймов. Так вот, DTW алгоритм должен находить дистанцию между последовательностями эти самых “подвекторов” размерности M. То есть в качестве значений матрицы расстояний должны использовать расстояния (евклидовы) между mfcc-“подвекторами” фреймов.



У меня не было возможности проверить работу данного подхода на большой “обучающей” выборке. Результаты же тестов на выборке из 3х экземпляров для каждого слова в несинтетических условиях показали мягко говоря нелучший результат - 65% верных распознаваний.

Тем не менее моей задачей было создание максимального простого приложения для распознавания речи. Так сказать “proof of concept” :)


Внимательный читатель заметил, что статья содержит множество ссылок на GitHub-проект. Тут стоит отметить, что это мой первый проект на С++ со времён университета. Так же это моя первая попытка рассчитать что-то сложнее среднего арифметического со времён того же университета... Другими словами it comes with absolutely no warranty (с) :)