
К старту курса по ML и DL рассказываем, как воспользоваться API Spotify, чтобы создать систему рекомендаций музыки под настроение на основе алгоритмов ML. Благодаря простоте систему легко настроить под ваши нужды: API Spotify возвращает понятные человеку признаки музыкального файла, например тембр. За подробностями приглашаем под кат.
Чтобы создать систему музыкальных рекомендаций, нужно абстрагироваться от жанров и — что сложнее — найти количественные методы превращения идей в полезный инструмент рекомендаций. Посмотрим, как это сделать. Методы рекомендаций делятся на две основные ветви.
Коллаборативная фильтрация
В ней поведение пользователя моделируется, а затем статистически прогнозируется, что понравится конкретному пользователю в конкретной ситуации. В ход идёт сакраментальное «другие пользователи также приобрели...». Учитывается и общая популярность. Так, случайному пользователю Дрейк по статистике понравится больше Slipknot. Особенно, если ему нравятся и другие рэп-исполнители.
Фильтрация по содержанию
Другой подход — извлечь данные из музыкального файла. Например, метаданные — это дата релиза, жанр или лейбл, под которым вышел трек. Или признаки самого аудио — от очевидных тональности и темпа до весьма абстрактных, математических признаков, например MFCC или измерение тембра. Напишем простую и эффективную систему рекомендаций без какого-либо машинного обучения… по крайней мере с вашей стороны.
Плюсы подхода
Этот подход принципиально отличается от методов коллаборативной фильтрации («другие пользователи также приобрели...»). Особенно он полезен тем, кто не работает с большой музыкальной платформой и не имеет миллиардов релевантных пользовательских точек данных. Ещё один плюс — быстрая и бесплатная реализация, от сбора данных до алгоритма.
Извлекаем данные
Извлечь аудиоданные с помощью алгоритмов или моделей машинного обучения поиска музыкальной информации — для новичка это сложно. К счастью, умные люди сделали API, где делятся секретами инновационной технологии.

API от Spotify бесплатный и имеет несколько характеристик настроения, извлекаемых прямо из внутренних моделей машинного обучения. Но как смоделировать настроение всего четырьмя характеристиками: «Танцевальностью», «Валентностью», «Возбуждением» и «Темпом»? Темп при этом [по мнению автора] едва ли отнесёшь к настроению. На самом деле нужны только две характеристики.
Психология: плоскость «валентность — возбуждение»

Плоскость «валентность — возбуждение» — одна из главных психологических моделей факторной структуры эмоций. Модель описывает, сколько существует независимых измерений эмоций. Эмоции сводятся к двум компонентам — возбуждению (активности или вялости) и валентности (позитиву или негативу).
У этой модели есть проблемы: к примеру, страх и гнев расположены близко, но у неё прекрасный баланс между сложностью и прогностической значимостью, поэтому модель Рассела находит широкое применение. А главное — она сочетается с «валентностью» и «возбуждением» в данных от Spotify. Посмотрим, как получить данные по API и реализуем простую, но эффективную систему рекомендаций.
Собираем данные
Авторизация
Для доступа к Spotify API регистрируем приложение согласно этому руководству или смотрим эту наглядную статью на Medium. Без авторизации мы не получим данные.
Подготовка кода
Нам нужна база данных о треках, пакет tekore, Client ID и Secret ID от Spotify для доступа к API. В каталоге проекта пишем скрипт authorization.py
:
import tekore as tk
def authorize():
CLIENT_ID = "ENTER YOUR CLIENT ID HERE"
CLIENT_SECRET = "ENTER YOUR CLIENT SECRET HERE"
app_token = tk.request_client_token(CLIENT_ID, CLIENT_SECRET)
return tk.Spotify(app_token)
В коде вводим Client ID и Client Secret ID. Скрипт разрешает доступ к Spotify API и возвращает объект для обращения к API, а ещё служит вспомогательным скриптом. Главное — он гарантирует, что Client ID и Client Secret ID скрыты.
Получение набора данных Spotify
Командой pip install pandas tqdm устанавливаем нужные пакеты. Копируем и запускаем код ниже и/или следуем краткому обзору кода.
#################
## PREPARATION ##
#################
# Import modules
import sys
# If your authentification script is not in the project directory
# append its folder to sys.path
sys.path.append("../spotify_api_web_app")
import authorization
import pandas as pd
from tqdm import tqdm
import time
# Authorize and call access object "sp"
sp = authorization.authorize()
# Get all genres
genres = sp.recommendation_genre_seeds()
# Set number of recommendations per genre
n_recs = 100
# Initiate a dictionary with all the information you want to crawl
data_dict = {"id":[], "genre":[], "track_name":[], "artist_name":[],
"valence":[], "energy":[]}
################
## CRAWL DATA ##
################
# Get recs for every genre
for g in tqdm(genres):
# Get n recommendations
recs = sp.recommendations(genres = [g], limit = n_recs)
# json-like string to dict
recs = eval(recs.json().replace("null", "-999").replace("false", "False").replace("true", "True"))["tracks"]
# Crawl data from each track
for track in recs:
# ID and Genre
data_dict["id"].append(track["id"])
data_dict["genre"].append(g)
# Metadata
track_meta = sp.track(track["id"])
data_dict["track_name"].append(track_meta.name)
data_dict["artist_name"].append(track_meta.album.artists[0].name)
# Valence and energy
track_features = sp.track_audio_features(track["id"])
data_dict["valence"].append(track_features.valence)
data_dict["energy"].append(track_features.energy)
# Wait 0.2 seconds per track so that the api doesnt overheat
time.sleep(0.2)
##################
## PROCESS DATA ##
##################
# Store data in dataframe
df = pd.DataFrame(data_dict)
# Drop duplicates
df.drop_duplicates(subset = "id", keep = "first", inplace = True)
df.to_csv("valence_arousal_dataset.csv", index = False)
Как работает код
Вспомогательный скрипт выполняет авторизацию.
С помощью
sp.recommendation_genre_seeds()
получаем все 120 жанров Spotify.Устанавливаем на максимум число рекомендаций на жанр (100).
Задаём словарь для всех данных из API.
Проходимся по каждому жанру и треку.
Получаем метаданные и аудио информацию и сохраняем в
data_dict
.Преобразуем словарь во фрейм pandas.
После удаления дублей идентификаторов экспортируем её в рабочий каталог.
У нас получился набор данных valence_arousal_dataset.csv
для системы рекомендаций.
Как использовать валентность и возбуждение для рекомендаций
Посмотрите на график ниже:

Каждый вектор с координатами «валентности» и «возбуждения» соединён с другими треками линиями. Мы видим, что эмоциональный профиль Thriller исполнителя Майкла Джексона больше похож на Rosanna.
Измерим длину векторов, или их «норму». Вот формула: sqrt(a²+b²). Пример:
Допустим «валентность» трека — 0,5, а его «возбуждение» — 1, то есть он имеет координаты (0,5, 1).
Тогда расстояние от начала координат до трека равно sqrt((0,5)² + 1²) = 1,12.
Тогда расстояние от начала координат до трека равно sqrt((0,5)² + 1²) = 1,12.
Точно так же нужно найти векторы, соединяющие трек со всеми остальными треками: применить формулу и взять вектор с наименьшей длиной.
Жёлтый вектор из точки p1 в точку p2 определяется как разность двух векторов: p2 - p1. Поэтому «расстояние настроения» между треками t1 и t2 равно норме (t2 - t1). Иначе говоря, из t2 вычитается t1 и по формуле вычисляется норма результирующего вектора. В коде на Python это реализуется просто:
def distance(p1, p2):
distance_x = p2[0] - p1[0]
distance_y = p2[1] - p1[1]
distance_vec = [distance_x, distance_y]
norm = (distance_vec[0]**2 + distance_vec[1]**2)**(1/2)
return norm
В своём коде я воспользовался numpy.linalg.norm(p2-p1)
, которая делает то же самое.
Проблемы
Ещё до реализации системы обозначим две статистические проблемы. Если они не важны для вас, пропускайте эту часть или вернитесь к ней позже.

У двух наших признаков совершенно разные распределения: у «валентности» оно близко к очень плоскому нормальному распределению, а у «возбуждения» сильно скошено.
Скачок на 0,2 «валентности» не всегда совпадает со скачком на 0,2 «возбуждения». Для модели это — недостаток. Она предполагает, что вектор (0,5, 0,5) так же близок к (0,7, 0,5), как и к (0,5, 0,7). Чтобы разрешить этот конфликт, применяется z-преобразование, но здесь мы его не рассматриваем.

Другая проблема показана на рисунке выше. В теории психологии валентность и возбуждение — два статистически независимых измерения эмоций. Но, если нанести все 12 000 треков из набора данных на плоскость «валентность — возбуждение», мы увидим, что при увеличении «валентности» увеличивается и «возбуждение».
Наклон линии регрессии 0,250 указывает на существенную корреляцию валентности и возбуждения. К сожалению, здесь нет решения: эта ошибка (по крайней мере в нашем случае) заложена в Spotify API.
Алгоритм рекомендаций
Окончательный вариант алгоритма рекомендаций прост. Пройдём его последние этапы. Весь алгоритм вы найдёте в этом блокноте.
Импортируем модули:
import pandas as pd
import random
import authorization # this is the script we created earlier
import numpy as np
from numpy.linalg import norm
Считываем данные 12 000 треков из фрейма:
df = pd.read_csv("valence_arousal_dataset.csv")
Комбинируем столбцы «валентности» и «возбуждения» и создаём для каждого трека единый вектор mood_vec:
df["mood_vec"] = df[["valence", "energy"]].values.tolist()
Последний этап перед реализацией алгоритма рекомендаций — авторизация для доступа к API Spotify:
sp = authorization.authorize()
Реализуем алгоритм рекомендаций на основе длины вектора:
def recommend(track_id, ref_df, sp, n_recs = 5):
# Crawl valence and arousal of given track from spotify api
track_features = sp.track_audio_features(track_id)
track_moodvec = np.array([track_features.valence, track_features.energy])
# Compute distances to all reference tracks
ref_df["distances"] = ref_df["mood_vec"].apply(lambda x: norm(track_moodvec-np.array(x)))
# Sort distances from lowest to highest
ref_df_sorted = ref_df.sort_values(by = "distances", ascending = True)
# If the input track is in the reference set, it will have a distance of 0, but should not be recommendet
ref_df_sorted = ref_df_sorted[ref_df_sorted["id"] != track_id]
# Return n recommendations
return ref_df_sorted.iloc[:n_recs]
Проверим алгоритм
Остаётся проверить, даёт ли алгоритм значимые результаты. У каждого трека на Spotify есть идентификатор, который содержится в ссылке на песню:

Получим ссылку: https://open.spotify.com/track/3JOVTQ5h8HGFnDdp4VT3MP?si=96f7844315434b0a. Идентификатор трека — это выделенная строка 3JOVTQ5h8HGFnDdp4VT3MP.
Gary Jules — Mad World
Добавим трек Mad World с «валентностью» 0,30 и «возбуждением» 0,06.
mad_world = "3JOVTQ5h8HGFnDdp4VT3MP"
recommend(track_id = mad_world, ref_df = df, sp = sp, n_recs = 5)
Лучшая рекомендация с «валентностью» 0,31 и «возбуждением» 0,05 — это Glory Manger от Harry Belafonte.
Неплохо! Хотя Glory Manger из совершенно другого жанра, алгоритм подобрал этот трек как имеющий схожие уровни «валентности» и «возбуждения».
Rosanna, Toto
Попробуем ещё! Rosanna имеет «валентность» 0,739 и «возбуждение» 0,513.
Лучшая рекомендация с «валентностью» 0,740 и «возбуждением» 0,504 — Sentimientos De Chartón от Duelo.
Снова жанр сильно отличается: это скорее латино, чем поп-рок. Но Sentimientos De Cartón передаёт чувство романтической тоски и одновременно ощущение движения, ритма, которые ощущаются и в Rosanna.
Заключение
Поздравляю! Вы добрались до этого места, а значит — либо создали свою первую систему рекомендаций под настроение, либо хотя бы немного узнали о том, как создать такую систему. Но есть ещё кое-что.
Как улучшить систему?
Чтобы расстояния стали точнее, можно применять z-преобразование для признаков «валентности» и «возбуждения».
В Spotify API есть и другие характеристики: «танцевальность», «акустичность», «темп». Подумайте, как развить плоскость «валентность — возбуждение» или разработать собственный набор переменных.
Интересно сочетание рекомендаций под настроение и методов коллаборативной фильтрации. После вычисления 10 лучших рекомендаций с «валентностью» и «возбуждением» стоит попробовать изменить порядок рекомендаций по «популярности» — такой признак также есть в API Spotify.
Можно дать пользователю возможность указать списки жанров — белый и чёрный.
Стоит расширить эталонный набор данных, изучив возможности Spotify API и проанализировав ещё больше треков. Большая база данных повышает вероятность найти почти идеальное соответствие треков.
Если вы хотите не только использовать машинное обучение, но и понять, как оно работает, то вы можете обратить внимание на наши курсы:
Также вы можете перейти на страницы из каталога, чтобы увидеть, как мы готовим специалистов в других направлениях.

Профессии и курсы
Data Science и Machine Learning
Python, веб-разработка
Мобильная разработка
Java и C#
От основ — в глубину
А также: