Целью этой статьи является выполнение исследовательского анализа данных музыкальных жанров, собранных с помощью веб-API Spotify. Прочтите предыдущую статью, в которой кратко описан процесс сбора данных.
- Зачем нужна идентификация жанра?
Определение музыкальных жанров может быть важно как для 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, электронная музыка, джаз, блюз, кантри, классика, металл, хип-хоп).
- Разделение данных
В приведенном ниже фрагменте кода показаны шаги для кодирования целевой переменной «жанр» и разделения данных на обучающие и тестовые наборы с соотношением 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.)
- Настройка параметров сложности моделей, таких как learning_rate, max_depth, min_split_loss, min_child_weight, reg_lambda, reg_alpha
- Настройка случайности для обучения модели путем выбора подмножества экземпляров и функций соответственно на основе параметров 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 за исходным кодом.