Развитие методов глубокого машинного обучения привело к росту популярности нейронных сетей в задачах распознавания образов, машинного перевода, генерации изображений и текстов и многих других. С 2009 года нейронные сети попытались применить напрямую в задачах обработки графов (к которым могут относиться системы веб-страниц, связанных ссылками, словари с определенными отношениями между словами, граф социальных связей и другие) и среди возможных задач можно определить поиск кластеров узлов, создание новых графов на основе имеющейся информации о структуре графа, расширение графа и предсказание новых связей и другие. Сейчас выделяют несколько типов нейронных сетей на основе графов - сверточные графовые сети (Convolutional Graph Network), графовые изоморные сети (Graph Isomorphism Network) и многие другие и они часто используются для анализа цитирования статей, исследования текста (представление предложения как графа с указанием типов отношений между словами), изучения взаимосвязанных структур (например, исследования белковых молекул, в частности сеть Alphafold использует модель GNN) и т.д. В статье мы рассмотрим некоторые общие вопросы создания и обучения графовых сетей на основе библиотеки Python Spektral.

Внутри Spektral используется Keras и топология нейронной сети может быть заданы стандартными механизмами Keras Layers, но изначально предлагается большое количество преднастроенных моделей. В Spektral исходные данные для обучения сети представляются в виде экземпляров объектов spektral.data.Graph, который определяется 4 атрибутами:

  • a - матрица связности (квадратный np.array для определения ребер, связывающих узлы или таблица связности)

  • x - свойства узлов (np.array со списком свойств)

  • e - свойства ребер (таблица связности, определяющая номера узлов, которые формируют ребро и список свойств ребра)

  • y - метки (одномерный или двумерный np.array для описания меток узлов или глобальных меток. В двумерном случае первый индекс - идентификатор узла, второй - идентификатор метки внутри узла, если их несколько)

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

Граф может быть сформирован как вручную, так и использованием вспомогательных загрузчиков, которые используют наборы данных из spektral.dataset.Dataset. Также представлено несколько готовых наборов данных по цитированию статей (пакет spectral.datasets.citation), reddit (spectral.datasets.graphsage.Reddit), описание структуры молекул QM9 (spektral.datasets.qm9.QM9) и многие другие.

Для исследования будем использовать базу данных цитирования Cora. Установим модуль pip install spektral и подготовим набор данных для дальнейшего обучения сети:

from spektral.datasets import Cora
dataset = Cora()
dataset

Cora(n_graphs=1)

dataset[0]
Graph(n_nodes=2708, n_node_features=1433, n_edge_features=None, n_labels=7)

Полученный dataset представляет собой коллекцию графов (в нашем случае один граф), который состоит из 2708 узлов, 1433 свойств у каждого узла, 7 меток, свойства у ребер не определены. В реальных датасетах может быть представлено большое количество графов (например, в описании структуры молекул) и можно использовать методы filter / map / apply для отбора и модификации индивидуальных графов в наборе данных.

Следующим шагом нужно подготовить данные для передачи в нейронную сеть. Для этого мы будем использовать загрузчики (loaders), которые создают несколько разные представления данных:

  • SingleLoader используется когда в наборе данных у нас только один граф

  • DisjointLoader объединяет несколько графов в один большой и преобразует его во входные векторы для нейронной сети

  • BatchLoader создает набор графов для обучения нейронной сети (аналогично тому, как представляется набор обучающих образцов в других типах сетей)

  • MixedLoader создает один большой граф, но при этом оставляет метки как независимые входные векторы

from spektral.data import SingleLoader
loader = SingleLoader(dataset)
loader.load()

Дальше создадим непосредственно нейронную сеть. Фактически основную задачу выполняют первые слои сети и нам доступно несколько вариантов графовых сетей в пакете spektral.layers (например, GCNConv - сверточные слои для Graph Convolutional Network, GraphSageConv для данных, собранных по алгоритму GraphSage и др., подробно о различиях в топологиях разных моделей графовых сетей можно почитать здесь). Сеть создается обычным для Keras способом, через реализацию Model:

class MyFirstGNN(Model):

    def __init__(self, n_hidden, n_labels):
        super().__init__()
        self.graph_conv = GCNConv(n_hidden)       //собственно сама сеть
        self.pool = GlobalSumPool()               //обобщение результатов
        self.dropout = Dropout(0.5)								//избегаем переобучения
        self.dense = Dense(n_labels, 'softmax')		//классификация по n_labels

    def call(self, inputs):
        out = self.graph_conv(inputs)
        out = self.dropout(out)
        out = self.pool(out)
        out = self.dense(out)

        return out

Дальше работа с сетью ничем не отличается от любых других реализаций сетей на основе Keras:

  • Создаем экземпляр модели

  • Компилируем модель с указанием функции потерь, оптимизатора и метрики

  • Обучаем модель на входных данных (получены после использования загрузчика)

  • Оцениваем точность модели

  • Применяем модель для классификации узлов или иным образом, который определяется топологией сети

model = MyFirstGnn()
model.compile(
    optimizer=Adam(0.01),
    loss=CategoricalCrossentropy(reduction="sum"),
    weighted_metrics=["acc"],
)

def mask_to_weights(mask):
    return mask.astype(np.float32) / np.count_nonzero(mask)


weights_tr, weights_va, weights_te = (
    mask_to_weights(mask)
    for mask in (dataset.mask_tr, dataset.mask_va, dataset.mask_te)
)

# sample_weight - веса, которые назначаются для ребер
loader_tr = SingleLoader(dataset, sample_weights=weights_tr)
loader_va = SingleLoader(dataset, sample_weights=weights_va)
model.fit(
    loader_tr.load(),
    steps_per_epoch=loader_tr.steps_per_epoch,
    validation_data=loader_va.load(),
    validation_steps=loader_va.steps_per_epoch,
    epochs=epochs,
    callbacks=[EarlyStopping(patience=patience, restore_best_weights=True)],
)

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

При написании статьи использовались материалы публикации Graph Neural Networks in TensorFlow and Keras with Spektral Daniele Grattarola and Cesare Alippi (https://arxiv.org/abs/2006.12138)

В преддверии старта курса Python для аналитики приглашаю всех желающих на бесплатный урок в рамках которого рассмотрим возможности применения в Python практик использования RFM сегментации клиентов на примере.

Комментарии (0)