К старту курса по ML и DL рассказываем, как воспользоваться API Spotify, чтобы создать систему рекомендаций музыки под настроение на основе алгоритмов ML. Благодаря простоте систему легко настроить под ваши нужды: API Spotify возвращает понятные человеку признаки музыкального файла, например тембр. За подробностями приглашаем под кат.


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

Коллаборативная фильтрация

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

Фильтрация по содержанию

Другой подход — извлечь данные из музыкального файла. Например, метаданные — это дата релиза, жанр или лейбл, под которым вышел трек. Или признаки самого аудио — от очевидных тональности и темпа до весьма абстрактных, математических признаков, например MFCC или измерение тембра. Напишем простую и эффективную систему рекомендаций без какого-либо машинного обучения… по крайней мере с вашей стороны.

Плюсы подхода

Этот подход принципиально отличается от методов коллаборативной фильтрации («другие пользователи также приобрели...»). Особенно он полезен тем, кто не работает с большой музыкальной платформой и не имеет миллиардов релевантных пользовательских точек данных. Ещё один плюс — быстрая и бесплатная реализация, от сбора данных до алгоритма.

Извлекаем данные

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

Фрагмент документации API Spotify о доступных характеристиках аудио
Фрагмент документации API Spotify о доступных характеристиках аудио

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

Психология: плоскость «валентность — возбуждение»

Плоскость «валентность — возбуждение» с расположением эмоций (Russel, 1980)
Плоскость «валентность — возбуждение» с расположением эмоций (Russel, 1980)

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

У этой модели есть проблемы: к примеру, страх и гнев расположены близко, но у неё прекрасный баланс между сложностью и прогностической значимостью, поэтому модель Рассела находит широкое применение. А главное — она сочетается с «валентностью» и «возбуждением» в данных от 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)

Как работает код

  1. Вспомогательный скрипт выполняет авторизацию. 

  2. С помощью sp.recommendation_genre_seeds() получаем все 120 жанров Spotify. 

  3. Устанавливаем на максимум число рекомендаций на жанр (100). 

  4. Задаём словарь для всех данных из API. 

  5. Проходимся по каждому жанру и треку. 

  6. Получаем метаданные и аудио информацию и сохраняем в data_dict.

  7. Преобразуем словарь во фрейм pandas.

  8. После удаления дублей идентификаторов экспортируем её в рабочий каталог.

У нас получился набор данных valence_arousal_dataset.csv для системы рекомендаций.

Как использовать валентность и возбуждение для рекомендаций

Посмотрите на график ниже: 

Векторы на плоскости «валентность — возбуждение». Расположение приблизительное
Векторы на плоскости «валентность — возбуждение». Расположение приблизительное

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

Измерим длину векторов, или их «норму». Вот формула: sqrt(a²+b²). Пример:

  1. Допустим «валентность» трека — 0,5, а его «возбуждение» — 1, то есть он имеет координаты (0,5, 1). 

  2. Тогда расстояние от начала координат до трека равно 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 и проанализировав ещё больше треков. Большая база данных повышает вероятность найти почти идеальное соответствие треков.

Если вы хотите не только использовать машинное обучение, но и понять, как оно работает, то вы можете обратить внимание на наши курсы:

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

Профессии и курсы

Комментарии (6)


  1. OlegZH
    24.10.2021 22:01
    +1

    Не совсем ясна постановка задачи. Что даёт пользователю рекомендация? Пытаться спрогнозировать то, что может понравиться? Не вполне корректная постановка вопроса. Мы далеко не всегда слушаем то, что нравится. Мы познаём мир. Желательно, познавать его в цветущем многообразии. Оценка нравится/не_нравится — это, лишь, некая оценка, дающаяся постфактум. Попытаться нарисовать потрет слушателя — хорошая задача. Но это потребует дополнительного анкетирования. И тут сам процесс подсчётов может оказаться важнее основного процесса (прослушивания музыки). Помниться, ещё Педро Домингес говорил в аналогичной ситуации (может быть, и по отношению к тому же Spotify), что самим разработчикам потребовалось 400 различных параметров, чтобы рекомендательная система давала хоть какие-то удовлетворительные результаты.


    1. nktkz
      24.10.2021 22:48

      здесь судя по всему ищется ближайший сосед в пространстве возбуждение-валентность


      1. OlegZH
        25.10.2021 19:53

        Цель рекомендательной системы — предложить послушать композицию, которая понравится слушателю (прочитать книгу, посмотреть фильм, купить товар и т.д. и т.п.). Вот я и спрашиваю, насколько это нужно. Попытаться построить портрет слушателя — хорошая задача, но зачем ему что-то предлагать?


        1. nktkz
          26.10.2021 22:11

          ну автор так видит
          система рекомендаций, которая показывает самую похожую композицию


  1. feel_OS_off
    25.10.2021 09:49

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


  1. Sergey-Aleksandrovich
    26.10.2021 00:27

    Как насчет "фильтрации по теории музыки": тональность, гамма, гармония (диатоническая/хроматическая/полимодальная/центрального созвучия/...), музыкальная форма?...

    У "товара" есть "характеристики" по которым можно фильтровать (для некоторых потребителей ведь важно из какого материала крестовина кресла и есть ли глютен в смузи). Понимаю, что на первых порах это подход для "сведущих", но он даст, как минимум, два следствия:

    • учет по большему числу признаков (а, следовательно, и анализ предпочтений);

    • повышение уровня обознанности (как о "товаре", так и о своих "вкусах").