Tensorflow2.0+ GCN

轉發支持5秒後獲得錦鯉~

這篇文章直接上代碼,對於圖神經網絡的理論還沒有整理完畢,這是第一版的tensorflow2.0 G​CN實現。谷歌今年推出tf2.0後,建議我們還是快速更新代碼,​因為這次API的變化確實很大!另外後續我也會發表pytorch版本的GCN​實例,​敬請關注。後面直播開課後大家可以去聽聽,不求打賞,只求推廣~​

graph.py

<code>import tensorflow as tf
from tensorflow.keras import activations, initializers, constraints
from tensorflow.keras import regularizers
from tensorflow.keras.layers import Layer
import tensorflow.keras.backend as K


class GraphConvolution(Layer):
"""Basic graph convolution layer as in https://arxiv.org/abs/1609.02907"""
def __init__(self, units, support=1,
activation=None,
use_bias=True,
kernel_initializer='glorot_uniform',
bias_initializer='zeros',
kernel_regularizer=None,
bias_regularizer=None,
activity_regularizer=None,
kernel_constraint=None,
bias_constraint=None,
**kwargs):
if 'input_shape' not in kwargs and 'input_dim' in kwargs:
kwargs['input_shape'] = (kwargs.pop('input_dim'),)

super(GraphConvolution, self).__init__(**kwargs)
self.units = units
self.activation = activations.get(activation)
self.use_bias = use_bias
self.kernel_initializer = initializers.get(kernel_initializer)
self.bias_initializer = initializers.get(bias_initializer)
self.kernel_regularizer = regularizers.get(kernel_regularizer)
self.bias_regularizer = regularizers.get(bias_regularizer)
self.activity_regularizer = regularizers.get(activity_regularizer)
self.kernel_constraint = constraints.get(kernel_constraint)
self.bias_constraint = constraints.get(bias_constraint)
self.supports_masking = True
self.support = support
assert support >= 1.0

def compute_output_shape(self, input_shapes):
features_shape = input_shapes[0]
output_shape = (features_shape[0], self.units)
return output_shape # (batch_size, output_dim)

def build(self, input_shapes):
features_shape = input_shapes[0]
assert len(features_shape) == 2
input_dim = features_shape[1]
self.kernel = self.add_weight(shape=(input_dim * self.support,
self.units),
initializer=self.kernel_initializer,
name='kernel',
regularizer=self.kernel_regularizer,
constraint=self.kernel_constraint)
if self.use_bias:
self.bias = self.add_weight(shape=(self.units,),
initializer=self.bias_initializer,
name='bias',
regularizer=self.bias_regularizer,
constraint=self.bias_constraint)
else:
self.bias = None
self.built = True

# core code
def call(self, inputs, mask=None):
features = inputs[0]
basis = inputs[1:] # this is a list
supports = list()
for i in range(self.support):
# A * X
supports.append(K.dot(basis[i], features))
supports = K.concatenate(supports, axis=1)
# A * X * W
output = K.dot(supports, self.kernel)
if tf.is_tensor(self.bias) :
output += self.bias
return self.activation(output)

def get_config(self):
config = {'units': self.units,
'support': self.support,
'activation': activations.serialize(self.activation),
'use_bias': self.use_bias,
'kernel_initializer': initializers.serialize(
self.kernel_initializer),
'bias_initializer': initializers.serialize(
self.bias_initializer),
'kernel_regularizer': regularizers.serialize(
self.kernel_regularizer),
'bias_regularizer': regularizers.serialize(
self.bias_regularizer),
'activity_regularizer': regularizers.serialize(
self.activity_regularizer),
'kernel_constraint': constraints.serialize(
self.kernel_constraint),
'bias_constraint': constraints.serialize(self.bias_constraint)
}
base_config = super(GraphConvolution, self).get_config()
return dict(list(base_config.items()) + list(config.items()))
/<code>

utils.py

<code>from __future__ import print_function

import scipy.sparse as sp
import numpy as np
from scipy.sparse.linalg.eigen.arpack import eigsh, ArpackNoConvergence


def encode_onehot(labels):
classes = 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)

print('Dataset(adj) has {} nodes, {} edges, {} features.'.format(adj.shape[0], edges.shape[0], features.shape[1]))

return features.todense(), adj, labels


def normalize_adj(adj, symmetric=True):
if symmetric:
d = sp.diags(np.power(np.array(adj.sum(1)), -0.5).flatten(), 0)
a_norm = adj.dot(d).transpose().dot(d).tocsr()
else:
d = sp.diags(np.power(np.array(adj.sum(1)), -1).flatten(), 0)
a_norm = d.dot(adj).tocsr()
return a_norm


def preprocess_adj(adj, symmetric=True):
adj = adj + sp.eye(adj.shape[0])
adj = normalize_adj(adj, symmetric)
return adj


def sample_mask(idx, l):
mask = np.zeros(l)
mask[idx] = 1
return np.array(mask, dtype=np.bool)


def get_splits(y):
idx_train = range(140)
idx_val = range(200, 500)
idx_test = range(500, 1500)
y_train = np.zeros(y.shape, dtype=np.int32)
y_val = np.zeros(y.shape, dtype=np.int32)
y_test = np.zeros(y.shape, dtype=np.int32)
y_train[idx_train] = y[idx_train]
y_val[idx_val] = y[idx_val]
y_test[idx_test] = y[idx_test]
train_mask = sample_mask(idx_train, y.shape[0])
return y_train, y_val, y_test, idx_train, idx_val, idx_test, train_mask


def categorical_crossentropy(preds, labels):
return np.mean(-np.log(np.extract(labels, preds)))


def accuracy(preds, labels):
return np.mean(np.equal(np.argmax(labels, 1), np.argmax(preds, 1)))


def evaluate_preds(preds, labels, indices):

split_loss = list()
split_acc = list()

for y_split, idx_split in zip(labels, indices):
split_loss.append(categorical_crossentropy(preds[idx_split], y_split[idx_split]))
split_acc.append(accuracy(preds[idx_split], y_split[idx_split]))

return split_loss, split_acc


def normalized_laplacian(adj, symmetric=True):
adj_normalized = normalize_adj(adj, symmetric)
laplacian = sp.eye(adj.shape[0]) - adj_normalized
return laplacian


def rescale_laplacian(laplacian):
try:
print('Calculating largest eigenvalue of normalized graph Laplacian...')
largest_eigval = eigsh(laplacian, 1, which='LM', return_eigenvectors=False)[0]
except ArpackNoConvergence:
print('Eigenvalue calculation did not converge! Using largest_eigval=2 instead.')
largest_eigval = 2

scaled_laplacian = (2. / largest_eigval) * laplacian - sp.eye(laplacian.shape[0])
return scaled_laplacian


def chebyshev_polynomial(X, k):
"""Calculate Chebyshev polynomials up to order k. Return a list of sparse matrices."""
print("Calculating Chebyshev polynomials up to order {}...".format(k))

T_k = list()
T_k.append(sp.eye(X.shape[0]).tocsr())
T_k.append(X)

def chebyshev_recurrence(T_k_minus_one, T_k_minus_two, X):
X_ = sp.csr_matrix(X, copy=True)
return 2 * X_.dot(T_k_minus_one) - T_k_minus_two

for i in range(2, k+1):
T_k.append(chebyshev_recurrence(T_k[-1], T_k[-2], X))

return T_k


def sparse_to_tuple(sparse_mx):
if not sp.isspmatrix_coo(sparse_mx):
sparse_mx = sparse_mx.tocoo()
coords = np.vstack((sparse_mx.row, sparse_mx.col)).transpose()
values = sparse_mx.data
shape = sparse_mx.shape
return coords, values, shape
/<code>

train.py

<code>import tensorflow as tf
from kegra.utils import *
from tensorflow.keras.models import Model
from tensorflow.keras.layers import Input
from tensorflow.keras.layers import Dense
from tensorflow.keras.layers import Flatten
from tensorflow.keras.layers import Dropout
from tensorflow.keras.optimizers import Adam
from tensorflow.keras.regularizers import l2
from kegra.layers.graph import GraphConvolution


class Config(object):
dataset = 'cora'
filter = 'localpool' # Local pooling filters (see 'renormalization trick' in Kipf & Welling, arXiv 2016)
# filter = 'chebyshev' # Chebyshev polynomial basis filters (Defferard et al., NIPS 2016)
max_degree = 2 # maximum polynomial degree
sym_norm = True # symmetric (True) vs. left-only (False) normalization
NB_EPOCH = 20
PATIENCE = 10 # early stopping patience
support = 1
epochs = 100


def convert_sparse_matrix_to_sparse_tensor(x):
coo = x.tocoo()
indices = np.mat([coo.row, coo.col]).transpose()
return tf.SparseTensor(indices, coo.data, coo.shape)


def get_inputs(adj, x):
if Config.filter == 'localpool':
print('Using local pooling filters...')
adj_ = preprocess_adj(adj, Config.sym_norm)
adj_ = adj_.todense()
graph = [x, adj_]
adj_input = [Input(batch_shape=(None, None), sparse=False, name='adj_input')]
elif Config.filter == 'chebyshev':
print('Using Chebyshev polynomial basis filters...')
L = normalized_laplacian(adj, Config.sym_norm)
L_scaled = rescale_laplacian(L)
T_k = chebyshev_polynomial(L_scaled, Config.max_degree)
support = Config.max_degree + 1
graph = [x] + T_k
adj_input = [Input(batch_shape=(None, None), sparse=False, name='adj_input') for _ in range(support)]
else:
raise Exception('Invalid filter type.')
return graph, adj_input


def build_model(x, y, adj_input):
fea_input = Input(batch_shape=(None, x.shape[1]), name='fea_input')
net = Dropout(0.2)(fea_input)
net = GraphConvolution(512, Config.support, activation='relu', kernel_regularizer=l2(5e-4))([net] + adj_input)
net = Dropout(0.2)(net)
net = GraphConvolution(256, Config.support, activation='relu', kernel_regularizer=l2(5e-4))([net] + adj_input)
net = Dropout(0.2)(net)
net = GraphConvolution(128, Config.support, activation='relu', kernel_regularizer=l2(5e-4))([net] + adj_input)
net = Dropout(0.2)(net)
net = GraphConvolution(64, Config.support, activation='relu', kernel_regularizer=l2(5e-4))([net] + adj_input)
net = Dropout(0.2)(net)
net = Flatten()(net)
output = Dense(y.shape[1], activation='softmax')(net)
# output = GraphConvolution(y.shape[1], Config.support, activation='softmax')([net] + adj_input)
model = Model(inputs=[fea_input] + adj_input, outputs=output)
model.compile(loss='categorical_crossentropy', optimizer=Adam(lr=0.01))
return model


def train_model(x, y, model, train_mask, y_train, y_val, idx_train, idx_val, batch_size):
for i in range(Config.epochs):
model.fit(x, y, sample_weight=train_mask, batch_size=batch_size, epochs=1, shuffle=False, verbose=1)
y_pred = model.predict(x, batch_size=batch_size)
train_val_loss, train_val_acc = evaluate_preds(y_pred, [y_train, y_val], [idx_train, idx_val])
print("train_loss= {:.2f}".format(train_val_loss[0]), "train_acc= {:.2f}".format(train_val_acc[0]),
"val_loss= {:.2f}".format(train_val_loss[1]), "val_acc= {:.2f}".format(train_val_acc[1]))
return model


def estimate_model(model, x, y_test, idx_test, batch_size):
y_pred = model.predict(x, batch_size=batch_size)
test_loss, test_acc = evaluate_preds(y_pred, [y_test], [idx_test])
print("Test set results:", "loss= {:.2f}".format(test_loss[0]), "accuracy= {:.4f}".format(test_acc[0]))


def main():
x, adj, y = load_data(dataset=Config.dataset)
batch_size = adj.shape[1]
x /= x.sum(1).reshape(-1, 1) # Normalize X
y_train, y_val, y_test, idx_train, idx_val, idx_test, train_mask = get_splits(y)
x_graph, adj_input = get_inputs(adj, x)
model = build_model(x, y, adj_input)
model = train_model(x_graph, y, model, train_mask, y_train, y_val, idx_train, idx_val, batch_size)
estimate_model(model, x_graph, y_test, idx_test, batch_size)


if __name__ == '__main__':
main()/<code>


分享到:


相關文章: