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


Однако, я не умею строить летающие тарелки или синтезировать вирусы, а потому речь пойдет про терминаторов, а точнее о том как этим трудягам помочь отыскать Джона Коннора.


image


Мой рукодельный терминатор будет несколько упрощён — он не сможет ходить, стрелять, говорить "I'll be back". Единственное на что он будет способен — распознать голос Коннора, ежели он его услышит (ну или, например, Черчилля, если его тоже надо будет найти).


Общая схема


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


image
image


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


Классификаторы


Что представляют собой "признаки"? На самом деле это всего лишь набор чисел, характеризующих оратора — вектор в многомерном пространстве. А задача классификатора заключается в том, чтобы построить функцию отображения этого многомерного пространство в пространство действительных чисел. Другими словами, его задача состоит в получении числа, которое бы характеризовало меру схожести.


  • Euclid$\qquad \qquad D = \sqrt{\sum\limits_{i=1} { \left| {x_i} \right|}} \qquad \qquad \qquad $ Евклидово расстояние
  • SVM $\qquad \qquad y = \left\langle {\mathbf{w, x}} \right\rangle + w_0 \qquad \qquad \quad \; $ Разделяющая гиперплоскость
  • MLP $\qquad \qquad $ image $\qquad $ Сеть нелинейных функций

$ \qquad \qquad$


Эксперименты


Бинарная классификация


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


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


image



Euclid


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


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


image


Положительное значение относительно порога – классификация женского голоса. Отрицательное – мужского.


SVM


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


image


В данном случае порогом является число 0, так как все положительные результаты должны соответствовать одному классу, отрицательные — другому. Однако для улучшения сравнения с другими методами результаты были сжаты в 2 раза и смещены на 0,5.


MLP


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


image


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



Задача аутентификации


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


image


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


Euclid


Классификация по «обучаемому» множеству. Хотя для данного алгоритма понятие «обучаемое множество» неприменимо, так как самого обучения не происходит. Вычисляются средние значения расстояний внутри класса и между классами и нормализация: первому случаю ставится в соответствие значение 1, в противном случае – 0.


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


image


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


SVM


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


image


Интерпретация данных таблиц для SVM аналогична таблицам для алгоритма Евклида, т.е. 0.5 является пороговым значением.


MLP


Для использования многослойного персептрона на этапе настройки подавались 9-мерные вектора в качестве выходных значений системы. В этом случае для каждого входного вектора признаков на выходе MLP аппроксимировал значения 9 функций, отвечающих за задачу классификации.


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


image


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


Сравнение методов классификации


Наиболее заметными результатами, полученными в ходе экспериментов являются низкий уровень точности методом Евклида и очень высокие вычислительные затраты на настройку многослойного персептрона: длительность настройки MLP превышала длительность настройки SVM в несколько сотен раз на одном и том же наборе данных.


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


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


В качестве двух основных характеристик любой системы идентификации можно принять ошибки первого и второго рода. В теории радиолокации их обычно называют «ложная тревога» или «пропуск цели», а в системах идентификации наиболее устоявшиеся понятия — FAR (False Acceptance Rate) и FRR(False Rejection Rate). Первое число характеризует вероятность ложного совпадения биометрических характеристик двух людей. Второе – вероятность отказа доступа человеку, имеющего допуск. Система тем лучше, чем меньше значение FRR при одинаковых значениях FAR.


Ниже представлены FAR/FRR графики созданных мной систем для задачи аутентификации.


image


Хороший обзор биометрических систем идентификации и оценка значений их FAR/FRR можно найти тут.



Euclid:


  • Наиболее быстрый и простой алгоритм
  • Ухудшение характеристик при масштабировании
  • Низкое качество классификации


SVM:


  • Наилучшая обобщающая способность
  • Требует малые вычислительные ресурсы на этапе обучения
  • Необходимость создавать N различных SVM


MLP:


  • Наименьшая ошибка обучения
  • Отсутствие понятия классификации
  • Требователен к вычислительным ресурсам на этапе обучения
  • Необходимость переобучения всей системы при регистрации нового диктора


Звукоуловители


Вся аудиоинформацию система получает в виде массива аудиопотока и значения частоты квантования.


Массив формируется путем считывания из файлов mp3, flac, wav или любого другого формата, вырезается нужный фрагмент, усредняется в одну дорожку и вычитается постоянная составляющая:


Выделение аудиопотока
    filename = 'sample/Irina.mp3';
    [x, fs] = audioread(filename);

    % x - массив аудио-потока
    % fs - частота дискретизации

    %%  установка параметров
    sec_start = 0.5;
    sec_stop = 10;

    frame_start = max(sec_start*fs, 1);
    frame_stop = min(length(x), sec_stop*fs);

    sec_start = (frame_start / fs) - fs;
    sec_stop = frame_stop / fs;

    %% обрезка
    x = x(frame_start : frame_stop, :);
    x = mean(x, 2);
    x = x - mean(x);

Извлечение признаков


image


Предобработка


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


Выбросы


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


image


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


Функция удаления выбросов

image


function [ y ] = outlier_suppression( x, fs )

  buffer_duration = 0.1;             % in seconds
  accumulation_time = 3;             % in seconds
  overlap = 50;                      % in percent
  outlier_threshold = 7;

  buffer_size = round( ((1 + 2*(overlap / 100))*buffer_duration) * fs);
  sample_size = round(buffer_duration * fs);

  y = x;
  cumm_power = 0;
  amount_iteration = floor(length(y) / sample_size);

  for iter = 1 : amount_iteration

      start_index = (iter - 1) * sample_size;
      start_index = max(round(start_index - (overlap / 100)*sample_size), 1);
      end_index = min(start_index + buffer_size, length(x));

      buffer = y(start_index : end_index);
      power = norm(buffer);

      if ((accumulation_time/buffer_duration < iter) 
           && ((outlier_threshold*cumm_power/iter) < power))
          buffer = buffer .* (1 - tukeywin(end_index - start_index + 1, 0.8));
          y(start_index : end_index) = buffer;

          cumm_power = cumm_power + (cumm_power/iter);
          continue
      end

      cumm_power = cumm_power + power;
  end

end

После обработки представленный выше сэмпл выглядит вот так:


image


Низкочастотный шум


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


Для избавления от подобных составляющих был выбран эллиптический ФВЧ 3-го порядка с частотой среза, равной 30Гц, и обеспечивающий подавление 20 дБ в полосе задержания. Выбор эллиптического фильтра был сделан из-за того, что эллиптические фильтры обеспечивают более крутой (по сравнению с фильтрами Баттерворта и Чебышева) спад АЧХ в переходной зоне между полосами пропускания и задерживания. Платой за это является наличие равномерных пульсаций АЧХ как в полосе пропускания, так и в полосе задерживания.


Ниже представлены сигнал до и после фильтрации соответственно:
image
image


Voice Activity Detection


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


Вычисление мощностных характеристик


В качестве мощностных характеристик сигнала и шума использовались следующие формулы:
image


Функция вычисления мощностей
function [ signal_power_array, noise_power_array ] = compute_power_arrays(x,fs)

  buffer_duration = 10 * 10^(-3);    % in seconds
  overlap = 50;                      % in percent

  sample_size = round(buffer_duration * fs);
  buffer_size = round( ((1 + 2*(overlap / 100))*buffer_duration) * fs);

  signal_power_array = zeros(floor(length(x) / sample_size), 1);
  noise_power_array = zeros(length(signal_power_array), 1);

  for iter = 1 : length(signal_power_array)
      start_index = (iter - 1) * sample_size;
      start_index = max(round(start_index - (overlap / 100)*sample_size), 1);
      end_index = min(start_index + buffer_size, length(x));

      buffer = x(start_index : end_index);
      signal_power_array(iter) = norm(buffer);
      noise_power_array(iter) = norm(abs(diff(sign(buffer))));
  end

end

Сглаживание характеристик и нормализация

Полученные массивы мощностей фильтруются и нормируются от 0 до 1


%% filtering
gw = gausswin(8);
signal_power_array = filter(gw, 1, signal_power_array);
signal_power_array = signal_power_array - base_value(signal_power_array);
signal_power_array = max(signal_power_array, 0);
signal_power_array = signal_power_array ./ max(signal_power_array);

gw = gausswin(16);
noise_power_array = filter(gw, 1, noise_power_array);
noise_power_array = noise_power_array - base_value(noise_power_array);
noise_power_array = max(noise_power_array, 0);
noise_power_array = -noise_power_array + max(noise_power_array);
noise_power_array = noise_power_array ./ max(noise_power_array);
noise_power_array = (1 - noise_power_array);

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


function [ value ] = base_value( array )
  [nelem, centres] = hist(array, 100);
  [~, index_max] = max(nelem);
  value = centres(index_max);
end

Нахождение границ фраз


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


Выделение фраз
%%  find the end of phrases
phrase_detection = zeros(size(signal_power_array));
max_magn = 0;
for iter = 1 : length(signal_power_array)
    max_magn = max(max_magn, signal_power_array(iter));
    if (signal_power_array(iter) < 2*threshold*max_magn)
        phrase_detection(iter) = 1;
        max_magn = 0;
    end
end

%%  find the start of phrases
max_magn = 0;
for iter = length(signal_power_array) : -1 : 1
    max_magn = max(max_magn, signal_power_array(iter));
    if (signal_power_array(iter) < 2*threshold*max_magn)
        phrase_detection(iter) = 1;
        max_magn = 0;
    end
end

%% post processing of phrase detection array
phrase_detection = not(phrase_detection);
phrase_detection(1) = 0;
phrase_detection(end) = 0;
edge_phrase = diff(phrase_detection);

%%  create phrase array
phrase_counter = 0;
for iter = 1 : length(edge_phrase)
    if (0 < edge_phrase(iter))
        phrase_counter = phrase_counter + 1;
        phrase(phrase_counter).start = iter;
    elseif (edge_phrase(iter) < 0)
        start = phrase(phrase_counter).start;
        phrase(phrase_counter).end = iter;
        phrase(phrase_counter).power = mean(signal_power_array(start : iter));
        phrase(phrase_counter).noise_power = mean(noise_power_array(start : iter));
    end
end

Объединение фраз


Зачастую длительность вычисленных фраз оказывается менее 100мс. В связи с этим, разработан алгоритм, объединяющий короткие близко расположенные фразы в одну. Для этого сравниваются энергии соседних участков, и если они отличаются менее чем на установленный порог, то данные участки объединяются. При этом происходил перерасчет энергии объединенных фраз соразмерно длительности и мощности каждого.


Объединение коротких фраз
%%  union small phrases
length_threshold = 16;
iter = 1;
while (iter < length(phrase))
    phrase_length = phrase(iter).end - phrase(iter).start;
    if ((phrase_length<length_threshold) && (iter<length(phrase)) && (iter~=1))

        lookup_power = phrase(iter+1).power;
        behind_power = phrase(iter-1).power;

        if (abs(lookup_power-phrase(iter).power) < abs(behind_power-phrase(iter).power))
            next_iter = iter + 1;
        else
            next_iter = iter - 1;
        end
        power_ratio = phrase(next_iter).power / phrase(iter).power;
        power_ratio = min(power_ratio, 1 / power_ratio);
        if ( power_ratio < threshold )
            iter = iter + 1;
            continue;
        end
        min_index = min(phrase(iter).start, phrase(next_iter).start);
        max_index = max(phrase(iter).end, phrase(next_iter).end);

        phrase_next_length = (phrase(next_iter).end - phrase(next_iter).start);

        phrase(next_iter).power = ...
((phrase(iter).power*phrase_length)+(phrase(next_iter).power*phrase_next_length)) / (phrase_length + phrase_next_length);

        phrase(next_iter).noise_power = ...
((phrase(iter).noise_power*phrase_length)+phrase(next_iter).noise_power*phrase_next_length)) / (phrase_length + phrase_next_length);

        phrase_detection((min_index+1) : (max_index-1)) = phrase_detection(phrase(next_iter).start+1);

        phrase(next_iter).start = min_index;
        phrase(next_iter).end = max_index;

        phrase(iter) = [];
    else
        iter = iter + 1;
    end
end

Сравнение с порогом, создание идентификаторов активности и объединение соседних участков одного класса.
%% union neibours phrases
threshold = 0.1;
iter = 1;
while (iter <= length(phrase))
    if (phrase_power_threshold < phrase(iter).power)
        phrase(iter).voice = 1;
    elseif ((phrase_power_threshold < 5*phrase(iter).power) ...
            && (phrase(iter).noise_power < 0.5*phrase(iter).power))
        phrase(iter).voice = 1;
    else
        phrase(iter).voice = 0;
    end

    if (iter ~= 1)
        if (phrase(iter-1).voice == phrase(iter).voice)
            phrase(iter-1).end = phrase(iter).end;
            phrase_detection(phrase(iter-1).start+1 : phrase(iter-1).end-1) = phrase(iter-1).voice;
            phrase(iter) = [];
            continue;
        end
    end
    iter = iter + 1;
end

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


Расширение голоса во времени
%%  compute real sample indexes
for iter = 1 : length(phrase)
    start_index = (phrase(iter).start - 1) * sample_size + 1;
    end_index = phrase(iter).end * sample_size;
    phrase(iter).start = start_index;
    phrase(iter).end = end_index;
end
phrase(1).start = 1;
phrase(end).end = length(x);

%% spread voice in time
spread_voice = round(50 * 10^(-3) * fs);
iter = 2;
while (iter < length(phrase))
    if (phrase(iter).voice)
        phrase(iter).start = phrase(iter).start - spread_voice;
        phrase(iter).end = phrase(iter).end + spread_voice;
    else
        phrase(iter).start = phrase(iter - 1).end + 1;
        phrase(iter).end = phrase(iter + 1).start - 1 - spread_voice;
    end

    phrase(iter).start = max(phrase(iter).start, 1);
    phrase(iter).end = min(phrase(iter).end, length(x));

    if (phrase(iter).start < phrase(iter).end)
        iter = iter + 1;
    else
        phrase(iter-1).end = phrase(iter).end;
        phrase(iter) = [];
    end
end
phrase(1).start = 1;
phrase(1).end = phrase(2).start - 1;
phrase(end).start = phrase(end - 1).end + 1;
phrase(end).end = length(x);

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


Создание выходного массива
%% create output sample array
y = zeros(size(x));
phrase_space = zeros(size(x));

betta = 3;
for iter = 1 : length(phrase)
    phrase_length = phrase(iter).start : phrase(iter).end;
    if (phrase(iter).voice)
        y(phrase_length) = x(phrase_length) .* kaiser(length(phrase_length), betta);
        phrase_space(phrase_length) = -1;
    else
        y(phrase_length) = zeros(size(phrase_length));
    end
    if (0 < phrase(iter).end)
        phrase_space(phrase(iter).end) = 0;
    end
end

%% delete zero data
for iter = length(phrase) : -1 : 1
    phrase_length = phrase(iter).start : phrase(iter).end;
    if (~phrase(iter).voice)
        y(phrase_length) = [];
    end
end

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


  • Голубым цветом отмечен аудиопоток.
  • Красным цветом — мощность речевого сигнала
  • Зеленым цветом — всё, что связано с шумом

image


Тестирование


Для того, чтобы проверить валидность, точность и устойчивость работы системы VAD, система была протестирована примерно на 20 аудиозаписей различных людей. Во всех случаях тестирование происходило на глаз и "ухо" — смотрел график и слушал уплотненный сигнал. Ошибкой считались пропуск слов или, наоборот, принятие периодов затишья.


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


image


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


image


MFCC


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


Про MFCC можно прочитать здесь, здесь, здесь или здесь.


Логарифмическая децимация спектра


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


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


function [ index_space ] = log_index_space( amount_point, input_length, decimation_speed )
  index_space = exp(((0:(amount_point-1))./(amount_point-1)).*decimation_speed);
  index_space = index_space - index_space(1);
  index_space = index_space ./ index_space(end);
  index_space = round((input_length - 1) .* index_space + 1);
end

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


Децимация спектра
function [ y ] = decimation_spectrum( freq_signal, index_space )

  y = zeros(size(index_space));
  min_value = min(freq_signal);
  freq_local = freq_signal - min_value;

  for iter = 2 : (length(index_space)-1)
      index_frame = index_space(iter - 1) : index_space(iter + 1);
      frame = freq_local(index_frame);

      if (4 < length(index_frame))
          gw = gausswin(round(length(index_frame) / 2));
          frame = filter(gw, 1, frame) ./ (round(length(index_frame)) / 4);
      end
      y(iter) = median(frame);
  end
  y(1) = mean(y);
  y(end) = y(end-1);

  y = y + min_value;

end

function [sample_spectr,log_space]=eject_spectr(sample_length,sample,fs,band_pass)

  %% set parameters
  band_start = band_pass(1);
  band_stop = band_pass(2);
  decimation_speed = 2.9;
  amount_point = 800;

  %% create spaces
  spectrum_space = 0 : fs/length(sample) : (fs/2) - (1/length(sample));
  spectrum_space = spectrum_space(spectrum_space <= band_stop);

  sample_index_space = ...
       log_index_space(amount_point, length(spectrum_space), decimation_speed);

  %% create spectral density
  sample_spectr = 10*log10(abs(fft(sample,length(sample))) ./ (length(sample)*sample_length));
  sample_spectr = sample_spectr(1 : length(spectrum_space));

  sample_spectr = decimation_spectrum( sample_spectr, sample_index_space );

end

Пример логарифмической децимации представлен ниже, где верхний график является логарифмическим спектром от 0 до 5000Гц содержащий более 16000 точек, а нижний – выделенный массив основных компонент размерностью 800 точек.


image


Построение LogLog спектрограммы


О чём речь? Для чего все эти старания? Почему нельзя построить обычную спектрограмму средствами матлаба в одну строчку и не париться со всем, что было сказано ранее? Но нельзя, и вот почему. На примере ниже показана типовая спектрограмма голоса. Какие в ней недочеты?


  • Огромное количество точек. Нет, их правда много. Настолько, что у моего компьютера заканчивалась оперативная память при попытке построить спектрограмму 15 секундной записи.
  • Большая часть информации посвящена шуму. Зачем мне спектр на 20кГц, если ни один нормальный человек никогда не говорит с такой частотой?
  • С 2.5 по 3 секунду и с 4.5 по 5 секунду нет активности голоса. Это шум. Вдобавок он в целевой частотной зоне, а потому может катастрофически сказаться на систему классификации в дальнейшем.
  • Нет четкой абсолютной шкалы мощности. Так как аудиозаписи могут быть различной громкости, то и итоговая мощность в спектре может сильно отличатся даже у одного и того же человека в разное время.
  • Не логарифмическая магнитуда. Громкость человеческого голоса, как и разрешающая способность нашего слуха по мощности логарифмические.
  • Не логарифмическая шкала частот. В связи с этим действительно полезная информация сосредоточена лишь в малой части спектра.

image


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


  • Во-первых, это явно выраженные пики мощности. Теперь можно отличить спектр двух людей просто на глаз.
  • Во-вторых, нет зон затишья, они вырезаны. Поэтому и длительность меньше.
  • В-третьих, здесь всего 800 точек на каждую частотную дорожку. Да 800 точек, а не 20 000, как на примере выше. Теперь не придется перезагружать комп, когда случайно напутал с длительностью.
  • В-четвертых, нормализация мощности спектра. Было решено ограничиться мощностью спектра в 25 децибел. Всё, что отличалось от максимума более, чем на 25 децибел считалось равным нулю.

image


В то же время, по-прежнему большая часть информации посвящена шуму. Поэтому есть смысл обрезать частотную выборку на 8кГц, как показано ниже.


image


Построение LogLog спектрограммы
%% spetrogramm

buffer_duration = 0.1;    % in seconds
overlap = 50;             % in percent

sample_size = round(buffer_duration * fs);
buffer_size = round( ((1 + 2*(overlap / 100))*buffer_duration) * fs);

time_space = 0 : sample_size/fs : length(y)/fs - 1/(fs);
[~, log_space] = eject_spectr(buffer_size, x(1 : buffer_size), fs, band_pass);

% array of power buffer samples
db_range = 25;
spectrum_array = zeros(length(time_space), length(log_space));
for iter = 1 : length(time_space)

    start_index = (iter - 1) * sample_size;
    start_index = max(round(start_index - (overlap / 100)*sample_size), 1);
    end_index = min(start_index + buffer_size, length(y));

    phrase_space = start_index : end_index;

    buffer = y(phrase_space) ./ max(y(phrase_space));
    buffer = buffer .* kaiser(length(buffer), betta);

    spectr = eject_spectr(buffer_size, buffer, fs, band_pass);
    spectr = max(spectr - (max(spectr) - db_range), 0);
    spectrum_array(iter, 1 : length(log_space)) = spectr;

end

spectrum_array = spectrum_array';

win_len = 11;
win = fspecial('gaussian',win_len,2);
win = win ./ max(win(:));
spectrum_array  = filter2(win, spectrum_array) ./ (win_len*2);

Статистическая обработка


Что мы имеем на данный момент? Мы имеем 800-мерный случайный процесс, реализация которого зависит от произносимого звука, настроения и интонации спикера. Однако параметры самого процесса зависят исключительно от физических параметров голосового тракта спикера и потому постоянны. Главный вопрос состоит в том, сколько таких параметров, и как их оценивать.


"Да прибудет с тобой ЦПТ"


Почему бы не представить каждый из $800$ процессов в виде суммы $k$ различных Гауссовых процессов? Таким образом получается всего $800*k$ различных процессов. Для такого подхода даже придумали название — GMM.


Итак, осталось разобраться только с тем, чему равен этот $k$. Берем спектрограмму, строим по ней гистограмму и вуаля:


image


После просмотров нескольких таких гистограмм моя лень праздновала решительную победу и установила $k = 1$. Вдогонку она предложила забить на составление ковариационной матрицы связи различных частот и считать каждую частотную компоненту независимой, что не есть правда. Лень — она такая.


Теперь осталось только посчитать оценки МО и СКО для каждой частотной компоненты, а после объединить в один вектор главных компонент.


std_spectr = std(spectrum_array, 0, 1);
mean_spectr = mean(spectrum_array, 1);

principal_component = cat(2, mean_spectr, std_spectr);

Заключение


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


При реализации систем на динамических признаках такими преимуществами могут быть:


  • дешевизна аппаратуры
  • малая чувствительность к шумам
  • простота регистрации человека и съема данных.

Комментарии (6)


  1. hDrummer
    28.08.2017 16:58
    +5

    Труд проделан немалый, но заплюсовал сразу за первое прочитанное предложение:

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

    Оно шикарно.


    1. Vasyutka
      28.08.2017 22:27

      Я даже не знаю… теперь наверное цитировать в каждой своей статье это буду в самом начале :). Подписываюсь под каждым словом!


  1. BalinTomsk
    28.08.2017 20:37

    Надо бы в выборку кастратов добавить и мальчиковые голоса до ломки — наверняка алгоритм oшибаeтся на них. Ну и выборку раcширить — например участниками конкурса Голос.


    1. pan-alexey
      29.08.2017 13:16

      Я думаю намного лучше будет создать еще одну группу — не определен. Т.к. существует моменты в которых и группа людей не однозначно определяет м/ж


  1. AVI-crak
    29.08.2017 01:34

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

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

    После уверенного распознания нескольких букв, можно подключать второй контур — распознавание слов. Тут всё намного сложнее. В устной речи нет явных признаков пунктуации, это всё предназначено для письменного общения — для наиболее полного восстановления смысловых форм. Чтоб вас могли отличить от робота.
    Самое неприятное — в сети практически нет работ по этому направлению. Теоретиков не считаем, их как грязи.
    Перевести все звуки речи в буквенный массив, можно и на слабеньком мк. Но для полноценного распознавания речи необходим огромный массив мыслеформ. Но просто набор слов, а их связи. Именно связи будут влиять на смысл сказанного.

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

    Подобную реакцию можно масштабировать на все мыслеформы, и тогда одно_битный пьяный электрик научится думать не только про себя.
    К сожалению, последнее требование не выполнимо в реальном времени даже на очень больших и мощных машинах. В усечённом варианте — вполне, но не в полном.


    1. nad_oby
      29.08.2017 21:41

      Всё это мило, но какое отношение это имеет к теме статьи.
      Тут ставится достаточно узкая задача, ещё немного сужается и успешно решается.
      При этом автор разбирает в паре узких мест, почему он сделал тот или иной, неочевидный, на первый взгляд выбор.
      Это и делает статью по настоящему полезной на мой взгляд.
      А пьяные однобитые электрики меня смутили и никакой ясности не добавили.
      И к конструктивному диалогу не побудили.