轉發支持5秒後獲得錦鯉~
這篇文章直接上代碼,對於圖神經網絡的理論還沒有整理完畢,這是第一版的tensorflow2.0 GCN實現。谷歌今年推出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>
閱讀更多 極意 的文章