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

По этой причине потоковые сервисы изучили способы категоризации музыки, чтобы можно было давать персональные рекомендации. Один метод включает в себя прямой анализ необработанной аудиоинформации в данной песне, оценивая необработанные данные по различным показателям. Сегодня мы будем изучать данные, собранные исследовательской группой, известной как The Echo Nest. Наша цель — просмотреть этот набор данных и классифицировать песни как «хип-хоп» или «рок», не слушая ни одной из них самостоятельно. При этом мы узнаем, как очищать наши данные, выполнять некоторую исследовательскую визуализацию данных и использовать сокращение функций для подачи наших данных с помощью некоторых простых алгоритмов машинного обучения, таких как деревья решений и логистическая регрессия.

Для начала давайте загрузим метаданные о наших треках вместе с метриками треков, собранными The Echo Nest. Песня — это нечто большее, чем ее название, исполнитель и количество прослушиваний. У нас есть еще один набор данных с музыкальными характеристиками каждого трека, такими как танцевальность и акустика по шкале от -1 до 1. Они существуют в двух разных файлах в разных форматах — CSV и JSON. Хотя CSV является популярным форматом файлов для обозначения табличных данных, JSON — еще один распространенный формат файлов, в котором базы данных часто возвращают результаты заданного запроса.

Давайте начнем с создания двух панд DataFrames из этих файлов, которые мы можем объединить, чтобы у нас были функции и метки (часто также называемые X и y) для классификации позже.

import pandas as pd
# Read in track metadata with genre labels
tracks = pd.read_csv('datasets/fma-rock-vs-hiphop.csv')
# Read in track metrics with the features
echonest_metrics = pd.read_json('datasets/echonest-metrics.json',precise_float=True)
# Merge the relevant columns of tracks and echonest_metrics
echo_tracks = echonest_metrics.merge(tracks[['genre_top','track_id']], on='track_id')
# Inspect the resultant dataframe
echo_tracks.info()
<class 'pandas.core.frame.DataFrame'>
Int64Index: 4802 entries, 0 to 4801
Data columns (total 10 columns):
acousticness        4802 non-null float64
danceability        4802 non-null float64
energy              4802 non-null float64
instrumentalness    4802 non-null float64
liveness            4802 non-null float64
speechiness         4802 non-null float64
tempo               4802 non-null float64
track_id            4802 non-null int64
valence             4802 non-null float64
genre_top           4802 non-null object
dtypes: float64(8), int64(1), object(1)
memory usage: 412.7+ KB

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

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

Чтобы понять, есть ли в наших данных какие-либо сильно коррелированные функции, мы будем использовать встроенные функции в пакете pandas.

# Create a correlation matrix
corr_metrics = echo_tracks.corr()
corr_metrics.style.background_gradient()

Разделение наших данных

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

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

# Import train_test_split function and Decision tree classifier
from sklearn.model_selection import train_test_split
# Create features
features = echo_tracks.drop(["genre_top","track_id"],axis=1).values
# Create labels
labels = echo_tracks['genre_top'].values
# Split our data
train_features, test_features, train_labels, test_labels = train_test_split(features,labels, random_state=10)

Нормализация данных объекта

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

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

Однако, поскольку PCA использует абсолютную дисперсию признака для ротации данных, признак с более широким диапазоном значений будет подавлять и смещать алгоритм по отношению к другим признакам. Чтобы избежать этого, мы должны сначала нормализовать наши функции обучения и тестирования. Для этого существует несколько способов, но чаще всего используется стандартизация, при которой все функции имеют среднее значение = 0 и стандартное отклонение = 1 (результатом является z-показатель).

# Import the StandardScaler
from sklearn.preprocessing import StandardScaler
# Scale the features and set the values to a new variable
scaler = StandardScaler()
# Scale train_features and test_features
scaled_train_features = scaler.fit_transform(train_features)
scaled_test_features = scaler.transform(test_features)

Анализ основных компонентов на наших масштабированных данных

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

Графики осыпей отображают количество компонентов в зависимости от дисперсии, объясняемой каждым компонентом, отсортированные в порядке убывания дисперсии. Графики осыпи помогают нам лучше понять, какие компоненты объясняют достаточную дисперсию наших данных. При использовании графиков с осыпями «локоть» (крутой спуск от одной точки данных к другой) на графике обычно используется для выбора подходящего порога.

# This is just to make plots appear in the notebook
%matplotlib inline
# Import our plotting module, and PCA class
import matplotlib.pyplot as plt
from sklearn.decomposition import PCA
# Get our explained variance ratios from PCA using all features
pca = PCA()
pca.fit(scaled_train_features)
exp_variance = pca.explained_variance_ratio_
# plot the explained variance using a barplot
fig, ax = plt.subplots()
ax.bar(range(pca.n_components_),exp_variance)
ax.set_xlabel('Principal Component #')

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

Но еще не все потеряно! Вместо этого мы также можем посмотреть на график кумулятивной объясненной дисперсии, чтобы определить, сколько признаков требуется, чтобы объяснить, скажем, около 85% дисперсии (отсечки здесь несколько произвольны и обычно определяются «эмпирическими правилами»). Как только мы определим подходящее количество компонентов, мы сможем выполнить PCA с этим количеством компонентов, в идеале уменьшив размерность наших данных.

# 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='--')

Проецирование на наши особенности

На графике мы увидели, что 6 признаков (помните, что индексация начинается с 0) могут объяснить 85% дисперсии!

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

# Perform PCA with the chosen number of components and project data onto components
pca = PCA(n_components=6)
# Fit and transform the scaled training features using pca
train_pca = pca.fit_transform(scaled_train_features)
# Fit and transform the scaled test features using pca
test_pca = pca.transform(scaled_test_features)

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

Здесь мы будем использовать простой алгоритм, известный как дерево решений. Деревья решений — это основанные на правилах классификаторы, которые принимают функции и следуют «древовидной структуре» бинарных решений, чтобы в конечном итоге классифицировать точку данных по одной из двух или более категорий. Помимо того, что деревья решений просты в использовании и интерпретации, они позволяют нам визуализировать «логическую блок-схему», которую модель генерирует на основе обучающих данных.

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

# Import Decision tree classifier
from sklearn.tree import DecisionTreeClassifier
# Train our decision tree
tree = DecisionTreeClassifier(random_state=10)
tree.fit(train_pca,train_labels)
# Predict the labels for the test data
pred_labels_tree = tree.predict(test_pca)

Сравните наше дерево решений с логистической регрессией.

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

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

# Import LogisticRegression
from sklearn.linear_model import LogisticRegression
# Train our logistic regression and predict labels for the test set
logreg = LogisticRegression(random_state=10)
logreg.fit(train_pca,train_labels)
pred_labels_logit = logreg.predict(test_pca)
# Create the classification report for both models
from sklearn.metrics import classification_report
class_rep_tree = classification_report(test_labels,pred_labels_tree)
class_rep_log = classification_report(test_labels,pred_labels_logit)
print("Decision Tree: \n", class_rep_tree)
print("Logistic Regression: \n", class_rep_log)

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

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

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

# 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']
# 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(
    features, labels, random_state=10)
train_pca = pca.fit_transform(scaler.fit_transform(train_features))
test_pca = pca.transform(scaler.transform(test_features))

Улучшает ли балансировка нашего набора данных предвзятость модели?

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

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

# Train our decision tree on the balanced data
tree =  DecisionTreeClassifier(random_state=10)
tree.fit(train_pca,train_labels)
pred_labels_tree = tree.predict(test_pca)
# Train our logistic regression on the balanced data
logreg = LogisticRegression(random_state=10)
logreg.fit(train_pca,train_labels)
pred_labels_logit = logreg.predict(test_pca)
# Compare the models
class_rep_tree = classification_report(test_labels,pred_labels_tree)
class_rep_log = classification_report(test_labels,pred_labels_logit)
print("Decision Tree: \n", class_rep_tree)
print("Logistic Regression: \n", class_rep_log)

Использование перекрестной проверки для оценки наших моделей

Успех! Балансировка наших данных устранила предвзятость в сторону более распространенного класса. Чтобы понять, насколько хорошо работают наши модели, мы можем применить так называемую перекрестную проверку (CV). Этот шаг позволяет нам сравнивать модели более строго.

Прежде чем мы сможем выполнить перекрестную проверку, нам нужно будет создать конвейеры для масштабирования наших данных, выполнения PCA и создания экземпляра нашей выбранной модели — DecisionTreeClassifier или LogisticRegression.

Поскольку то, как наши данные разделены на обучающие и тестовые наборы, может повлиять на производительность модели, CV пытается разделить данные несколькими способами и протестировать модель на каждом из разделений. Несмотря на то, что существует множество различных методов CV, каждый со своими преимуществами и недостатками, здесь мы будем использовать метод, известный как K-fold CV. K-fold сначала разбивает данные на K разных подмножеств одинакового размера. Затем он итеративно использует каждое подмножество в качестве тестового набора, а остальные данные используют в качестве наборов поездов. Наконец, мы можем агрегировать результаты каждой складки для окончательной оценки производительности модели.

from sklearn.model_selection import KFold, cross_val_score
from sklearn.pipeline import Pipeline
tree_pipe = Pipeline([("scaler", StandardScaler()), ("pca", PCA(n_components=6)), 
                      ("tree", DecisionTreeClassifier(random_state=10))])
logreg_pipe = Pipeline([("scaler", StandardScaler()), ("pca", PCA(n_components=6)), 
                        ("logreg", LogisticRegression(random_state=10))])
# Set up our K-fold cross-validation
kf = KFold(n_splits=10)
# Train our models using KFold cv
tree_score = cross_val_score(tree_pipe,features, labels, cv=kf)
logit_score = cross_val_score(logreg_pipe,features, labels, cv=kf)
# Print the mean of each array of scores
print("Decision Tree:", np.mean(tree_score), "Logistic Regression:",np.mean(logit_score))
Decision Tree: 0.7582417582417582 
Logistic Regression: 0.782967032967033

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

Код: https://github.com/abh2050/Codes/blob/master/Song%20recommender%20from%20Audio%20Data.ipynb

Источник: DataCamp.com