Автор статьи: Виктория Ляликова

Привет, Хабр! В этой статье поработаем с аудиофайлами, используя библиотеку librosa и алгоритмы Machine learning.

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

Можно сказать, что аудио является физическим представлением звука, частота которого находится в диапазоне от 20Гц до 20 килогерц.  Эти звуки доступны во многих форматах, что позволяет компьютеру их анализировать, например mp3, wma, wav.

Для работы с аудиосигналом его необходимо оцифровать, т.е. преобразовать звуковую волну в ряд чисел. Это делается путем измерения амплитуды звука через фиксированные промежутки времени. Каждое такое измерение называется выборкой, а частота выборки — количеством выборок в секунду. Например, обычная частота дискретизации составляет около 44 100 выборок в секунду. Это означает, что 10-секундный музыкальный клип будет содержать 441 000 семплов.

Благодаря дискретизации звука из аудиозаписей можно извлекать достаточно большое число различных характеристик, которые помогают в дальнейшем анализе аудио. Среди таких характеристик можно выделить, например, мел-кепстральные коэффициент (MFCCs), спектр, спектограмма, спектральный центроид (Spectral Centroid), спектральный спад (Spectral Rolloff)  и др.

Чтобы подробнее разобраться с данными характеристиками, установим библиотеку Librosa, которая используется для анализа звуковых сигналов, но больше ориентирована на музыку

pip install librosa

или

conda install -c conda-forge librosa

И здесь хочется сделать небольшое отступление. Я работаю в Jupyter Notebook и часто получается так, что ты набираешь эти волшебные команды и, …. ничего не работает. И тут тогда начинается, гуглишь, пробуешь различные варианты, которые обещают исправить проблему установки, пытаешься все переустановить заново, создать новую виртуальную среду, и все равно ничего не работает. Вот именно с этой библиотекой у меня было много проблем, в разные моменты времени появлялись разные ошибки и ничего не работало. Но потом, каким-то волшебным образом Librosa все-таки установилась, причем, я уже даже не знаю, что именно мне помогло.

Но есть предположение, что команда

.pip install librosa –-user оказалась волшебной.

Теперь можно импортировать библиотеку и начинать работать.

import librosa

Классификацию аудиофайлов будем проводить по набору данных, содержащим коллекцию из аудиофайлов по 10 различным жанрам. Каждый жанр имеет по 100 аудиофайлов продолжительностью по 3 и 30 секунд.

Загрузка аудио и извлечение значимых характеристик

Используя функцию librosa.load() можно считать конкретные звуки из нашего набора данных.

import os # библиотека для работы с файлами
dir = '***/datasets/Data/genres_original' #задаем директорию с данными
file = dir+'/blues/blues.00000.wav'
signal, sr = librosa.load(file, sr = 22050) # загружаем файл

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

print(signal.shape, sr)
(661794,) 22050

Посмотрим на наш аудиосигнал

print(signal)
[ 0.00732422  0.01660156  0.00762939 ... -0.05560303 -0.06106567
 -0.06417847]

Полученный аудиосигнал можно представить в виде звуковой волны с помощью функции librosa.display.waveshow()

import matplotlib.pyplot as plt
import librosa.display as ld
plt.figure(figsize=(12,4))
ld.waveshow(signal, sr=sr)

Вертикальная сторона представляет амплитуду звука, а горизонтальная ось — время, затраченное на воспроизведение звука на определенной частоте.

А с помощью функции IPyhon.display() получим плеер в блокноте, где можно воспроизвести аудиофайл.

import IPython
display(IPython.display.Audio(signal, rate = sr))

Звук можно перевести из временной области в частотную область с помощью быстрого преобразования Фурье таким образом, что после этого мы получим спектр сигнала. В librosa для этих целей есть функция librosa.stft() c такими параметрами как n_fft - длина оконного сигнала после заполнения нулями и hop_length - размер кадра или размер быстрого преобразования Фурье.

n_fft = 2048
ft = np.abs(librosa.stft(signal[:n_fft], hop_length = n_fft+1))
plt.plot(ft)
plt.title('Spectrum')
plt.xlabel('Frequency Bin')
plt.ylabel('Amplitude')

Спектрограмма является визуальным способом представления уровня или громкости сигнала на различных частотах, присутствующих в формах волны. То есть показывает интенсивность частот во времени. Это дает представление времени по оси x, частоты по оси y, а соответствующие амплитуды представляются цветом

С помощью функции librosa.display.specshow() можно посмотреть на спектрограмму аудиосигнала:

X = librosa.stft(signal)
s = librosa.amplitude_to_db(abs(X))
ld.specshow(s, sr=sr, x_axis = 'time', y_axis='linear')
plt.colorbar()

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

Мел-кепcтральные коэффициенты можно найти с помощью функции librosa.feature.mfcc()

mfccs = librosa.feature.mfcc(y=signal, sr=sr, n_mfcc = 40, hop_length=512)
mfccs
mfccs.shape
(40, 1293)

n_mfcc - количество коэффициентов MFCC

hop_length - размер кадра

Размер матрицы мел-коэффициентов вычисляется как

[n_mfcc, len(signal)//hop_length+1]

То есть, если n_mels = 40, hop_length = 512, тогда

len(signal)//hop_length+1 = 661794//512+1 = 1292+1 = 1293.

Также мы можем построить мел-спектрограмму с помощью функции librosa.feature.melspectrogram(). Мел-спектрограмма представляет собой спектрограмму, преобразованную в мел-шкалу.

melspectrum = librosa.feature.melspectrogram(y=signal, sr = sr,
                                        	hop_length =512, n_mels = 40)

Спектральный центроид (Spectral Centroid) является хорошим показателем яркости звука, широко используется в качестве автоматической меры музыкального тембра. То есть центроид показывает где расположен центр масс звука. В блюзовых композициях частоты распределены равномерно, в металле спектроид лежит ближе к концу спектра. В Librosa используется функция librosa.feature.spectral_centroid()

cent = librosa.feature.spectral_centroid(y=signal, sr=sr)
plt.figure(figsize=(15,5))
plt.semilogy(cent.T, label='Spectral centroid')
plt.ylabel('Hz')
plt.legend()

array([[1936.83283904, 1820.36294357, 1780.31673025, ..., 2770.21094705, 2661.92181327, 2604.75205139]])

Спектральны спад  (Spectral Rolloff) представляет собой частоту, ниже которой лежит определенный процент от общей спектральной энергии. В librosa используется функция librosa.feature.spectral_rolloff()

rolloff = librosa.feature.spectral_rolloff(y=signal, sr=sr)
plt.figure(figsize=(15,5))
plt.semilogy(rolloff.T, label='Roll-off frequency')
plt.ylabel('Hz')
plt.legend()

array([[4005.17578125, 3520.67871094, 3348.41308594, ..., 5792.43164062, 5577.09960938, 5361.76757812]])

Скорость пересечения нуля (Zero crossing Rate) является частотой изменения знака сигнала, то есть частота, с которой сигнал меняется с положительного на отрицательный и обратно. Например, для металла и рока этот параметр обычно выше, чем для других жанров, из-за большого количества ударных. В librosa используется функция librosa.feature.zero_crossing_rate()

zrate=librosa.feature.zero_crossing_rate(signal)
plt.figure(figsize=(14,5))
plt.semilogy(zrate.T, label='Fraction')
plt.ylabel('Fraction per Frame')
plt.legend()

array([[0.03808594, 0.06054688, 0.07861328, ..., 0.14550781, 0.13623047, 0.10058594]])

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

Например, можно также выбирать средние значения и стандартные отклонения мел-кепстральных коэффициентов:

mfcc_mean = np.mean(librosa.feature.mfcc(y=signal, sr=sr), axis=1)
mfcc_std = = np.std(librosa.feature.mfcc(y=signal, sr=sr), axis=1)

средние значения и стандартные отклонения спектрального центроида

cent_mean = np.mean(cent)
cent_std = np.std(cent)

средние значения и стандартные отклонения спектрального спада и т.д.

roloff_mean = np.mean(roloff)
croloff_std = np.std(roloff)

Потом все эти значения можно записать в датафрейм и работать с ними

df = pd.DataFrame(audio_data)
df['labels'] = labels

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

audio_files = []
labels = []
labelind = -1
for label in os.listdir(dir):
	labelind +=1
	label_path = os.path.join(dir, label)
	for audio_file in os.listdir(label_path):
    	audio_file_path = os.path.join(label_path, audio_file)
    	audio_files.append(audio_file_path)
    	labels.append(labelind)

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

def preprocess_audio(audio_file_path):
	audio, sr = librosa.load(audio_file_path)
	mfcc_mean = np.mean(librosa.feature.mfcc(y=audio, sr=sr),   axis=1)
	return abs(mfcc_mean)

Получим список audio_data цифровых значений для всех аудиофайлов:

audio_data =[]
for audio_file in audio_files:
	mfccs_mean = preprocess_audio(audio_file)
	audio_data.append(mfccs_mean)

Создадим массивы из характеристик аудиофайлов и их меток

audio_data = np.array(audio_data)
labels = np.array(labels)

Таким образом, можно все характеристики аудиофайлов объединить в один датафрейм и далее с ним работать. 

Построение модели

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

df = pd.read_csv(f'{dir}/features_3_sec.csv')
df

В нашем датафрейме 2 столбца (filename и length), которые далее нам не понадобятся, поэтому удалим их.

df = df.iloc[0:, 2:]

Теперь определим вектор с данными для обучения (X) и вектор соответствующим их меток (y). Данные для обучения представляют собой значимые характеристики аудиоданных, всего 57 значений. Разберемся с метками классов. 

df['label'].unique()
array(['blues', 'classical', 'country', 'disco', 'hiphop', 'jazz',
       'metal', 'pop', 'reggae', 'rock'], dtype=object)

Они имеют категориальное представление, поэтому преобразуем их в численное представление с помощью метода LabelEncoder().

class_list=df.iloc[:,-1] # создаем список классов
convertor = preprocessing.LabelEncoder()
y=convertor.fit_transform(class_list) # конвертируем признаки

Теперь перейдем к данным для обучения Х. Удалим из нашего датафрейма столбец с метками.

X = df.loc[:, df.columns !='label']

Нормализуем наш целевой вектор с помощью метода StandardScaler()

from sklearn import preprocessing
cols = X.columns
scaler = preprocessing.StandardScaler()
np_scaled = scaler.fit_transform(X)
X = pd.DataFrame(np_scaled, columns = cols)

Разделим выборку на тестовую и обучающую

from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.3, random_state=42 )

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

Импортируем необходимые модули:

from sklearn.naive_bayes import GaussianNB
from sklearn.linear_model import LogisticRegression
from sklearn.neighbors import KNeighborsClassifier
from sklearn.ensemble import RandomForestClassifier
from sklearn.svm import SVC
from sklearn.neural_network import MLPClassifier
from xgboost import XGBClassifier
from sklearn.metrics import confusion_matrix, accuracy_score, roc_auc_score, roc_curve

Создадим дополнительную функцию по работе с алгоритмами обучения:

def model_assess(model, title = "Default"):
	model.fit(X_train, y_train)
	preds = model.predict(X_test)
	print('Accuracy', title, ':', round(accuracy_score(y_test, preds), 5), '\n')

Построим модели и оценим точность.

# алгорит Баейса
nb = GaussianNB()
model_assess(nb, "Naive Bayes")
# алгоритм k-ближайших соседей
knn = KNeighborsClassifier(n_neighbors=10)
model_assess(knn, "KNN")
# метод опорных векторов
svm = SVC(decision_function_shape="ovo")
model_assess(svm, "Support Vector Machine")
# логистическая регрессия
lg = LogisticRegression(random_state=0, solver='lbfgs', multi_class='multinomial')
model_assess(lg, "Logistic Regression")
# случайный лес
rforest = RandomForestClassifier(n_estimators=1000, max_depth=10, random_state=0)
model_assess(rforest, "Random Forest")
# многослойный персептрон
nn = MLPClassifier(solver='lbfgs', alpha=1e-5, hidden_layer_sizes=(5000, 10), random_state=1)
model_assess(nn, "Neural Nets")
# деревья с градиентным бустингом
xgb = XGBClassifier(n_estimators=1000)
model_assess(xgb, "XGBClassifier")

Видим, что самый низкий показатель точности 0,52 у алгоритма Байеса, а самый высокий 0,9 у алгоритма XGBClassifier.

Рекомендации аудиокомпозиций

А теперь попробуем  порекомендовать пользователю аудиокомпозиции, используя метод cosine_similarity() из библиотеки scikit-learn. Данный метод вычисляет косинусное сходство между двумя ненулевыми векторами и основан на косинусе угла между ними, что дает значение от -1 до 1. Значение -1 означает, что векторы противоположны, 0 представляет ортогональные векторы, а значение 1 означает подобные векторы.

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

df1 = pd.read_csv(f'{dir}/features_30_sec.csv',index_col=0)
labels = df1[['label']]
df1 = df1.drop(columns=['length','label'])

Далее переведем наш датафрейм в матрицу размером 1000х57 и вычислим косинусное сходство между векторами.

from sklearn import preprocessing
from sklearn.metrics.pairwise import cosine_similarity
scaled=preprocessing.scale(df1)
similarity = cosine_similarity(scaled)

И теперь полученную матрицу сходства представим в виде датафрейма:

similarity_labels = pd.DataFrame(similarity)
similarity_names = similarity_labels.set_index(labels.index)
similatity_names.columns = labels.index

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

name = 'rock.00087.wav'
series = pd.DataFrame(similarity_names[name].sort_values(ascending = False))
series = series.loc[(series[name]>0.90)]
series = series.drop(name)
print("\n*******\nSimilar songs to ", name)
print(series.head(5))

Получаем названия рекомендованных композиций и их косинусное сходство.

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

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

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


  1. Nasreddin_Hodja
    10.06.2023 20:19

    Интересно, а с классификацией по поджанрам металла оно справиться способно?

    Или по идее проще задачка: отсеять металл записи с вокалом, оставив инструментальные композиции.