예제 #1
0
    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
예제 #2
0
    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]
예제 #3
0
    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]
예제 #4
0
    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]
예제 #5
0
 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
예제 #6
0
    def update_graph(self, g: GraphTensor, state: Union[tf.Tensor, array],
                     output: Union[tf.Tensor, array]) -> GraphObject:
        """
        :param g: (GraphTensor) single GraphTensor element the update process is based on
        :param state: (tensor) output of the net_state model of a single gnn layer
        :param output: (tensor) output of the net_output model of a single gnn layer
        :return: (GraphTensor) a new GraphTensor where actual state and/or output are integrated in nodes/arcs label
        """
        # copy graph to preserve original graph data
        g = g.copy()

        # define tensors with shape[1]==0 so that it can be concatenate with tf.concat()
        nodeplus = tf.zeros((g.nodes.shape[0], 0), dtype='float32')
        arcplus = tf.zeros((g.arcs.shape[0], 0), dtype='float32')

        # check state
        if self.get_state: nodeplus = tf.concat([nodeplus, state], axis=1)

        # check output
        if self.get_output:
            # process output to make it shape compatible.
            # Note that what is concatenated is not nodeplus/arcplus, but out, as it has the same length of nodes/arcs
            mask = tf.logical_and(g.set_mask, g.output_mask)

            # scatter_nd creates a zeros matrix 'node or arcs-compatible' with the elements of output located in mask==True
            out = tf.scatter_nd(tf.where(mask),
                                output,
                                shape=(len(mask), output.shape[1]))

            if self.GNNS_TYPE == GNNedgeBased:
                arcplus = tf.concat([arcplus, out], axis=1)
            else:
                nodeplus = tf.concat([nodeplus, out], axis=1)

        g.nodes = tf.concat([g.nodes, nodeplus], axis=1)
        g.arcs = tf.concat([g.arcs, arcplus], axis=1)
        return g
예제 #7
0
    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()
예제 #8
0
    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
예제 #9
0
 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]
예제 #10
0
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