Пошаговое руководство по использованию PyTorch Geometric

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

Среди различных типов GNN наиболее распространенной и широко применяемой моделью стали Graph Convolutional Networks (GCN). GCN являются инновационными из-за их способности использовать как функции узла, так и его местоположение для прогнозирования, обеспечивая эффективный способ обработки данных, структурированных графом.

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

PyTorch Geometric — это специализированное расширение PyTorch, созданное специально для разработки и реализации GNN. Это продвинутая, но удобная библиотека, которая предоставляет полный набор инструментов для облегчения машинного обучения на основе графов. Чтобы начать наше путешествие, потребуется установка PyTorch Geometric. Если вы используете Google Colab, PyTorch уже должен быть на месте, поэтому все, что нам нужно сделать, это выполнить несколько дополнительных команд.

Весь код доступен на Google Colab и GitHub.

!pip install torch_geometric
import torch
import numpy as np
import networkx as nx
import matplotlib.pyplot as plt

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

🌐 I. Графические данные

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

В этой статье мы изучим печально известный и часто используемый набор данных Клуб карате Закари.

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

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

Давайте импортируем набор данных с помощью встроенной функции PyG и попробуем понять, какой Datasets объект он использует.

from torch_geometric.datasets import KarateClub
# Import dataset from PyTorch Geometric
dataset = KarateClub()
# Print information
print(dataset)
print('------------')
print(f'Number of graphs: {len(dataset)}')
print(f'Number of features: {dataset.num_features}')
print(f'Number of classes: {dataset.num_classes}')
KarateClub()
------------
Number of graphs: 1
Number of features: 34
Number of classes: 4

Этот набор данных имеет только 1 график, где каждый узел имеет вектор признаков из 34 измерений и является частью одного из четырех классов (наши четыре группы). На самом деле объект Datasets можно рассматривать как набор объектов Data (граф).

Мы можем дополнительно изучить наш уникальный граф, чтобы узнать о нем больше.

# Print first element
print(f'Graph: {dataset[0]}')
Graph: Data(x=[34, 34], edge_index=[2, 156], y=[34], train_mask=[34])

Объект Data особенно интересен. Распечатав его, мы получим хорошее резюме изучаемого нами графика:

  • x=[34, 34] — это матрица признаков узла с формой (количество узлов, количество признаков). В нашем случае это означает, что у нас есть 34 узла (наши 34 члена), каждый узел связан с 34-мерным вектором признаков.
  • edge_index=[2, 156] представляет связность графа (как узлы связаны) с формой (2, количество направленных ребер).
  • y=[34] – это метки достоверности узлов. В этой задаче каждый узел относится к одному классу (группе), поэтому у нас есть одно значение для каждого узла.
  • train_mask=[34] — это необязательный атрибут, который указывает, какие узлы следует использовать для обучения со списком операторов True или False.

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

data = dataset[0]
print(f'x = {data.x.shape}')
print(data.x)
x = torch.Size([34, 34])
tensor([[1., 0., 0.,  ..., 0., 0., 0.],
        [0., 1., 0.,  ..., 0., 0., 0.],
        [0., 0., 1.,  ..., 0., 0., 0.],
        ...,
        [0., 0., 0.,  ..., 1., 0., 0.],
        [0., 0., 0.,  ..., 0., 1., 0.],
        [0., 0., 0.,  ..., 0., 0., 1.]])

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

Теперь давайте напечатаем индекс ребра.

print(f'edge_index = {data.edge_index.shape}')
print(data.edge_index)
edge_index = torch.Size([2, 156])
tensor([[ 0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  1,  1,
          1,  1,  1,  1,  1,  1,  1,  2,  2,  2,  2,  2,  2,  2,  2,  2,  2,  3,
          3,  3,  3,  3,  3,  4,  4,  4,  5,  5,  5,  5,  6,  6,  6,  6,  7,  7,
          7,  7,  8,  8,  8,  8,  8,  9,  9, 10, 10, 10, 11, 12, 12, 13, 13, 13,
         13, 13, 14, 14, 15, 15, 16, 16, 17, 17, 18, 18, 19, 19, 19, 20, 20, 21,
         21, 22, 22, 23, 23, 23, 23, 23, 24, 24, 24, 25, 25, 25, 26, 26, 27, 27,
         27, 27, 28, 28, 28, 29, 29, 29, 29, 30, 30, 30, 30, 31, 31, 31, 31, 31,
         31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 33, 33,
         33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33],
        [ 1,  2,  3,  4,  5,  6,  7,  8, 10, 11, 12, 13, 17, 19, 21, 31,  0,  2,
          3,  7, 13, 17, 19, 21, 30,  0,  1,  3,  7,  8,  9, 13, 27, 28, 32,  0,
          1,  2,  7, 12, 13,  0,  6, 10,  0,  6, 10, 16,  0,  4,  5, 16,  0,  1,
          2,  3,  0,  2, 30, 32, 33,  2, 33,  0,  4,  5,  0,  0,  3,  0,  1,  2,
          3, 33, 32, 33, 32, 33,  5,  6,  0,  1, 32, 33,  0,  1, 33, 32, 33,  0,
          1, 32, 33, 25, 27, 29, 32, 33, 25, 27, 31, 23, 24, 31, 29, 33,  2, 23,
         24, 33,  2, 31, 33, 23, 26, 32, 33,  1,  8, 32, 33,  0, 24, 25, 28, 32,
         33,  2,  8, 14, 15, 18, 20, 22, 23, 29, 30, 31, 33,  8,  9, 13, 14, 15,
         18, 19, 20, 22, 23, 26, 27, 28, 29, 30, 31, 32]])

В теории графов и сетевом анализе связь между узлами хранится с использованием различных структур данных. edge_index — одна из таких структур данных, в которой соединения графа хранятся в двух списках (156 направленных ребер, что соответствует 78 двунаправленным ребрам). Причина этих двух списков в том, что один список хранит исходные узлы, а второй идентифицирует узлы назначения.

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

Наоборот, более интуитивно понятный и простой способ представить связность графа – использовать матрицу смежности A. Это квадратная матрица, в которой каждый элемент Aᵢⱼs указывает наличие или отсутствие ребра от узла i до узла j на графике. Другими словами, ненулевой элемент Aᵢⱼ подразумевает соединение узла i с узлом j, а ноль указывает на отсутствие прямого соединения.

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

Матрица смежности может быть выведена из edge_index с функцией полезности to_dense_adj().

from torch_geometric.utils import to_dense_adj
A = to_dense_adj(data.edge_index)[0].numpy().astype(int)
print(f'A = {A.shape}')
print(A)
A = (34, 34)
[[0 1 1 ... 1 0 0]
 [1 0 1 ... 0 0 0]
 [1 1 0 ... 0 1 0]
 ...
 [1 0 0 ... 0 1 1]
 [0 0 1 ... 1 0 1]
 [0 0 0 ... 1 1 0]]

С данными графа узлы относительно редко бывают тесно связаны между собой. Как видите, наша матрица смежности A является разреженной (заполнена нулями).

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

Напротив, ярлыки истинной истины легко понять.

print(f'y = {data.y.shape}')
print(data.y)
y = torch.Size([34])
tensor([1, 1, 1, 1, 3, 3, 3, 1, 0, 1, 3, 1, 1, 1, 0, 0, 3, 1, 0, 1, 0, 1, 0, 0,
        2, 2, 0, 0, 2, 0, 0, 2, 0, 0])

Наши метки истинности узла, хранящиеся в y, просто кодируют номер группы (0, 1, 2, 3) для каждого узла, поэтому у нас есть 34 значения.

Наконец, давайте напечатаем маску поезда.

print(f'train_mask = {data.train_mask.shape}')
print(data.train_mask)
train_mask = torch.Size([34])
tensor([ True, False, False, False,  True, False, False, False,  True, False,
        False, False, False, False, False, False, False, False, False, False,
        False, False, False, False,  True, False, False, False, False, False,
        False, False, False, False])

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

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

  • is_directed() указывает, является ли график направленным. Ориентированный граф означает, что матрица смежности несимметрична, т. е. направление ребер имеет значение в связях между узлами.
  • isolated_nodes() проверяет, не не связаны ли некоторые узлы с остальной частью графа. Эти узлы могут создавать проблемы в таких задачах, как классификация, из-за отсутствия соединений.
  • has_self_loops() указывает, что хотя бы один узел соединен сам с собой. Это отличается от понятия петли: петля подразумевает путь, который начинается и заканчивается в одном и том же узле, пересекая другие узлы между ними.

В контексте набора данных клуба карате Закари все эти свойства возвращают False. Это означает, что граф не направлен, не имеет изолированных узлов и ни один из его узлов не связан сам с собой.

print(f'Edges are directed: {data.is_directed()}')
print(f'Graph has isolated nodes: {data.has_isolated_nodes()}')
print(f'Graph has loops: {data.has_self_loops()}')
Edges are directed: False
Graph has isolated nodes: False
Graph has loops: False

Наконец, мы можем преобразовать график из PyTorch Geometric в популярную библиотеку графиков NetworkX, используя to_networkx. Это особенно полезно для визуализации небольшого графика с networkx и matplotlib.

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

from torch_geometric.utils import to_networkx
G = to_networkx(data, to_undirected=True)
plt.figure(figsize=(12,12))
plt.axis('off')
nx.draw_networkx(G,
                pos=nx.spring_layout(G, seed=0),
                with_labels=True,
                node_size=800,
                node_color=data.y,
                cmap="hsv",
                vmin=-2,
                vmax=3,
                width=0.8,
                edge_color="grey",
                font_size=14
                )
plt.show()

На этом графике клуба карате Закари показаны наши 34 узла, 78 (двунаправленных) ребер и 4 метки 4 разных цветов. Теперь, когда мы рассмотрели основы загрузки и обработки набора данных с помощью PyTorch Geometric, мы можем представить архитектуру Graph Convolutional Network.

✉️ II. Граф сверточной сети

Этот раздел направлен на введение и создание слоя свертки графа с нуля.

В традиционных нейронных сетях линейные слои применяют линейное преобразование к входящим данным. Это преобразование преобразует входные объекты x в скрытые векторы h с помощью весовой матрицы 𝐖. Игнорируя на данный момент предубеждения, это можно выразить так:

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

Мы можем обогатить наше представление узла, объединив его функции с функциями его соседей. Эта операция называется сверткой или агрегацией соседей. Представим окрестность узла i, включая саму себя, как С.

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

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

Мы почти там! Представлено Kipf et al. (2016), сверточный слой графа имеет одно последнее улучшение.

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

Обратите внимание, что когда i и j имеют одинаковое количество соседей, это эквивалентно нашему собственному слою. Теперь давайте посмотрим, как реализовать это на Python с помощью PyTorch Geometric.

🧠 III. Внедрение GCN

PyTorch Geometric предоставляет функцию GCNConv, которая напрямую реализует сверточный слой графа.

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

В следующем блоке кода мы определяем слой GCN с трехмерным скрытым слоем.

from torch.nn import Linear
from torch_geometric.nn import GCNConv

class GCN(torch.nn.Module):
    def __init__(self):
        super().__init__()
        self.gcn = GCNConv(dataset.num_features, 3)
        self.out = Linear(3, dataset.num_classes)
    def forward(self, x, edge_index):
        h = self.gcn(x, edge_index).relu()
        z = self.out(h)
        return h, z
model = GCN()
print(model)
GCN(
  (gcn): GCNConv(34, 3)
  (out): Linear(in_features=3, out_features=4, bias=True)
)

Если бы мы добавили второй слой GCN, наша модель агрегировала бы не только векторы признаков от соседей каждого узла, но и от соседей этих соседей.

Мы можем сложить несколько слоев графа, чтобы агрегировать все более и более отдаленные значения, но есть одна загвоздка: если мы добавим слишком много слоев, агрегирование станет настолько интенсивным, что все вложения в конечном итоге будут выглядеть одинаково. Это явление называется чрезмерным сглаживанием и может стать настоящей проблемой, если у вас слишком много слоев.

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

Цикл обучения стандартный: мы пытаемся предсказать правильные метки и сравниваем результаты GCN со значениями, хранящимися в data.y. Ошибка вычисляется по кросс-энтропийной потере и передается обратно с Адамом для точной настройки весов и смещений нашей GNN. Наконец, мы печатаем метрики каждые 10 эпох.

criterion = torch.nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.02)
# Calculate accuracy
def accuracy(pred_y, y):
    return (pred_y == y).sum() / len(y)
# Data for animations
embeddings = []
losses = []
accuracies = []
outputs = []
# Training loop
for epoch in range(201):
    # Clear gradients
    optimizer.zero_grad()
    # Forward pass
    h, z = model(data.x, data.edge_index)
    # Calculate loss function
    loss = criterion(z, data.y)
    # Calculate accuracy
    acc = accuracy(z.argmax(dim=1), data.y)
    # Compute gradients
    loss.backward()
    # Tune parameters
    optimizer.step()
    # Store data for animations
    embeddings.append(h)
    losses.append(loss)
    accuracies.append(acc)
    outputs.append(z.argmax(dim=1))
    # Print metrics every 10 epochs
    if epoch % 10 == 0:
        print(f'Epoch {epoch:>3} | Loss: {loss:.2f} | Acc: {acc*100:.2f}%')
Epoch   0 | Loss: 1.40 | Acc: 41.18%
Epoch  10 | Loss: 1.21 | Acc: 47.06%
Epoch  20 | Loss: 1.02 | Acc: 67.65%
Epoch  30 | Loss: 0.80 | Acc: 73.53%
Epoch  40 | Loss: 0.59 | Acc: 73.53%
Epoch  50 | Loss: 0.39 | Acc: 94.12%
Epoch  60 | Loss: 0.23 | Acc: 97.06%
Epoch  70 | Loss: 0.13 | Acc: 100.00%
Epoch  80 | Loss: 0.07 | Acc: 100.00%
Epoch  90 | Loss: 0.05 | Acc: 100.00%
Epoch 100 | Loss: 0.03 | Acc: 100.00%
Epoch 110 | Loss: 0.02 | Acc: 100.00%
Epoch 120 | Loss: 0.02 | Acc: 100.00%
Epoch 130 | Loss: 0.02 | Acc: 100.00%
Epoch 140 | Loss: 0.01 | Acc: 100.00%
Epoch 150 | Loss: 0.01 | Acc: 100.00%
Epoch 160 | Loss: 0.01 | Acc: 100.00%
Epoch 170 | Loss: 0.01 | Acc: 100.00%
Epoch 180 | Loss: 0.01 | Acc: 100.00%
Epoch 190 | Loss: 0.01 | Acc: 100.00%
Epoch 200 | Loss: 0.01 | Acc: 100.00%

Большой! Без особого удивления мы достигаем 100% точности на тренировочном наборе (полный набор данных). Это означает, что наша модель научилась правильно относить каждого члена клуба карате к своей правильной группе.

Мы можем создать аккуратную визуализацию, анимировав график и проследив эволюцию прогнозов GNN в процессе обучения.

%%capture
from IPython.display import HTML
from matplotlib import animation
plt.rcParams["animation.bitrate"] = 3000
def animate(i):
    G = to_networkx(data, to_undirected=True)
    nx.draw_networkx(G,
                    pos=nx.spring_layout(G, seed=0),
                    with_labels=True,
                    node_size=800,
                    node_color=outputs[i],
                    cmap="hsv",
                    vmin=-2,
                    vmax=3,
                    width=0.8,
                    edge_color="grey",
                    font_size=14
                    )
    plt.title(f'Epoch {i} | Loss: {losses[i]:.2f} | Acc: {accuracies[i]*100:.2f}%',
              fontsize=18, pad=20)
fig = plt.figure(figsize=(12, 12))
plt.axis('off')
anim = animation.FuncAnimation(fig, animate, \
            np.arange(0, 200, 10), interval=500, repeat=True)
html = HTML(anim.to_html5_video())
display(html)

Первые прогнозы случайны, но через некоторое время GCN отлично помечает каждый узел. Действительно, окончательный график такой же, как тот, который мы построили в конце первого раздела. Но что на самом деле узнает GCN?

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

Давайте напечатаем вложения, изученные нашей моделью.

# Print embeddings
print(f'Final embeddings = {h.shape}')
print(h)
Final embeddings = torch.Size([34, 3])
tensor([[1.9099e+00, 2.3584e+00, 7.4027e-01],
        [2.6203e+00, 2.7997e+00, 0.0000e+00],
        [2.2567e+00, 2.2962e+00, 6.4663e-01],
        [2.0802e+00, 2.8785e+00, 0.0000e+00],
        [0.0000e+00, 0.0000e+00, 2.9694e+00],
        [0.0000e+00, 0.0000e+00, 3.3817e+00],
        [0.0000e+00, 1.5008e-04, 3.4246e+00],
        [1.7593e+00, 2.4292e+00, 2.4551e-01],
        [1.9757e+00, 6.1032e-01, 1.8986e+00],
        [1.7770e+00, 1.9950e+00, 6.7018e-01],
        [0.0000e+00, 1.1683e-04, 2.9738e+00],
        [1.8988e+00, 2.0512e+00, 2.6225e-01],
        [1.7081e+00, 2.3618e+00, 1.9609e-01],
        [1.8303e+00, 2.1591e+00, 3.5906e-01],
        [2.0755e+00, 2.7468e-01, 1.9804e+00],
        [1.9676e+00, 3.7185e-01, 2.0011e+00],
        [0.0000e+00, 0.0000e+00, 3.4787e+00],
        [1.6945e+00, 2.0350e+00, 1.9789e-01],
        [1.9808e+00, 3.2633e-01, 2.1349e+00],
        [1.7846e+00, 1.9585e+00, 4.8021e-01],
        [2.0420e+00, 2.7512e-01, 1.9810e+00],
        [1.7665e+00, 2.1357e+00, 4.0325e-01],
        [1.9870e+00, 3.3886e-01, 2.0421e+00],
        [2.0614e+00, 5.1042e-01, 2.4872e+00],
...
        [2.1778e+00, 4.4730e-01, 2.0077e+00],
        [3.8906e-02, 2.3443e+00, 1.9195e+00],
        [3.0748e+00, 0.0000e+00, 3.0789e+00],
        [3.4316e+00, 1.9716e-01, 2.5231e+00]], grad_fn=<ReluBackward0>)

Как видите, вложения не обязательно должны иметь те же размеры, что и векторы признаков. Здесь я решил уменьшить количество измерений с 34 (dataset.num_features) до трех, чтобы получить хорошую визуализацию в 3D.

Давайте построим эти вложения до того, как произойдет какое-либо обучение, в эпоху 0.

# Get first embedding at epoch = 0
embed = h.detach().cpu().numpy()
fig = plt.figure(figsize=(12, 12))
ax = fig.add_subplot(projection='3d')
ax.patch.set_alpha(0)
plt.tick_params(left=False,
                bottom=False,
                labelleft=False,
                labelbottom=False)
ax.scatter(embed[:, 0], embed[:, 1], embed[:, 2],
           s=200, c=data.y, cmap="hsv", vmin=-2, vmax=3)
plt.show()

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

Давайте посмотрим, как они будут развиваться с течением времени, поскольку GCN становится все лучше и лучше в классификации узлов.

%%capture
def animate(i):
    embed = embeddings[i].detach().cpu().numpy()
    ax.clear()
    ax.scatter(embed[:, 0], embed[:, 1], embed[:, 2],
           s=200, c=data.y, cmap="hsv", vmin=-2, vmax=3)
    plt.title(f'Epoch {i} | Loss: {losses[i]:.2f} | Acc: {accuracies[i]*100:.2f}%',
              fontsize=18, pad=40)
fig = plt.figure(figsize=(12, 12))
plt.axis('off')
ax = fig.add_subplot(projection='3d')
plt.tick_params(left=False,
                bottom=False,
                labelleft=False,
                labelbottom=False)
anim = animation.FuncAnimation(fig, animate, \
              np.arange(0, 200, 10), interval=800, repeat=True)
html = HTML(anim.to_html5_video())
display(html)

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

Вложения не уникальны для GNN: их можно найти повсюду в глубоком обучении. Они также не обязательно должны быть трехмерными: на самом деле они редко бывают трехмерными. Например, такие языковые модели, как BERT, производят вложения с размерностью 768 или даже 1024.

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

Заключение

Сверточные сети Graph — невероятно универсальная архитектура, которую можно применять во многих контекстах. В этой статье мы ознакомились с библиотекой PyTorch Geometric и такими объектами, как Datasets и Data. Затем мы успешно восстановили сверточный слой графа с нуля. Затем мы применили теорию на практике, внедрив GCN, что дало нам понимание практических аспектов и того, как взаимодействуют отдельные компоненты. Наконец, мы визуализировали процесс обучения и получили четкое представление о том, что он включает в себя для такой сети.

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

Помимо GCN, исследователи предложили множество уровней и архитектур GNN. В следующей статье мы представим архитектуру Graph Attention Network (GAT), которая динамически вычисляет коэффициент нормализации GCN и важность каждого соединения с помощью механизма внимания.

Если вы хотите узнать больше о графовых нейронных сетях, погрузитесь глубже в мир GNN с моей книгой Практические графовые нейронные сети.

Следующая статья



Узнайте больше о машинном обучении и поддержите мою работу одним щелчком мыши — станьте участником Medium здесь:



Если вы уже являетесь участником, вы можете подписаться на меня на Medium.