Эва Бателаан, Томас Бринк и Бенджамин Виттенбринк

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

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

Согласно Ежегодному музыкальному отчету Digital Music Alliance за 2018 год, плейлисты становятся все более популярными среди потребителей музыки. Фактически, 54% потребителей сообщили, что теперь они слушают плейлисты чаще, чем альбомы [1]. Следовательно, спрос на бесшовные высококачественные плейлисты также растет. Действительно, Spotify — один из самых популярных сервисов потоковой передачи музыки — на сегодняшний день насчитывает более 4 миллиардов плейлистов, созданных и опубликованных пользователями. Однако создание целостных списков воспроизведения может быть сложным и трудоемким. Лично мы слишком хорошо это знаем: по крайней мере, один из нас просто просматривает их раздел «понравившиеся песни»!

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

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



Данные

(следуйте инструкциям в разделах Загрузка и обработка данных и Построение набора графических данных нашего Colab)

Набор данных Spotify Million Playlist стал ценным ресурсом как для исследователей, так и для энтузиастов данных. Первоначально выпущенный Spotify Research исключительно для RecSys Challenge 2018 [2], набор данных позже стал общедоступным на AIcrowd в октябре 2020 года [1]. Если вы заинтересованы в изучении набора данных, вы можете легко получить к нему бесплатный доступ (хотя требуется регистрация) по этой ссылке.

Набор данных состоит из ошеломляющего миллиона плейлистов Spotify, созданных в период с января 2010 года по октябрь 2017 года, с информацией о треках, сведениями об исполнителях и данными об альбомах. В общей сложности набор данных может похвастаться более чем 2 миллионами уникальных песен от почти 300 000 исполнителей, что, по данным AIcrowd, делает его «крупнейшим общедоступным набором данных музыкальных плейлистов в мире».

Чтобы дать вам представление о том, как выглядят данные, мы предоставили пример плейлиста JSON ниже.

Вы можете подумать, что миллион списков воспроизведения, каждый со своими песнями и метаданными, занимают много места. И ты будешь прав! Папка с полным набором данных занимает примерно 34 гигабайта и состоит из 1000 файлов JSON, содержащих по 1000 списков воспроизведения в каждом. Для наших демонстрационных целей в этом посте нам определенно не нужно использовать все данные. Вместо этого мы сосредоточимся на подвыборке, используя только первые 50 файлов. Как только вы освоитесь с этими данными и нашими моделями, мы рекомендуем вам попробовать свои силы на более крупных выборках!

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

Мы хотим отметить, что первоначальная настройка (с 4 типами узлов) хорошо подходит для очень интересного анализа гетерогенных графов, поэтому, если вам понравился этот пост и вы хотите узнать больше, это было бы отличным местом для начала. исследовать дальше! Однако для целей этого поста мы исключим узлы исполнителя и альбома. Это оставляет нам двудольный граф между списками воспроизведения и дорожками, где ребро указывает, что дорожка содержится в списке воспроизведения. Мы иллюстрируем эту обработку на изображении ниже.

Получившийся граф по-прежнему имеет 512 тысяч узлов и более 3,3 миллиона ребер. К сожалению, учитывая наши вычислительные ресурсы в Colab (25 ГБ системной ОЗУ, 15 ГБ ОЗУ графического процессора), этот граф слишком велик для обучения наших моделей. В результате нам нужно будет определить подграф, чтобы продолжить наш анализ. Для этого мы используем понятие «K-ядра» из теории графов, которое определяется как максимально связный подграф, в котором все узлы имеют степень не ниже K. Это означает, что любой узел, который не находится в K-ядре, должен иметь степень строго меньше K. Практически, взятие K-ядра нашего графа позволяет выделить центральный, высокосвязный подграф исходного графа, где каждый плейлист содержит не менее K треков, и каждый такой трек является участник как минимум K-1 других плейлистов. Мы включили визуализацию K-ядра ниже.

В этом посте мы установили K = 30, хотя вы можете поэкспериментировать с другими значениями. Наши настройки дают подграф из 35 тысяч узлов и около 1,6 миллиона ребер. Таким образом, подграф уменьшает размер нашего пространства узлов в 15 раз и вдвое уменьшает количество ребер. Более того, этот подграф значительно плотнее: средняя степень узлов в подграфе примерно равна 90, что почти в семь раз больше, чем в исходном графе (примерно 13). Это должно значительно помочь процессу обучения, позволяя нашим методам лучше собирать и распространять информацию по структуре графа, что приводит к лучшему встраиванию узлов, что необходимо для точных прогнозов.

В частности, наш последний подграф содержит 34 810 узлов (22 563 списка воспроизведения и 12 247 дорожек) с 1 578 286 ребрами. Мы кратко повторяем шаги для получения этого подграфа ниже:

  1. Загрузите первые 50 файлов JSON, содержащих 50 000 списков воспроизведения.
  2. Отбросьте информацию об исполнителе и альбоме, чтобы создать двудольный график между плейлистами и треками.
  3. Вычислите 30-ядро этого графа.

Ниже мы визуализируем небольшой подграф этого графа с 1875 узлами и 5057 ребрами.

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

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

Это разделение приводит к следующему поведению:

  1. Во время обучения мы используем ребра прохождения сообщения поезда, чтобы предсказать ребра контроля поезда.
  2. Во время проверки мы используем все ребра поезда, чтобы предсказать ребра контроля проверки.
  3. Во время тестирования мы используем все ребра обучения и проверки, чтобы предсказать ребра контроля теста.

Это означает, что количество ребер, используемых для обмена сообщениями, увеличивается по мере того, как мы переходим от обучения к проверке и тестированию. Мы придерживаемся разделения 70–15–15 процентов.

Пример такого разделения показан на изображении ниже.

Подход

(следите за разделами Проектирование нашей модели и Определение функций обучения и тестирования нашего Colab)

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

Зачем использовать графовые нейронные сети?

Глядя на исходную задачу RecSys, ни одна из лучших команд не использовала какие-либо графические методы. На самом деле слово «график» даже не встречается в статье «Анализ подходов, принятых в ACM RecSys Challenge 2018 для автоматического продолжения музыкального плейлиста» [3]! Учитывая растущую популярность и успех графовых нейронных сетей (GNN) в других задачах рекомендательных систем и в других, более общих областях, мы подумали, что это будет прекрасная возможность изучить потенциал GNN.

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

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

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

Построение наших моделей

Одним из известных подходов к рекомендательным системам является совместная фильтрация (CF). Методы CF можно разделить на три категории: матричная факторизация, метод окрестности и метод, основанный на правилах [4]. Методы матричной факторизации разлагают матрицу взаимодействия пользователя и элемента на вложения более низкой размерности, которые фиксируют скрытые функции пользователей и элементов. Методы на основе соседства полагаются на сходство между пользователями или элементами для формирования кластеров, из которых они делают рекомендации, в то время как методы на основе правил используют явные правила для создания рекомендаций. Также распространены гибридные методы, сочетающие несколько методов. Хотя эти методы оказались успешными во многих приложениях, они могут быть не столь эффективны для захвата более сложных шаблонов и зависимостей в данных, как нейронные методы, такие как графовые нейронные сети.

Среди различных моделей на основе графов мы решили изучить сверточные слои LightGCN (LGConv), Graph Attention Network (GATConv) и GraphSAGE (SAGEConv) на предмет их уникальных особенностей и сильных сторон. LGConv, например, имеет облегченную архитектуру, которая делает его легко масштабируемым для больших наборов данных, обычно используемых в рекомендательных системах. Его простота и сосредоточенность на вложениях позволяют ему эффективно изучать представления узлов в графе, что делает его сильным претендентом на решение нашей задачи. С другой стороны, GATConv предлагает механизм внимания, который может динамически фокусироваться на соответствующих частях графика, что приводит к более точным рекомендациям. Этот механизм позволяет GATConv взвешивать важность соседних узлов, помогая модели выделить наиболее важные связи между плейлистами и треками. Наконец, SAGEConv добавляет сложности и разнообразия к представлению сообщений по сравнению с LGConv, но при этом остается масштабируемым.

Одно замечание по реализации перед погружением: для всех наших графовых моделей нейронных сетей мы используем библиотеку PyTorch Geometric (PyG). Это библиотека, построенная на основе PyTorch и легко интегрируемая в стандартную среду программирования для глубокого обучения. Большая часть кода, который мы предоставляем в этой статье, и связанный с ним Colab предполагают, что у вас есть работающая и актуальная версия как PyG, так и PyTorch. См. документацию здесь.

Мы начнем с LightGCN, который считается одним из самых современных алгоритмов нейронной совместной фильтрации [5]. Как уже говорилось, эта модель очень легкая, поскольку она исключает все обучаемые весовые матрицы и функции активации, так что единственными обучаемыми параметрами модели являются вложения на начальном уровне. Это упрощение также снижает риск чрезмерного сглаживания, которое происходит, когда вложения узлов становятся неразличимыми после нескольких уровней агрегации.

Мы предлагаем простую реализацию этого с помощью библиотеки PyG ниже. Мы делаем это исключительно в демонстрационных целях, чтобы продемонстрировать функциональность PyG и слоев MessagePassing. Мы надеемся, что это дает некоторое представление о том, как графовые нейронные сети на самом деле работают под капотом. В нашем Colab мы используем официальную версию, реализованную на torch_geometric.nn.conv.LGConv, так как она лучше оптимизирована и надежнее.

class LGConv(MessagePassing):
    def __init__(self, **kwargs):  
        super(LGConv, self).__init__(**kwargs)

    def forward(self, x, edge_index, size = None):
        # calculate normalization term (in the above: 1 / sqrt(...))
        src, dst = edge_index
        node_degree = torch_geometric.utils.degree(dst)
        node_degree_inv_sqrt = node_degree.pow(-0.5)
        norm = node_degree_inv_sqrt[src] * node_degree_inv_sqrt[dst]
        # pass to propagate (returns the term on the RHS in above)
        out = self.propagate(edge_index, x = x, norm = norm, size = size)
        return out

    def message(self, x_j, norm):
        # calculating normalized message (term in the sum above)
        return x_j * norm.view(-1, 1)

    # Note: could also just pass aggr = 'add' to __init__()
    def aggregate(self, inputs, index, dim_size = None):
        # aggregation (the sum above)
        out = torch_scatter.scatter(inputs, index, dim=self.node_dim, reduce='sum')
        return out

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

В дополнение к использованию LightGCN мы исследуем два альтернативных подхода, которые используют ту же общую архитектуру, но заменяют вызовы LGConv более сложными сверточными слоями. Мы особенно заинтересованы в проверке способности модели фиксировать сложные структурные отношения. Первый вариант заменяет LGConv на GATConv, сверточный уровень, присутствующий в сети Graph Attention Network (GAT), как показано ниже [7]. Мы определяем несколько головок внимания, а также определяем линейный слой для сопоставления объединенных выходных данных с исходным измерением встраивания. Второй вариант использует SAGEConv (от GraphSAGE) в качестве заменяющего сверточного слоя, также показанного ниже [8].

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

Для реализации этих сверточных слоев (GATConv и SAGEConv) обратитесь к документации PyG по этой ссылке. Для реализации общей архитектуры GNN мы сохраняем то же, что и с LightGCN, просто заменяя его сверточный слой. Ниже мы приводим пример для этого.

class GCN(torch.nn.Module):
    def __init__(
        self,
        ...
        conv_layer = "LGC",
        ...
    ):
        ...
        # initialize convolutional layers 
        self.conv_layer = conv_layer
        if conv_layer == "LGC":  
          self.convs = ModuleList([LGConv(**kwargs) for _ in range(num_layers)])
        elif conv_layer == "GAT": # initialize GAT layer with multiple heads
          n_heads = 5
          self.convs = ModuleList([
              GATConv(embedding_dim, embedding_dim, heads = n_heads, 
                      dropout = 0.5, **kwargs) for _ in range(num_layers)
          ])
          self.linears = ModuleList([Linear(n_heads * embedding_dim, 
                                     embedding_dim) for _ in range(num_layers)])

        elif conv_layer == "SAGE": #  initialize GraphSAGE conv
          self.convs = ModuleList([ 
              SAGEConv(embedding_dim, embedding_dim, **kwargs) 
              for _ in range(num_layers)
          ])

Получение метрики подобия

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

Самый простой подход — и подход, который мы будем использовать, — это взять скалярное произведение между двумя вложениями, что является часто используемой метрикой сходства. Это эффективно вычисляет, насколько два вектора перекрываются в многомерном пространстве вложения как по величине, так и по направлению.

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

Определение нашей потери

Теперь мы знаем, как рассчитать рекомендации. Как узнать, хороши ли они? Чтобы ответить на этот вопрос, мы вводим наш основной критерий оценки: Recall@K. Этот критерий определяется как доля релевантных треков (в нашем случае треков, содержащихся в плейлисте), которые прогнозируются в топе K. Ниже мы приводим иллюстрацию.

Глядя на этот критерий, мы видим очевидную проблему: эта метрика не дифференцируема, так как же нам ее оптимизировать? Чтобы решить эту проблему, нам нужно будет найти дифференцируемую цель, которая хорошо согласуется с нашей метрикой Recall@K. Наша функция потери кандидата — это потеря байесовского персонализированного рейтинга (BPR), которая обеспечивает «персонализированное» (т. е. зависящее от плейлиста) количество хороших и плохих рекомендаций. Чтобы вычислить эту функцию потерь, нам нужны оценки для пар положительных и отрицательных ребер. В этом случае отрицательный край — это просто край, которого нет на графике, а положительный край — это существующая комбинация трека и плейлиста, которую мы хотим предсказать — подробнее о том, как мы их выберем позже. Пока предположим, что для каждого положительного ребра (i, j_+) у нас есть доступ к отрицательному ребру (i, j_-). Затем мы можем определить функцию потерь для пользователя i как:

Мы можем реализовать это на Python следующим образом.

class BPRLoss(_Loss):
    def __init__(self, lambda_reg: float = 0, **kwargs):
        super().__init__(None, None, "sum", **kwargs)
        self.lambda_reg = lambda_reg

    def forward(self, positives: Tensor, negatives: Tensor,
                parameters: Tensor = None) -> Tensor:
        n_pairs = positives(0)
        log_prob = F.logsigmoid(positives - negatives).sum()

        regularization = 0
        if self.lambda_reg != 0:
            regularization = self.lambda_reg * parameters.norm(p=2).pow(2)

        return (-log_prob + regularization) / n_pairs 

Теперь давайте немного вернемся к отрицательным краям. Как упоминалось выше, положительное ребро (i, j_+) — это просто ребро наблюдения (существующее ребро, которое мы хотим предсказать) в графе, а отрицательное ребро — несуществующее, например (i, k). Для наших целей отрицательный край всегда будет определяться как пара положительному краю, в частности, исходящий из того же узла плейлиста, что и положительный край. Идея заключается в том, что мы хотим закодировать в модели следующее: «Плейлист i похож на трек j, но не на трек k». . Без каких-либо отрицательных краев вы можете убедить себя, что в конечном итоге все будет иметь одинаковое точное вложение! Это явно нежелательно. Таким образом, отрицательное ребро заставляет модель изучать разные представления для разных кластеров.

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

def sample_negative_edges_nocheck(data, num_playlists, num_tracks, device = None):
  # this function is called _nocheck, as it does not check for positive edges 
  # before generating the negative edges

  playlists = data.edge_label_index[0, :]
  
  # generate random index of track to select
  tracks = torch.randint(num_playlists, num_playlists + num_tracks - 1, size = data.edge_label_index[1, :].size())
  tracks = tracks.to(device)
  
  # stack together to get  dim = (2, len(playlists))
  neg_edge_index = torch.stack((playlists, tracks), dim = 0)

  # negative edges so assign label of zero 
  neg_edge_label = torch.zeros(neg_edge_index.shape[1])
  neg_edge_label = neg_edge_label.to(device)
    
  return neg_edge_index, neg_edge_label

Если вас все еще беспокоит наличие положительных фронтов в нашей функции выборки, не волнуйтесь! В нашем Colab мы также определяем функцию sample_negative_edges, которая сначала определяет маску для фильтрации всех положительных ребер, а затем случайным образом выбирает. Тем не менее, мы думаем, что этот код значительно менее ясен для понимания и не предлагает какого-либо существенного улучшения производительности. Таким образом, мы предпочитаем представить выше простой случайный метод выборки с отрицательным фронтом.

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

def sample_hard_negative_edges(data, model, num_playlists, num_tracks, device=None, batch_size=500, frac_sample = 1):
     # get embeddings 
    with torch.no_grad():
        embeddings = model.get_embedding(data.edge_index)
        playlists_embeddings = embeddings[:num_playlists].to(device)
        tracks_embeddings = embeddings[num_playlists:].to(device)

    positive_playlists, positive_tracks = data.edge_label_index
    num_edges = positive_playlists.size(0)

    # Create a boolean mask for all the positive edges
    positive_mask = torch.zeros(num_playlists, num_tracks, device=device, dtype=torch.bool)
    positive_mask[positive_playlists, positive_tracks - num_playlists] = True

    neg_edges_list = []
    neg_edge_label_list = []

    # need to loop in batch, as matmul will overflow otherwise
    for batch_start in range(0, num_edges, batch_size):
        batch_end = min(batch_start + batch_size, num_edges)

        # calculate scores
        batch_scores = torch.matmul(
            playlists_embeddings[positive_playlists[batch_start:batch_end]], tracks_embeddings.t()
        )

        # Set the scores of the positive edges to negative infinity
        batch_scores[positive_mask[positive_playlists[batch_start:batch_end]]] = -float("inf")

        # Select the top k highest scoring negative edges for each playlist in the current batch
        # positive edges will be at the very end
        _, top_indices = torch.topk(batch_scores, int(frac_sample * num_tracks), dim=1)
        selected_indices = torch.randint(0, int(frac_sample * num_tracks), size = (batch_end - batch_start, ))
        top_indices_selected = top_indices[torch.arange(batch_end - batch_start), selected_indices] + n_playlists

        # Create the negative edges tensor for the current batch
        neg_edges_batch = torch.stack(
            (positive_playlists[batch_start:batch_end], top_indices_selected), dim=0
        )
        neg_edge_label_batch = torch.zeros(neg_edges_batch.shape[1], device=device)

        neg_edges_list.append(neg_edges_batch)
        neg_edge_label_list.append(neg_edge_label_batch)

    # Concatenate the batch tensors
    neg_edges = torch.cat(neg_edges_list, dim=1)
    neg_edge_label = torch.cat(neg_edge_label_list)

    return neg_edges, neg_edge_label

Результаты

(следите за разделами Обучение наших моделей и Визуализация результатов нашего Colab)

Теперь мы готовы обучить модель! Для выполнения обновлений нейронной сети мы используем оптимизатор Adam со скоростью обучения 0,01. Мы изучаем 64-мерные вложения, при этом мы включаем 3 сверточных слоя при использовании GATConv и SAGEConv и 4 слоя при использовании LGConv. Проверив динамику сходимости моделей, мы решили обучить 300 эпох. Мы запускаем все модели, используя как нашу случайную, так и нашу жесткую отрицательную выборку, где мы учитываем как фиксированные, так и обучаемые веса слоев.

Как уже говорилось, мы используем метрику Recall@K в качестве основного показателя оценки. Для этого поста мы выбрали значение K = 300, что, по нашему мнению, является разумным выбором, учитывая, что у нас примерно 12 250 дорожек. Это значение K означает, что мы выбираем примерно 2,5% рекомендуемых треков. Мы чувствовали, что порог в один процент может быть слишком узким фильтром, в то время как пять процентов могут быть просто слишком большими в абсолютных значениях (примерно 600 дорожек), чтобы быть суперзначимыми. Таким образом, мы сочли K = 300 удачным компромиссом.

Ниже мы отображаем потерю BPR при проверке всех наших моделей за 300 эпох. Слева мы отображаем только модели со случайной отрицательной выборкой. Справа также представлены модели с жесткой отрицательной выборкой. Мы опускаем потери при обучении на графиках для ясности, но во всех сценариях они ниже потерь при проверке, как мы и ожидали.

Глядя на график слева, более сложные свертки — GATConv и SAGEConv — могут обеспечить значительно меньшие потери при проверке по сравнению с LGConv. В начальных 50 эпохах для GATConv и SAGEConv существует большая нестабильность, вероятно, из-за сложности модели, но в конечном итоге это устранено. Тем не менее, для сходимости GATConv явно требуется больше эпох, чем для других. В целом, SAGEConv показывает лучшие результаты с точки зрения производительности, стабильности и скорости конвергенции. Теперь, обращая наше внимание на жесткую отрицательную выборку справа, сразу возникает впечатление, что потери на самом деле увеличиваются после определенного количества эпох. Это невероятно интересно и иллюстрирует растущую сложность выборки жесткого отрицательного края. Поскольку потери в поездах все еще снижаются, вполне вероятно, что модели могут в определенной степени подходить к более сложным образцам в обучающих данных, что затем приводит к ухудшению общей производительности проверки.

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

Сравнивая модели друг с другом, опять же, SAGEConv работает лучше, за ней следуют GATConv и LGConv. Еще одно интересное замечание заключается в том, что методы случайной и жесткой отрицательной выборки, по-видимому, приводят к аналогичным характеристикам для LightGCN в течение первых 200 эпох. После этого случайная выборка, по-видимому, сошлась, в то время как жесткая выборка начинает увеличиваться. Такое поведение соответствует идее нашей жесткой отрицательной выборки; сначала наша модель получает «общее» представление о том, что такое хорошие и плохие прогнозы (почти случайная отрицательная выборка), а по мере того, как мы постепенно увеличиваем сложность отрицательных выборок, мы затем позволяем нашей модели учиться разделять хорошие и плохие прогнозы. в более конкретном пространстве вложения. Это приводит к улучшению производительности даже после многих эпох.

Приведенные выше цифры были получены путем установки альфа-вектора весов слоев одинаковым для всех слоев. Мы также провели эксперименты с альфой в качестве обучаемого параметра. Однако для краткости эти результаты опущены, так как результаты для всех методов весьма схожи. Кроме того, у нас есть возможность вместо этого использовать стандартную бинарную перекрестную потерю энтропии и регистрировать площадь ROC и кривую (ROC-AUC). Не стесняйтесь исследовать это в нашем Colab!

Визуализации

(следите за информацией в разделе Визуализации нашего Colab)

После создания моделей мы решили визуализировать некоторые из вложений, созданных одной из самых эффективных моделей. Мы выбрали для изучения 30-ядерную модель SAGEConv, которая была обучена на 50 файлах данных.

Поскольку наши вложения имеют 64 измерения, мы использовали аппроксимацию и проекцию равномерного многообразия, чтобы свести вложения к трем измерениям. Мы изобразили редуцированные вложения по эпохам. Мы создали график для модели, в которой использовалась случайная отрицательная выборка (слева), и один график для модели, в которой использовалась жесткая отрицательная выборка (справа). Мы обнаружили, что по мере обучения нашей модели встраивание в обоих случаях начало формировать плотный кластер, который трансформировался и перемещался в группе от эпохи к эпохе. Небольшое подмножество списков воспроизведения, обычно около 630 узлов, формировало небольшой кластер, который часто отделялся от более крупного кластера. Однако при дальнейшем анализе оказалось, что узлы в этом меньшем кластере не совпадают в разные эпохи. Поэтому нам было трудно извлечь смысл из этих визуализаций. Также возможно, что большая часть отличительной и/или релевантной информации теряется при сжатии вложений в низкоразмерное пространство.

Изучив общее распределение всех вложений плейлистов, мы решили изучить вложения плейлистов, в которых участвуют определенные исполнители. В частности, мы исследовали плейлисты, в которых фигурировали Стеклянные животные (идентификатор исполнителя: 4yvcSjfu4PC0CYQyLy4wSq) и Гриффин (идентификатор исполнителя: 2ZRQcIgzPCVaT9XKhXZIzh), поскольку эти исполнители появлялись достаточно часто, чтобы иметь потенциал для значимых кластеров, но не были слишком распространены, чтобы доминировать в плейлистах. Мы определили все плейлисты, в которых есть Стеклянные животные, но не Гриффин (желтый), плейлисты, в которых есть и то и другое (серый), и плейлисты, в которых есть Гриффин, но нет Стеклянных животных (синий). Мы обнаружили, что график плейлистов, в котором представлены только эти исполнители, похож на график всех плейлистов, что позволяет предположить, что один исполнитель в плейлисте мало влияет на встраивание.

Заключение

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

Мы призываем тех, кто заинтересован в улучшении своего понимания нейронных сетей на основе графов и получении дополнительной информации о современных методах рекомендательных систем на основе графов, изучить материалы курса и ресурсы, доступные через Stanford’s CS 224W!

Ссылки

[1] Исследования Spotify. (2020). Spotify Million Playlist Dataset Challenge. AIcrowd.com.
[2] Ч. В. Чен, П. Лемер, М. Шедл и Х. Замани. (2018). Recsys Challenge 2018: Автоматическое продолжение музыкального плейлиста. Материалы 12-й конференции ACM по рекомендательным системам.
[3] H. Zamani, M. Schedl, P. Lamere и C.W. Chen. (2019). Анализ подходов, использованных в ACM RecSys Challenge 2018 для автоматического продолжения музыкального плейлиста. Транзакции ACM по интеллектуальным системам и технологиям.
[4] X. Su и T. Khoshgoftaar. (2009). Обзор методов совместной фильтрации. Достижения в области искусственного интеллекта.
[5] X. He, K. Deng, X. Wang, Y. Li and Y. Zhang & M. Wang. (2020). LightGCN: упрощение и усиление сети свертки графов для рекомендации. Материалы 43-й Международной конференции ACM SIGIR по исследованиям и разработкам в области информационного поиска.
[6] П. Величкович, Г. Кукурулл, А. Казанова, А. Ромеро, П. Лио, и Ю. Бенжио (2017). Графические сети внимания. препринт arXiv arXiv:1710.10903.
[7] W. Hamilton, R. Ying, and J. Leskovec. (2018). Обучение индуктивному представлению на больших графах. Достижения в области нейронных систем обработки информации, 30