Pull to refresh
94.4
Rating
SkillFactory
Школа Computer Science

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

SkillFactory corporate blog Python *Programming *API *Sound
Translation
Tutorial
Original author: Max Hilsdorf

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

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

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

Профессии и курсы
Tags:
Hubs:
Total votes 5: ↑4 and ↓1 +3
Views 4.6K
Comments Comments 6

Information

Founded
Location
Россия
Website
www.skillfactory.ru
Employees
201–500 employees
Registered
Representative
Skillfactory School