За последние несколько лет потоковые сервисы стали основным средством, с помощью которого большинство людей слушают свою любимую музыку. Однако это может означать, что пользователям может быть сложно искать новую музыку, которая соответствует их вкусам.
Следовательно, стриминговые сервисы нацелены на категоризацию музыки для получения индивидуальных рекомендаций. Анализ в этой статье будет рассматривать наборы данных, чтобы классифицировать песни как «хип-хоп» или «рок», не слушая ни одной из них. Процесс решения проблем будет следовать этапам очистки данных, EDA и сокращения использования функций с последующим построением модели логистической регрессии на основе машинного обучения.
Давайте начнем с загрузки информации о наших треках вместе с метриками треков, собранными The Echo Nest. Кроме того, у нас также есть данные о музыкальных особенностях каждого трека, таких как танцевальность и акустика.
#Importing the pandas library import pandas as pd
# Reading the track metadata with genre labels tracks = pd.read_csv('fma-rock-vs-hiphop.csv') # Read in track metrics with the features echonest_metrics = pd.read_json('echonest-metrics.json')
tracks.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 17734 entries, 0 to 17733 Data columns (total 21 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 track_id 17734 non-null int64 1 bit_rate 17734 non-null int64 2 comments 17734 non-null int64 3 composer 166 non-null object 4 date_created 17734 non-null object 5 date_recorded 1898 non-null object 6 duration 17734 non-null int64 7 favorites 17734 non-null int64 8 genre_top 17734 non-null object 9 genres 17734 non-null object 10 genres_all 17734 non-null object 11 information 482 non-null object 12 interest 17734 non-null int64 13 language_code 4089 non-null object 14 license 17714 non-null object 15 listens 17734 non-null int64 16 lyricist 53 non-null object 17 number 17734 non-null int64 18 publisher 52 non-null object 19 tags 17734 non-null object 20 title 17734 non-null object dtypes: int64(8), object(13) memory usage: 2.8+ MB
echonest_metrics.info()
<class 'pandas.core.frame.DataFrame'> Int64Index: 13129 entries, 0 to 13128 Data columns (total 9 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 track_id 13129 non-null int64 1 acousticness 13129 non-null float64 2 danceability 13129 non-null float64 3 energy 13129 non-null float64 4 instrumentalness 13129 non-null float64 5 liveness 13129 non-null float64 6 speechiness 13129 non-null float64 7 tempo 13129 non-null float64 8 valence 13129 non-null float64 dtypes: float64(8), int64(1) memory usage: 1.0 MB
# Merging relevant columns of tracks and echonest_metrics echo_tracks = echonest_metrics.merge(tracks[['genre_top', 'track_id']], on='track_id') # Inspecting the resultant dataframe echo_tracks.info()
<class 'pandas.core.frame.DataFrame'> Int64Index: 4802 entries, 0 to 4801 Data columns (total 10 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 track_id 4802 non-null int64 1 acousticness 4802 non-null float64 2 danceability 4802 non-null float64 3 energy 4802 non-null float64 4 instrumentalness 4802 non-null float64 5 liveness 4802 non-null float64 6 speechiness 4802 non-null float64 7 tempo 4802 non-null float64 8 valence 4802 non-null float64 9 genre_top 4802 non-null object dtypes: float64(8), int64(1), object(1) memory usage: 412.7+ KB
Парные отношения между непрерывными переменными
В процессе предлагается избегать использования переменных, которые имеют сильную корреляцию друг с другом, поскольку они могут привести к избыточности функций. Следовательно, лучше всего:
- Сохраняйте модель простой, чтобы уменьшить переоснащение и улучшить интерпретируемость и
- Используйте меньше функций, чтобы сделать процесс вычислительно эффективным.
# Building the correlation matrix
corr_metrics = echonest_metrics.corr()
corr_metrics.style.background_gradient()
Нормализация данных функции
Приведенный выше результат не показывает какой-либо значительной корреляции. Следовательно, мы можем перейти к сокращению количества функций с помощью «анализа главных компонентов» (PCA).
Разницу между жанрами можно объяснить несколькими особенностями. PCA вращает данные по оси наибольшей дисперсии, что позволяет нам определить относительный вклад каждой особенности наших данных в дисперсию между классами.
Однако PCA использует абсолютную дисперсию объекта для поворота данных. Это означает, что функция с более широким диапазоном значений будет преобладать и смещать алгоритм по сравнению с другими функциями. Чтобы справиться с этим, давайте сначала нормализуем данные с помощью стандартизации, чтобы сгенерировать все функции со средним значением = 0 и стандартным отклонением = 1, как показано ниже.
# Defining the features features = echo_tracks.drop(columns=['genre_top', 'track_id']) # Defining labels labels = echo_tracks['genre_top']
# Importing the StandardScaler from sklearn.preprocessing import StandardScaler # Scaling the features and setting the values to a new variable scaler = StandardScaler() scaled_train_features = scaler.fit_transform(features)
Анализ главных компонентов на масштабированных данных
Теперь мы готовы использовать PCA, чтобы определить, насколько мы можем уменьшить размерность данных. Кроме того, чтобы найти количество компонентов, которые нужно использовать заранее, мы можем построить графики осыпи и графики совокупных объясненных соотношений. Диаграммы-осыпи отображают количество компонентов в зависимости от дисперсии, объясняемой каждым компонентом, отсортированных в порядке убывания дисперсии. «Изгиб», который представляет собой крутой спад от одной точки данных к другой, видимый на графике, определяет подходящую отсечку.
%matplotlib inline # Importing the plotting module and PCA import matplotlib.pyplot as plt from sklearn.decomposition import PCA # Getting explained variance ratios from PCA using all features pca = PCA() pca.fit(scaled_train_features) exp_variance = pca.explained_variance_ratio_ # Plotting the explained variance using a barplot fig, ax = plt.subplots() ax.bar(range(pca.n_components_), exp_variance) ax.set_xlabel('Principal Component #')
Text(0.5, 0, 'Principal Component #')
Дальнейшая визуализация PCA
Приведенный выше осыпной график не дает четкого представления, поэтому вместо этого давайте посмотрим на график совокупной объясненной дисперсии, чтобы определить, сколько функций требуется для объяснения примерно 85% дисперсии.
# Import numpy
import numpy as np
# Calculate the cumulative explained variance
cum_exp_variance = np.cumsum(exp_variance)
# Plot the cumulative explained variance and draw a dashed line at 0.85.
fig, ax = plt.subplots()
ax.plot(cum_exp_variance)
ax.axhline(y=0.85, linestyle='--')
# choose the n_components where about 85% of our variance can be explained
n_components = 6
# Perform PCA with the chosen number of components and project data onto components
pca = PCA(n_components, random_state=10)
pca.fit(scaled_train_features)
pca_projection = pca.transform(scaled_train_features)
Модель логистической регрессии
Теперь мы можем использовать низкоразмерную проекцию данных PCA для классификации песен по жанрам. Давайте создадим модель логистической регрессии, сначала разделив наш набор данных на подмножества «поезд» и «тест», где «поезд» используется для обучения нашей модели, а набор данных «тест» позволяет проверить производительность модели. Кроме того, логистическая регрессия использует логистическую функцию для вычисления вероятности того, что данная точка данных принадлежит определенному классу.
# Import train_test_split function from sklearn.model_selection import train_test_split # Split our data train_features, test_features, train_labels, test_labels = train_test_split(pca_projection, labels, random_state=10)
# Import LogisticRegression from sklearn.linear_model import LogisticRegression # Training our logisitic regression logreg = LogisticRegression(random_state=10) logreg.fit(train_features, train_labels) pred_labels_logit = logreg.predict(test_features) # Creating the classification report for the model from sklearn.metrics import classification_report class_rep_log = classification_report(test_labels, pred_labels_logit) print("Logistic Regression: \n", class_rep_log)
Logistic Regression: precision recall f1-score support Hip-Hop 0.77 0.54 0.64 235 Rock 0.90 0.96 0.93 966 accuracy 0.88 1201 macro avg 0.83 0.75 0.78 1201 weighted avg 0.87 0.88 0.87 1201
Балансировка данных для лучшей производительности
Отчет о классификации показывает нам, что рок-песни довольно хорошо классифицируются, но хип-хоп-песни непропорционально ошибочно классифицируются как рок-песни. Для классификации рока гораздо больше точек данных, чем для хип-хопа, что искажает способность нашей модели различать классы. Это также означает, что большая часть точности нашей логистической модели определяется ее способностью классифицировать только рок-песни, и, следовательно, это не очень хорошие результаты.
Чтобы учесть это, мы можем взвесить значение правильной классификации в каждом классе обратно пропорционально количеству точек данных для каждого класса.
# Subset a balanced proportion of data points hop_only = echo_tracks.loc[echo_tracks['genre_top'] == 'Hip-Hop'] rock_only = echo_tracks.loc[echo_tracks['genre_top'] == 'Rock'] # subset only the rock songs, and take a sample the same size as there are hip-hop songs rock_only = rock_only.sample(hop_only.shape[0], random_state=10) # Concatenate the dataframes hop_only and rock_only rock_hop_bal = pd.concat([rock_only, hop_only]) # The features, labels, and pca projection are created for the balanced dataframe features = rock_hop_bal.drop(['genre_top', 'track_id'], axis=1) labels = rock_hop_bal['genre_top'] pca_projection = pca.fit_transform(scaler.fit_transform(features)) # Redefine the train and test set with the pca_projection from the balanced data train_features, test_features, train_labels, test_labels = train_test_split( pca_projection, labels, random_state=10)
rock_hop_bal.info()
<class 'pandas.core.frame.DataFrame'> Int64Index: 1820 entries, 773 to 4801 Data columns (total 10 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 track_id 1820 non-null int64 1 acousticness 1820 non-null float64 2 danceability 1820 non-null float64 3 energy 1820 non-null float64 4 instrumentalness 1820 non-null float64 5 liveness 1820 non-null float64 6 speechiness 1820 non-null float64 7 tempo 1820 non-null float64 8 valence 1820 non-null float64 9 genre_top 1820 non-null object dtypes: float64(8), int64(1), object(1) memory usage: 156.4+ KB
Балансировка улучшает смещение модели?
Набор данных теперь сбалансирован, но из него удалено много точек данных. Давайте теперь проверим, улучшает ли балансировка наших данных смещение модели в сторону классификации «Камень», сохраняя при этом общую эффективность классификации.
# Training our logistic regression on the balanced data logreg = LogisticRegression(random_state=10) logreg.fit(train_features, train_labels) pred_labels_logit = logreg.predict(test_features) print("Logistic Regression: \n", classification_report(test_labels, pred_labels_logit))
Logistic Regression: precision recall f1-score support Hip-Hop 0.84 0.80 0.82 230 Rock 0.80 0.85 0.83 225 accuracy 0.82 455 macro avg 0.82 0.82 0.82 455 weighted avg 0.82 0.82 0.82 455
Перекрестная проверка для оценки
Балансировка наших данных устранила предвзятость, но чтобы получить хорошее представление о реальной производительности, мы можем применить перекрестную проверку (CV). CV пытается разделить данные несколькими способами и протестировать модель на каждом из разделений. Таким образом, K-кратное CV сначала разбивает данные на K различных подмножеств одинакового размера и итеративно использует каждое подмножество в качестве тестового набора, используя оставшуюся часть данных как наборы поездов. Наконец, он суммирует результаты для каждого сгиба и выдает окончательную оценку производительности модели.
from sklearn.model_selection import KFold, cross_val_score # Setting up K-fold cross-validation kf = KFold(10) logreg = LogisticRegression(random_state=10) # Training our model using KFold cv logit_score = cross_val_score(logreg, pca_projection, labels, cv=kf) # Print the mean of each array o scores print("Logistic Regression:", np.mean(logit_score))
Logistic Regression: 0.782967032967033