Пишем систему рекомендаций музыки на основе ML

от автора

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

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

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

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


ссылка на оригинал статьи https://habr.com/ru/articles/585182/