[Предисловие]: я просмотрел много объяснений и видео о графовых сетях и графовых сверточных сетях и раньше. Теперь понимание графовой сети нельзя углубить только из текстовой информации, поэтому нам нужно взглянуть на часть кода. Теперь начните читать первую статью и код графовой сети, чтобы официально войти в область научных исследований графовой сети.
- Название статьи: «ГРАФИЧЕСКИЕ СЕТИ ВНИМАНИЯ»
- Статья перенесена из: публичного аккаунта 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)
Среди них ключевыми функциями являются:
- sp - разреженная библиотечная функция scipy, операция с разреженной матрицей;
-
sp.coo_matrix(a,b,c,shape,dtype)
Эта функция заключается в построении технологической матрицы. b — строка матрицы, c — столбец матрицы, a — номер строки b и столбца c, shape — размер построенной разреженной матрицы. Эта функция не понятна, вы можете перейти на Baidu. Таким образом, возвращаемое значение, которое мы получаем, представляет собой матрицу, а элементы в ней — от id цитируемого документа до id цитируемого документа. 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_att
GraphAttentionLayer составляет всю сеть. - На начальном этапе функции сначала подвергаются случайному исключению,Процент отсева настолько велик, что я не знаю, такая ли это графическая сеть, как шесть саспенсов..
- После модели отсева пройдите через 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)
- Сначала используйте h, чтобы получить скрытые переменные через torch.mm, аналогично полносвязному слою, уменьшая 1433 объекта до 8 объектов (nhid=8);
-
e = self._prepare_attentional_mechanism_input(Wh)
Этот абзац должен стать нововведением данной статьи. Этот абзац действительно слишком абстрактен. Вы можете понять его смысл только после прочтения статьи. В любом случае форма e, возвращаемая этой функцией, имеет вид (2708, 2708) -
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 легко переобучить. Так что лучше выбрать начало и середину