Сегодня ясный и практический взгляд на теорию GCN. Мы рассмотрим реализацию PyTorch 🔦 GCN 👩‍🎓 от Kipf. Затем мы применим то, что узнали, к ненавистному набору данных Twitter 🔥

Мои предыдущие посты о графах и машинном обучении:

Поддержите меня, когда я присоединюсь к Medium по моей реферальной ссылке:



Добро пожаловать в мою серию статей о графовых нейронных сетях! В предыдущем посте мы исследовали теорию и математические основы графовых сверточных нейронных сетей. Сегодня мы рассмотрим реализация GCN на PyTorch от Kipf, попытаемся упростить реализацию и применить GCN к набору данных ненависть/обычные пользователи Twitter. Наслаждаться :)

Перевести теорию GCN на PyTorch

Наследование PyTorch nn.Module

В PyTorch обычной практикой является создание собственного модуля для создания модели с использованием torch.nn.Module Это создает объект Python, который позволяет создавать сложные модули. Пользовательские модули — это классы, являющиеся дочерними элементами пакета torch.nn.Module, унаследовавшие все методы и атрибуты:

import torch.nn as nn
class MyNetwork(nn.Module):
r""" Define a childern class of nn.Module, implementing my network"""
def __init__(self, in_size, output_size):
r""" Constructore, define input size and output size of the network"""
super(MyNetwork, self).__init__()
# define whatever layer you want to use
self.linear = nn.Linear(in_size, output_size)
def forward(self, x):
r""" You have to define the forward step"""
out = self.linear(x)
return out

class MyNetwork(nn.Module) определяет дочерний класс, который затем, аналогично nn.Module, хочет, чтобы конструктор был определен с размером входной и выходной сети ( in_size и output_size соответственно). В конструкторе вызывается суперконструктор super(). Это позволяет создать объект из пакета torch.nn.Module внутри объекта MyNetwork без его явной инициализации. Затем можно настроить сетевые слои и шаг forward, который, опять же, унаследован от nn.Module.

Применение nn.Module к базовой операции GCN:layers.py

По этой же схеме Kipf реализует логику GCN в layers.py: https://github.com/tkipf/pygcn/blob/master/pygcn/layers.py. который представляет собой произведение матрицы графа смежности на входной массив признаков графа и веса i-го слоя:

import math
import torch
from torch.nn.parameter import Parameter
from torch.nn.modules.module import Module
class GraphConvolution(Module):
"""
Class to implement Graph Convlution NN
"""
def __init__(self, in_features, out_features, bias=True):
r""" Define the constructor. Here we need to specify the number
of input and output features
Parameters
----------
in_features: int, number of nodes for a graph
out_features: int, number of targets
bias: bool, whether we want to have the bias term or not
"""
# define the super constructor
super(GraphConvolution, self).__init__()
self.in_features = in_features
self.out_features = out_features
# parameterise weights
self.weight = Parameter(torch.FloatTensor(in_features, out_features))
if bias:
self.bias = Parameter(torch.FloatTensor(out_features))
else:
self.register_parameter('bias', None)
self.reset_parameters()
def reset_parameters(self):
r""" Reset of parameters. Parameterized weights with auniform distribution"""
stdv = 1. / math.sqrt(self.weight.size(1))
self.weight.data.uniform_(-stdv, stdv)
if self.bias is not None:
self.bias.data.uniform_(-stdv, stdv)
def forward(self, input, adj):
r""" Define the forward step. Here we perform a matrix multiplication
between input graph and weigths.
The output is computed as a sparse matrix multiplication between the adjacency
matrix and the support result.
Parameters
----------
input: matrix/tensor input graph matrix
adj: self connected adjacency matrix from graph
"""
support = torch.mm(input, self.weight)
output = torch.spmm(adj, support)
if self.bias is not None:
return output + self.bias
else:
return output
view raw layers.py hosted with ❤ by GitHub

Параметры веса и смещения определяются как torch.nn.parameter объектов и генерируются путем случайной выборки из равномерного распределения. Шаг forward обновляет веса слоев, вычисляя уравнение 10 из предыдущей статьи.

Объединение слоев: модель GCN в models.py

Теперь, когда у нас реализован базовый строительный блок, мы можем приступить к реализации модели GCN. Согласно статье Кипфа, GCN состоит из 2 слоев, одного входного слоя и одного скрытого слоя, которые объединяются посредством активации ReLu. Кроме того, у нас может быть отсеваемый слой, в котором часть входных тренировочных данных отбрасывается, для дальнейшего прогнозирования слоев прочности. Конечный результат задается как логарифм softmax, экв. 1

import torch.nn as nn
import torch.nn.functional as F
# import the basic layer operation
from pygcn.layers import GraphConvolution
class GCN(nn.Module):
r""" Define the main GCN model"""
def __init__(self, nfeat, nhid, nclass, dropout):
r""" The model take the input feature dimension, the hidden dimension size
the number of classes and possible dropout probability
Parameter
---------
nfeat: int, number of input features
nhid: int, number of features for the hidden layer
nclass: int, number of target classes
dropout: float, dropout percentage
"""
super(GCN, self).__init__()
self.gc1 = GraphConvolution(nfeat, nhid)
self.gc2 = GraphConvolution(nhid, nclass)
self.dropout = dropout
def forward(self, x, adj):
r""" Define the forward step
Parameters
-----------
x: array/tensor input features
adj: array/tensor self term adjacency matrix
"""
x = F.relu(self.gc1(x, adj))
x = F.dropout(x, self.dropout, training=self.training)
x = self.gc2(x, adj)
return F.log_softmax(x, dim=1)
view raw models.py hosted with ❤ by GitHub

Подготовка входных данных

После настройки базовой модели мы можем взглянуть на входные данные. Kipf предложил данные из датасета Cora (https://paperswithcode.com/dataset/cora;
CC0: Public Domain)[1,4]. Набор данных Cora состоит из документов по машинному обучению, которые классифицируются по 7 классам: case_based, genetic_algorithms, neural_networks, probabilistic_methods, reinforcement_learning, rule_learning, theory. Общее количество бумаг равно 2708, что и будет количеством узлов. С удалением корней и стоп-слов окончательный набор данных будет содержать только 1433 уникальных слова, что и будет количеством признаков. Таким образом, граф может быть представлен матрицей 2708 x 1433, где единицы и нули зависят от наличия определенного слова. Из бумажного файла цитат можно получить список ребер и создать из него матрицу смежности 2708 x 2708. Данные создаются через служебный скрипт utils.py. Как только эти элементы созданы, можно переходить к этапу обучения.

Обучение модели GCN с помощью набора данных Cora

Рис.4 суммирует все этапы GCN. Данные загружаются для создания матрицы признаков X и матрицы смежности adj. Эти элементы могут быть приняты моделью GCN, которая преобразует данные посредством умножения Лапласа, возвращая вероятность 7 классов для каждого узла.

На рис. 5 показан фрагмент кода Кипфа для обучения модели GCN. Основной бит — это функция поезда. Здесь изначально градиент устанавливается равным нулю через optimiser.zero_grad(). Затем выходные данные модели вычисляются как output = model(features, adj). Наконец, выполняется проверка производительности и потери модели с вычислением отрицательного логарифмического правдоподобия, которое обновляет веса сети, и точности, которые оценивают качество модели по сравнению с набором данных для оценки.

import torch
import torch.nn.functional as F
import torch.optim as optim
from pygcn.utils import load_data, accuracy
from pygcn.models import
# Load data
adj, features, labels, idx_train, idx_val, idx_test = load_data()
# Model and optimizer - we have args to parsse
model = GCN(nfeat=features.shape[1],
nhid=args.hidden,
nclass=labels.max().item() + 1,
dropout=args.dropout)
# call an optimizer
optimizer = optim.Adam(model.parameters(),
lr=args.lr, weight_decay=args.weight_decay)
# define the training function
def train(epoch):
r""" Define a train function for the GCN model
Parameters
----------
epoch: int, the number of epochs
"""
# model inherits train from nn.Module
model.train()
# set up the gradient
optimizer.zero_grad()
# output from the model
output = model(features, adj)
# compute the neg. likelihood loos
loss_train = F.nll_loss(output[idx_train], labels[idx_train])
# compute the accuracy
acc_train = accuracy(output[idx_train], labels[idx_train])
# spread the loss across the network
loss_train.backward()
# update the weights
optimizer.step()
if not args.fastmode:
# Evaluate validation set performance separately,
# deactivates dropout during validation run.
model.eval()
output = model(features, adj)
# evaluation step
loss_val = F.nll_loss(output[idx_val], labels[idx_val])
acc_val = accuracy(output[idx_val], labels[idx_val])
print('Epoch: {:04d}'.format(epoch+1),
'loss_train: {:.4f}'.format(loss_train.item()),
'acc_train: {:.4f}'.format(acc_train.item()),
'loss_val: {:.4f}'.format(loss_val.item()),
'acc_val: {:.4f}'.format(acc_val.item()),
'time: {:.4f}s'.format(time.time() - t))
# Train model
t_total = time.time()
for epoch in range(args.epochs):
train(epoch)
print("Optimization Finished!")
print("Total time elapsed: {:.4f}s".format(time.time() - t_total))
view raw train.py hosted with ❤ by GitHub

Играйте с GCN и предсказывайте ненавистных пользователей в Twitter

Пришло время поиграть с причудливым набором данных и GCN. Набор данных — ненавистные пользователи Твиттера[2] (CC0: Public Domain License) с описаниями в его статье [2]. Набор данных имеет 100 000 пользователей. 5000 пользователей помечены как разжигающие ненависть, а именно, они размещают посты с ненавистью в Твиттере. У каждого пользователя есть набор определенных и разработанных вручную функций, users_neightborhood_anon.csv, где твиты были закодированы с помощью вложений GloVe [3]. Цель состоит в том, чтобы использовать сетевую информацию, чтобы предсказать, какие пользователи также могут быть ненавистными. Пример свободно вдохновлен этим руководством: https://stellargraph.readthedocs.io/en/v1.0.0rc1/demos/interpretability/gcn/hateful-twitters-interpretability.html

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

Во-первых, нам нужно загрузить данные и разархивировать файлы. В этом примере мы собираемся использовать API Kaggle через блокнот Google Colab:

!pip install kaggle --upgrade
# download from your kaggle account the token file kaggle.json
!mkdir /root/.kaggle 
!cp kaggle.json /root/.kaggle/. 
!mkdir dataset 
!kaggle datasets download -d manoelribeiro/hateful-users-on-twitter -p dataset/
# unzip the files 
!unzip dataset/*.zip

Для этого примера нужны файлы users_neighborhood_anon.csv и список ребер из users.edges.

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

import networkx as nx
import pandas as pd
import numpy as np
import seaborn as sns
import itertools
import os
from sklearn.decomposition import PCA
from sklearn.manifold import TSNE
from sklearn.linear_model import LogisticRegressionCV
from sklearn import preprocessing, feature_extraction
from sklearn.model_selection import train_test_split
from sklearn import metrics
import matplotlib.pyplot as plt
import seaborn as sns
from scipy.sparse import csr_matrix, lil_matrix
%matplotlib inline
def data_cleaning(feat):
r""" Clean the input data, removing the manually engineered features
We are going to use the network information only.
Parameters
----------
feat: pd.DataFrame, input dataframe with feature per node [100'000 x 1039]
Return
------
feat: pd.DataFrame, clean features [100'000 x 206]
"""
feat = feat.drop(columns=["hate_neigh", "normal_neigh"])
# Convert target values in hate column from strings to integers (0,1,2)
feat["hate"] = np.where(
feat["hate"] == "hateful", 1, np.where(feat["hate"] == "normal", 0, 2)
)
# missing information
number_of_missing = feat.isnull().sum()
number_of_missing[number_of_missing != 0]
# Replace NA with 0
feat.fillna(0, inplace=True)
# dropping info about suspension and deletion as it is should not be use din the predictive model
feat.drop(
feat.columns[feat.columns.str.contains("is_|_glove|c_|sentiment")],
axis=1,
inplace=True,
)
# drop hashtag feature
feat.drop(["hashtags"], axis=1, inplace=True)
# drop centrality based measures
feat.drop(
columns=["betweenness", "eigenvector", "in_degree", "out_degree"], inplace=True
)
feat.drop(columns=["created_at"], inplace=True)
return feat
# features
users_feat = pd.read_csv("users_neighborhood_anon.csv")
# clear the data and reduce feats to 206
features = data_cleaning(users_feat)
# normalize feature values
df_values = features.iloc[:, 2:].values
pt = preprocessing.PowerTransformer(method="yeo-johnson", standardize=True)
df_values_log = pt.fit_transform(df_values)
features.iloc[:, 2:] = df_values_log
features.index = features.index.map(str)
features.drop(columns=['user_id'], inplace=True)
view raw preprocess.py hosted with ❤ by GitHub

Отсюда мы можем начать подготовку набора данных для прогнозирования с помощью GCN. Во-первых, мы выбираем всех тех пользователей, которые были помечены как normal и hate (всего 4971). Затем можно прочитать ребра и выбрать подграф с индексами узлов, которые были отфильтрованы, и, наконец, мы можем создать список смежности.

import scipy.sparse as sp
# select all the users which have been labelled !=2
annotated_users = features[features['hate']!=2]
# these are the nodes features
annotated_users_features = annotated_users.drop(columns=['hate'])
# these are the targets
annotated_users_targets = torch.LongTensor(annotated_users['hate'].to_list())
# now read the edges
edges_nx = nx.read_edgelist("users.edges")
# and select the subgraph we need for all the users!=2
idxs = annotated_users.index.tolist()
edges_needed = edges_nx.subgraph(idxs)
# create a sparse adjacency matrix and add the self term
adj = sp.coo_matrix(nx.adjacency_matrix(edges_needed))
adj = adj + adj.T.multiply(adj.T > adj) - adj.multiply(adj.T > adj)
view raw create_adj.py hosted with ❤ by GitHub

Матрица смежности — это разреженный тип, созданный с помощью scipy.sparse.coo_martrix, как мы видели в коде Кипфа.

Последним шагом является подготовка матрицы смежности и функций для загрузки GCN. В этом случае мы собираемся нормализовать значения признаков, так как свойства смежности уже нормализованы, и мы собираемся определить некоторые выборочные показатели для обучения (0–1000), оценки (1000–1200) и тестирования ( 1200–1400).

def normalize(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 sparse_mx_to_torch_sparse_tensor(sparse_mx):
"""Convert a scipy sparse matrix to a torch sparse tensor."""
sparse_mx = sparse_mx.tocoo().astype(np.float32)
indices = torch.from_numpy(
np.vstack((sparse_mx.row, sparse_mx.col)).astype(np.int64))
values = torch.from_numpy(sparse_mx.data)
shape = torch.Size(sparse_mx.shape)
return torch.sparse.FloatTensor(indices, values, shape)
annotated_users_features = normalize(annotated_users_features)
final_features = torch.FloatTensor(np.array(annotated_users_features))
adj = sparse_mx_to_torch_sparse_tensor(adj)
idx_train = np.arange(0,1000)
idx_val = np.arange(1000, 1200)
idx_test = np.arange(1200, 1400)

Тренируйте GCN на ненавистном наборе данных Twitter

Последний шаг точно следует коду Кипфа. Мы собираемся назвать модель GCN и оптимизатор Адама. Оттуда мы запускаем программу обучения для заданного количества эпох и будем измерять точность.

import time
import numpy as np
import torch
import torch.nn.functional as F
import torch.optim as optim
def accuracy(output, labels):
preds = output.max(1)[1].type_as(labels)
correct = preds.eq(labels).double()
correct = correct.sum()
return correct / len(labels)
def train(model,\
optimizer,\
epoch,\
features,\
adj,\
labels,
idx_train,\
idx_val):
model.train()
optimizer.zero_grad()
output = model(features, adj)
loss_train = F.nll_loss(output[idx_train], labels[idx_train])
acc_train = accuracy(output[idx_train], labels[idx_train])
loss_train.backward()
optimizer.step()
loss_val = F.nll_loss(output[idx_val], labels[idx_val])
acc_val = accuracy(output[idx_val], labels[idx_val])
print('Epoch: {:04d}'.format(epoch+1),
'loss_train: {:.4f}'.format(loss_train.item()),
'acc_train: {:.4f}'.format(acc_train.item()),
'loss_val: {:.4f}'.format(loss_val.item()),
'acc_val: {:.4f}'.format(acc_val.item()))
model = GCN(nfeat = final_features.shape[1],\
nhid = 16,\
nclass = 2,\
dropout = 0.0)
optimizer = optim.Adam(model.parameters(),\
lr= 0.01,\
weight_decay=5e-4)
for epoch in range(50):
train(model,\
optimizer,\
epoch,\
final_features,\
adj,\
annotated_users_targets,\
idx_train,\
idx_val)
view raw train_gcn.py hosted with ❤ by GitHub

В этом случае мы определили размер скрытого слоя равным 16, но не стесняйтесь изменять его, а количество выходных классов равно 2 (normal или hate). Мы можем собрать данные о потерях и точности по эпохам и увидеть сходимость после 50 эпох, в то время как точность достигает 0,89 для оценочного набора.

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

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

На сегодня это все :) Оставайтесь с нами для следующего графического приключения!

Пожалуйста, не стесняйтесь присылать мне электронное письмо с вопросами или комментариями по адресу: stefanobosisio1@gmail.com или прямо здесь, в Medium.

Библиография

  1. Сен, Притхвирадж и др. «Коллективная классификация сетевых данных». Журнал AI 29.3 (2008): 93–93.
  2. Рибейро, Маноэль Орта и др. «Характеризация и обнаружение ненавистных пользователей в Твиттере». Двенадцатая международная конференция AAAI в Интернете и социальных сетях. 2018.
  3. Пеннингтон, Джеффри, Ричард Сочер и Кристофер Д. Мэннинг. «Перчатка: глобальные векторы для представления слов». Материалы конференции 2014 года по эмпирическим методам обработки естественного языка (EMNLP). 2014.
  4. МакКаллум, Эндрю Качитес и др. «Автоматизация создания интернет-порталов с помощью машинного обучения». Информационный поиск 3.2 (2000): 127–163.