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



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


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


Ну что, пожалуй, пора перейти от слов к делу. Поехали!


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


Все объяснения я буду приводить на примере звука дв��гателя с поломкой, взятого из этого ролика на YouTube.


Скачанный с ютуба файл (можно скачать с помощью браузерных расширений или просто изменив в ссылке youtube на ssyoutube) конвертируем в wav формат с помощью ffmpeg:


ffmpeg -i input_video.mp4 -c:a pcm_s16le -ar 16000 -ac 1  engine_sound.wav

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



Если простым языком, то звук — это волна, и на осциллограмме наблюдаются значения амплитуды этой волны в заданные моменты времени.


Чтобы получить из такого представления спектрограмму, нам потребуется преобразование Фурье. С его помощью можно получить амплитудно-частотное представление звука или амплитудный спектр. Такой спектр показывает, на какой частоте и с какой амплитудой выражен исследуемый сигнал.


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


начальный фрагмент спектрограммы сигнала


На этой спектрограмме видно, что звук двигателя при отсутствии постукивания "выглядит" примерно одинаково, и выражен на частотах в окрестности 600, 1200, 2400 и 4800 Гц. Звук стука, который беспокоит владельца, очень хорошо различим в диапазоне частот 600-1200 Гц с 5 по 8 секунду. Поскольку запись сделана в довольно шумных условиях на улице, на спектрограмме эти шумы также присутствуют, что несколько усложняет нашу задачу.


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


Рассчитать спектрограммы можно с помощью библиотеки librosa следующим образом:


from librosa.util import buf_to_float
from librosa.core import stft  # функция для вычисления спектрограммы
import numpy as np
from scipy.io import wavfile  # для работы с wav-файлами

def cut_wav(path_to_wav, start_time, end_time):
    sr, wav_data = wavfile.read(path_to_wav)

    return wav_data[int(sr * start_time): int(sr * end_time)]

def get_stft(wav_data):
    feat = np.abs(stft(buf_to_float(wav_data), n_fft=fft_size, hop_length=fft_step))

    # транспонирование - ставим ось времени на первое место
    return feat.T

wav_path = './engine_sound.wav'
train_features = []
# готовим признаки для обучения, time_list - содержит разметку данных
for [ts, te] in time_list:
    wav_part = cut_wav(wav_path, ts, te)
    spec = get_stft(wav_part)
    train_features.append(spec)
X_train = np.vstack(train_features)

# готовим признаки для теста
full_wav_data = wavfile.read(wav_path)[1]
X_test = get_stft(full_wav_data)

Решение


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


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


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


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


from sklearn.mixture import GaussianMixture

n_components = 3
gmm_clf = GaussianMixture(n_components)
gmm_clf.fit(X_train)

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


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


n_seconds = len(full_wav_data) // sr
gmm_scores = []
# правдоподобие на каждую секунду
for i in range(n_seconds - 1):
    test_sec = X_test[(i * sr) // fft_step: ((i + 1) * sr) // fft_step, :]
    sc = gmm_clf.score(test_sec)
    gmm_scores.append(sc)

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



Выводы


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


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


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


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


Полный код на github — здесь