Сеть графа | Сети внимания графа | ICLR 2018 | Объяснение кода

искусственный интеллект алгоритм
Сеть графа | Сети внимания графа | ICLR 2018 | Объяснение кода

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

  • Название статьи: «ГРАФИЧЕСКИЕ СЕТИ ВНИМАНИЯ»
  • Статья перенесена из: публичного аккаунта WeChat «Алхимия машинного обучения»
  • Автор заметки: Brother Alchemy
  • Контактное лицо: WeChat cyx645016617 (приглашаем к общению и совместному прогрессу)
  • Бумажный портал:АР Вест V.org/PDF/1710.10…

1 Реализация кода

  • код гитхаб:GitHub.com/Diego999/friends…
  • Оценка: Этот github краткий и понятный.После загрузки набора данных Cora вы можете запустить его, напрямую изменив путь. Мое объяснение кода здесь также основано на содержании этого github.

1.1 Экспериментальные результаты

Поскольку я впервые читаю статью GNN, я не знаю, как она будет развиваться после 2018 года (но предполагается, что она будет развиваться взрывным образом), результат Graph Attention Network:

Видно, что точность cora составляет около 0,83, и результаты, которые я тестировал с официальным кодом, таковы:

По крайней мере, это относительно солидное исследование.

1.2 Чтение данных

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

  • на основе случая
  • Генетический алгоритм
  • Нейронные сети
  • Вероятностный метод
  • обучение с подкреплением
  • изучение правил
  • теория

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

После выделения корней и удаления окончаний осталось всего 1433 уникальных слова. Все слова с частотой документа менее 10 удаляются.

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

import numpy as np
import scipy.sparse as sp
import torch


def encode_onehot(labels):
    # The classes must be sorted before encoding to enable static class encoding.
    # In other words, make sure the first class always maps to index 0.
    classes = sorted(list(set(labels)))
    classes_dict = {c: np.identity(len(classes))[i, :] for i, c in enumerate(classes)}
    labels_onehot = np.array(list(map(classes_dict.get, labels)), dtype=np.int32)
    return labels_onehot


def load_data(path="./data/cora/", dataset="cora"):
    """Load citation network dataset (cora only for now)"""
    print('Loading {} dataset...'.format(dataset))

    idx_features_labels = np.genfromtxt("{}/{}.content".format(path, dataset), dtype=np.dtype(str))
    features = sp.csr_matrix(idx_features_labels[:, 1:-1], dtype=np.float32)
    labels = encode_onehot(idx_features_labels[:, -1])

    # build graph
    idx = np.array(idx_features_labels[:, 0], dtype=np.int32)
    idx_map = {j: i for i, j in enumerate(idx)}
    edges_unordered = np.genfromtxt("{}/{}.cites".format(path, dataset), dtype=np.int32)
    edges = np.array(list(map(idx_map.get, edges_unordered.flatten())), dtype=np.int32).reshape(edges_unordered.shape)
    adj = sp.coo_matrix((np.ones(edges.shape[0]), (edges[:, 0], edges[:, 1])), shape=(labels.shape[0], labels.shape[0]), dtype=np.float32)

    # build symmetric adjacency matrix
    adj = adj + adj.T.multiply(adj.T > adj) - adj.multiply(adj.T > adj)

    features = normalize_features(features)
    adj = normalize_adj(adj + sp.eye(adj.shape[0]))

    idx_train = range(140)
    idx_val = range(200, 500)
    idx_test = range(500, 1500)

    adj = torch.FloatTensor(np.array(adj.todense()))
    features = torch.FloatTensor(np.array(features.todense()))
    labels = torch.LongTensor(np.where(labels)[1])

    idx_train = torch.LongTensor(idx_train)
    idx_val = torch.LongTensor(idx_val)
    idx_test = torch.LongTensor(idx_test)

    return adj, features, labels, idx_train, idx_val, idx_test


def normalize_adj(mx):
    """Row-normalize sparse matrix"""
    rowsum = np.array(mx.sum(1))
    r_inv_sqrt = np.power(rowsum, -0.5).flatten()
    r_inv_sqrt[np.isinf(r_inv_sqrt)] = 0.
    r_mat_inv_sqrt = sp.diags(r_inv_sqrt)
    return mx.dot(r_mat_inv_sqrt).transpose().dot(r_mat_inv_sqrt)


def normalize_features(mx):
    """Row-normalize sparse matrix"""
    rowsum = np.array(mx.sum(1))
    r_inv = np.power(rowsum, -1).flatten()
    r_inv[np.isinf(r_inv)] = 0.
    r_mat_inv = sp.diags(r_inv)
    mx = r_mat_inv.dot(mx)
    return mx


def accuracy(output, labels):
    preds = output.max(1)[1].type_as(labels)
    correct = preds.eq(labels).double()
    correct = correct.sum()
    return correct / len(labels)

Среди них ключевыми функциями являются:

  1. sp - разреженная библиотечная функция scipy, операция с разреженной матрицей;
  2. sp.coo_matrix(a,b,c,shape,dtype)Эта функция заключается в построении технологической матрицы. b — строка матрицы, c — столбец матрицы, a — номер строки b и столбца c, shape — размер построенной разреженной матрицы. Эта функция не понятна, вы можете перейти на Baidu. Таким образом, возвращаемое значение, которое мы получаем, представляет собой матрицу, а элементы в ней — от id цитируемого документа до id цитируемого документа.
  3. adj = adj + adj.T.multiply(adj.T > adj) - adj.multiply(adj.T > adj)

Этот метод заключается в том, чтобы сделать направленное указание в двунаправленную матрицу смежности. Первый добавленный фактор многократно добавит ситуацию цитирования самого себя (такой ситуации не будет в статье, но она может возникнуть в других графовых сетях, которые узлы соединяют сами с собой). Вычитаемый коэффициент предназначен для того, чтобы избежать вышеупомянутого повторного расчета самого соединения. 4.normalize_featureОчень просто разделить признаки каждой выборки на их сумму. Так что сумма собственных значений каждой выборки равна 1. 5.normalia_adjКак и в описанном выше процессе, строки и столбцы образцов стандартизированы.Конкретную логику трудно объяснить четко, и вы можете испытать ее на себе.

1.3 Модельная часть

output = model(features, adj)
loss_train = F.nll_loss(output[idx_train], labels[idx_train])

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

model = GAT(nfeat=features.shape[1], 
                nhid=8, 
                nclass=int(labels.max()) + 1, 
                dropout=0.6, 
                nheads=8, 
                alpha=0.2)

При построении GAT nfeat представляет количество признаков каждого образца, здесь 1433, nhid — неопределенное значение, nclass — классификационная категория, nheads — неопределенное значение, а alpha = 0,2 — неопределенное значение.

class GAT(nn.Module):
    def __init__(self, nfeat, nhid, nclass, dropout, alpha, nheads):
        """Dense version of GAT."""
        super(GAT, self).__init__()
        self.dropout = dropout

        self.attentions = [GraphAttentionLayer(nfeat, nhid, dropout=dropout, alpha=alpha, concat=True) for _ in range(nheads)]
        for i, attention in enumerate(self.attentions):
            self.add_module('attention_{}'.format(i), attention)

        self.out_att = GraphAttentionLayer(nhid * nheads, nclass, dropout=dropout, alpha=alpha, concat=False)

    def forward(self, x, adj):
        x = F.dropout(x, self.dropout, training=self.training)
        x = torch.cat([att(x, adj) for att in self.attentions], dim=1)
        x = F.dropout(x, self.dropout, training=self.training)
        x = F.elu(self.out_att(x, adj))
        return F.log_softmax(x, dim=1)

Выше приведен класс модели pytorch для построения модели. Его можно найти:

  • При наличии нескольких nheads в self.attentions будет несколько GraphAttentionLayer. Добавить последнийself.out_attGraphAttentionLayer составляет всю сеть.
  • На начальном этапе функции сначала подвергаются случайному исключению,Процент отсева настолько велик, что я не знаю, такая ли это графическая сеть, как шесть саспенсов..
  • После модели отсева пройдите через GraphAttentionLayer, определенный ранее разными nheads, а затем объедините все результаты;
  • После очередного выпадения продолжайтеsefl.out_attВот и все. Наконец, просто используйте softmax.

Теперь ключом является построение GraphAttentionLayer.

1.4 GraphAttentionLayer

class GraphAttentionLayer(nn.Module):
    """
    Simple GAT layer, similar to https://arxiv.org/abs/1710.10903
    """
    def __init__(self, in_features, out_features, dropout, alpha, concat=True):
        super(GraphAttentionLayer, self).__init__()
        self.dropout = dropout
        self.in_features = in_features
        self.out_features = out_features
        self.alpha = alpha
        self.concat = concat

        self.W = nn.Parameter(torch.empty(size=(in_features, out_features)))
        nn.init.xavier_uniform_(self.W.data, gain=1.414)
        self.a = nn.Parameter(torch.empty(size=(2*out_features, 1)))
        nn.init.xavier_uniform_(self.a.data, gain=1.414)

        self.leakyrelu = nn.LeakyReLU(self.alpha)

    def forward(self, h, adj):
        Wh = torch.mm(h, self.W) # h.shape: (N, in_features), Wh.shape: (N, out_features)
        e = self._prepare_attentional_mechanism_input(Wh)

        zero_vec = -9e15*torch.ones_like(e)
        attention = torch.where(adj > 0, e, zero_vec)
        attention = F.softmax(attention, dim=1)
        attention = F.dropout(attention, self.dropout, training=self.training)
        h_prime = torch.matmul(attention, Wh)

        if self.concat:
            return F.elu(h_prime)
        else:
            return h_prime

    def _prepare_attentional_mechanism_input(self, Wh):
        # Wh.shape (N, out_feature)
        # self.a.shape (2 * out_feature, 1)
        # Wh1&2.shape (N, 1)
        # e.shape (N, N)
        Wh1 = torch.matmul(Wh, self.a[:self.out_features, :])
        Wh2 = torch.matmul(Wh, self.a[self.out_features:, :])
        # broadcast add
        e = Wh1 + Wh2.T
        return self.leakyrelu(e)

    def __repr__(self):
        return self.__class__.__name__ + ' (' + str(self.in_features) + ' -> ' + str(self.out_features) + ')'

Прямая функция в этом GraphAttentionLayer (GAL), h — это функции, форма должна быть (2708, 1433), adj — матрица смежности узла, а форма (2708, 2708)

  1. Сначала используйте h, чтобы получить скрытые переменные через torch.mm, аналогично полносвязному слою, уменьшая 1433 объекта до 8 объектов (nhid=8);
  2. e = self._prepare_attentional_mechanism_input(Wh)Этот абзац должен стать нововведением данной статьи. Этот абзац действительно слишком абстрактен. Вы можете понять его смысл только после прочтения статьи. В любом случае форма e, возвращаемая этой функцией, имеет вид (2708, 2708)
  3. torch.whereЭто новая функция. Операции на месте, такие как A[A>x] = 1, неуправляемы, поэтому я собираюсь использовать функцию torch.where(condiciton, B, A). A, удовлетворяющий условию, будет заменен на B в соответствующей позиции. Таким образом, в коде значение позиции, в которой матрица смежности zero_vec больше 0, будет заменено значением соответствующей позиции только что вычисленного e. Это аттенитон, концепция, которая представляет различную важность критических узлов по сравнению с этим узлом. Затем происходит отсев, а затем умножаются внимание и W. это конец.

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

1.5 Сомнение

Эта строка кода:

# init部分
self.attentions = [GraphAttentionLayer(nfeat, nhid, dropout=dropout, alpha=alpha, concat=True) for _ in range(nheads)]
# forward部分
x = torch.cat([att(x, adj) for att in self.attentions], dim=1)

Зачем строить 8 одинаковых GraphAttentionLayer? Я чувствую, что вы используете 8 идентичных сверточных слоев параллельно, что на самом деле не дает эффекта улучшения функций.

Поэтому я использовал разные nheads, чтобы поэкспериментировать здесь, чтобы посмотреть, повлияет ли это на результаты эксперимента:

nheads test acc
8 0.84
4 0.848
2 0.841
1 0.8450
12 0.8480

Экспериментальные результаты показывают, что на самом деле количество nheads мало влияет на реальную ситуацию, но поскольку это было сделано здесь, давайте посмотрим на влияние nhid на результаты эксперимента, здесь мы выбираем nheads как 1.

nhid test acc
8 0.84
16 0.8500
32 0.8400
64 0.8350
4 0.7940

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