text: #!/usr/bin/env python
  # coding: utf-8
  
  # # How to Do Deep Learning on Graphs with Graph Convolutional Networks ## Part 2: Semi-Supervised Learning with
  # Spectral Graph Convolutions This notebook accompanies my Medium article with the above title for readers to try out
  # and explore graph convolutional networks for themselves. You can find the article [here](
  # https://towardsdatascience.com/how-to-do-deep-learning-on-graphs-with-graph-convolutional-networks-62acf5b143d0).
  # To run the notebook, install the packages specified in the accompanying requirements.txt file.
  
  # #  Load Karate Club
  
  from collections import namedtuple
  from networkx import read_edgelist,set_node_attributes
  from pandas import read_csv,Series
  from numpy import array
  
  from logger import logger
  
  DataSet = namedtuple(
      'DataSet',
      field_names=['X_train','y_train','X_test','y_test','network']
  )
  
  
  def load_karate_club():
      network = read_edgelist(
          'karate.edgelist',
          nodetype=int)
  
      attributes = read_csv(
          'karate.attributes.csv',
          index_col=['node'])
  
      for attribute in attributes.columns.values:
          set_node_attributes(
              network,
              values=Series(
                  attributes[attribute],
                  index=attributes.index).to_dict(),
              name=attribute
          )
  
      X_train,y_train = map(array,zip(*[
          ([node],data['role'] == 'Administrator')
          for node,data in network.nodes(data=True)
          if data['role'] in {'Administrator','Instructor'}
      ]))
      X_test,y_test = map(array,zip(*[
          ([node],data['community'] == 'Administrator')
          for node,data in network.nodes(data=True)
          if data['role'] == 'Member'
      ]))
  
      return DataSet(
          X_train,y_train,
          X_test,y_test,
          network)
  
  
  from networkx import to_numpy_matrix,degree_centrality,betweenness_centrality,shortest_path_length
  import mxnet.ndarray as nd
  
  zkc = load_karate_club()
  
  A = to_numpy_matrix(zkc.network)
  A = nd.array(A)
  
  X_train = zkc.X_train
  y_train = zkc.y_train
  X_test = zkc.X_test
  y_test = zkc.y_test
  
  # # Layer Implementations
  from mxnet.gluon import HybridBlock
  from mxnet.gluon.nn import Activation
  import mxnet.ndarray as nd
  
  
  class SpectralRule(HybridBlock):
      def __init__(self,A,in_units,out_units,activation='relu',**kwargs):
          super().__init__(**kwargs)
          I = nd.eye(*A.shape)
          A_hat = A.copy() + I
  
          D = nd.sum(A_hat,axis=0)
          D_inv = D ** -0.5
          D_inv = nd.diag(D_inv)
  
          A_hat = D_inv * A_hat * D_inv
  
          self.in_units,self.out_units = in_units,out_units
  
          with self.name_scope():
              self.A_hat = self.params.get_constant('A_hat',A_hat)
              self.W = self.params.get(
                  'W',shape=(self.in_units,self.out_units)
              )
              if activation == 'identity':
                  self.activation = lambda X:X
              else:
                  self.activation = Activation(activation)
  
      def hybrid_forward(self,F,X,A_hat,W):
          # print(A_hat[0].asnumpy())
          aggregate = F.dot(A_hat,X)
          propagate = self.activation(
              F.dot(aggregate,W))
          return propagate
  
  
  class LogisticRegressor(HybridBlock):
      def __init__(self,in_units,**kwargs):
          super().__init__(**kwargs)
          with self.name_scope():
              self.w = self.params.get(
                  'w',shape=(1,in_units)
              )
  
              self.b = self.params.get(
                  'b',shape=(1,1)
              )
  
      def hybrid_forward(self,F,X,w,b):
          # Change shape of b to comply with MXnet addition API
          b = F.broadcast_axis(b,axis=(0,1),size=(34,1))
          y = F.dot(X,w,transpose_b=True) + b
  
          return F.sigmoid(y)
  
  
  # # Models
  
  
  from mxnet.gluon.nn import HybridSequential,Activation
  from mxnet.ndarray import array
  from mxnet.initializer import Uniform
  from mxnet.gluon.loss import SigmoidBinaryCrossEntropyLoss
  
  
  def build_features(A,X):
      hidden_layer_specs = [(4,'tanh'),(2,'tanh')]  # Format: (units in layer, activation function)
      in_units = in_units = X.shape[1]
  
      features = HybridSequential()
      with features.name_scope():
          for i,(layer_size,activation_func) in enumerate(hidden_layer_specs):
              layer = SpectralRule(
                  A,in_units=in_units,out_units=layer_size,
                  activation=activation_func)
              features.add(layer)
  
              in_units = layer_size
      return features,in_units
  
  
  def build_model(A,X):
      model = HybridSequential()
  
      with model.name_scope():
          features,out_units = build_features(A,X)
          model.add(features)
          logger.info("GCN Summary: \n{}".format(model))
  
          classifier = LogisticRegressor(out_units)
          model.add(classifier)
          logger.info("GCN + LR Summary: \n{}".format(model))
  
      model.hybridize()
      model.initialize(Uniform(1))
  
      return model,features
  
  
  # ## Model 1: Identity Matrix as Features
  
  
  X_1 = I = nd.eye(*A.shape)
  model_1,features_1 = build_model(A,X_1)
  model_1(X_1)
  
  # ## Model 2: Distance to Administrator and Instructor as Additional Features
  
  
  X_2 = nd.zeros((A.shape[0],2))
  node_distance_instructor = shortest_path_length(zkc.network,target=33)
  node_distance_administrator = shortest_path_length(zkc.network,target=0)
  
  for node in zkc.network.nodes():
      X_2[node][0] = node_distance_administrator[node]
      X_2[node][1] = node_distance_instructor[node]
  
  
  X_2 = nd.concat(X_1,X_2)
  model_2,features_2 = build_model(A,X_2)
  model_2(X_2)
  
  # # Train and Test Models
  # get_ipython().run_line_magic('time','')
  from mxnet import autograd
  from mxnet.gluon import Trainer
  from mxnet.ndarray import sum as ndsum
  import numpy as np
  
  
  def train(model,features,X,X_train,y_train,epochs):
      cross_entropy = SigmoidBinaryCrossEntropyLoss(from_sigmoid=True)
      trainer = Trainer(model.collect_params(),'sgd',{'learning_rate':0.001,'momentum':1})
  
      feature_representations = [features(X).asnumpy()]
  
      for e in range(1,epochs + 1):
          cum_loss = 0
          cum_preds = []
  
          for i,x in enumerate(X_train.flatten()):
              y = array(y_train)[i]
              with autograd.record():
                  preds = model(X)[x]
                  loss = cross_entropy(preds,y)
                  # logger.debug("x:[{}], y:[{}], pred:[{}], loss:[{}]".format(x,y,preds,loss.asscalar()))
              loss.backward()
              trainer.step(1)
  
              cum_loss += loss.asscalar()
              cum_preds += [preds.asscalar()]
  
          feature_representations.append(features(X).asnumpy())
  
          if (e % (epochs // 10)) == 0:
              logger.debug(f"Epoch {e}/{epochs} -- Loss: {cum_loss: f}")
              logger.debug(cum_preds)
      return feature_representations
  
  
  def predict(model,X,nodes):
      preds = model(X)[nodes].asnumpy().flatten()
      return np.where(preds >= 0.5,1,0)
  
  
  # ## Performance of Model 1
  
  # In[ ]:
  
  
  from sklearn.metrics import classification_report
  
  feature_representations_1 = train(model_1,features_1,X_1,X_train,y_train,epochs=5000)
  y_pred_1 = predict(model_1,X_1,X_test)
  logger.debug(classification_report(y_test,y_pred_1))
  
  # ## Performance of Model 2
  
  # In[ ]:
  
  
  feature_representations_2 = train(model_2,features_2,X_2,X_train,y_train,epochs=250)
  y_pred_2 = predict(model_2,X_2,X_test)
  logger.debug(classification_report(y_test,y_pred_2))
  