Обзор Graph Attention Network (GAT) с визуализированной реализацией
Обзор GAT с визуализацией

Понимание графовых нейронных сетей (GNN) становится все более актуальным, поскольку трансформеры продолжают решать графовые проблемы, такие как Open Graph Benchmark. Даже если естественный язык – все, что нужно графу, GNN остаются плодотворным источником вдохновения для будущих методов.
В этом посте я рассмотрю реализацию простого слоя GNN, а затем покажу модификации для простого слоя графового внимания, описанного в статье ICLR под названием Graph Attention Networks.

Изначально представим, что у нас есть граф текстовых документов, представленный в виде направленного ациклического графа (DAG). Документ 0 имеет ребра к документам 1, 2 и 3, поэтому в нулевой строке есть единицы для этих столбцов.
Для визуализации я буду использовать Graphbook, инструмент для визуального моделирования искусственного интеллекта. Подробнее о том, как понять визуальные представления в Graphbook, можно узнать из моего другого поста.
- Познакомьтесь с BLIVA мультимодельной большой языковой моделью для более эффективного обработки вопросов с текстовым содержанием визуального характера
- Расшифровка машинного обучения
- Рубик и Марков
У нас также есть некоторые признаки узлов для каждого документа. Я передал каждый документ в BERT в виде одномерного массива текстов [5], чтобы получить векторные представления размерности [5, 768] в выходе Pooler.
В учебных целях я возьму только первые 8 измерений вывода BERT в качестве признаков узлов, чтобы мы могли легче следить за формами данных. Теперь у нас есть матрица смежности и признаки узлов.
Слой GNN
Общая формула для слоя GNN заключается в том, что для каждого узла мы берем всех соседей каждого узла, складываем признаки, умноженные на матрицу весов, и затем пропускаем через функцию активации. Я создал пустой блок с этой формулой в качестве заголовка и передал в него матрицу смежности и признаки узлов, а затем реализую эту формулу внутри блока.
При реализации этой формулы мы не хотим запускать цикл. Если мы сможем полностью векторизовать это, то обучение и прогнозирование с использованием графических процессоров будет гораздо быстрее, потому что умножение может быть выполнено в один шаг. Поэтому мы транслируем (т.е. распространяем) признаки узлов в 3D форму, поэтому у нас была форма [5, 8] признаков узлов, а теперь у нас будет форма [5, 5, 8], где каждая ячейка в 0-м измерении повторяет признаки узлов. Теперь мы можем рассматривать последнее измерение как признаки “соседей”. У каждого узла есть набор из 5 возможных соседей.
Мы не можем напрямую распространить признаки узлов из формы [5, 8] в форму [5, 5, 8]. Вместо этого сначала мы распространяем их до формы [25, 8], потому что при распространении каждое измерение в форме должно быть больше или равно исходному измерению. Поэтому мы получаем части формы 5 и 8 (get_sub_arrays), затем умножаем первую, чтобы получить 25, а затем объединяем их все вместе. Наконец, мы изменяем форму полученного [25, 8] обратно на [5, 5, 8], и мы можем убедиться в Graphbook, что каждый набор признаков узлов в последних 2 измерениях идентичен.
Затем мы также хотим транслировать матрицу смежности в ту же форму. Это означает, что для каждой 1 в матрице смежности в строке i и столбце j, есть строка из 1.0s с количеством признаков num_feats в измерении [i, j]. Таким образом, в этой матрице смежности строка 0 имеет 1 в 1-м, 2-м и 3-м столбцах, поэтому в первой ячейке есть строка из num_feats 1.0s в строках 1, 2 и 3 (т.е. [0, 1:3, :]).
Реализация здесь довольно простая, просто преобразуйте матрицу смежности в десятичное число и выполняйте трансляцию из формы [5, 5] в форму [5, 5, 8]. Теперь мы можем поэлементно умножить эту маску смежности на наши увеличенные признаки соседних узлов.
Мы также хотим добавить петлю к матрице смежности, чтобы при суммировании признаков соседей мы также включали собственные признаки этого узла.
После выполнения поэлементного умножения (и включения петли) мы получаем признаки соседей для каждого узла и нули для узлов, которые не соединены ребром (не являются соседними). Для нулевого узла это включает признаки для узлов от 0 до 3. Для третьего узла это включает третий и четвертый узлы.
Затем мы изменяем форму на [25, 8], чтобы каждый признак соседа был своей собственной строкой, и пропускаем это через параметризованный линейный слой с выбранным вами скрытым размером. Здесь я выбрал 32 и сохранил его как глобальную константу, чтобы его можно было повторно использовать. Выход линейного слоя будет [25, hidden_size]. Просто измените форму этого выхода, создайте форму [5, 5, hidden_size] и теперь мы наконец готовы к части суммирования формулы!
Мы суммируем по средней размерности (индекс размерности 1), чтобы суммировать признаки соседей для каждого узла. Результатом является набор узловых вложений размером [5, hidden_size], прошедших через 1 слой. Просто объедините эти слои вместе, и у вас есть сеть GNN, и следуйте руководствам с https://www.youtube.com/@Graphbook по обучению.
Слой внимания графа
Из статьи, секретный ингредиент слоя внимания графа – это коэффициент внимания, указанный в приведенной выше формуле. По сути, мы объединяем эмбеддинги узлов, которые находятся на ребре, и проходим через еще один линейный слой перед применением softmax.
Эти коэффициенты внимания затем используются для вычисления линейной комбинации признаков, соответствующих исходным признакам узлов.
Нам нужно сделать так, чтобы признаки каждого узла были увеличены для каждого соседа, а затем объединять их с признаками соседних узлов.
Секретный ингредиент заключается в том, чтобы получить признаки узла, повторенные для каждого соседа. Для этого мы меняем размерности 0 и 1 повторенных признаков узла перед маскировкой.
Результат все еще является массивом формы [5, 5, 8], но теперь каждая строка в [i, :, :] одинакова и соответствует признаку узла i. Теперь мы можем использовать покомпонентное умножение для создания повторяющихся признаков узла только тогда, когда они содержат соседа. Наконец, мы объединяем их с признаками соседей, созданными для GNN, и получаем объединенные признаки.
Мы почти готовы! Теперь, когда у нас есть объединенные признаки, мы можем пропустить их через линейный слой. Нам нужно изменить форму обратно на [5, 5, hidden_size], чтобы мы могли применить softmax по средней размерности и получить наши коэффициенты внимания.
Теперь, когда у нас есть наши коэффициенты внимания формы [5, 5, hidden_size], что фактически является одним вложением на графовом ребре для нашего графа с n узлами. В статье говорится, что они должны быть транспонированы (размерности изменены), поэтому я сделал это перед ReLU, а затем применил softmax по последней размерности, чтобы они были нормализованы для каждого индекса размерности скрытого размера. Мы умножаем эти коэффициенты на исходные вложения узлов. Напомним, что исходные вложения узлов имели форму [5, 5, 8], где 8 было произвольно выбрано из среза первых 8 признаков кодировки BERT наших текстовых документов.
Умножение формы [5, hidden_size, 5] на форму [5, 5, 8] дает форму [5, hidden_size, 8]. Затем мы суммируем по размерности скрытого размера, чтобы наконец вывести [5, 8], соответствующую нашей исходной форме ввода. Теперь мы также можем пропустить это через нелинейность, например, еще один ReLU, и затем повторить этот слой несколько раз.
Заключение
На данный момент мы рассмотрели визуальную реализацию одиночных слоев GNN и слоев GAT. Вы можете найти проект в этом репозитории на GitHub. В статье они также объясняют, как они расширяют метод для многоголового внимания. Дайте мне знать в комментариях, если вы хотите, чтобы я также рассмотрел эту часть или если есть что-то еще, что вы хотели бы, чтобы я рассмотрел с помощью Graphbook.