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

  1. Зачем нужна идентификация жанра?

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

2. Описание данных

С помощью веб-API Spotify мы собрали следующие функции для каждого из треков.

Audio features
--------------
  danceability - How suitable a track is for dancing. (1.0 is the most danceable.)
  energy - A measure of intensity and activity. (1.0 is the most energetic.)
  key - The key the track is in. 
  loudness - The average overall loudness of a track in decibels (dB). (Ranges from -60 and 0)
  mode - Indicates the modality of a track (major:1 or minor:0)
  speechiness - Speechiness detects the presence of spoken words in a track.(1.0 is the most speechiness.)
  acousticness - A confidence measure from 0.0 to 1.0 of whether the track is acoustic. (1.0 is the most confidence.)
  instrumentalness -  A Track's vocal level. (1.0 is the most instrumentalness thus no vocals.)
  liveness - Detects the presence of an audience in the recording. (A value above 0.8 liveness provides strong likelihood that the track is performed live.)
  valence - Describing the musical positiveness conveyed by a track. (1.0 is the most positive.)
  tempo -  Beats per minute (BPM)/ pace.
  duration_ms - Duration of the track in milliseconds.
  time_signature - How many beats per measure we are playing and what kind of note value that beat is in.


Generic features
----------------
  id - Tracks's Spotify ID.
  popularity - The higher the value the more popular the track is.
  genre - Genre of the track.
  sub_genre - Sub-genre of the track.

мы собрали 10 901 трек, принадлежащий к 10 основным музыкальным жанрам. Ниже представлено распределение треков по жанрам.

3. Визуализация данных

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

sns.set(rc = {'figure.figsize':(10,10)})
sns.kdeplot(data=df, x='danceability', hue='genre', fill=True).set_title(col)

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

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

  • Танцевальность. У хип-хопа самая высокая танцевальная способность, а у классики самая низкая. (Да, мы это уже знали!)
  • Энергия — у металла самая высокая энергия, а у классического — самая низкая.
  • Громкость. Металл имеет самую высокую громкость, а классическая — самую низкую.

Далее давайте посмотрим на темп, валанс и речь.

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

  • Темп. Для метала характерен самый высокий средний темп (129,12), а для классики — самый низкий.
  • Valance. У блюза самая высокая валентность (0,58), а у классической (0,15) самая низкая.
  • Речь.Хип-хоп имеет самую высокуюречесть (0,23), а кантри (0,04) — самую низкую.

Далее, давайте посмотрим на режим, тональность и живучесть.

Интересно, что в приведенных выше случаях мы не можем наблюдать каких-либо существенных различий между разными жанрами. Например, во всех жанрах есть дорожки, принадлежащие соответственно двум разным модальностям:мажорному (1) и минорному (0). Следовательно, такая переменная не будет очень полезной для различения жанров. Однако некоторые аудиофункции, которые мы собрали, различаются в зависимости от типа жанра. Это действительно хорошая новость, поскольку мы планируем использовать их для построения модели классификации для предсказания жанра.

4. Обучение моделей машинного обучения

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

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

  1. Разделение данных

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

X= df[col_list]
y= df['genre']

#label encoder for y
le = LabelEncoder().fit(y)
y = pd.Series(le.transform(y))

#train test split
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

У нас есть 8720 экземпляров в тренировочном наборе и 2181 в тестовом наборе с частотами ниже класса.

metal        1574
hip hop      1328
edm          1018
rock          845
blues         831
classical     747
r&b           744
pop           616
country       560
jazz          457

2. Выбор функции

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

Mutual Information (independent;target) = Entropy(independent) - Entropy(independent;target)

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

#information gain/ mutual info

from sklearn.feature_selection import mutual_info_classif

mutual_info = mutual_info_classif(X_train, y_train)
mutual_info = pd.Series(mutual_info, X_train.columns).sort_values()

Кажется, что все функции имеют некоторое количество общей информации, совместно используемой с целевой переменной. В этом случае мы устанавливаем порог и выбираем верхние 80% функций. Следовательно, мы исключаем key, mode и liveness, которыеимеют самый низкий уровень взаимной информации по сравнению с остальными.

3. Корреляционный анализ

В этом разделе мы проводим корреляционный анализ признаков. Ниже приведены некоторые заслуживающие внимания наблюдения.

  • Энергия имеет сильную положительную корреляцию с громкостью (0,83) и сильную отрицательную корреляцию с акустикой (-0,8).
  • Танцевальность имеет умеренно положительную корреляцию с валентностью (0,52).
  • Громкость умеренно отрицательно коррелирует с инструментальностью (-0,72) и акустикой (-0,62).

Мы видим, что энергия имеет очень высокую корреляцию (> 0,8) с громкостью и акустикой. Следовательно, мы опускаем функцию энергии, чтобы избежать мультиколлинеарности.

X_train.drop(columns=['energy'],axis=1, inplace=True)
X_test.drop(columns=['energy'],axis=1, inplace=True)

4. Модельное обучение

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

danceability, loudness, speechiness, acousticness, instrumentalness, liveness, valence, tempo, duration_ms, time_signature

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

from sklearn.linear_model import LogisticRegression
from sklearn.svm import SVC
from sklearn.neighbors import KNeighborsClassifier
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
from xgboost import XGBClassifier


#Build ML pipeline with data balancing and model training
pipeline = imbPipeline(steps=[
    ('scale', StandardScaler()),
    ('smote', SMOTE()),
    ('clf', RandomForestClassifier(random_state=42))
    ] )

clfs = []
clfs.append(LogisticRegression(max_iter=200, C=0.5, random_state=42))
clfs.append(SVC(random_state=42))
clfs.append(KNeighborsClassifier(n_neighbors=5))
clfs.append(DecisionTreeClassifier(max_depth=7, min_samples_split=5, random_state=42))
clfs.append(RandomForestClassifier(n_estimators=100, max_depth=7, min_samples_split=5, random_state=42))
clfs.append(GradientBoostingClassifier(n_estimators=100, learning_rate=0.05, random_state=42))
clfs.append(XGBClassifier(n_estimatorsint = 300,random_state=42))

for classifier in clfs:
    #set estimator as a seperate parameter
    pipeline.set_params(clf = classifier)

    #fit data
    pipeline.fit(X_train, y_train)

    scores = cross_validate(pipeline, X_train, y_train, cv = 5, return_train_score=True)

Ниже приведены результаты первоначального обучения модели. Мы можем заметить, что XGBClassifier сообщает о самой высокой точности обучения и перекрестной проверки, за которой следует Gradient Boost.

+-----------------------------+---------------+--------------+
|   Algorithm                 | Training ACC  | CV accuracy  |
+-----------------------------+---------------+--------------+
| LogisticRegression          |     0.57      |   0.56       |
| SVC                         |     0.67      |   0.61       |
| KNeighborsClassifier        |     0.76      |   0.53       |
| DecisionTreeClassifier      |     0.55      |   0.50       |
| RandomForestClassifier      |     0.65      |   0.60       |
| GradientBoostingClassifier  |     0.69      |   0.62       |
| XGBClassifier               |     0.99      |   0.64       |
+-----------------------------+------------------------------+

5. Выбор модели

О наилучшей точности CV сообщает XGBClassifier (0,64) вместе с точностью обучения 0,99. Этот разрыв между двумя значениями точности показывает, что модель XGB переоснащена обучающими данными. Это вызвано моделью с высокой дисперсией и низким смещением. Другими словами, это означает, что у нас есть очень сложная модель, способная запоминать тренировочные данные вместо изучения обобщаемого шаблона. Некоторые из наиболее распространенных способов уменьшить сложность модели — использовать регуляризацию, выбор признаков, дополнительные обучающие данные, добавление шума, увеличение данных и т. д.

В классификаторе XGB регуляризация может быть достигнута путем настройки двух наборов параметров. (Обратитесь к этому для параметров XGB.)

  1. Настройка параметров сложности моделей, таких как learning_rate, max_depth, min_split_loss, min_child_weight, reg_lambda, reg_alpha
  2. Настройка случайности для обучения модели путем выбора подмножества экземпляров и функций соответственно на основе параметров subsample и colsample_bytree.

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

from imblearn.pipeline import Pipeline

pipe = Pipeline([
        ('smote', SMOTE()),
        ('xgb', XGBClassifier())
    ])

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

from sklearn.model_selection import GridSearchCV
params = {
    'xgb__learning_rate': [0.3, 0.5],
    'xgb__max_depth': [2, 3],
    'xgb__min_child_weight': [5, 10],
    'xgb__n_estimators': [100, 200],
    'xgb__gamma': [1, 2],
    'xgb__colsample_bytree': [0.5, 0.8],
    'xgb__colsample_bytree': [0.8, 1.0],
}
grid_search = GridSearchCV(estimator=pipe, param_grid=params, scoring="f1_macro", n_jobs=1, cv=3, error_score="raise", verbose=2)


grid_search.fit(X_train, y_train)

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

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

6. Оценка

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

#predict on test set using the best model
y_pred = grid_search.best_estimator_.predict(X_test)

#convert the predicted target variables (encoded) to its original form. 
y_pred_original = le.inverse_transform(y_pred)
y_test_original = le.inverse_transform(y_test)

#pass predictions for evaluation. Refer next code block
evalaute(grid_search, y_test_original, y_pred_original)

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

#ML model evalution

from sklearn.metrics import accuracy_score
from sklearn.metrics import confusion_matrix
from sklearn.metrics import precision_score
from sklearn.metrics import recall_score
from sklearn.metrics import f1_score
from sklearn.metrics import ConfusionMatrixDisplay, confusion_matrix
from sklearn import metrics



def evalaute(pipeline, y_test_original, y_pred_original, show=True):

    if show: 
        print(metrics.classification_report(y_test_original, y_pred_original, digits=3))


        fig, ax = plt.subplots(figsize=(12, 8))
        plt.grid(False)
        cm = confusion_matrix(y_test_original, y_pred_original)
        cmp = ConfusionMatrixDisplay(cm, display_labels=np.unique(y_test_original))
        cmp.plot(ax=ax)

        plt.show();
    else:
        rep  = metrics.classification_report(y_test_original, y_pred_original, digits=3,  output_dict=True)
        df_rep = pd.DataFrame(rep).transpose()
        return df_rep

Точность испытаний модели показана ниже. Мы видим, что наша модель показывает общую точность 0,647 и оценку F1 0,65. Мы получили повышение точности на 0,007 после выбора модели.

               precision    recall  f1-score   support

       blues      0.586     0.560     0.573       200
   classical      0.906     0.886     0.896       185
     country      0.447     0.645     0.528       124
        edm       0.590     0.534     0.561       234
     hip hop      0.814     0.774     0.793       340
        jazz      0.704     0.660     0.681       144
       metal      0.808     0.789     0.798       399
         pop      0.430     0.497     0.461       149
         r&b      0.441     0.435     0.438       191
        rock      0.481     0.470     0.475       215

    accuracy                          0.647      2181
   macro avg      0.621     0.625     0.620      2181
weighted avg      0.656     0.647     0.650      2181

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

Жанры поп, r&b и рок имеют наименьшую точность около ‹ 0,5. Мы можем визуализировать результаты классификации в матрице путаницы, как показано ниже. Глядя на метрики путаницы, мы видим, что в большинстве случаев поп-класс ошибочно классифицируется как edm. Класс r&b ошибочно классифицируется как хип-хоп, а класс рока ошибочно классифицируется как металл или кантри. Как мы уже пробовали с контролем переобучения, следующим шагом будет настройка входных данных для повышения производительности этих классов (например, увеличение данных или использование дополнительных данных).

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