Подробное и иллюстрированное пошаговое руководство по работе «Graph Attention Networks» Величковича и др. с реализацией PyTorch предлагаемой модели.

Введение

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

Сообщения (вложения) передаются между узлами графа через несколько уровней GNN. Каждый узел объединяет сообщения от своих соседей, чтобы обновлять свое представление. Этот процесс повторяется на всех уровнях, что позволяет узлам получать представления, содержащие более полную информацию о графе. Некоторыми из важных вариантов GNN могут быть GraphSAGE [2], Graph Convolution Network [3] и т. д. Вы можете изучить больше вариантов GNN здесь.

Graph Attention Networks (GAT)[1] — это особый класс GNN, которые были предложены для улучшения этой схемы передачи сообщений. Они представили обучаемый механизм внимания, который позволяет узлу решать, какие соседние узлы более важны при агрегировании сообщений из их локального окружения, назначая вес между каждым исходным и целевым узлом вместо агрегирования информации от всех соседей с помощью равные веса.

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

В этом посте мы рассмотрим важную часть оригинальной статьи «Сети графического внимания», написанной Величковичем и др. [1], объясните эти части и одновременно реализуйте идеи, предложенные в статье, с использованием среды PyTorch, чтобы лучше понять интуицию метода GAT.

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

Проходя через бумагу

Раздел 1 — Введение

После широкого обзора существующих методов в литературе по обучению графовому представлению в разделе 1, «Введение», представлена ​​сеть графового внимания (GAT). Авторы упоминают:

  1. Общий вид встроенного механизма внимания.
  2. Три свойства GAT, а именно эффективность вычислений, общая применимость ко всем узлам и удобство использования в индуктивном обучении.
  3. Контрольные показатели и наборы данных, по которым они оценивали производительность GAT.

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

Раздел 2 — Архитектура GAT

В этом разделе, который составляет основную часть документа, подробно изложена архитектура сети Graph Attention Network. Чтобы продолжить объяснение, предположим, что предлагаемая архитектура работает на графе с N узлами (V = {vᵢ};i=1,…,N)и каждый узел представлен вектором hᵢ из F elements,с любой произвольной настройкой ребер, существующих между узлами.

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

Как подчеркивалось выше, для этого сначала они заявляют, что все векторы признаков входных узлов (hᵢ) для слоя GA линейно преобразуются (т. е. умножаются на весовая матрица W), в PyTorch это обычно делается следующим образом:

import torch
from torch import nn

# in_features -> F and out_feature -> F'
in_features = ...
out_feature = ...

# instanciate the learnable weight matrix W (FxF')
W = nn.Parameter(torch.empty(size=(in_features, out_feature)))

#  Initialize the weight matrix W
nn.init.xavier_normal_(W)

# multiply W and h (h is input features of all the nodes -> NxF matrix)
h_transformed = torch.mm(h, W)

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

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

Это делается путем вычисления взвешенной суммы характеристик соседних узлов, за которой следует нелинейная функция активацииσ. Эта взвешенная сумма также известна как «шаг агрегации» в общих операциях уровня GNN, согласно литературе по Graph ML.

Эти веса ​​αᵢⱼ ∈ [0, 1] изучаются и вычисляются с помощью механизма внимания, который обозначает важность функции neighbor j для узла i во время передачи и агрегирования сообщений.

Теперь давайте посмотрим, как эти веса ​​внимания αᵢⱼвычисляются для каждой пары узла iи его соседа j:

Короче говоря, веса ​​внимания αᵢⱼрассчитываются следующим образом:

Где eᵢⱼявляютсяпоказателями внимания, а функция Softmax применяется так, что все веса находятся в [0 , 1] интервал и сумма к 1.

Оценка внимания eᵢⱼтеперь рассчитывается междукаждым узлом i и его соседями j Nчерез функцию внимания a(…) как таковую:

Где || обозначает конкатенацию двух преобразованных вложений узлов, а a – вектор обучаемых параметров (т. е. , параметры внимания) размером 2 * F' (удвоенный размер преобразованных вложений).

А (aᵀ)является транспонированием вектора a, в результате чего получается целое выражение aᵀ [Кто|| Whⱼ]является точечным(внутренним) произведениеммежду "a"и конкатенацией преобразованных вложений.

Вся операция показана ниже:

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

# instanciate the learnable attention parameter vector `a`
a = nn.Parameter(torch.empty(size=(2 * out_feature, 1)))

# Initialize the parameter vector `a`
nn.init.xavier_normal_(a)

# we obtained `h_transformed` in the previous code snippet

# calculating the dot product of all node embeddings
# and first half the attention vector parameters (corresponding to neighbor messages)
source_scores = torch.matmul(h_transformed, self.a[:out_feature, :])

# calculating the dot product of all node embeddings
# and second half the attention vector parameters (corresponding to target node)
target_scores = torch.matmul(h_transformed, self.a[out_feature:, :])

# broadcast add 
e = source_scores + target_scores.T
e = self.leakyrelu(e)

Последняя часть фрагмента кода (# широковещательное добавление) суммирует все исходные и целевые оценки один к одному, в результате чего получается NxN матрица, содержащая все оценки eᵢⱼ. (показано ниже)

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

Это можно сделать, назначив большую отрицательную оценку (приблизительно -∞) элементам в матрице оценок между узлами с несуществующими ребрами, чтобы их соответствующие веса внимания становились равными нулю после softmax.

Мы можем добиться этого, используя матрицу смежности графа. Матрица смежности представляет собой матрицу NxN с 1 в строке i и столбце j, если между узлами iиj и 0 в другом месте. Итак, мы создаем маску, присваивая -∞ нулевым элементам матрицы смежности и присваивая 0 в другом месте. Затем мы добавляем маску в нашу матрицу показателей. и примените функцию softmax к ее строкам.

connectivity_mask = -9e16 * torch.ones_like(e)
# adj_mat is the N by N adjacency matrix
e = torch.where(adj_mat > 0, e, connectivity_mask) # masked attention scores
        
# attention coefficients are computed as a softmax over the rows
# for each column j in the attention score matrix e
attention = F.softmax(e, dim=-1)

Наконец, согласно документу, после получения оценок внимания и маскирования их существующими краями мы получаем веса ​​внимания αᵢⱼ, выполняя softmax над строками матрицы оценок. .

И, как обсуждалось ранее, мы вычисляем взвешенную сумму вложений узлов:

# final node embeddings are computed as a weighted average of the features of its neighbors
h_prime = torch.matmul(attention, h_transformed)

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

Процесс многоголового внимания и агрегации показан ниже:

Чтобы завершить реализацию в более чистой модульной форме (в виде модуля PyTorch) и включить многоголовую функциональность внимания, вся реализация Graph Attention Layer выполняется следующим образом. :

import torch
from torch import nn
import torch.nn.functional as F

################################
###  GAT LAYER DEFINITION    ###
################################

class GraphAttentionLayer(nn.Module):

    def __init__(self, in_features: int, out_features: int,
                 n_heads: int, concat: bool = False, dropout: float = 0.4,
                 leaky_relu_slope: float = 0.2):
        super(GraphAttentionLayer, self).__init__()

        self.n_heads = n_heads # Number of attention heads
        self.concat = concat # wether to concatenate the final attention heads
        self.dropout = dropout # Dropout rate

        if concat: # concatenating the attention heads
            self.out_features = out_features # Number of output features per node
            assert out_features % n_heads == 0 # Ensure that out_features is a multiple of n_heads
            self.n_hidden = out_features // n_heads
        else: # averaging output over the attention heads (Used in the main paper)
            self.n_hidden = out_features

        #  A shared linear transformation, parametrized by a weight matrix W is applied to every node
        #  Initialize the weight matrix W 
        self.W = nn.Parameter(torch.empty(size=(in_features, self.n_hidden * n_heads)))

        # Initialize the attention weights a
        self.a = nn.Parameter(torch.empty(size=(n_heads, 2 * self.n_hidden, 1)))

        self.leakyrelu = nn.LeakyReLU(leaky_relu_slope) # LeakyReLU activation function
        self.softmax = nn.Softmax(dim=1) # softmax activation function to the attention coefficients

        self.reset_parameters() # Reset the parameters


    def reset_parameters(self):

        nn.init.xavier_normal_(self.W)
        nn.init.xavier_normal_(self.a)

    def _get_attention_scores(self, h_transformed: torch.Tensor):
        
        source_scores = torch.matmul(h_transformed, self.a[:, :self.n_hidden, :])
        target_scores = torch.matmul(h_transformed, self.a[:, self.n_hidden:, :])

        # broadcast add 
        # (n_heads, n_nodes, 1) + (n_heads, 1, n_nodes) = (n_heads, n_nodes, n_nodes)
        e = source_scores + target_scores.mT
        return self.leakyrelu(e)

    def forward(self,  h: torch.Tensor, adj_mat: torch.Tensor):

        n_nodes = h.shape[0]

        # Apply linear transformation to node feature -> W h
        # output shape (n_nodes, n_hidden * n_heads)
        h_transformed = torch.mm(h, self.W)
        h_transformed = F.dropout(h_transformed, self.dropout, training=self.training)

        # splitting the heads by reshaping the tensor and putting heads dim first
        # output shape (n_heads, n_nodes, n_hidden)
        h_transformed = h_transformed.view(n_nodes, self.n_heads, self.n_hidden).permute(1, 0, 2)
        
        # getting the attention scores
        # output shape (n_heads, n_nodes, n_nodes)
        e = self._get_attention_scores(h_transformed)

        # Set the attention score for non-existent edges to -9e15 (MASKING NON-EXISTENT EDGES)
        connectivity_mask = -9e16 * torch.ones_like(e)
        e = torch.where(adj_mat > 0, e, connectivity_mask) # masked attention scores
        
        # attention coefficients are computed as a softmax over the rows
        # for each column j in the attention score matrix e
        attention = F.softmax(e, dim=-1)
        attention = F.dropout(attention, self.dropout, training=self.training)

        # final node embeddings are computed as a weighted average of the features of its neighbors
        h_prime = torch.matmul(attention, h_transformed)

        # concatenating/averaging the attention heads
        # output shape (n_nodes, out_features)
        if self.concat:
            h_prime = h_prime.permute(1, 0, 2).contiguous().view(n_nodes, self.out_features)
        else:
            h_prime = h_prime.mean(dim=0)

        return h_prime

Затем авторы проводят сравнение между GAT и некоторыми другими существующими методологиями/архитектурами GNN. Они утверждают, что:

  1. GAT в вычислительном отношении более эффективны, чем некоторые существующие методы, благодаря возможности вычислять веса внимания и выполнять локальную агрегациюпараллельно.
  2. GAT могут назначать разную важность соседним узлам при объединении сообщений, что может обеспечить скачок в емкости модели и повысить интерпретируемость.
  3. GAT учитывает полное соседство узлов (не требует выборки от соседей) и не предполагает какого-либо порядка внутри узлов.
  4. GAT можно переформулировать как конкретный пример MoNet (Monti et al., 2016), установив псевдокоординатную функцию в виде u(x, y) = f(x)||f(y), где f(x) представляет (потенциально преобразованные с помощью MLP) признаки узла x и || — это конкатенация; и весовая функция будет wj(u) = softmax(MLP(u))

Раздел 3 — Оценка

В третьем разделе статьи авторы сначала описывают тесты, наборы данных и задачи, по которым оценивается GAT. Затем они представляют результаты своей оценки модели.

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

  • Индуктивное обучение. Это тип контролируемой учебной задачи, в которой модель обучается только на наборе помеченных обучающих примеров, а обученная модель оценивается и тестируется на примерах, за которыми во время обучения никто не наблюдал. Это тип обучения, который известен как обычное контролируемое обучение.
  • Трансдуктивное обучение. В задачах этого типа все данные, включая данные об обучении, проверке и тестировании, используются во время обучения. Но на каждом этапе модель получает доступ только к соответствующему набору меток. Это означает, что во время обучения модель обучается только с использованием потери, возникающей в результате обучающих экземпляров и меток, но функции тестирования и проверки используются для передачи сообщений. Это в основном из-за структурной и контекстуальной информации, существующей в примерах.

Наборы данных
В статье для оценки GAT используются четыре эталонных набора данных, три из которых соответствуют трансдуктивному обучению, а еще один используется в качестве задачи индуктивного обучения.

Все наборы данных трансдуктивного обучения, а именно Cora, Citeseer и Pubmed (Sen et al., 2008), представляют собой графики цитирования, в котором узлы — это опубликованные документы, а ребра (соединения) — это ссылки между ними, а признаки узла — это элементы представления документа набором слов. Набор данных для индуктивного обучения представляет собой набор данных белково-белковое взаимодействие (PPI), содержащий графики различных тканей человека (Zitnik & Leskovec, 2017). Наборы данных более подробно описаны ниже:

Настройка и результаты

  • Для трех трансдуктивных задач настройка, используемая для обучения, следующая:
    Они используют 2 слоя GAT — слой 1 использует
    - K = 8 головок внимания
    - F' = 8 выходной элемент затемнения на голову
    - ELUактивация
    и для второго слоя [Cora & Городской | Опубликовано]
    - [1 | 8] внимание головы сколичеством классоввыходной диммер
    - активация Softmax для вывода вероятности классификации
    и для всей сети< br /> - Отсевс p = 0,6
    - L2 регуляризация с λ = [0,0005| 0,001]
  • Для трех трансдуктивных задач для обучения используются следующие настройки:
    Три уровня —
    - Уровень 1 и 2:K = 4 | F’ = 256 | ELU
    - Уровень 3: K = 6 | F’ = классы C | Сигмовидная (с несколькими метками)
    без регуляризации и отсева

Реализация первой настройки в PyTorch выполняется ниже с использованием слоя, который мы определили ранее:

class GAT(nn.Module):

    def __init__(self,
        in_features,
        n_hidden,
        n_heads,
        num_classes,
        concat=False,
        dropout=0.4,
        leaky_relu_slope=0.2):

        super(GAT, self).__init__()

        # Define the Graph Attention layers
        self.gat1 = GraphAttentionLayer(
            in_features=in_features, out_features=n_hidden, n_heads=n_heads,
            concat=concat, dropout=dropout, leaky_relu_slope=leaky_relu_slope
            )
        
        self.gat2 = GraphAttentionLayer(
            in_features=n_hidden, out_features=num_classes, n_heads=1,
            concat=False, dropout=dropout, leaky_relu_slope=leaky_relu_slope
            )

    def forward(self, input_tensor: torch.Tensor , adj_mat: torch.Tensor):


        # Apply the first Graph Attention layer
        x = self.gat1(input_tensor, adj_mat)
        x = F.elu(x) # Apply ELU activation function to the output of the first layer

        # Apply the second Graph Attention layer
        x = self.gat2(x, adj_mat)

        return F.softmax(x, dim=1) # Apply softmax activation function

После тестирования авторы сообщают о следующей производительности для четырех эталонных тестов, демонстрирующих сопоставимые результаты GAT по сравнению с существующими методами GNN.

Заключение

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

Кроме того, вы можете получить доступ к полному коду, использованному в этом посте, содержащему код обучения и проверки в этом репозитории GitHub.

Я был бы рад услышать любые мысли или любые предложения / изменения в сообщении.

Рекомендации

[1] — Graph Attention Networks (2017), Петар Величкович, Гиллем Кукурулл, Аранта Казанова, Адриана Ромеро, Пьетро Лио, Йошуа Бенжио. arXiv:1710.10903v3

[2] —Индуктивное репрезентативное обучение на больших графах (2017), Уильям Л. Гамильтон, Рекс Ин, Юре Лесковец. arXiv:1706.02216v4

[3] — Полууправляемая классификация с помощью сверточных сетей графов (2016 г.),Томас Н. Кипф, Макс Веллинг. arXiv:1609.02907v4