def Loop( self, g: Union[GraphObject, GraphTensor], *, training: bool = False ) -> tuple[list[tf.Tensor], tf.Tensor, list[tf.Tensor]]: """ Process a single GraphObject/GraphTensor element g, returning iteration, states and output """ # transform GraphObject in GraphTensor if isinstance(g, GraphObject): g = GraphTensor.fromGraphObject(g) # copy graph, to preserve original one by the state/output integrating process gtmp = g.copy() # forward pass K, outs = list(), list() for idx, gnn in enumerate(self.gnns[:-1]): if isinstance(gnn, GNNgraphBased): k, state, out = super(GNNgraphBased, gnn).Loop(gtmp, training=training) outs.append(tf.matmul(gtmp.NodeGraph, out, transpose_a=True)) else: k, state, out = gnn.Loop(gtmp, training=training) outs.append(out) K.append(k) # update graph with state and output of the current GNN layer, to feed next GNN layer gtmp = self.update_graph(g, state, out) k, state, out = self.gnns[-1].Loop(gtmp, training=training) return K + [k], state, outs + [out]
def Loop(self, g: Union[GraphObject, GraphTensor], *, training: bool = False) -> tuple[int, tf.Tensor, tf.Tensor]: """ Process a single GraphObject/GraphTensor element g, returning iteration, states and output """ # transform GraphObject in GraphTensor if isinstance(g, GraphObject): g = GraphTensor.fromGraphObject(g) # initialize states and iters for convergence loop # including aggregated neighbors' label and aggregated incoming arcs' label aggregated_arcs = tf.sparse.sparse_dense_matmul(g.ArcNode, g.arcs[:, 2:]) aggregated_nodes = tf.zeros(shape=(g.nodes.shape[0], 0), dtype='float32') if self.state_vect_dim > 0: state = tf.random.normal((g.nodes.shape[0], self.state_vect_dim), stddev=0.1, dtype='float32') aggregated_nodes = tf.concat([aggregated_nodes, tf.sparse.sparse_dense_matmul(g.Adjacency, g.nodes)], axis=1) else: state = tf.constant(g.nodes, dtype='float32') state_old = tf.ones_like(state, dtype='float32') k = tf.constant(0, dtype='float32') training = tf.constant(training, dtype=bool) # loop until convergence is reached k, state, state_old, *_ = tf.while_loop(self.condition, self.convergence, [k, state, state_old, g.nodes, g.Adjacency, aggregated_nodes, aggregated_arcs, training]) # out_st is the converged state for the filtered nodes, depending on g.set_mask mask = tf.logical_and(g.set_mask, g.output_mask) input_to_net_output = self.apply_filters(state, g.nodes, g.Adjacency, g.arcs[:, 2:], mask) # compute the output of the gnn network out = self.net_output(input_to_net_output, training=training) return k, state, out
def evaluate_single_graph(self, g: Union[GraphObject, GraphTensor], training: bool) -> tuple: """ Evaluate single GraphObject/GraphTensor element g in test mode (training == False) :param g: (GraphObject/GraphTensor) single GraphObject/GraphTensor element :param training: (bool) set internal models behavior, s.t. they work in training or testing mode :return: (tuple) convergence iteration (int), loss value (matrix), target and output (matrices) of the model """ # transform GraphObject in GraphTensor if isinstance(g, GraphObject): g = GraphTensor.fromGraphObject(g) # get targets targs = self.GNNS_TYPE.get_filtered_tensor(g, g.targets) loss_weights = self.GNNS_TYPE.get_filtered_tensor(g, g.sample_weights) # graph processing it, _, out = self.Loop(g, training=training) # if class_metrics != 1, else it does not modify loss values if training and self.training_mode == 'residual': loss = self.loss_function(targs, tf.reduce_mean(out, axis=0), ** self.loss_args) * loss_weights else: loss = tf.reduce_mean([ self.loss_function(targs, o, **self.loss_args) * loss_weights for o in out ], axis=0) return it, tf.reduce_sum(loss), targs, out[-1]
def predict( self, g: Union[GraphObject, GraphTensor], idx: Union[int, list[int], range, str] = -1 ) -> Union[tf.Tensor, list[tf.Tensor]]: """ Get LGNN one or more output(s) in test mode (training == False) for graph g of type GraphObject/GraphTensor :param g: (GraphObject/GraphTensor) single GraphObject/GraphTensor element :param idx: set the layer whose output is wanted to be returned. More than one layer output can be returned, setting idx as ordered list/range :return: a list of output(s) of the model processing graph g """ all_LAYERS = range(self.LAYERS) # check idx type and in case re-order idx list if isinstance(idx, int): assert idx in all_LAYERS elif isinstance(idx, (list, range)): assert all(i in all_LAYERS for i in idx) idx = sorted(idx) elif idx == 'all': idx = all_LAYERS else: raise ValueError( 'param <idx> must be 1.int; 2.list of ordered ints in range(self.LAYERS); 3. str "all"' ) # transform GraphObject in GraphTensor if isinstance(g, GraphObject): g = GraphTensor.fromGraphObject(g) # get only outputs, without iteration and states out = self.Loop(g, training=False)[-1] return out[idx].numpy() if isinstance( idx, int) else [out[i].numpy() for i in idx]
def checktype( elem: Optional[Union[GraphObject, GraphTensor, list[GraphObject, GraphTensor]]] ) -> list[GraphTensor]: """ check if type(elem) is correct. If so, return None or a list of GraphObjects/GraphTensor """ if elem is None: pass elif isinstance(elem, GraphTensor): elem = [elem] elif isinstance(elem, GraphObject): elem = [GraphTensor.fromGraphObject(elem)] elif isinstance(elem, (list, tuple)) and all( isinstance(g, (GraphObject, GraphTensor)) for g in elem): elem = [ GraphTensor.fromGraphObject(g) if isinstance(g, GraphObject) else g for g in elem ] else: raise TypeError( 'Error - <gTr> and/or <gVa> are not GraphObject/GraphTensor or LIST/TUPLE of GraphObjects/GraphTensors' ) return elem
def __init__(self, graph: GraphObject, problem_based: str, batch_size: int = 32, shuffle: bool = True): """ Initialization """ self.data = graph self.graph_tensor = GraphTensor.fromGraphObject(graph) self.problem_based = problem_based self.batch_size = batch_size self.shuffle = shuffle self.dtype = tf.keras.backend.floatx() self.gen_set_mask_idx = np.argwhere(self.data.set_mask).squeeze() self.on_epoch_end()
def Loop(self, g: Union[GraphObject, GraphTensor], *, training: bool = False) -> tuple[int, tf.Tensor, tf.Tensor]: """ Process a single graph, returning iteration, states and output. Output of graph-based problem is the averaged nodes output """ # check compatibility between graph g and GNN type if g.NodeGraph is None: raise ValueError('WRONG GNN. NodeGraph is None: GNN is graph-based, while problem is non graph-based.') # transform GraphObject in GraphTensor if isinstance(g, GraphObject): g = GraphTensor.fromGraphObject(g) # get iter, states and output of every nodes from GNNnodeBased iter, state_nodes, out_nodes = super().Loop(g, training=training) # obtain a single output for each graph, by using nodegraph matrix to the output of all of its nodes nodegraph = tf.constant(g.NodeGraph, dtype='float32') out_gnn = tf.matmul(nodegraph, out_nodes, transpose_a=True) return iter, state_nodes, out_gnn
def on_epoch_end(self): """ Updates indexes after each epoch """ if self.shuffle: np.random.shuffle(self.data) graphs = [GraphObject.merge(self.data[i * self.batch_size: (i + 1) * self.batch_size], problem_based=self.problem_based, aggregation_mode=self.aggregation_mode) for i in range(len(self))] self.graph_tensors = [GraphTensor.fromGraphObject(g) for g in graphs]
def prepare_LKO_data(dataset: Union[GraphObject, list[GraphObject], list[list[GraphObject]]], problem_based: str, number_of_batches: int = 10, useVa: bool = False, seed: Optional[float] = None, normalize_method: str = 'gTr', aggregation_mode: str = 'average') \ -> tuple[Union[list[GraphTensor], list[list[GraphTensor]]], list[GraphTensor], Optional[list[GraphTensor]]]: """ Prepare dataset for Leave K-Out procedure. The output of this function must be passed as arg[0] of model.LKO() method. :param dataset: a single GraphObject OR a list of GraphObject OR list of lists of GraphObject on which <gnn> has to be valuated > NOTE: for graph-based problem, if type(dataset) == list of GraphObject, s.t. len(dataset) == number of graphs in the dataset, then i-th class will may be have different frequencies among batches [so the i-th class may me more present in a batch and absent in another batch]. Otherwise, if type(dataset) == list of lists, s.t. len(dataset) == number of classes AND len(dataset[i]) == number of graphs belonging to i-th class, then i-th class will have the same frequency among all the batches [so the i-th class will be as frequent in a single batch as in the entire dataset]. :param problem_based: (str) for specifying the problem ['n'-node based, 'a'-arc based or 'g'-graph based] :param number_of_batches: (int) define how many batches will be considered in LKO procedure. :param useVa: (bool) if True, Early Stopping is considered during learning procedure; None otherwise. :param seed: (int or None) for fixed-shuffle options. :param normalize_method: (str) in ['','gTr,'all'], see normalize_graphs for details. If equal to '', no normalization is performed. :param aggregation_mode: (str) for aggregation method during dataset creation. See GraphObject for details.""" assert number_of_batches > 1 + useVa # Shuffling procedure: set or not seed parameter, then shuffle classes and/or elements in each class/dataset if seed: np.random.seed(seed) # define useful lambda function to be used in any case flatten = lambda l: [item for sublist in l for item in sublist] # define lists for LKO -> output gTRs, gTEs, gVAs = list(), list(), list() # SINGLE GRAPH CASE: batches are obtaind by setting set_masks for training, test and validation (if any) if isinstance(dataset, GraphObject): zero_mask = np.zeros(len(dataset.set_mask), dtype=bool) # normalization procedure - available only on GraphObject if normalize_method: normalize_graphs(dataset, None, None, based_on=normalize_method) # convert GraphObject to GraphTensor dataset = GraphTensor.fromGraphObject(dataset) # only set_mask differs in graphs, since nodes and arcs are exactly the same mask_indicess = np.arange(len(zero_mask)) np.random.shuffle(mask_indicess) masks = np.array_split(mask_indicess, number_of_batches) for i in range(len(masks)): M = masks.copy() # test batch mTe = M.pop(i) maskTe = zero_mask.copy() maskTe[mTe] = True gTe = dataset.copy() gTe.set_mask = tf.constant(maskTe, dtype=bool) # validation batch gVa = None if useVa: mVa = M.pop(-1) maskVa = zero_mask.copy() maskVa[mVa] = True gVa = dataset.copy() gVa.set_mask = tf.constant(maskTe, dtype=bool) # training batch - all the others mTr = flatten(M) maskTr = zero_mask.copy() maskTr[mTr] = True gTr = dataset.copy() gTr.set_mask = tf.constant(maskTe, dtype=bool) # append batch graphs gTRs.append(gTr) gTEs.append(gTe) gVAs.append(gVa) # MULTI GRAPH CASE: dataset is a list of graphs or a list of lists of graphs. :param dataset_ for details elif isinstance(dataset, list): # check type if dataset is a list if all(isinstance(i, GraphObject) for i in dataset): dataset = [dataset] assert all(len(i) > number_of_batches for i in dataset) assert all(isinstance(i, list) for i in dataset) and all(isinstance(j, GraphObject) for i in dataset for j in i) # shuffle entire dataset or classes sub-dataset for i in dataset: np.random.shuffle(i) # get dataset batches and flatten lists to obtain a list of lists, then shuffle again to mix classes inside batches dataset_batches = [getbatches(elem, problem_based, aggregation_mode, -1, number_of_batches, False) for i, elem in enumerate(dataset)] flattened = [flatten([i[j] for i in dataset_batches]) for j in range(number_of_batches)] for i in flattened: np.random.shuffle(i) # Final dataset for LKO procedure: merge graphs belonging to classes/dataset to obtain 1 GraphObject per batch dataset = [GraphObject.merge(i, problem_based=problem_based, aggregation_mode=aggregation_mode) for i in flattened] # Transform all the GraphObjects in GraphTensors # dataset = [GraphTensor.fromGraphObject(g) for g in dataset] # split dataset in training/validation/test set for i in range(len(dataset)): gTr = dataset.copy() gTe = gTr.pop(i) gVa = gTr.pop(-1) if useVa else None # normalization procedure if normalize_method: normalize_graphs(gTr, gTe, gVa, based_on=normalize_method) # append batch graphs # GraphObject->GraphTensor conversion is here because of the normalization procedure which is available only on GraphObjects gTRs.append([GraphTensor.fromGraphObject(g) for g in gTr]) gTEs.append(GraphTensor.fromGraphObject(gTe)) gVAs.append(GraphTensor.fromGraphObject(gVa) if gVa is not None else None) else: raise TypeError('param <dataset> must be a GraphObject, a list of GraphObjects or a list of lists of Graphobjects') return gTRs, gTEs, gVAs