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

Следовательно, стриминговые сервисы нацелены на категоризацию музыки для получения индивидуальных рекомендаций. Анализ в этой статье будет рассматривать наборы данных, чтобы классифицировать песни как «хип-хоп» или «рок», не слушая ни одной из них. Процесс решения проблем будет следовать этапам очистки данных, 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