├── Code ├── cgnn │ ├── utils │ │ ├── __init__.py │ │ ├── Settings.py │ │ ├── Formats.py │ │ ├── Loss.py │ │ └── Graph.py │ ├── generators │ │ ├── __init__.py │ │ ├── functions_default.py │ │ ├── random_graph_generator.py │ │ └── generators.py │ ├── __init__.py │ ├── GraphModel.py │ ├── PairwiseModel.py │ ├── GNN.py │ ├── CGNN.py │ └── CGNN_confounders.py ├── requirements.txt ├── setup.py ├── generator │ └── ce_multi_generator.lua └── LICENSE.md ├── Example_pairwise_targets.csv ├── Example_graph_confounders_target.csv ├── Example_graph_confounders_skeleton.csv ├── Example_graph_target.csv ├── Example_graph_skeleton.csv ├── .gitignore ├── run_GNN_pairwise_inference.py ├── run_CGNN_graph.py ├── run_CGNN_graph_hidden_variables.py └── README.md /Code/cgnn/utils/__init__.py: -------------------------------------------------------------------------------- 1 | from .Formats import CCEPC_PairsFileReader 2 | -------------------------------------------------------------------------------- /Code/cgnn/generators/__init__.py: -------------------------------------------------------------------------------- 1 | from .random_graph_generator import RandomGraphGenerator 2 | 3 | -------------------------------------------------------------------------------- /Code/requirements.txt: -------------------------------------------------------------------------------- 1 | numpy 2 | scipy 3 | scikit-learn 4 | tensorflow 5 | joblib 6 | pandas 7 | -------------------------------------------------------------------------------- /Example_pairwise_targets.csv: -------------------------------------------------------------------------------- 1 | SampleID,Target 2 | pair1,1.0 3 | pair2,1.0 4 | pair3,-1.0 5 | pair4,-1.0 6 | pair5,1.0 7 | -------------------------------------------------------------------------------- /Example_graph_confounders_target.csv: -------------------------------------------------------------------------------- 1 | Cause,Effect 2 | V0,V2 3 | V0,V4 4 | V2,V5 5 | V0,V7 6 | V1,V7 7 | V0,V9 8 | V1,V10 9 | V0,V11 10 | V5,V12 11 | V2,V13 12 | V2,V14 13 | V0,V15 14 | V11,V15 15 | V10,V16 16 | V14,V16 17 | V1,V17 18 | V7,V18 19 | V11,V19 20 | V9,V20 21 | V5,V20 22 | V15,V21 23 | -------------------------------------------------------------------------------- /Example_graph_confounders_skeleton.csv: -------------------------------------------------------------------------------- 1 | Cause,Effect 2 | V11,V9 3 | V12,V14 4 | V17,V19 5 | V0,V2 6 | V0,V4 7 | V2,V5 8 | V0,V7 9 | V1,V7 10 | V0,V9 11 | V1,V10 12 | V0,V11 13 | V5,V12 14 | V2,V13 15 | V2,V14 16 | V0,V15 17 | V11,V15 18 | V10,V16 19 | V14,V16 20 | V1,V17 21 | V7,V18 22 | V11,V19 23 | V9,V20 24 | V5,V20 25 | V15,V21 26 | -------------------------------------------------------------------------------- /Example_graph_target.csv: -------------------------------------------------------------------------------- 1 | Node1,Node2 2 | V0,V2 3 | V0,V3 4 | V0,V4 5 | V2,V5 6 | V3,V6 7 | V0,V7 8 | V1,V7 9 | V0,V8 10 | V0,V9 11 | V3,V9 12 | V1,V10 13 | V0,V11 14 | V3,V11 15 | V5,V12 16 | V6,V12 17 | V2,V13 18 | V2,V14 19 | V6,V14 20 | V0,V15 21 | V11,V15 22 | V10,V16 23 | V14,V16 24 | V8,V17 25 | V1,V17 26 | V7,V18 27 | V8,V19 28 | V11,V19 29 | V9,V20 30 | V5,V20 31 | V15,V21 32 | -------------------------------------------------------------------------------- /Code/cgnn/__init__.py: -------------------------------------------------------------------------------- 1 | import tensorflow as tf 2 | from .utils.Graph import DirectedGraph, UndirectedGraph 3 | from .CGNN import CGNN 4 | from .CGNN_confounders import CGNN_confounders 5 | from .GNN import GNN 6 | from .generators import __init__ 7 | from .utils import Loss 8 | from .utils.Settings import SETTINGS 9 | 10 | 11 | __all__ = ['DirectedGraph', 'UndirectedGraph', 'CGNN', 'CGNN_confounders', 'GNN'] 12 | 13 | -------------------------------------------------------------------------------- /Example_graph_skeleton.csv: -------------------------------------------------------------------------------- 1 | Node1,Node2 2 | V0,V2 3 | V0,V3 4 | V0,V4 5 | V2,V5 6 | V3,V6 7 | V0,V7 8 | V1,V7 9 | V0,V8 10 | V0,V9 11 | V3,V9 12 | V1,V10 13 | V0,V11 14 | V3,V11 15 | V5,V12 16 | V6,V12 17 | V2,V13 18 | V2,V14 19 | V6,V14 20 | V0,V15 21 | V11,V15 22 | V10,V16 23 | V14,V16 24 | V8,V17 25 | V1,V17 26 | V7,V18 27 | V8,V19 28 | V11,V19 29 | V9,V20 30 | V5,V20 31 | V15,V21 32 | -------------------------------------------------------------------------------- /Code/setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright (C) 2016 Olivier Goudet 3 | # Licence: Apache 2.0 4 | 5 | try: 6 | from setuptools import setup 7 | except ImportError: 8 | from distutils.core import setup 9 | 10 | 11 | def setup_package(): 12 | setup(name='cgnn', 13 | version='1.0', 14 | description='Causal Generative Neural Networks', 15 | url='https://github.com/GoudetOlivier/CGNN', 16 | author='Olivier Goudet', 17 | author_email='olivier.goudet@lri.fr', 18 | license='Apache 2.0', 19 | packages=['cgnn']) 20 | 21 | 22 | if __name__ == '__main__': 23 | setup_package() 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | *.xml 6 | *~ 7 | .idea 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | env/ 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | wheels/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *,cover 50 | .hypothesis/ 51 | 52 | -------------------------------------------------------------------------------- /run_GNN_pairwise_inference.py: -------------------------------------------------------------------------------- 1 | import cgnn 2 | from cgnn.utils import CCEPC_PairsFileReader as CC 3 | import pandas as pd 4 | 5 | # Params 6 | cgnn.SETTINGS.GPU = True 7 | cgnn.SETTINGS.NB_GPU = 2 8 | cgnn.SETTINGS.NB_JOBS = 8 9 | cgnn.SETTINGS.h_layer_dim = 30 10 | 11 | #Setting for CGNN-Fourier 12 | #cgnn.SETTINGS.use_Fast_MMD = True 13 | #cgnn.SETTINGS.NB_RUNS = 64 14 | 15 | #Setting for CGNN-MMD 16 | cgnn.SETTINGS.use_Fast_MMD = False 17 | cgnn.SETTINGS.NB_RUNS = 32 18 | 19 | datafile = "Example_pairwise_pairs.csv" 20 | 21 | print("Processing " + datafile + "...") 22 | data = CC(datafile, scale=True) 23 | model = cgnn.GNN(backend="TensorFlow") 24 | predictions = model.predict_dataset(data, printout=datafile + '_printout.csv') 25 | predictions = pd.DataFrame(predictions, columns=["Predictions"]) 26 | 27 | print('Processed ' + datafile) 28 | predictions.to_csv(datafile + "_predictions_GNN.csv") 29 | -------------------------------------------------------------------------------- /run_CGNN_graph.py: -------------------------------------------------------------------------------- 1 | import cgnn 2 | import sys 3 | import pandas as pd 4 | 5 | # Params 6 | cgnn.SETTINGS.GPU = True 7 | cgnn.SETTINGS.NB_GPU = 2 8 | cgnn.SETTINGS.NB_JOBS = 8 9 | cgnn.SETTINGS.NB_RUNS = 32 10 | 11 | 12 | datafile = "Example_graph_numdata.csv" 13 | skeletonfile = "Example_graph_skeleton.csv" 14 | 15 | print("Processing " + datafile + "...") 16 | undirected_links = pd.read_csv(skeletonfile) 17 | 18 | umg = cgnn.UndirectedGraph(undirected_links) 19 | data = pd.read_csv(datafile) 20 | 21 | GNN = cgnn.GNN(backend="TensorFlow") 22 | p_directed_graph = GNN.orient_graph(data, umg, printout=datafile + '_printout.csv') 23 | gnn_res = pd.DataFrame(p_directed_graph.get_list_edges(descending=True), columns=['Cause', 'Effect', 'Score']) 24 | gnn_res.to_csv(datafile + "_pairwise_predictions.csv") 25 | 26 | CGNN = cgnn.CGNN(backend="TensorFlow") 27 | directed_graph = CGNN.orient_directed_graph(data, p_directed_graph) 28 | cgnn_res = pd.DataFrame(directed_graph.get_list_edges(descending=True), columns=['Cause', 'Effect', 'Score']) 29 | cgnn_res.to_csv(datafile + "_predictions.csv") 30 | 31 | print('Processed ' + datafile) 32 | -------------------------------------------------------------------------------- /run_CGNN_graph_hidden_variables.py: -------------------------------------------------------------------------------- 1 | import cgnn 2 | import pandas as pd 3 | from sklearn.preprocessing import scale 4 | 5 | # Params 6 | cgnn.SETTINGS.GPU = True 7 | cgnn.SETTINGS.NB_GPU = 2 8 | cgnn.SETTINGS.NB_JOBS = 8 9 | cgnn.SETTINGS.NB_RUNS = 32 10 | 11 | datafile = "Example_graph_confounders_numdata.csv" 12 | skeletonfile = "Example_graph_confounders_skeleton.csv" 13 | 14 | 15 | data = pd.read_csv(datafile) 16 | skeleton_links = pd.read_csv(skeletonfile) 17 | 18 | skeleton = cgnn.UndirectedGraph(skeleton_links) 19 | 20 | data = pd.DataFrame(scale(data),columns=data.columns) 21 | 22 | GNN = cgnn.GNN(backend="TensorFlow") 23 | p_directed_graph = GNN.orient_graph_confounders(data, skeleton, printout= datafile + '_printout.csv') 24 | 25 | gnn_res = pd.DataFrame(p_directed_graph.get_list_edges(descending=True), columns=['Cause', 'Effect', 'Score']) 26 | gnn_res.to_csv(datafile + "_pairwise_predictions.csv") 27 | CGNN_confounders = cgnn.CGNN_confounders(backend="TensorFlow") 28 | directed_graph = CGNN_confounders.orient_directed_graph(data, p_directed_graph) 29 | cgnn_res = pd.DataFrame(directed_graph.get_list_edges(descending=True), columns=['Cause', 'Effect', 'Score']) 30 | 31 | cgnn_res.to_csv(datafile + "_confounders_predictions.csv") 32 | 33 | 34 | -------------------------------------------------------------------------------- /Code/cgnn/utils/Settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Settings file for CGNN algorithm 3 | Defining all global variables 4 | Authors : Anonymous Author 5 | Date : 8/05/2017 6 | """ 7 | 8 | 9 | class DefaultSettings(object): 10 | __slots__ = ("h_layer_dim", 11 | "train_epochs", 12 | "test_epochs", 13 | "NB_RUNS", 14 | "NB_JOBS", 15 | "GPU", 16 | "NB_GPU", 17 | "GPU_OFFSET", 18 | "learning_rate", 19 | "init_weights", 20 | "use_Fast_MMD", 21 | "nb_vectors_approx_MMD", 22 | "complexity_graph_param", 23 | "max_nb_points") 24 | 25 | def __init__(self): # Define here the default values of the parameters 26 | self.NB_RUNS = 32 27 | self.NB_JOBS = 1 28 | self.GPU = True 29 | self.NB_GPU = 1 30 | self.GPU_OFFSET = 0 31 | self.learning_rate = 0.01 32 | self.init_weights = 0.05 33 | self.max_nb_points = 1500 34 | 35 | # CGNN 36 | self.h_layer_dim = 20 37 | self.train_epochs = 1000 38 | self.test_epochs = 500 39 | self.use_Fast_MMD = False 40 | self.nb_vectors_approx_MMD = 100 41 | self.complexity_graph_param = 0.00005 42 | 43 | 44 | 45 | SETTINGS = DefaultSettings() 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tensorflow Implementation of the CGNN 2 | 3 | Code provided to reproduce the results from the article "Learning Functional Causal Models with Generative Neural Networks" 4 | 5 | Requirements: 6 | numpy 7 | scipy 8 | scikit-learn 9 | tensorflow 10 | joblib 11 | pandas 12 | 13 | ### In order to run the CGNN and launch the experiments: 14 | 1) First install the CGNN package. Enter in the code directory. Run the command line "python setup.py install develop --user" 15 | 16 | 2) Launch the example python script for pairwise inference: "python run_GNN_pairwise_inference.py" 17 | 18 | 3) Launch the example python script for graph reconstruction from a skeleton: "python run_CGNN_graph.py" 19 | 20 | 4) Launch the example python script for graph reconstruction in presence of hidden variables: "python run_CGNN_graph_hidden_variables.py" 21 | 22 | 5) The complete datasets used in the article may be found at the following url: 23 | - pairwise datasets : http://dx.doi.org/10.7910/DVN/3757KX 24 | - graph datasets : http://dx.doi.org/10.7910/DVN/UZMB69 25 | 26 | 27 | # Fast Pytorch implementation of CGNN available in the CDT 28 | 29 | A faster implementation of CGNN in pytorch in available in the CausalDiscoveryToolBox (CDT) 30 | 31 | https://github.com/Diviyan-Kalainathan/CausalDiscoveryToolbox 32 | 33 | arXiv paper of the CDT: https://arxiv.org/abs/1903.02278 34 | 35 | -------------------------------------------------------------------------------- /Code/cgnn/utils/Formats.py: -------------------------------------------------------------------------------- 1 | """ 2 | Formatting functions 3 | Author: Diviyan Kalainathan 4 | Date : 2/06/17 5 | 6 | """ 7 | from pandas import DataFrame, read_csv 8 | from numpy import array 9 | from sklearn.preprocessing import scale as scaler 10 | 11 | 12 | def CCEPC_PairsFileReader(filename, scale=True): 13 | """ Converts a ChaLearn Cause effect pairs challenge format into numpy.ndarray 14 | 15 | :param filename: 16 | :type filename: str 17 | :return: Dataframe composed of (SampleID, a (numpy.ndarray) , b (numpy.ndarray)) 18 | :rtype: pandas.DataFrame 19 | """ 20 | 21 | def convert_row(row, scale): 22 | """ Convert a CCEPC row into numpy.ndarrays 23 | 24 | :param row: 25 | :type row: pandas.Series 26 | :return: tuple of sample ID and the converted data into numpy.ndarrays 27 | :rtype: tuple 28 | """ 29 | a = row["A"].split(" ") 30 | b = row["B"].split(" ") 31 | 32 | if a[0] == "": 33 | a.pop(0) 34 | b.pop(0) 35 | 36 | if a[-1] == "": 37 | a.pop(-1) 38 | b.pop(-1) 39 | 40 | a = array([float(i) for i in a]) 41 | b = array([float(i) for i in b]) 42 | if scale: 43 | a = scaler(a) 44 | b = scaler(b) 45 | return row['SampleID'], a, b 46 | 47 | data = read_csv(filename) 48 | conv_data = [] 49 | 50 | for idx, row in data.iterrows(): 51 | conv_data.append(convert_row(row, scale)) 52 | df = DataFrame(conv_data, columns=['SampleID', 'A', 'B']) 53 | return df 54 | 55 | 56 | -------------------------------------------------------------------------------- /Code/cgnn/generators/functions_default.py: -------------------------------------------------------------------------------- 1 | """ 2 | Basic functions for causal generation 3 | Author : David Lopez-Paz, Facebook AI Research, modified by Diviyan Kalainathan 4 | """ 5 | 6 | import numpy as np 7 | from sklearn.preprocessing import scale 8 | from scipy.interpolate import UnivariateSpline as sp 9 | import warnings 10 | import random 11 | 12 | with warnings.catch_warnings(): 13 | warnings.filterwarnings("ignore", category=DeprecationWarning) 14 | from sklearn.mixture import GMM 15 | 16 | 17 | def cause(n, k=4, p1=2, p2=2): 18 | g = GMM(k) 19 | g.means_ = p1 * np.random.randn(k, 1) 20 | g.covars_ = np.power(abs(p2 * np.random.randn(k, 1) + 1), 2) 21 | g.weights_ = abs(np.random.rand(k, 1)) 22 | g.weights_ = g.weights_ / sum(g.weights_) 23 | # return scale(g.sample(n)).flatten() 24 | return np.random.uniform(-1, 1, n) 25 | # return g.sample(n).flatten() 26 | 27 | 28 | def noise(n, v): 29 | return v * np.random.rand(1) * np.random.randn(n, 1) + random.sample([2, -2], 1) 30 | # np.random.randint(-1,1) 31 | 32 | 33 | def mechanism(x, d): 34 | g = np.linspace(min(x) - np.std(x), max(x) + np.std(x), d); 35 | return sp(g, np.random.randn(d))(x.flatten())[:, np.newaxis] 36 | 37 | 38 | def effect(x, n, v, d=4): 39 | y = np.array(x) 40 | # return scale(scale(mechanism(y,d))+noise(n,v)).flatten() 41 | return scale(mechanism(y, d)).flatten() 42 | 43 | 44 | def rand_bin(x): 45 | numCat1 = np.random.randint(2, 20) 46 | maxstd = 3 47 | x = scale(x) 48 | bins = np.linspace(-maxstd, maxstd, num=numCat1 + 1) 49 | x = np.digitize(x, bins) - 1 50 | 51 | return x 52 | -------------------------------------------------------------------------------- /Code/cgnn/GraphModel.py: -------------------------------------------------------------------------------- 1 | """ 2 | Pairwise causal models base class 3 | Author: Diviyan Kalainathan 4 | Date : 7/06/2017 5 | """ 6 | from .utils.Graph import DirectedGraph, UndirectedGraph 7 | 8 | 9 | class GraphModel(object): 10 | """ Base class for all pairwise causal inference models 11 | 12 | Usage for undirected/directed graphs and CEPC df format. 13 | """ 14 | def __init__(self): 15 | """ Init. """ 16 | super(GraphModel, self).__init__() 17 | 18 | def predict(self, df_data, graph=None, **kwargs): 19 | """ Orient an undirected graph using the pairwise method defined by the subclass 20 | Requirement : Name of the nodes in the graph correspond to name of the variables in df_data 21 | 22 | :param df_data: 23 | :param graph: UndirectedGraph or DirectedGraph or None 24 | :return: Directed graph w/ weights 25 | :rtype: DirectedGraph 26 | """ 27 | if graph is None: 28 | return self.create_graph_from_data(df_data, **kwargs) 29 | elif type(graph) == DirectedGraph: 30 | return self.orient_directed_graph(df_data, graph, **kwargs) 31 | elif type(graph) == UndirectedGraph: 32 | return self.orient_undirected_graph(df_data, graph, **kwargs) 33 | else: 34 | print('Unknown Graph type') 35 | raise ValueError 36 | 37 | def orient_undirected_graph(self, data, umg, **kwargs): 38 | 39 | raise NotImplementedError 40 | 41 | def orient_directed_graph(self, data, dag, **kwargs): 42 | 43 | raise NotImplementedError 44 | 45 | def create_graph_from_data(self, data, **kwargs): 46 | 47 | raise NotImplementedError 48 | 49 | 50 | -------------------------------------------------------------------------------- /Code/cgnn/utils/Loss.py: -------------------------------------------------------------------------------- 1 | """ 2 | Implementation of Losses 3 | Author : Diviyan Kalainathan & Olivier Goudet 4 | Date : 09/03/2017 5 | """ 6 | 7 | import tensorflow as tf 8 | import numpy as np 9 | 10 | bandwiths_gamma = [0.005, 0.05, 0.25, 0.5, 1, 5, 50] 11 | 12 | def MMD_loss_tf(xy_true, xy_pred): 13 | 14 | N, _ = xy_pred.get_shape().as_list() 15 | 16 | X = tf.concat([xy_pred, xy_true], 0) 17 | XX = tf.matmul(X, tf.transpose(X)) 18 | X2 = tf.reduce_sum(X * X, 1, keep_dims=True) 19 | exponent = -2*XX + X2 + tf.transpose(X2) 20 | 21 | s1 = tf.constant(1.0 / N, shape=[N, 1]) 22 | s2 = -tf.constant(1.0 / N, shape=[N, 1]) 23 | s = tf.concat([s1, s2], 0) 24 | S = tf.matmul(s, tf.transpose(s)) 25 | 26 | loss = 0 27 | 28 | for i in range(len(bandwiths_gamma)): 29 | kernel_val = tf.exp(-bandwiths_gamma[i] * exponent) 30 | loss += tf.reduce_sum(S * kernel_val) 31 | 32 | return loss 33 | 34 | 35 | def rp(k,s,d): 36 | 37 | return tf.transpose(tf.concat([tf.concat([2*si*tf.random_normal([k,d], mean=0, stddev=1) for si in s], axis = 0), tf.random_uniform([k*len(s),1], minval=0, maxval=2*np.pi)], axis = 1)) 38 | 39 | def f1(x,wz,N): 40 | 41 | ones = tf.ones((N, 1)) 42 | x_ones = tf.concat([x, ones], axis = 1) 43 | mult = tf.matmul(x_ones,wz) 44 | 45 | return tf.cos(mult) 46 | 47 | def Fourier_MMD_Loss_tf(xy_true, xy_pred,nb_vectors_approx_MMD): 48 | 49 | N, nDim = xy_pred.get_shape().as_list() 50 | 51 | wz = rp(nb_vectors_approx_MMD, bandwiths_gamma, nDim) 52 | 53 | e1 = tf.sqrt(2/nb_vectors_approx_MMD)*tf.reduce_mean(f1(xy_true, wz, N), axis=0) 54 | e2 = tf.sqrt(2/nb_vectors_approx_MMD)*tf.reduce_mean(f1(xy_pred, wz, N), axis=0) 55 | 56 | return tf.reduce_sum((e1 - e2) ** 2) 57 | 58 | 59 | 60 | 61 | def MomentMatchingLoss_tf(xy_true, xy_pred, nb_moment = 1): 62 | """ k-moments loss, k being a parameter. These moments are raw moments and not normalized 63 | 64 | """ 65 | loss = 0 66 | for i in range(1, nb_moment): 67 | mean_pred = tf.reduce_mean(xy_pred**i, 0) 68 | mean_true = tf.reduce_mean(xy_true**i, 0) 69 | loss += tf.sqrt(tf.reduce_sum((mean_true - mean_pred)**2)) # L2 70 | 71 | return loss 72 | -------------------------------------------------------------------------------- /Code/cgnn/PairwiseModel.py: -------------------------------------------------------------------------------- 1 | """ 2 | Pairwise causal models base class 3 | Author: Diviyan Kalainathan 4 | Date : 7/06/2017 5 | """ 6 | from .utils.Graph import DirectedGraph 7 | from sklearn.preprocessing import scale 8 | from pandas import DataFrame 9 | 10 | 11 | class Pairwise_Model(object): 12 | """ Base class for all pairwise causal inference models 13 | 14 | Usage for undirected/directed graphs and CEPC df format. 15 | """ 16 | 17 | def __init__(self): 18 | """ Init. """ 19 | super(Pairwise_Model, self).__init__() 20 | 21 | def predict_proba(self, a, b, idx=0): 22 | """ Prediction method for pairwise causal inference. 23 | predict is meant to be overridden in all subclasses 24 | 25 | :param a: Variable 1 26 | :param b: Variable 2 27 | :return: probability (Value : 1 if a->b and -1 if b->a) 28 | :rtype: float 29 | """ 30 | raise NotImplementedError 31 | 32 | def predict_dataset(self, x, printout=None): 33 | """ Causal prediction of a pairwise dataset (x,y) 34 | 35 | :param x: Pairwise dataset 36 | :param printout: print regularly predictions 37 | :type x: cepc_df format 38 | :return: predictions probabilities 39 | :rtype: list 40 | """ 41 | 42 | pred = [] 43 | res = [] 44 | for idx, row in x.iterrows(): 45 | 46 | a = scale(row['A'].reshape((len(row['A']), 1))) 47 | b = scale(row['B'].reshape((len(row['B']), 1))) 48 | 49 | pred.append(self.predict_proba(a, b,idx)) 50 | 51 | if printout is not None: 52 | res.append([row['SampleID'], pred[-1]]) 53 | DataFrame(res, columns=['SampleID', 'Predictions']).to_csv( 54 | printout, index=False) 55 | return pred 56 | 57 | def orient_graph(self, df_data, umg, printout=None): 58 | """ Orient an undirected graph using the pairwise method defined by the subclass 59 | Requirement : Name of the nodes in the graph correspond to name of the variables in df_data 60 | 61 | :param df_data: dataset 62 | :param umg: UndirectedGraph 63 | :param printout: print regularly predictions 64 | :return: Directed graph w/ weights 65 | :rtype: DirectedGraph 66 | """ 67 | 68 | edges = umg.get_list_edges_without_duplicate() 69 | graph = DirectedGraph() 70 | res = [] 71 | idx = 0 72 | 73 | for edge in edges: 74 | a, b = edge 75 | weight = self.predict_proba(scale(df_data[a].as_matrix()), scale(df_data[b].as_matrix()),idx) 76 | 77 | if weight > 0: # a causes b 78 | graph.add(a, b, weight) 79 | else: 80 | graph.add(b, a, abs(weight)) 81 | if printout is not None: 82 | res.append([str(a) + '-' + str(b), weight]) 83 | DataFrame(res, columns=['SampleID', 'Predictions']).to_csv( 84 | printout, index=False) 85 | 86 | idx += 1 87 | 88 | graph.remove_cycle_without_deletion() 89 | 90 | return graph 91 | 92 | def orient_graph_confounders(self, df_data, umg, printout=None): 93 | """ Orient an undirected graph using the pairwise method defined by the subclass 94 | Requirement : Name of the nodes in the graph correspond to name of the variables in df_data 95 | 96 | :param df_data: dataset 97 | :param umg: UndirectedGraph 98 | :param printout: print regularly predictions 99 | :return: Directed graph w/ weights 100 | :rtype: DirectedGraph 101 | """ 102 | 103 | edges = umg.get_list_edges_without_duplicate() 104 | graph = DirectedGraph(skeleton = umg) 105 | res = [] 106 | idx = 0 107 | 108 | 109 | for edge in edges: 110 | a, b = edge 111 | weight = self.predict_proba(scale(df_data[a].as_matrix()), scale(df_data[b].as_matrix()),idx) 112 | 113 | if weight > 0: # a causes b 114 | graph.add(a, b, weight) 115 | else: 116 | graph.add(b, a, abs(weight)) 117 | if printout is not None: 118 | res.append([str(a) + '-' + str(b), weight]) 119 | DataFrame(res, columns=['SampleID', 'Predictions']).to_csv( 120 | printout, index=False) 121 | 122 | idx += 1 123 | 124 | graph.remove_cycles() 125 | return graph 126 | -------------------------------------------------------------------------------- /Code/generator/ce_multi_generator.lua: -------------------------------------------------------------------------------- 1 | local xlua = require 'xlua' local pl_data = require 'pl.data' local pl_string = require 'pl.stringx' local rk = require 'randomkit' local function matrix2file(matrix, fname) local out = io.open(fname, "w") for i=1,matrix:size(1) do for j=1,matrix:size(2) do out:write(matrix[i][j]) if j == matrix:size(2) then out:write("\n") else out:write(" ") end end end out:close() end local function normalize(x) local y = x:clone() local m = x:mean(1):mul(-1) local s = x:std(1):add(1e-3) y:add(m:expandAs(y)):cdiv(s:expandAs(y)) return y end local function gaussian(m) local m = m or 2600 local a = torch.randn(m)[1] local b = torch.randn(m)[1] return torch.randn(m,1):mul(a):add(b) end local function uniform(m) local m = m or 2600 local a = torch.randn(m)[1] local b = torch.randn(m)[1] return normalize(torch.rand(m,1)):mul(a):add(b) end local function linear(x) local a = torch.randn(1)[1] local b = torch.randn(1)[1] foo = function(x) local f = x:clone() return f:mul(a):add(b) end return foo end local function parabola(x) local a = torch.randn(1)[1] local b = torch.randn(1)[1] foo = function(x) local f = torch.pow(x,2) return f:mul(a):add(b) end return foo end local function post_additive(x,f,n) return f(x):add(n) end local function post_multiplicative(x,f,n) return f(x):cmul(n) end local function pre_additive(x,f,n) return f(torch.add(x,n)) end local function pre_multiplicative(x,f,n) return f(torch.cmul(x,n)) end local marginals = { gaussian = gaussian, uniform = uniform } local tab_ma={"gaussian","uniform"} local mechanisms = { linear = linear, parabola = parabola } local tab_me = { "linear", "parabola" } local combinations = { post_additive = post_additive, post_multiplicative = post_multiplicative, pre_additive = pre_additive, pre_multiplicative = pre_multiplicative } local tab_co={ "post_additive", "post_multiplicative", "pre_additive", "pre_multiplicative" } local function draw_rand_parameters() cause_j = tab_ma[torch.Tensor(1):random(1,#tab_ma)[1]] noise_j = tab_ma[torch.Tensor(1):random(1,#tab_ma)[1]] mechanism_j = tab_me[torch.Tensor(1):random(1,#tab_me)[1]] combination_j = tab_co[torch.Tensor(1):random(1,#tab_co)[1]] return cause_j,noise_j,mechanism_j,combination_j end local function pair(cause, mechanism, noise, combination) local x = marginals[cause]() local f = mechanisms[mechanism]() local n = marginals[noise]() local y = combinations[combination](x,f,n) return torch.cat(normalize(x), normalize(y), 2) end local function identifiable(cause, mechanism, noise, combination) if((cause == "gaussian") and (mechanism == "linear") and (noise == "gaussian")) then if((combination == "post_additive") or (combination == "pre_additive")) then return false end end return true end local function r_pair(typep) local cause_l,noise_l,mechanism_l,combination_l=draw_rand_parameters() if(identifiable(cause_l, mechanism_l, noise_l, combination_l)) then --print(cause_l) local x = marginals[cause_l]() local f = mechanisms[mechanism_l]() local n = marginals[noise_l]() local y = combinations[combination_l](x,f,n) if typep==1 then return torch.cat(normalize(x), normalize(y), 2) elseif typep==2 then return torch.cat(normalize(y), normalize(x), 2) elseif typep==3 then local cause_l,noise_l,mechanism_l,combination_l=draw_rand_parameters() if(identifiable(cause_l, mechanism_l, noise_l, combination_l)) then local f = mechanisms[mechanism_l]() local n = marginals[noise_l]() local z = combinations[combination_l](x,f,n) return torch.cat(normalize(z), normalize(y), 2) else return r_pair(typep) end else local cause_l,noise_l,mechanism_l,combination_l=draw_rand_parameters() local x = marginals[cause_l]() local f = mechanisms[mechanism_l]() local n = marginals[noise_l]() local y2 = combinations[combination_l](x,f,n) return torch.cat(normalize(y2), normalize(y), 2) end else return r_pair(typep) end end j=1 nb_pairs= 4000 local meta = io.open('pairmeta.txt', "w") while j < nb_pairs do --X->Y pair_i = r_pair(1) matrix2file(pair_i, "pairF" .. j .. ".txt") meta:write("pairF" .. j .. ' ' .. '1\n') j=j+1 xlua.progress(j,nb_pairs) --X<-Y pair_i = r_pair(2) matrix2file(pair_i, "pairF" .. j .. ".txt") meta:write("pairF" .. j .. ' ' .. '2\n') j=j+1 xlua.progress(j,nb_pairs) --X||Y pair_i = r_pair(3) matrix2file(pair_i, "pairF" .. j .. ".txt") meta:write("pairF" .. j .. ' ' .. '3\n') j=j+1 xlua.progress(j,nb_pairs) --X_|_Y pair_i = r_pair(4) matrix2file(pair_i, "pairF" .. j .. ".txt") meta:write("pairF" .. j .. ' ' .. '4\n') j=j+1 xlua.progress(j,nb_pairs) end meta:close() -------------------------------------------------------------------------------- /Code/cgnn/generators/random_graph_generator.py: -------------------------------------------------------------------------------- 1 | from .functions_default import (noise, cause, effect, rand_bin) 2 | from ..utils.Graph import DirectedGraph 3 | from random import shuffle 4 | from sklearn.preprocessing import scale 5 | import numpy.random as rd 6 | import pandas as pd 7 | import numpy as np 8 | import operator as op 9 | 10 | 11 | def series_to_cepc_kag(A, B, idxpair): 12 | strA = '' 13 | strB = '' 14 | for i in A.values: 15 | strA += ' ' 16 | strA += str(i) 17 | 18 | for i in B.values: 19 | strB += ' ' 20 | strB += str(i) 21 | 22 | return pd.DataFrame([['pair' + str(idxpair), strA, strB]], columns=['SampleID', 'A', 'B']) 23 | 24 | 25 | class RandomGraphGenerator: 26 | def __init__(self, 27 | num_nodes=200, 28 | max_joint_causes=4, 29 | noise_qty=.7, 30 | number_points=500, 31 | categorical_rate=.20): 32 | 33 | self.nodes = num_nodes 34 | self.noise = noise_qty 35 | self.n_points = number_points 36 | self.cat_rate = categorical_rate 37 | self.num_max_parents = max_joint_causes 38 | self.joint_functions = [op.add, op.mul] 39 | self.causes = None 40 | self.graph = None 41 | self.data = None 42 | self.result_links = None 43 | self.cat_data = None 44 | self.cat_var = None 45 | 46 | print('Init OK') 47 | 48 | def generate(self, gen_cat=True): 49 | print('--Beginning Fast build--') 50 | # Drawing causes 51 | self.causes = [i for i in range(np.random.randint( 52 | 2, self.nodes / np.floor(np.sqrt(self.nodes))))] 53 | self.causes = list(set(self.causes)) 54 | self.data = pd.DataFrame(None) 55 | layer = [[]] 56 | for i in self.causes: 57 | self.data['V' + str(i)] = cause(self.n_points) 58 | layer[0].append(i) 59 | 60 | generated_nodes = len(self.causes) 61 | 62 | links = [] 63 | while generated_nodes < self.nodes: 64 | print( 65 | '--Generating nodes : {} out of ~{}'.format(generated_nodes, self.nodes)) 66 | layer.append([]) # new layer 67 | 68 | num_nodes_layer = np.random.randint(2, len(layer[-2]) + 2) 69 | for i in range(num_nodes_layer): 70 | layer[-1].append(generated_nodes) 71 | # draw causes 72 | last_idx = layer[-2][-1] 73 | parents = list(set([np.random.randint(0, last_idx) 74 | for i in range( 75 | self.num_max_parents)])) # np.random.randint(self.num_max_parents - 1, self.num_max_parents))])) 76 | child = [] 77 | # Compute each cause's contribution 78 | for par in parents: 79 | links.append(['V' + str(par), 'V' + str(generated_nodes)]) 80 | child.append( 81 | effect(self.data['V' + str(par)], self.n_points, self.noise)) 82 | # Combine contributions 83 | shuffle(child) 84 | result = child[0] 85 | for i in child[1:]: 86 | rd_func = self.joint_functions[np.random.randint( 87 | 0, len(self.joint_functions))] 88 | result = op.add(result, i) 89 | # Add a final noise 90 | rd_func = self.joint_functions[np.random.randint( 91 | 0, len(self.joint_functions))] 92 | if rd_func == op.mul: 93 | noise_var = noise(self.n_points, self.noise).flatten() 94 | result = rd_func(result + abs(min(result)), 95 | noise_var + abs(min(noise_var))) 96 | # +abs(min(result)) 97 | else: 98 | result = rd_func(result, noise( 99 | self.n_points, self.noise).flatten()) 100 | result = scale(result) 101 | 102 | self.data['V' + str(generated_nodes)] = result 103 | 104 | generated_nodes += 1 105 | self.result_links = pd.DataFrame(links, columns=["Cause", "Effect"]) 106 | print('--Dataset Generated--') 107 | if gen_cat: 108 | print('--Converting variables to categorical--') 109 | actual_cat_rate = 0.0 110 | self.cat_var = [] 111 | self.cat_data = self.data.copy() 112 | while actual_cat_rate < self.cat_rate: 113 | print( 114 | '--Converting, Actual rate: {:3.3f}/{}--'.format(actual_cat_rate, self.cat_rate)) 115 | var = np.random.randint(0, self.nodes) 116 | while var in self.cat_var: 117 | var = np.random.randint(0, self.nodes) 118 | self.cat_var.append(var) 119 | self.cat_data['V' + str(var)] = rand_bin( 120 | list(self.cat_data['V' + str(var)])) 121 | actual_cat_rate = float(len(self.cat_var)) / self.nodes 122 | 123 | self.cat_var = pd.DataFrame(self.cat_var) 124 | print('Build Directed Graph') 125 | self.graph = DirectedGraph() 126 | self.graph.add_multiple_edges([list(i)+[1] for i in self.result_links.as_matrix()]) 127 | 128 | print('--Done !--') 129 | return self.get_data() 130 | 131 | def get_data(self): 132 | # Returns Target, Numerical data, Mixed Data and Index of categorical 133 | # variables 134 | try: 135 | return self.graph, self.data, self.cat_data, self.cat_var 136 | except NameError: 137 | print('Please compute graph using .generate(), graph not build yet') 138 | raise NameError 139 | 140 | def save_data(self, filename): 141 | try: 142 | self.result_links 143 | except NameError: 144 | print('Please compute graph using .generate(), graph not build yet') 145 | raise NameError 146 | self.result_links.to_csv( 147 | filename + '_target.csv', sep=',', index=False) 148 | self.data.to_csv(filename + '_numdata.csv', sep=',', index=False) 149 | try: 150 | self.cat_data.to_csv(filename + '_catdata.csv', 151 | sep=',', index=False) 152 | self.cat_var.to_csv(filename + '_catindex.csv', 153 | sep=',', index=False) 154 | except AttributeError: 155 | pass 156 | print('Saved files : ' + filename) 157 | 158 | def generate_pairs(self, num_pairs): 159 | pairs_df = pd.DataFrame() 160 | target_df = pd.DataFrame() 161 | while len(pairs_df.index) < num_pairs: 162 | self.fast_build(gen_cat=False) 163 | for idxlk, link in self.result_links.iterrows(): 164 | if rd.randint(0, 2): 165 | df = series_to_cepc_kag(self.data['V' + str(link.Cause)], 166 | self.data['V' + str(link.Effect)], 167 | len(pairs_df.index)) 168 | tar = pd.DataFrame([['pair' + str(len(pairs_df.index)), 1.0]], 169 | columns=['SampleID', 'Target']) 170 | else: 171 | df = series_to_cepc_kag(self.data['V' + str(link.Effect)], 172 | self.data['V' + str(link.Cause)], 173 | len(pairs_df.index)) 174 | tar = pd.DataFrame([['pair' + str(len(pairs_df.index)), -1.0]], 175 | columns=['SampleID', 'Target']) 176 | 177 | pairs_df = pd.concat([pairs_df, df]) 178 | target_df = pd.concat([target_df, tar]) 179 | pairs_df.to_csv('p_graphgen_G' + 180 | str(self.num_max_parents) + 181 | '_N' + str(self.nodes) + 182 | '_pairs.csv', index=False) 183 | target_df.to_csv('p_graphgen_G' + 184 | str(self.num_max_parents) + 185 | '_N' + str(self.nodes) + 186 | '_targets.csv', index=False) 187 | print('Done!') 188 | 189 | return pairs_df, target_df 190 | -------------------------------------------------------------------------------- /Code/cgnn/GNN.py: -------------------------------------------------------------------------------- 1 | """ 2 | GNN : Generative Neural Networks for causal inference (pairwise) 3 | Authors : Olivier Goudet & Diviyan Kalainathan 4 | Ref: 5 | Date : 10/05/2017 6 | """ 7 | import os 8 | import tensorflow as tf 9 | 10 | #os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3' 11 | 12 | import numpy as np 13 | from .utils.Loss import MMD_loss_tf as MMD_tf 14 | from .utils.Loss import Fourier_MMD_Loss_tf as Fourier_MMD_tf 15 | from .utils.Settings import SETTINGS 16 | from joblib import Parallel, delayed 17 | from sklearn.preprocessing import scale 18 | from .PairwiseModel import Pairwise_Model 19 | import pandas as pd 20 | 21 | def init(size, **kwargs): 22 | """ Initialize a random tensor, normal(0,kwargs(SETTINGS.init_weights)). 23 | 24 | :param size: Size of the tensor 25 | :param kwargs: init_std=(SETTINGS.init_weights) Std of the initialized normal variable 26 | :return: Tensor 27 | """ 28 | init_std = kwargs.get('init_std', SETTINGS.init_weights) 29 | return tf.random_normal(shape=size, stddev=init_std) 30 | 31 | 32 | class GNN_tf(object): 33 | def __init__(self, N, run=0, pair=0, **kwargs): 34 | """ Build the tensorflow graph, the first column is set as the cause and the second as the effect 35 | 36 | :param N: Number of examples to generate 37 | :param run: for log purposes (optional) 38 | :param pair: for log purposes (optional) 39 | :param kwargs: h_layer_dim=(SETTINGS.h_layer_dim) Number of units in the hidden layer 40 | :param kwargs: learning_rate=(SETTINGS.learning_rate) learning rate of the optimizer 41 | :param kwargs: use_Fast_MMD=(SETTINGS.use_Fast_MMD) use fast MMD option 42 | :param kwargs: nb_vectors_approx_MMD=(SETTINGS.nb_vectors_approx_MMD) nb vectors 43 | """ 44 | 45 | h_layer_dim = kwargs.get('h_layer_dim', SETTINGS.h_layer_dim) 46 | learning_rate = kwargs.get('learning_rate', SETTINGS.learning_rate) 47 | use_Fast_MMD = kwargs.get('use_Fast_MMD', SETTINGS.use_Fast_MMD) 48 | nb_vectors_approx_MMD = kwargs.get('nb_vectors_approx_MMD', SETTINGS.nb_vectors_approx_MMD) 49 | 50 | self.run = run 51 | self.pair = pair 52 | self.X = tf.placeholder(tf.float32, shape=[None, 1]) 53 | self.Y = tf.placeholder(tf.float32, shape=[None, 1]) 54 | 55 | W_in = tf.Variable(init([2, h_layer_dim], **kwargs)) 56 | b_in = tf.Variable(init([h_layer_dim], **kwargs)) 57 | W_out = tf.Variable(init([h_layer_dim, 1], **kwargs)) 58 | b_out = tf.Variable(init([1], **kwargs)) 59 | 60 | theta_G = [W_in, b_in, 61 | W_out, b_out] 62 | 63 | 64 | e = tf.random_normal([N, 1], mean=0, stddev=1) 65 | 66 | hid = tf.nn.relu(tf.matmul(tf.concat([self.X, e], 1), W_in) + b_in) 67 | out_y = tf.matmul(hid, W_out) + b_out 68 | 69 | if(use_Fast_MMD): 70 | self.G_dist_loss_xcausesy = Fourier_MMD_tf(tf.concat([self.X, self.Y], 1), tf.concat([self.X, out_y], 1), nb_vectors_approx_MMD) 71 | else: 72 | self.G_dist_loss_xcausesy = MMD_tf(tf.concat([self.X, self.Y], 1), tf.concat([self.X, out_y], 1)) 73 | 74 | self.G_solver_xcausesy = (tf.train.AdamOptimizer(learning_rate=learning_rate) 75 | .minimize(self.G_dist_loss_xcausesy, var_list=theta_G)) 76 | 77 | config = tf.ConfigProto() 78 | config.gpu_options.allow_growth = True 79 | self.sess = tf.Session(config=config) 80 | self.sess.run(tf.global_variables_initializer()) 81 | 82 | def train(self, data, verbose=True, **kwargs): 83 | """ Train the GNN model 84 | 85 | :param data: data corresponding to the graph 86 | :param verbose: verbose 87 | :param kwargs: train_epochs=(SETTINGS.nb_epoch_train) number of train epochs 88 | :return: None 89 | """ 90 | train_epochs = kwargs.get('train_epochs', SETTINGS.train_epochs) 91 | 92 | for it in range(train_epochs): 93 | _, G_dist_loss_xcausesy_curr = self.sess.run( 94 | [self.G_solver_xcausesy, self.G_dist_loss_xcausesy], 95 | feed_dict={self.X: data[:, [0]], self.Y: data[:, [1]]} 96 | ) 97 | 98 | if verbose: 99 | if it % 100 == 0: 100 | print('Pair:{}, Run:{}, Iter:{}, score:{}'. 101 | format(self.pair, self.run, 102 | it, G_dist_loss_xcausesy_curr)) 103 | 104 | def evaluate(self, data, verbose=True, **kwargs): 105 | """ Test the model 106 | 107 | :param data: data corresponding to the graph 108 | :param verbose: verbose 109 | :param kwargs: test_epochs=(SETTINGS.nb_epoch_test) number of test epochs 110 | :return: mean MMD loss value of the CGNN structure on the data 111 | """ 112 | test_epochs = kwargs.get('test_epochs', SETTINGS.test_epochs) 113 | avg_score = 0 114 | 115 | for it in range(test_epochs): 116 | score = self.sess.run([self.G_dist_loss_xcausesy], feed_dict={self.X: data[:, [0]], self.Y: data[:, [1]]}) 117 | 118 | avg_score += score[0] 119 | 120 | if verbose: 121 | if it % 100 == 0: 122 | print('Pair:{}, Run:{}, Iter:{}, score:{}'.format(self.pair, self.run, it, score[0])) 123 | 124 | tf.reset_default_graph() 125 | 126 | return avg_score / test_epochs 127 | 128 | 129 | def tf_evalcausalscore_pairwise(df, idx, run, **kwargs): 130 | GNN = GNN_tf(df.shape[0], run, idx, **kwargs) 131 | GNN.train(df, **kwargs) 132 | return GNN.evaluate(df, **kwargs) 133 | 134 | 135 | def tf_run_instance(m, idx, run, **kwargs): 136 | """ Execute the CGNN, by init, train and eval either on CPU or GPU 137 | 138 | :param m: data corresponding to the config : (N, 2) data, [:, 0] cause and [:, 1] effect 139 | :param run: number of the run (only for print) 140 | :param idx: number of the idx (only for print) 141 | :param kwargs: gpu=(SETTINGS.GPU) True if GPU is used 142 | :param kwargs: nb_gpu=(SETTINGS.NB_GPU) Number of available GPUs 143 | :param kwargs: gpu_offset=(SETTINGS.GPU_OFFSET) number of gpu offsets 144 | :return: MMD loss value of the given structure after training 145 | """ 146 | gpu = kwargs.get('gpu', SETTINGS.GPU) 147 | nb_gpu = kwargs.get('nb_gpu', SETTINGS.NB_GPU) 148 | gpu_offset = kwargs.get('gpu_offset', SETTINGS.GPU_OFFSET) 149 | 150 | if (m.shape[0] > SETTINGS.max_nb_points): 151 | 152 | p = np.random.permutation(m.shape[0]) 153 | m = m[p[:int(SETTINGS.max_nb_points)],:] 154 | 155 | 156 | 157 | run_i = run 158 | if gpu: 159 | with tf.device('/gpu:' + str(gpu_offset + run_i % nb_gpu)): 160 | XY = tf_evalcausalscore_pairwise(m, idx, run, **kwargs) 161 | with tf.device('/gpu:' + str(gpu_offset + run_i % nb_gpu)): 162 | YX = tf_evalcausalscore_pairwise(m[:, [1, 0]], idx, run, **kwargs) 163 | return [XY, YX] 164 | else: 165 | return [tf_evalcausalscore_pairwise(m, idx, run, **kwargs), 166 | tf_evalcausalscore_pairwise(np.fliplr(m), idx, run, **kwargs)] 167 | 168 | 169 | 170 | class GNN(Pairwise_Model): 171 | """ 172 | Shallow Generative Neural networks, models the causal directions x->y and y->x with a 1-hidden layer neural network 173 | and a MMD loss. The causal direction is considered as the "best-fit" between the two directions 174 | """ 175 | 176 | def __init__(self, backend="PyTorch"): 177 | super(GNN, self).__init__() 178 | self.backend = backend 179 | 180 | def predict_proba(self, a, b,idx=0, **kwargs): 181 | 182 | backend_alg_dic = {"TensorFlow": tf_run_instance} 183 | if len(np.array(a).shape) == 1: 184 | a = np.array(a).reshape((-1, 1)) 185 | b = np.array(b).reshape((-1, 1)) 186 | 187 | nb_jobs = kwargs.get("nb_jobs", SETTINGS.NB_JOBS) 188 | nb_runs = kwargs.get("nb_runs", SETTINGS.NB_RUNS) 189 | m = np.hstack((a, b)) 190 | m = m.astype('float32') 191 | 192 | 193 | result_pair = Parallel(n_jobs=nb_jobs)(delayed(backend_alg_dic[self.backend])( 194 | m, idx, run, **kwargs) for run in range(nb_runs)) 195 | 196 | score_AB = np.mean([runpair[0] for runpair in result_pair]) 197 | score_BA = np.mean([runpair[1] for runpair in result_pair]) 198 | 199 | for runpair in result_pair: 200 | print(runpair[0]) 201 | print(score_AB) 202 | 203 | for runpair in result_pair: 204 | print(runpair[1]) 205 | print(score_BA) 206 | 207 | return (score_BA - score_AB) / (score_BA + score_AB) 208 | -------------------------------------------------------------------------------- /Code/LICENSE.md: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | Copyright 2017 Olivier Goudet 179 | 180 | Licensed under the Apache License, Version 2.0 (the "License"); 181 | you may not use this file except in compliance with the License. 182 | You may obtain a copy of the License at 183 | 184 | http://www.apache.org/licenses/LICENSE-2.0 185 | 186 | Unless required by applicable law or agreed to in writing, software 187 | distributed under the License is distributed on an "AS IS" BASIS, 188 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 189 | See the License for the specific language governing permissions and 190 | limitations under the License. -------------------------------------------------------------------------------- /Code/cgnn/generators/generators.py: -------------------------------------------------------------------------------- 1 | """ Regression and generation functions 2 | Author: Diviyan Kalainathan & Olivier Goudet 3 | Date : 30/06/17 4 | """ 5 | import numpy as np 6 | import tensorflow as tf 7 | from sklearn.linear_model import LassoLars 8 | from sklearn.svm import SVR 9 | 10 | from Code.cgnn.CGNN import CGNN_tf as CGNN 11 | from ..utils.Loss import MMD_loss_tf as MMD 12 | from ..utils.Settings import SETTINGS 13 | 14 | 15 | def init(size): 16 | """ Initialize a random tensor, normal(0,SETTINGS.init_weights). 17 | :param size: Size of the tensor 18 | :return: Tensor 19 | """ 20 | return tf.random_normal(shape=size, stddev=SETTINGS.init_weights) 21 | 22 | 23 | class FullGraphPolynomialModel_tf(object): 24 | def __init__(self, N, graph, list_nodes, run=0, idx=0, **kwargs): 25 | """ Build the tensorflow graph of the 2nd-degree Polynomial generator structure 26 | 27 | :param N: Number of points 28 | :param graph: Graph to be run 29 | :param run: number of the run (only for log) 30 | :param idx: number of the idx (only for log) 31 | :param kwargs: learning_rate=(SETTINGS.learning_rate) learning rate of the optimizer 32 | 33 | """ 34 | super(FullGraphPolynomialModel_tf, self).__init__() 35 | learning_rate = kwargs.get('learning_rate', SETTINGS.learning_rate) 36 | 37 | self.run = run 38 | self.idx = idx 39 | n_var = len(list_nodes) 40 | 41 | self.all_real_variables = tf.placeholder(tf.float32, shape=[None, n_var]) 42 | alpha = tf.Variable(init([1, 1])) 43 | generated_variables = {} 44 | theta_G = [alpha] 45 | 46 | while len(generated_variables) < n_var: 47 | for var in list_nodes: 48 | # Check if all parents are generated 49 | par = graph.get_parents(var) 50 | 51 | if (var not in generated_variables and 52 | set(par).issubset(generated_variables)): 53 | 54 | # Generate the variable 55 | W_in = tf.Variable(init([int((len(par) + 2) * (len(par) + 1) / 2), 1])) 56 | 57 | input_v = [] 58 | input_v.append(tf.ones([N, 1])) 59 | for i in par: 60 | input_v.append(generated_variables[i]/((len(par) + 2) * (len(par) + 1) / 2)) 61 | # Renormalize w/ number of inputs? 62 | input_v.append(tf.random_normal([N, 1], mean=0, stddev=1)) 63 | 64 | out_v = 0 65 | cpt = 0 66 | for i in range(len(par) + 2): 67 | for j in range(i + 1, len(par) + 2): 68 | out_v += W_in[cpt] * tf.multiply(input_v[i], input_v[j]) 69 | cpt += 1 70 | 71 | generated_variables[var] = out_v 72 | theta_G.extend([W_in]) 73 | 74 | listvariablegraph = [] 75 | for var in list_nodes: 76 | listvariablegraph.append(generated_variables[var]) 77 | 78 | self.all_generated_variables = tf.concat(listvariablegraph, 1) 79 | self.G_dist_loss_xcausesy = MMD(self.all_real_variables, self.all_generated_variables) 80 | 81 | # var_list = theta_G 82 | self.G_solver_xcausesy = (tf.train.AdamOptimizer( 83 | learning_rate=learning_rate).minimize(self.G_dist_loss_xcausesy, 84 | var_list=theta_G)) 85 | 86 | config = tf.ConfigProto() 87 | config.gpu_options.allow_growth = True 88 | 89 | self.sess = tf.Session(config=config) 90 | self.sess.run(tf.global_variables_initializer()) 91 | 92 | def train(self, data, verbose=True, **kwargs): 93 | """ Train the polynomial model by fitting on data using MMD 94 | 95 | :param data: data to fit 96 | :param verbose: verbose 97 | :param kwargs: train_epochs=(SETTINGS.nb_epoch_train) number of train epochs 98 | :return: Train loss at the last epoch 99 | """ 100 | train_epochs = kwargs.get('train_epochs', SETTINGS.train_epochs) 101 | for it in range(train_epochs): 102 | _, G_dist_loss_xcausesy_curr = self.sess.run( 103 | [self.G_solver_xcausesy, self.G_dist_loss_xcausesy], 104 | feed_dict={self.all_real_variables: data} 105 | ) 106 | 107 | if verbose: 108 | if it % 10 == 0: 109 | print('Pair:{}, Run:{}, Iter:{}, score:{}'. 110 | format(self.idx, self.run, 111 | it, G_dist_loss_xcausesy_curr)) 112 | 113 | return G_dist_loss_xcausesy_curr 114 | 115 | def evaluate(self, data, verbose=True): 116 | """ Run the model to generate data and output 117 | 118 | :param data: input data 119 | :param verbose: verbose 120 | :return: Generated data 121 | """ 122 | 123 | sumMMD_tr = 0 124 | 125 | for it in range(1): 126 | 127 | MMD_tr, generated_variables = self.sess.run([self.G_dist_loss_xcausesy, 128 | self.all_generated_variables], 129 | feed_dict={self.all_real_variables: data}) 130 | if verbose: 131 | print('Pair:{}, Run:{}, Iter:{}, score:{}'.format(self.idx, self.run, it, MMD_tr)) 132 | 133 | tf.reset_default_graph() 134 | 135 | return generated_variables 136 | 137 | 138 | def full_graph_polynomial_generator_tf(df_data, graph, idx=0, run=0, **kwargs): 139 | """ Run the full graph polynomial generator 140 | 141 | :param df_data: data 142 | :param graph: the graph to model 143 | :param idx: index (optional, for log purposes) 144 | :param run: no of run (optional, for log purposes) 145 | :param kwargs: gpu=(SETTINGS.GPU) True if GPU is used 146 | :param kwargs: nb_gpu=(SETTINGS.NB_GPU) Number of available GPUs 147 | :param kwargs: gpu_offset=(SETTINGS.GPU_OFFSET)number of gpu offsets 148 | :return: Generated data using the graph structure 149 | """ 150 | 151 | gpu = kwargs.get('gpu', SETTINGS.GPU) 152 | nb_gpu = kwargs.get('nb_gpu', SETTINGS.NB_GPU) 153 | gpu_offset = kwargs.get('gpu_offset', SETTINGS.GPU_OFFSET) 154 | 155 | list_nodes = graph.get_list_nodes() 156 | print(list_nodes) 157 | data = df_data[list_nodes].as_matrix() 158 | data = data.astype('float32') 159 | 160 | if gpu: 161 | with tf.device('/gpu:' + str(gpu_offset + run % nb_gpu)): 162 | 163 | model = FullGraphPolynomialModel_tf(df_data.shape[0], graph, list_nodes, run, idx, **kwargs) 164 | loss = model.train(data, **kwargs)/() 165 | if np.isfinite(loss): 166 | return model.evaluate(data) 167 | else: 168 | print('Has not converged, re-running graph inference') 169 | return full_graph_polynomial_generator_tf(df_data, graph, **kwargs) 170 | 171 | else: 172 | model = FullGraphPolynomialModel_tf(len(df_data), graph, list_nodes, run, idx, **kwargs) 173 | loss = model.train(data, **kwargs) 174 | if np.isfinite(loss): 175 | return model.evaluate(data) 176 | else: 177 | print('Has not converged, re-running graph inference') 178 | return full_graph_polynomial_generator_tf(df_data, graph, **kwargs) 179 | 180 | 181 | def CGNN_generator_tf(df_data, graph, idx=0, run=0, **kwargs): 182 | """ Run the full graph polynomial generator 183 | 184 | :param df_data: data 185 | :param graph: the graph to model 186 | :param idx: index (optional, for log purposes) 187 | :param run: no of run (optional, for log purposes) 188 | :param kwargs: gpu=(SETTINGS.GPU) True if GPU is used 189 | :param kwargs: nb_gpu=(SETTINGS.NB_GPU) Number of available GPUs 190 | :param kwargs: gpu_offset=(SETTINGS.GPU_OFFSET) number of gpu offsets 191 | :return: Generated data using the graph structure 192 | """ 193 | 194 | gpu = kwargs.get('gpu', SETTINGS.GPU) 195 | nb_gpu = kwargs.get('nb_gpu', SETTINGS.NB_GPU) 196 | gpu_offset = kwargs.get('gpu_offset', SETTINGS.GPU_OFFSET) 197 | 198 | list_nodes = graph.get_list_nodes() 199 | print(list_nodes) 200 | data = df_data[list_nodes].as_matrix() 201 | data = data.astype('float32') 202 | 203 | if gpu: 204 | with tf.device('/gpu:' + str(gpu_offset + run % nb_gpu)): 205 | 206 | model = CGNN(df_data.shape[0], graph, run, idx, h_layer_dim=3, **kwargs) 207 | loss = model.train(data, **kwargs) 208 | return model.generate(data) 209 | 210 | else: 211 | model = CGNN(len(df_data), graph, run, idx, **kwargs) 212 | loss = model.train(data, **kwargs) 213 | return model.generate(data) 214 | 215 | 216 | 217 | 218 | def polynomial_regressor(x, target, causes, fixed_noise=False, verbose=True, **kwargs): 219 | """ Regress data using a polynomial regressor of degree 2 220 | 221 | :param x: parents data 222 | :param target: target data 223 | :param causes: list of parent nodes 224 | :param train_epochs: number of train epochs 225 | :param fixed_noise : If the noise in the generation is fixed or not. 226 | :param verbose: verbose 227 | :return: generated data 228 | """ 229 | 230 | lr = kwargs.get('learning_rate', SETTINGS.learning_rate) 231 | train_epochs = kwargs.get('train_epochs', SETTINGS.train_epochs) 232 | n_ex = target.shape[0] 233 | if len(causes) == 0: 234 | causes = [] 235 | x = None 236 | if fixed_noise: 237 | x_input = th.FloatTensor(n_ex, 1).normal_() 238 | elif fixed_noise: 239 | x = th.FloatTensor(x) 240 | x_input = th.cat([x, th.FloatTensor(n_ex, 1).normal_()], 1) 241 | else: 242 | x_input = th.FloatTensor(x) 243 | target = Variable(th.FloatTensor(target)) 244 | model = PolynomialModel(len(causes), degree=2) 245 | if SETTINGS.GPU: 246 | model.cuda() 247 | target.cuda() 248 | if x_input is not None: 249 | x_input.cuda() 250 | criterion = MomentMatchingLoss(4) 251 | optimizer = th.optim.Adam(model.parameters(), lr=lr) 252 | 253 | for epoch in range(train_epochs): 254 | optimizer.zero_grad() 255 | y_tr = model(x_input, n_ex, fixed_noise=fixed_noise) 256 | x = Variable(x_input) 257 | loss = criterion(th.cat([y_tr, x], 1), th.cat([target.resize(target.size()[0], 1), x], 1)) 258 | loss.backward() 259 | optimizer.step() 260 | 261 | if verbose and epoch % 50 == 0: 262 | print('Epoch : {} ; Loss: {}'.format(epoch, loss.data.numpy())) 263 | 264 | return model(x_input, n_ex).data.numpy() 265 | 266 | 267 | def linear_regressor(x, target, causes): 268 | """ Regression and prediction using a lasso 269 | 270 | :param x: data 271 | :param target: target - effect 272 | :param causes: causes of the causal mechanism 273 | :return: regenerated data with the fitted model 274 | """ 275 | 276 | if len(causes) == 0: 277 | x= np.random.normal(size=(target.shape[0], 1)) 278 | 279 | lasso = LassoLars(alpha=1.) # no regularization 280 | lasso.fit(x, target) 281 | 282 | return lasso.predict(x) 283 | 284 | 285 | def support_vector_regressor(x, target, causes): 286 | """ Regression and prediction using a SVM (rbf) 287 | 288 | :param x: data 289 | :param target: target - effect 290 | :param causes: causes of the causal mechanism 291 | :return: regenerated data with the fitted model 292 | """ 293 | svr_rbf = SVR(kernel='rbf', C=1e3, gamma=0.1) 294 | if len(causes) == 0: 295 | x = np.random.normal(size=(target.shape[0], 1)) 296 | 297 | return svr_rbf.fit(x, target).predict(x) 298 | 299 | -------------------------------------------------------------------------------- /Code/cgnn/CGNN.py: -------------------------------------------------------------------------------- 1 | """ 2 | CGNN_graph_model 3 | Author : Olivier Goudet & Diviyan Kalainathan 4 | Ref : 5 | Date : 09/5/17 6 | """ 7 | 8 | import warnings 9 | from copy import deepcopy 10 | 11 | import numpy as np 12 | import pandas as pd 13 | import tensorflow as tf 14 | from joblib import Parallel, delayed 15 | from pandas import DataFrame 16 | from sklearn.preprocessing import scale 17 | 18 | from .GNN import GNN 19 | from .utils.Loss import MMD_loss_tf, Fourier_MMD_Loss_tf 20 | from .utils.Settings import SETTINGS 21 | from .GraphModel import GraphModel 22 | 23 | 24 | def init(size, **kwargs): 25 | """ Initialize a random tensor, normal(0,kwargs(SETTINGS.init_weights)). 26 | 27 | :param size: Size of the tensor 28 | :param kwargs: init_std=(SETTINGS.init_weights) Std of the initialized normal variable 29 | :return: Tensor 30 | """ 31 | init_std = kwargs.get('init_std', SETTINGS.init_weights) 32 | return tf.random_normal(shape=size, stddev=init_std) 33 | 34 | 35 | class CGNN_tf(object): 36 | def __init__(self, N, graph, run, idx, **kwargs): 37 | """ Build the tensorflow graph of the CGNN structure 38 | 39 | :param N: Number of points 40 | :param graph: Graph to be run 41 | :param run: number of the run (only for print) 42 | :param idx: number of the idx (only for print) 43 | :param kwargs: learning_rate=(SETTINGS.learning_rate) learning rate of the optimizer 44 | :param kwargs: h_layer_dim=(SETTINGS.h_layer_dim) Number of units in the hidden layer 45 | :param kwargs: use_Fast_MMD=(SETTINGS.use_Fast_MMD) use fast MMD option 46 | :param kwargs: nb_vectors_approx_MMD=(SETTINGS.nb_vectors_approx_MMD) nb vectors 47 | """ 48 | learning_rate = kwargs.get('learning_rate', SETTINGS.learning_rate) 49 | h_layer_dim = kwargs.get('h_layer_dim', SETTINGS.h_layer_dim) 50 | use_Fast_MMD = kwargs.get('use_Fast_MMD', SETTINGS.use_Fast_MMD) 51 | nb_vectors_approx_MMD = kwargs.get('nb_vectors_approx_MMD', SETTINGS.nb_vectors_approx_MMD) 52 | 53 | self.run = run 54 | self.idx = idx 55 | list_nodes = graph.get_list_nodes() 56 | n_var = len(list_nodes) 57 | 58 | self.all_real_variables = tf.placeholder(tf.float32, shape=[None, n_var]) 59 | 60 | generated_variables = {} 61 | theta_G = [] 62 | 63 | while len(generated_variables) < n_var: 64 | # Need to generate all variables in the graph using its parents : possible because of the DAG structure 65 | for var in list_nodes: 66 | # Check if all parents are generated 67 | par = graph.get_parents(var) 68 | if (var not in generated_variables and 69 | set(par).issubset(generated_variables)): 70 | # Generate the variable 71 | W_in = tf.Variable(init([len(par) + 1, h_layer_dim], **kwargs)) 72 | b_in = tf.Variable(init([h_layer_dim], **kwargs)) 73 | W_out = tf.Variable(init([h_layer_dim, 1], **kwargs)) 74 | b_out = tf.Variable(init([1], **kwargs)) 75 | 76 | input_v = [generated_variables[i] for i in par] 77 | input_v.append(tf.random_normal([N, 1], mean=0, stddev=1)) 78 | input_v = tf.concat(input_v, 1) 79 | 80 | out_v = tf.nn.relu(tf.matmul(input_v, W_in) + b_in) 81 | out_v = tf.matmul(out_v, W_out) + b_out 82 | 83 | generated_variables[var] = out_v 84 | theta_G.extend([W_in, b_in, W_out, b_out]) 85 | 86 | listvariablegraph = [] 87 | for var in list_nodes: 88 | listvariablegraph.append(generated_variables[var]) 89 | 90 | self.all_generated_variables = tf.concat(listvariablegraph, 1) 91 | 92 | if(use_Fast_MMD): 93 | self.G_dist_loss_xcausesy = Fourier_MMD_Loss_tf(self.all_real_variables, self.all_generated_variables,nb_vectors_approx_MMD) 94 | else: 95 | self.G_dist_loss_xcausesy = MMD_loss_tf(self.all_real_variables, self.all_generated_variables) 96 | 97 | self.G_solver_xcausesy = (tf.train.AdamOptimizer( 98 | learning_rate=learning_rate).minimize(self.G_dist_loss_xcausesy, 99 | var_list=theta_G)) 100 | 101 | config = tf.ConfigProto() 102 | config.gpu_options.allow_growth = True 103 | 104 | self.sess = tf.Session(config=config) 105 | self.sess.run(tf.global_variables_initializer()) 106 | 107 | def train(self, data, verbose=True, **kwargs): 108 | """ Train the initialized model 109 | 110 | :param data: data corresponding to the graph 111 | :param verbose: verbose 112 | :param kwargs: train_epochs=(SETTINGS.train_epochs) number of train epochs 113 | :return: None 114 | """ 115 | train_epochs = kwargs.get('train_epochs', SETTINGS.train_epochs) 116 | for it in range(train_epochs): 117 | 118 | _, G_dist_loss_xcausesy_curr = self.sess.run( 119 | [self.G_solver_xcausesy, self.G_dist_loss_xcausesy], 120 | feed_dict={self.all_real_variables: data} 121 | ) 122 | 123 | if verbose: 124 | if it % 100 == 0: 125 | print('Pair:{}, Run:{}, Iter:{}, score:{}'. 126 | format(self.idx, self.run, 127 | it, G_dist_loss_xcausesy_curr)) 128 | 129 | def evaluate(self, data, verbose=True, **kwargs): 130 | """ Test the model 131 | 132 | :param data: data corresponding to the graph 133 | :param verbose: verbose 134 | :param kwargs: test_epochs=(SETTINGS.test_epochs) number of test epochs 135 | :return: mean MMD loss value of the CGNN structure on the data 136 | """ 137 | test_epochs = kwargs.get('test_epochs', SETTINGS.test_epochs) 138 | sumMMD_tr = 0 139 | 140 | for it in range(test_epochs): 141 | 142 | MMD_tr = self.sess.run([self.G_dist_loss_xcausesy], feed_dict={ 143 | self.all_real_variables: data}) 144 | 145 | sumMMD_tr += MMD_tr[0] 146 | 147 | if verbose and it % 100 == 0: 148 | print('Pair:{}, Run:{}, Iter:{}, score:{}' 149 | .format(self.idx, self.run, it, MMD_tr[0])) 150 | 151 | tf.reset_default_graph() 152 | 153 | return sumMMD_tr / test_epochs 154 | 155 | def generate(self, data, **kwargs): 156 | 157 | generated_variables = self.sess.run([self.all_generated_variables], feed_dict={self.all_real_variables: data}) 158 | 159 | tf.reset_default_graph() 160 | return np.array(generated_variables)[0, :, :] 161 | 162 | 163 | def run_CGNN_tf(df_data, graph, idx=0, run=0, **kwargs): 164 | """ Execute the CGNN, by init, train and eval either on CPU or GPU 165 | 166 | :param df_data: data corresponding to the graph 167 | :param graph: Graph to be run 168 | :param run: number of the run (only for print) 169 | :param idx: number of the idx (only for print) 170 | :param kwargs: gpu=(SETTINGS.GPU) True if GPU is used 171 | :param kwargs: nb_gpu=(SETTINGS.nb_gpu) Number of available GPUs 172 | :param kwargs: gpu_offset=(SETTINGS.gpu_offset) number of gpu offsets 173 | :return: MMD loss value of the given structure after training 174 | """ 175 | gpu = kwargs.get('gpu', SETTINGS.GPU) 176 | nb_gpu = kwargs.get('nb_gpu', SETTINGS.NB_GPU) 177 | gpu_offset = kwargs.get('gpu_offset', SETTINGS.GPU_OFFSET) 178 | 179 | list_nodes = graph.get_list_nodes() 180 | df_data = df_data[list_nodes].as_matrix() 181 | data = df_data.astype('float32') 182 | 183 | if (data.shape[0] > SETTINGS.max_nb_points): 184 | p = np.random.permutation(data.shape[0]) 185 | data = data[p[:int(SETTINGS.max_nb_points)],:] 186 | 187 | if gpu: 188 | with tf.device('/gpu:' + str(gpu_offset + run % nb_gpu)): 189 | model = CGNN_tf(data.shape[0], graph, run, idx, **kwargs) 190 | model.train(data, **kwargs) 191 | return model.evaluate(data, **kwargs) 192 | else: 193 | model = CGNN_tf(data.shape[0], graph, run, idx, **kwargs) 194 | model.train(data, **kwargs) 195 | return model.evaluate(data, **kwargs) 196 | 197 | 198 | def hill_climbing(graph, data, run_cgnn_function, **kwargs): 199 | """ Optimize graph using CGNN with a hill-climbing algorithm 200 | 201 | :param graph: graph to optimize 202 | :param data: data 203 | :param run_cgnn_function: name of the CGNN function (depending on the backend) 204 | :param kwargs: nb_jobs=(SETTINGS.NB_JOBS) number of jobs 205 | :param kwargs: nb_runs=(SETTINGS.NB_RUNS) number of runs, of different evaluations 206 | :return: improved graph 207 | """ 208 | nb_jobs = kwargs.get("nb_jobs", SETTINGS.NB_JOBS) 209 | nb_runs = kwargs.get("nb_runs", SETTINGS.NB_RUNS) 210 | loop = 0 211 | tested_configurations = [graph.get_dict_nw()] 212 | improvement = True 213 | result = [] 214 | result_pairs = Parallel(n_jobs=nb_jobs)(delayed(run_cgnn_function)( 215 | data, graph, 0, run, **kwargs) for run in range(nb_runs)) 216 | 217 | score_network = np.mean([i for i in result_pairs if np.isfinite(i)]) 218 | globalscore = score_network 219 | 220 | print("Graph score : " + str(globalscore)) 221 | 222 | while improvement: 223 | loop += 1 224 | improvement = False 225 | list_edges = graph.get_list_edges() 226 | for idx_pair in range(len(list_edges)): 227 | edge = list_edges[idx_pair] 228 | test_graph = deepcopy(graph) 229 | test_graph.reverse_edge(edge[0], edge[1]) 230 | 231 | if (test_graph.is_cyclic() 232 | or test_graph.get_dict_nw() in tested_configurations): 233 | print('No Evaluation for {}'.format([edge])) 234 | else: 235 | print('Edge {} in evaluation :'.format(edge)) 236 | tested_configurations.append(test_graph.get_dict_nw()) 237 | result_pairs = Parallel(n_jobs=nb_jobs)(delayed(run_cgnn_function)( 238 | data, test_graph, idx_pair, run, **kwargs) for run in range(nb_runs)) 239 | 240 | score_network = np.mean([i for i in result_pairs if np.isfinite(i)]) 241 | 242 | print("Current score : " + str(score_network)) 243 | print("Best score : " + str(globalscore)) 244 | 245 | if score_network < globalscore: 246 | graph.reverse_edge(edge[0], edge[1]) 247 | improvement = True 248 | print('Edge {} got reversed !'.format(edge)) 249 | globalscore = score_network 250 | 251 | 252 | return graph 253 | 254 | 255 | 256 | 257 | def tabu_search(graph, data, run_cgnn_function, **kwargs): 258 | """ Optimize graph using CGNN with a hill-climbing algorithm 259 | 260 | :param graph: graph to optimize 261 | :param data: data 262 | :param run_cgnn_function: name of the CGNN function (depending on the backend) 263 | :param kwargs: nb_jobs=(SETTINGS.NB_JOBS) number of jobs 264 | :param kwargs: nb_runs=(SETTINGS.NB_RUNS) number of runs, of different evaluations 265 | :return: improved graph 266 | """ 267 | nb_jobs = kwargs.get("nb_jobs", SETTINGS.NB_JOBS) 268 | nb_runs = kwargs.get("nb_runs", SETTINGS.NB_RUNS) 269 | raise ValueError('Not Yet Implemented') 270 | 271 | 272 | class CGNN(GraphModel): 273 | """ 274 | CGNN Model ; Using generative models, generate the whole causal graph and improve causal 275 | direction predictions in the graph. 276 | """ 277 | 278 | def __init__(self, backend='PyTorch'): 279 | """ Initialize the CGNN Model. 280 | 281 | :param backend: Choose the backend to use, either 'PyTorch' or 'TensorFlow' 282 | """ 283 | super(CGNN, self).__init__() 284 | self.backend = backend 285 | 286 | if self.backend == 'TensorFlow': 287 | self.infer_graph = run_CGNN_tf 288 | elif self.backend == 'PyTorch': 289 | self.infer_graph = run_CGNN_th 290 | else: 291 | print('No backend known as {}'.format(self.backend)) 292 | raise ValueError 293 | 294 | def create_graph_from_data(self, data): 295 | print("The CGNN model is not able (yet?) to model the graph directly from raw data") 296 | raise ValueError 297 | 298 | def orient_directed_graph(self, data, dag, alg='HC', **kwargs): 299 | """ Improve a directed acyclic graph using CGNN 300 | 301 | :param data: data 302 | :param dag: directed acyclic graph to optimize 303 | :param alg: type of algorithm 304 | :param log: Save logs of the execution 305 | :return: improved directed acyclic graph 306 | """ 307 | data = DataFrame(scale(data.as_matrix()), columns=data.columns) 308 | alg_dic = {'HC': hill_climbing, 'tabu': tabu_search} 309 | return alg_dic[alg](dag, data, self.infer_graph, **kwargs) 310 | 311 | def orient_undirected_graph(self, data, umg, **kwargs): 312 | """ Orient the undirected graph using GNN and apply CGNN to improve the graph 313 | 314 | :param data: data 315 | :param umg: undirected acyclic graph 316 | :return: directed acyclic graph 317 | """ 318 | 319 | warnings.warn("The pairwise GNN model is computed on each edge of the UMG " 320 | "to initialize the model and start CGNN with a DAG") 321 | gnn = GNN(backend=self.backend, **kwargs) 322 | dag = gnn.orient_graph(data, umg, **kwargs) # Pairwise method 323 | return self.orient_directed_graph(data, dag, **kwargs) 324 | -------------------------------------------------------------------------------- /Code/cgnn/utils/Graph.py: -------------------------------------------------------------------------------- 1 | """ 2 | Graph utilities : Definition of classes 3 | Cycles detection & removal 4 | Author : Diviyan Kalainathan & Olivier Goudet 5 | Date : 21/04/2017 6 | """ 7 | 8 | import numpy as np 9 | from copy import deepcopy 10 | from collections import defaultdict 11 | import pandas as pd 12 | 13 | def list_to_dict(links): 14 | """ Create a dict out of a list of links 15 | 16 | :param links: list of links 17 | :type links: list 18 | :return: dictionary reprensenting the graph structure 19 | :rtype: dict 20 | """ 21 | dic = defaultdict(list) 22 | for link in links: 23 | dic[int(link[0][1:])].append(int(link[1][1:])) 24 | if int(link[1][1:]) not in dic: 25 | dic[int(link[1][1:])] = [] 26 | return dic 27 | 28 | 29 | class Graph(object): 30 | """ Base class for Graph structure""" 31 | 32 | def __init__(self, df=None, adjacency_matrix=False): 33 | """ Create a new graph structure""" 34 | self._graph = defaultdict(dict) 35 | connections = [] 36 | if df is not None: 37 | if adjacency_matrix: 38 | data = df.as_matrix() 39 | col = df.columns 40 | for i in range(data.shape[0]): 41 | for j in range(data.shape[0]): 42 | if i != j and data[i, j] > 0.001: 43 | connections.append([col[i], col[j], data[i, j]]) 44 | else: 45 | for idx, row in df.iterrows(): 46 | connections.append(row) 47 | 48 | self.add_multiple_edges(connections) 49 | 50 | def add_multiple_edges(self, connections): 51 | """ Add edges (list of tuple pairs) to graph 52 | 53 | :param connections: List of tuples (cause, effect, weight) 54 | :type connections: list 55 | """ 56 | 57 | for node1, node2, *weight in connections: 58 | if not weight: 59 | self.add(node1, node2) 60 | else: 61 | self.add(node1, node2, weight[0]) 62 | 63 | def add(self, node1, node2, weight=1): 64 | """ Add or update edge from node1 to node2 65 | 66 | :param node1: cause of the edge 67 | :param node2: effect of the edge 68 | :param weight: value of edge 69 | :type weight: float 70 | """ 71 | 72 | raise NotImplementedError 73 | 74 | def remove_edge(self, node1, node2): 75 | """ Remove the edge from node1 to node2 76 | 77 | :param node1: cause of the edge 78 | :param node2: effect of the edge 79 | """ 80 | raise NotImplementedError 81 | 82 | def get_parents(self, node): 83 | """ Get the list of parents of a node 84 | 85 | :param node: Selected node 86 | :return: list of parents of the nodes 87 | :rtype: list 88 | """ 89 | parents = [] 90 | for i in self._graph: 91 | if node in list(self._graph[i]): 92 | parents.append(i) 93 | return parents 94 | 95 | def get_list_nodes(self): 96 | """ Get list of all nodes in graph 97 | 98 | :return: List of nodes 99 | :rtype: list 100 | 101 | """ 102 | 103 | nodes = [] 104 | for i in self._graph: 105 | if i not in nodes: 106 | nodes.append(i) 107 | for j in list(self._graph[i]): 108 | if j not in nodes: 109 | nodes.append(j) 110 | return nodes 111 | 112 | def get_list_edges(self, order_by_weight=True, descending=False, return_weights=True): 113 | """ Get list of edges according to order defined by parameters 114 | 115 | :param order_by_weight: List of edges will be ordered by weight values 116 | :param descending: order elements by decreasing weights 117 | :param return_weights: return the list of weights 118 | :return: List of edges and their weights 119 | :rtype: (list,list)""" 120 | 121 | list_edges = [] 122 | weights = [] 123 | for i in self._graph: 124 | for j in list(self._graph[i]): 125 | list_edges.append([i, j]) 126 | weights.append(self._graph[i][j]) 127 | 128 | if order_by_weight and descending: 129 | weights, list_edges = (list(i) for i 130 | in zip(*sorted(zip(weights, list_edges), 131 | reverse=True))) 132 | elif order_by_weight: 133 | weights, list_edges = (list(i) for i 134 | in zip(*sorted(zip(weights, list_edges)))) 135 | if return_weights: 136 | return [[edge[0], edge[1], weight] for edge, weight in zip(list_edges, weights)] 137 | else: 138 | return list_edges 139 | 140 | def get_adjacency_matrix(self): 141 | """Get the adjacency matrix of the graph 142 | 143 | :return: Adjacency Matrix (size : Nodes x Nodes), List of nodes 144 | :rtype: (numpy.ndarray, list) 145 | """ 146 | 147 | nodes = self.get_list_nodes() 148 | edges = self.get_list_edges(order_by_weight=False) 149 | 150 | m = np.zeros((len(nodes), len(nodes))) 151 | 152 | for idx, e in enumerate(edges): 153 | cause = nodes.index(e[0]) 154 | effect = nodes.index(e[1]) 155 | 156 | m[cause, effect] = e[2] 157 | 158 | return m, nodes 159 | 160 | def get_dict_nw(self): 161 | """Get dictionary of graph without weight values 162 | 163 | :return: Dictionary of the directed graph 164 | :rtype: dict 165 | 166 | """ 167 | 168 | dict_nw = defaultdict(list) 169 | for i in self._graph: 170 | for j in list(self._graph[i]): 171 | dict_nw[i].append(j) 172 | if j not in dict_nw: 173 | dict_nw[j] = [] 174 | return dict(dict_nw) 175 | 176 | def remove_node(self, node): 177 | """ Remove all references to node 178 | 179 | :param node: node to remove 180 | :type node: str 181 | 182 | """ 183 | 184 | for n, cxns in self._graph.iteritems(): 185 | try: 186 | cxns.remove(node) 187 | except KeyError: 188 | pass 189 | try: 190 | del self._graph[node] 191 | except KeyError: 192 | pass 193 | 194 | def __str__(self): 195 | return '{}({})'.format(self.__class__.__name__, dict(self._graph)) 196 | 197 | 198 | class DirectedGraph(Graph): 199 | """ Graph data structure, directed. """ 200 | 201 | def __init__(self, df=None, adjacency_matrix=False, skeleton = False): 202 | self.skeleton = skeleton 203 | """ Create a new directed graph structure""" 204 | super(DirectedGraph, self).__init__(df, adjacency_matrix) 205 | 206 | def add(self, node1, node2, weight=1): 207 | """ Add or update directed edge from node1 to node2 208 | 209 | :param node1: cause of the edge 210 | :param node2: effect of the edge 211 | :param weight: value of edge 212 | :type weight: float 213 | """ 214 | 215 | self._graph[node1][node2] = weight 216 | return self 217 | 218 | def is_cyclic(self): 219 | """ 220 | Return True if the directed graph g has a cycle. 221 | g must be represented as a dictionary mapping vertices to 222 | iterables of neighbouring vertices. 223 | 224 | :return: True if the directed graph is cyclic 225 | :rtype: bool 226 | """ 227 | path = set() 228 | visited = set() 229 | 230 | def visit(vertex): 231 | if vertex in visited: 232 | return False 233 | visited.add(vertex) 234 | path.add(vertex) 235 | for neighbour in g.get(vertex, ()): 236 | if neighbour in path or visit(neighbour): 237 | return True 238 | path.remove(vertex) 239 | return False 240 | 241 | g = self.get_dict_nw() 242 | return any(visit(v) for v in g) 243 | 244 | def cycles(self): 245 | """Return the list of cycles of the directed graph g . 246 | g must be represented as a dictionary mapping vertices to 247 | iterables of neighbouring vertices. For example: 248 | 249 | :return: Cycles in the graph 250 | :rtype: list 251 | """ 252 | 253 | def dfs(graph, start, end): 254 | fringe = [(start, [])] 255 | # print(len(graph)) 256 | while fringe: 257 | state, path = fringe.pop() 258 | if path and state == end: 259 | yield path 260 | continue 261 | for next_state in graph[state]: 262 | if next_state in path: 263 | continue 264 | fringe.append((next_state, path + [next_state])) 265 | 266 | g = self.get_dict_nw() 267 | return [[node] + path for node in g for path in dfs(g, node, node) if path] 268 | 269 | def reverse_edge(self, node1, node2, weight=None): 270 | """ Reverse the edge between node1 and node2 271 | with possibly a new weight value 272 | 273 | :param node1: initial cause of the edge 274 | :param node2: initial effect of the edge 275 | :param weight: new value of edge 276 | :type weight: float 277 | """ 278 | 279 | if not weight: 280 | weight = self._graph[node1][node2] 281 | self.remove_edge(node1, node2) 282 | self.add(node2, node1, weight) 283 | 284 | def remove_edge(self, node1, node2): 285 | """ Remove the edge from node1 to node2 286 | 287 | :param node1: cause of the edge 288 | :param node2: effect of the edge 289 | """ 290 | del self._graph[node1][node2] 291 | if len(self._graph[node1]) == 0: 292 | del self._graph[node1] 293 | 294 | def remove_cycles(self, verbose=True): 295 | """ Remove all cycles in graph by using the weights 296 | 297 | The edges with the lowest weight values will be reversed or deleted. 298 | """ 299 | 300 | list_ordered_edges = self.get_list_edges(return_weights=False) 301 | while self.is_cyclic(): 302 | cc = self.cycles() 303 | # Select the first link: 304 | s_cycle = cc[0] 305 | r_edge = next(edge for edge in list_ordered_edges if 306 | any(edge == s_cycle[i:i + 2] 307 | for i in range(len(s_cycle) - 1))) 308 | print('CC:' + str(cc)) 309 | print(r_edge) 310 | # Look if the edge can't be reversed 311 | test_graph = deepcopy(self) 312 | test_graph.reverse_edge(r_edge[0], r_edge[1]) 313 | print(test_graph) 314 | if len(self.cycles()) < len(cc): 315 | self.reverse_edge(r_edge[0], r_edge[1]) 316 | if verbose: 317 | print('Link {} got reversed !'.format(r_edge)) 318 | 319 | else: # remove the edge 320 | self.remove_edge(r_edge[0], r_edge[1]) 321 | if verbose: 322 | print('Link {} got deleted !'.format(r_edge)) 323 | 324 | def remove_cycle_without_deletion(self): 325 | """ 326 | Return True if the directed graph g has a cycle. 327 | g must be represented as a dictionary mapping vertices to 328 | iterables of neighbouring vertices. 329 | 330 | :return: True if the directed graph is cyclic 331 | :rtype: bool 332 | """ 333 | g = self.get_dict_nw() 334 | print(g) 335 | path = set() 336 | visited = set() 337 | 338 | def visit(vertex): 339 | if vertex in visited: 340 | return False 341 | visited.add(vertex) 342 | path.add(vertex) 343 | for neighbour in g.get(vertex, ()): 344 | if neighbour in path : 345 | self.reverse_edge(vertex, neighbour) 346 | else : 347 | visit(neighbour) 348 | path.remove(vertex) 349 | 350 | for v in g: 351 | visit(v) 352 | 353 | def set_weight(self, node1, node2, weight): 354 | """ Remove the edge from node1 to node2 355 | 356 | :param node1: cause of the edge 357 | :param node2: effect of the edge 358 | :param weight: new weight of the edge 359 | """ 360 | self._graph[node1][node2] = weight 361 | 362 | def get_correlation_matrix(self, sigma): 363 | nodes = self.skeleton.get_list_nodes() 364 | edges = self.skeleton.get_list_edges_without_duplicate() 365 | m = np.zeros((len(nodes), len(nodes))) 366 | 367 | for idx in range(len(nodes)): 368 | m[idx, idx] = 1 369 | 370 | for idx, e in enumerate(edges): 371 | cause = nodes.index(e[0]) 372 | effect = nodes.index(e[1]) 373 | 374 | m[cause, effect] = sigma 375 | m[effect, cause] = sigma 376 | 377 | return m 378 | 379 | 380 | class UndirectedGraph(Graph): 381 | """ Graph data structure, undirected. """ 382 | 383 | def __init__(self, df=None, adjacency_matrix=False): 384 | """ Create a new undirected graph structure""" 385 | super(UndirectedGraph, self).__init__(df, adjacency_matrix) 386 | 387 | def add(self, node1, node2, weight=1): 388 | """ Add or update edge between node1 to node2 389 | 390 | :param node1: end1 of the edge 391 | :param node2: end2 of the edge 392 | :param weight: value of edge 393 | :type weight: float 394 | """ 395 | 396 | self._graph[node1][node2] = weight 397 | self._graph[node2][node1] = weight 398 | 399 | def remove_edge(self, node1, node2): 400 | """ Remove the edge from node1 to node2 401 | 402 | :param node1: cause of the edge 403 | :param node2: effect of the edge 404 | """ 405 | del self._graph[node1][node2] 406 | del self._graph[node2][node1] 407 | if len(self._graph[node1]) == 0: 408 | del self._graph[node1] 409 | 410 | if len(self._graph[node2]) == 0: 411 | del self._graph[node2] 412 | 413 | def get_neighbors(self, node): 414 | """ Get the list of neighbors of a node 415 | 416 | :param node: Selected node 417 | :return: list of neighbors of the nodes 418 | :rtype: list 419 | """ 420 | return self.get_parents(node) 421 | 422 | def get_list_edges_without_duplicate(self): 423 | """ Get list of edges according to order defined by parameters 424 | :return: List of edges 425 | :rtype: (list,list)""" 426 | 427 | list_edges = [] 428 | 429 | for i in self._graph: 430 | for j in list(self._graph[i]): 431 | if([j, i] not in list_edges): 432 | list_edges.append([i, j]) 433 | 434 | return list_edges 435 | 436 | 437 | -------------------------------------------------------------------------------- /Code/cgnn/CGNN_confounders.py: -------------------------------------------------------------------------------- 1 | """ 2 | CGNN_graph_model 3 | Author : Olivier Goudet & Diviyan Kalainathan 4 | Ref : 5 | Date : 09/5/17 6 | """ 7 | 8 | import warnings 9 | from copy import deepcopy 10 | 11 | import numpy as np 12 | import tensorflow as tf 13 | from joblib import Parallel, delayed 14 | # import torch as th 15 | # from torch.autograd import Variable 16 | from pandas import DataFrame 17 | from sklearn.preprocessing import scale 18 | 19 | from .GNN import GNN 20 | from .utils.Loss import MMD_loss_tf, Fourier_MMD_Loss_tf 21 | # from ...utils.Loss import MMD_loss_th 22 | from .utils.Settings import SETTINGS 23 | from .GraphModel import GraphModel 24 | 25 | 26 | def init(size, **kwargs): 27 | """ Initialize a random tensor, normal(0,kwargs(SETTINGS.init_weights)). 28 | 29 | :param size: Size of the tensor 30 | :param kwargs: init_std=(SETTINGS.init_weights) Std of the initialized normal variable 31 | :return: Tensor 32 | """ 33 | init_std = kwargs.get('init_std', SETTINGS.init_weights) 34 | return tf.random_normal(shape=size, stddev=init_std) 35 | 36 | 37 | class CGNN_confounders_tf(object): 38 | def __init__(self, N, graph, run, idx, **kwargs): 39 | """ Build the tensorflow graph of the CGNN structure 40 | 41 | :param N: Number of points 42 | :param graph: Graph to be run 43 | :param run: number of the run (only for print) 44 | :param idx: number of the idx (only for print) 45 | :param kwargs: learning_rate=(SETTINGS.learning_rate) learning rate of the optimizer 46 | :param kwargs: h_layer_dim=(SETTINGS.h_layer_dim) Number of units in the hidden layer 47 | :param kwargs: use_Fast_MMD=(SETTINGS.use_Fast_MMD) use fast MMD option 48 | :param kwargs: nb_vectors_approx_MMD=(SETTINGS.nb_vectors_approx_MMD) nb vectors 49 | """ 50 | learning_rate = kwargs.get('learning_rate', SETTINGS.learning_rate) 51 | h_layer_dim = kwargs.get('h_layer_dim', SETTINGS.h_layer_dim) 52 | use_Fast_MMD = kwargs.get('use_Fast_MMD', SETTINGS.use_Fast_MMD) 53 | nb_vectors_approx_MMD = kwargs.get('nb_vectors_approx_MMD', SETTINGS.nb_vectors_approx_MMD) 54 | 55 | self.run = run 56 | self.idx = idx 57 | list_nodes = graph.skeleton.get_list_nodes() 58 | 59 | n_var = len(list_nodes) 60 | 61 | self.all_real_variables = tf.placeholder(tf.float32, shape=[None, n_var]) 62 | 63 | generated_variables = {} 64 | theta_G = [] 65 | 66 | list_edges = graph.skeleton.get_list_edges_without_duplicate() 67 | 68 | confounder_variables = {} 69 | for edge in list_edges: 70 | noise_variable = tf.random_normal([N, 1], mean=0, stddev=1) 71 | confounder_variables[edge[0],edge[1]] = noise_variable 72 | confounder_variables[edge[1],edge[0]] = noise_variable 73 | 74 | while len(generated_variables) < n_var: 75 | # Need to generate all variables in the graph using its parents : possible because of the DAG structure 76 | for var in list_nodes: 77 | # Check if all parents are generated 78 | par = graph.get_parents(var) 79 | if (var not in generated_variables and set(par).issubset(generated_variables)): 80 | 81 | neighboorhood = graph.skeleton.get_neighbors(var) 82 | 83 | # Generate the variable 84 | W_in = tf.Variable(init([len(par) + len(neighboorhood) + 1, h_layer_dim], **kwargs)) 85 | b_in = tf.Variable(init([h_layer_dim], **kwargs)) 86 | W_out = tf.Variable(init([h_layer_dim, 1], **kwargs)) 87 | b_out = tf.Variable(init([1], **kwargs)) 88 | 89 | input_v = [generated_variables[i] for i in par] 90 | input_v.append(tf.random_normal([N, 1], mean=0, stddev=1)) 91 | 92 | 93 | for i in neighboorhood: 94 | input_v.append(confounder_variables[i,var]) 95 | 96 | input_v = tf.concat(input_v, 1) 97 | 98 | out_v = tf.nn.relu(tf.matmul(input_v, W_in) + b_in) 99 | out_v = tf.matmul(out_v, W_out) + b_out 100 | 101 | generated_variables[var] = out_v 102 | theta_G.extend([W_in, b_in, W_out, b_out]) 103 | 104 | 105 | listvariablegraph = [] 106 | for var in list_nodes: 107 | listvariablegraph.append(generated_variables[var]) 108 | 109 | self.all_generated_variables = tf.concat(listvariablegraph, 1) 110 | 111 | if(use_Fast_MMD): 112 | self.G_dist_loss_xcausesy = Fourier_MMD_Loss_tf(self.all_real_variables, self.all_generated_variables,nb_vectors_approx_MMD) 113 | else: 114 | self.G_dist_loss_xcausesy = MMD_loss_tf(self.all_real_variables, self.all_generated_variables) 115 | 116 | self.G_solver_xcausesy = (tf.train.AdamOptimizer( 117 | learning_rate=learning_rate).minimize(self.G_dist_loss_xcausesy, 118 | var_list=theta_G)) 119 | 120 | config = tf.ConfigProto() 121 | config.gpu_options.allow_growth = True 122 | 123 | self.sess = tf.Session(config=config) 124 | self.sess.run(tf.global_variables_initializer()) 125 | 126 | def train(self, data, verbose=True, **kwargs): 127 | """ Train the initialized model 128 | 129 | :param data: data corresponding to the graph 130 | :param verbose: verbose 131 | :param kwargs: train_epochs=(SETTINGS.train_epochs) number of train epochs 132 | :return: None 133 | """ 134 | train_epochs = kwargs.get('train_epochs', SETTINGS.train_epochs) 135 | for it in range(train_epochs): 136 | 137 | _, G_dist_loss_xcausesy_curr = self.sess.run( 138 | [self.G_solver_xcausesy, self.G_dist_loss_xcausesy], 139 | feed_dict={self.all_real_variables: data} 140 | ) 141 | 142 | if verbose: 143 | if it % 500 == 0: 144 | print('Pair:{}, Run:{}, Iter:{}, score:{}'. 145 | format(self.idx, self.run, 146 | it, G_dist_loss_xcausesy_curr)) 147 | 148 | def evaluate(self, data, verbose=True, **kwargs): 149 | """ Test the model 150 | 151 | :param data: data corresponding to the graph 152 | :param verbose: verbose 153 | :param kwargs: test_epochs=(SETTINGS.test_epochs) number of test epochs 154 | :return: mean MMD loss value of the CGNN structure on the data 155 | """ 156 | test_epochs = kwargs.get('test_epochs', SETTINGS.test_epochs) 157 | sumMMD_tr = 0 158 | 159 | for it in range(test_epochs): 160 | 161 | MMD_tr = self.sess.run([self.G_dist_loss_xcausesy], feed_dict={ 162 | self.all_real_variables: data}) 163 | 164 | sumMMD_tr += MMD_tr[0] 165 | 166 | if verbose and it % 500 == 0: 167 | print('Pair:{}, Run:{}, Iter:{}, score:{}' 168 | .format(self.idx, self.run, it, MMD_tr[0])) 169 | 170 | tf.reset_default_graph() 171 | 172 | return sumMMD_tr / test_epochs 173 | 174 | def generate(self, data, **kwargs): 175 | 176 | generated_variables = self.sess.run([self.all_generated_variables], feed_dict={self.all_real_variables: data}) 177 | 178 | tf.reset_default_graph() 179 | return np.array(generated_variables)[0, :, :] 180 | 181 | 182 | def run_CGNN_confounders_tf(df_data, graph, idx=0, run=0, **kwargs): 183 | """ Execute the CGNN, by init, train and eval either on CPU or GPU 184 | 185 | :param df_data: data corresponding to the graph 186 | :param graph: Graph to be run 187 | :param run: number of the run (only for print) 188 | :param idx: number of the idx (only for print) 189 | :param kwargs: gpu=(SETTINGS.GPU) True if GPU is used 190 | :param kwargs: nb_gpu=(SETTINGS.nb_gpu) Number of available GPUs 191 | :param kwargs: gpu_offset=(SETTINGS.gpu_offset) number of gpu offsets 192 | :return: MMD loss value of the given structure after training 193 | """ 194 | gpu = kwargs.get('gpu', SETTINGS.GPU) 195 | nb_gpu = kwargs.get('nb_gpu', SETTINGS.NB_GPU) 196 | gpu_offset = kwargs.get('gpu_offset', SETTINGS.GPU_OFFSET) 197 | 198 | list_nodes = graph.skeleton.get_list_nodes() 199 | 200 | df_data = df_data[list_nodes].as_matrix() 201 | 202 | data = df_data.astype('float32') 203 | 204 | if (data.shape[0] > SETTINGS.max_nb_points): 205 | p = np.random.permutation(data .shape[0]) 206 | data = data[p[:int(SETTINGS.max_nb_points)],:] 207 | 208 | if gpu: 209 | with tf.device('/gpu:' + str(gpu_offset + run % nb_gpu)): 210 | model = CGNN_confounders_tf(data.shape[0], graph, run, idx, **kwargs) 211 | model.train(data, **kwargs) 212 | return model.evaluate(data, **kwargs) 213 | else: 214 | model = CGNN_confounders_tf(data, graph, run, idx, **kwargs) 215 | model.train(data, **kwargs) 216 | return model.evaluate(data, **kwargs) 217 | 218 | 219 | def hill_climbing_confounders(graph, data, run_cgnn_function, **kwargs): 220 | """ Optimize graph using CGNN with a hill-climbing algorithm 221 | 222 | :param graph: graph to optimize 223 | :param data: data 224 | :param run_cgnn_function: name of the CGNN function (depending on the backend) 225 | :param kwargs: nb_jobs=(SETTINGS.NB_JOBS) number of jobs 226 | :param kwargs: nb_runs=(SETTINGS.NB_RUNS) number of runs, of different evaluations 227 | :return: improved graph 228 | """ 229 | nb_jobs = kwargs.get("nb_jobs", SETTINGS.NB_JOBS) 230 | nb_runs = kwargs.get("nb_runs", SETTINGS.NB_RUNS) 231 | loop = 0 232 | tested_configurations = [graph.get_dict_nw()] 233 | improvement = True 234 | result_pairs = Parallel(n_jobs=nb_jobs)(delayed(run_cgnn_function)(data, graph, 0, run, **kwargs) for run in range(nb_runs)) 235 | 236 | score_network = np.mean([i for i in result_pairs if np.isfinite(i)]) 237 | score_network += SETTINGS.complexity_graph_param*len(graph.get_list_edges()) 238 | globalscore = score_network 239 | 240 | while improvement: 241 | 242 | loop += 1 243 | improvement = False 244 | list_edges_to_evaluate = graph.skeleton.get_list_edges_without_duplicate() 245 | 246 | for idx_pair in range(0,len(list_edges_to_evaluate)): 247 | 248 | edge = list_edges_to_evaluate[idx_pair] 249 | 250 | print(edge) 251 | print(graph.get_list_edges(return_weights=False)) 252 | ### If edge already oriented in the graph 253 | if([edge[0], edge[1]] in graph.get_list_edges(return_weights=False) or [edge[1], edge[0]] in graph.get_list_edges(return_weights=False)): 254 | 255 | if([edge[0], edge[1]] in graph.get_list_edges(return_weights=False)): 256 | node1 = edge[0] 257 | node2 = edge[1] 258 | else: 259 | node2 = edge[0] 260 | node1 = edge[1] 261 | 262 | #### Test reverse edge 263 | test_graph = deepcopy(graph) 264 | test_graph.reverse_edge(node1, node2) 265 | 266 | if (test_graph.is_cyclic() 267 | or test_graph.get_dict_nw() in tested_configurations): 268 | print("No evaluation for edge " + str(node1) + " -> " + str(node2)) 269 | else: 270 | print("Reverse Edge " + str(node1) + " -> " + str(node2) + " in evaluation") 271 | tested_configurations.append(test_graph.get_dict_nw()) 272 | result_pairs = Parallel(n_jobs=nb_jobs)(delayed(run_cgnn_function)(data, test_graph, idx_pair, run, **kwargs) for run in range(nb_runs)) 273 | 274 | score_network = np.mean([i for i in result_pairs if np.isfinite(i)]) 275 | score_network += SETTINGS.complexity_graph_param * len(test_graph.get_list_edges()) 276 | 277 | print("Current score : " + str(score_network)) 278 | print("Best score : " + str(globalscore)) 279 | 280 | if score_network < globalscore: 281 | graph.reverse_edge(node1, node2) 282 | improvement = True 283 | print("Edge " + str(node1) + "->" + str(node2) + " got reversed !") 284 | globalscore = score_network 285 | node = node1 286 | node1 = node2 287 | node2 = node 288 | 289 | #### Test suppression 290 | test_graph = deepcopy(graph) 291 | test_graph.remove_edge(node1, node2) 292 | 293 | if (test_graph.get_dict_nw() in tested_configurations): 294 | print("Removing already evaluated for edge " + str(node1) + " -> " + str(node2)) 295 | else: 296 | print("Removing edge " + str(node1) + " -> " + str(node2) + " in evaluation") 297 | 298 | tested_configurations.append(test_graph.get_dict_nw()) 299 | result_pairs = Parallel(n_jobs=nb_jobs)(delayed(run_cgnn_function)( 300 | data, test_graph, idx_pair, run, **kwargs) for run in range(nb_runs)) 301 | 302 | score_network = np.mean([i for i in result_pairs if np.isfinite(i)]) 303 | score_network += SETTINGS.complexity_graph_param * len(test_graph.get_list_edges()) 304 | 305 | print("Current score : " + str(score_network)) 306 | print("Best score : " + str(globalscore)) 307 | 308 | if score_network < globalscore: 309 | graph.remove_edge(node1, node2) 310 | improvement = True 311 | print("Edge " + str(node1) + " -> " + str(node2) + " got removed, possible confounder !") 312 | globalscore = score_network 313 | 314 | else: 315 | #We keep the edge and its score is set to (score_network - globalscore) 316 | print("Edge " + str(node1) + " -> " + str(node2) + " not removed. Score edge : " + str(score_network - globalscore)) 317 | graph.set_weight(node1, node2, score_network - globalscore) 318 | 319 | 320 | ### Eval if a suppressed edge need to be restored 321 | else: 322 | 323 | node1 = edge[0] 324 | node2 = edge[1] 325 | 326 | #### Test add edge sens node1 -> node2 327 | test_graph = deepcopy(graph) 328 | test_graph.add(node1, node2) 329 | 330 | score_network_add_edge_node1_node2 = 9999 331 | 332 | if (test_graph.is_cyclic() 333 | or test_graph.get_dict_nw() in tested_configurations): 334 | print("No addition possible for " + str(node1) + " -> " + str(node2)) 335 | else: 336 | print("Addition of edge " + str(node1) + " -> " + str(node2) + " in evaluation :") 337 | tested_configurations.append(test_graph.get_dict_nw()) 338 | result_pairs = Parallel(n_jobs=nb_jobs)(delayed(run_cgnn_function)( 339 | data, test_graph, idx_pair, run, **kwargs) for run in range(nb_runs)) 340 | 341 | score_network_add_edge_node1_node2 = np.mean([i for i in result_pairs if np.isfinite(i)]) 342 | score_network_add_edge_node1_node2 += SETTINGS.complexity_graph_param * len(test_graph.get_list_edges()) 343 | 344 | print("score network add edge " + str(node1) + " -> " + str(node2) + " : " + str(score_network_add_edge_node1_node2)) 345 | 346 | #### Test add edge sens node2 -> node1 347 | test_graph = deepcopy(graph) 348 | test_graph.add(node2, node1) 349 | 350 | score_network_add_edge_node2_node1 = 9999 351 | 352 | if (test_graph.is_cyclic() 353 | or test_graph.get_dict_nw() in tested_configurations): 354 | print("No addition possible for edge " + str(node2) + " -> " + str(node1)) 355 | else: 356 | print("Addition of edge " + str(node2) + " -> " + str(node1) + " in evaluation :") 357 | tested_configurations.append(test_graph.get_dict_nw()) 358 | result_pairs = Parallel(n_jobs=nb_jobs)(delayed(run_cgnn_function)( 359 | data, test_graph, idx_pair, run, **kwargs) for run in range(nb_runs)) 360 | 361 | score_network_add_edge_node2_node1 = np.mean([i for i in result_pairs if np.isfinite(i)]) 362 | score_network_add_edge_node2_node1 += SETTINGS.complexity_graph_param * len(test_graph.get_list_edges()) 363 | 364 | print("score network add edge " + str(node2) + " -> " + str(node1) + " : " + str(score_network_add_edge_node2_node1)) 365 | 366 | print("Best score : " + str(globalscore)) 367 | 368 | if score_network_add_edge_node1_node2 < globalscore and score_network_add_edge_node1_node2 < score_network_add_edge_node2_node1: 369 | score_edge = globalscore - score_network_add_edge_node1_node2 370 | graph.add(node1, node2, score_edge) 371 | improvement = True 372 | print("Edge " + str(node1) + " -> " + str(node2) + " is added with score : " + str(score_edge) + " !") 373 | globalscore = score_network_add_edge_node1_node2 374 | elif score_network_add_edge_node2_node1 < globalscore and score_network_add_edge_node2_node1 < score_network_add_edge_node1_node2: 375 | score_edge = globalscore - score_network_add_edge_node2_node1 376 | graph.add(node2, node1, score_edge) 377 | improvement = True 378 | print("Edge " + str(node2) + " -> " + str(node1) + " is added with score : " + str(score_edge) + " !") 379 | globalscore = score_network_add_edge_node2_node1 380 | else : 381 | print("Edge not added, possible confounder " + str(node1) + " <-> " + str(node2)) 382 | 383 | return graph 384 | 385 | 386 | def exploratory_hill_climbing(graph, data, run_cgnn_function, **kwargs): 387 | """ Optimize graph using CGNN with a hill-climbing algorithm 388 | 389 | :param graph: graph to optimize 390 | :param data: data 391 | :param run_cgnn_function: name of the CGNN function (depending on the backend) 392 | :param kwargs: nb_jobs=(SETTINGS.NB_JOBS) number of jobs 393 | :param kwargs: nb_runs=(SETTINGS.NB_RUNS) number of runs, of different evaluations 394 | :return: improved graph 395 | """ 396 | nb_jobs = kwargs.get("nb_jobs", SETTINGS.NB_JOBS) 397 | nb_runs = kwargs.get("nb_runs", SETTINGS.NB_RUNS) 398 | 399 | nb_loops = 150 400 | exploration_factor = 10 # Average of number of edges to reverse at the beginning. 401 | assert exploration_factor < len(graph.get_list_edges()) 402 | 403 | loop = 0 404 | tested_configurations = [graph.get_dict_nw()] 405 | result_pairs = Parallel(n_jobs=nb_jobs)(delayed(run_cgnn_function)( 406 | data, graph, 0, run, **kwargs) for run in range(nb_runs)) 407 | 408 | score_network = np.mean([i for i in result_pairs if np.isfinite(i)]) 409 | globalscore = score_network 410 | 411 | print("Graph score : " + str(globalscore)) 412 | 413 | while loop < nb_loops: 414 | loop += 1 415 | list_edges = graph.get_list_edges() 416 | 417 | possible_solution=False 418 | while not possible_solution: 419 | test_graph = deepcopy(graph) 420 | selected_edges = np.random.choice(len(list_edges), 421 | max(int(exploration_factor * ((nb_loops-loop)/nb_loops)**2), 1)) 422 | for edge in list_edges[selected_edges]: 423 | test_graph.reverse_edge() 424 | if not (test_graph.is_cyclic() 425 | or test_graph.get_dict_nw() in tested_configurations): 426 | possible_solution = True 427 | 428 | print('Reversed Edges {} in evaluation :'.format(list_edges[selected_edges])) 429 | tested_configurations.append(test_graph.get_dict_nw()) 430 | result_pairs = Parallel(n_jobs=nb_jobs)(delayed(run_cgnn_function)( 431 | data, test_graph, loop, run, **kwargs) for run in range(nb_runs)) 432 | 433 | score_network = np.mean([i for i in result_pairs if np.isfinite(i)]) 434 | 435 | print("Current score : " + str(score_network)) 436 | print("Best score : " + str(globalscore)) 437 | 438 | if score_network < globalscore: 439 | graph.reverse_edge(edge[0], edge[1]) 440 | print('Edge {} got reversed !'.format(list_edges[selected_edges])) 441 | globalscore = score_network 442 | 443 | return graph 444 | 445 | 446 | def tabu_search(graph, data, run_cgnn_function, **kwargs): 447 | """ Optimize graph using CGNN with a hill-climbing algorithm 448 | 449 | :param graph: graph to optimize 450 | :param data: data 451 | :param run_cgnn_function: name of the CGNN function (depending on the backend) 452 | :param kwargs: nb_jobs=(SETTINGS.NB_JOBS) number of jobs 453 | :param kwargs: nb_runs=(SETTINGS.NB_RUNS) number of runs, of different evaluations 454 | :return: improved graph 455 | """ 456 | nb_jobs = kwargs.get("nb_jobs", SETTINGS.NB_JOBS) 457 | nb_runs = kwargs.get("nb_runs", SETTINGS.NB_RUNS) 458 | raise ValueError('Not Yet Implemented') 459 | 460 | 461 | class CGNN_confounders(GraphModel): 462 | """ 463 | CGNN Model ; Using generative models, generate the whole causal graph and improve causal 464 | direction predictions in the graph. 465 | """ 466 | 467 | def __init__(self, backend='PyTorch'): 468 | """ Initialize the CGNN Model. 469 | 470 | :param backend: Choose the backend to use, either 'PyTorch' or 'TensorFlow' 471 | """ 472 | super(CGNN_confounders, self).__init__() 473 | self.backend = backend 474 | 475 | if self.backend == 'TensorFlow': 476 | self.infer_graph = run_CGNN_confounders_tf 477 | elif self.backend == 'PyTorch': 478 | self.infer_graph = run_CGNN_th 479 | else: 480 | print('No backend known as {}'.format(self.backend)) 481 | raise ValueError 482 | 483 | def create_graph_from_data(self, data): 484 | print("The CGNN model is not able (yet?) to model the graph directly from raw data") 485 | raise ValueError 486 | 487 | def orient_directed_graph(self, data, dag, alg='HC', **kwargs): 488 | """ Improve a directed acyclic graph using CGNN 489 | 490 | :param data: data 491 | :param dag: directed acyclic graph to optimize 492 | :param alg: type of algorithm 493 | :param log: Save logs of the execution 494 | :return: improved directed acyclic graph 495 | """ 496 | data = DataFrame(scale(data.as_matrix()), columns=data.columns) 497 | alg_dic = {'HC': hill_climbing_confounders, 'tabu': tabu_search, 'EHC': exploratory_hill_climbing} 498 | return alg_dic[alg](dag, data, self.infer_graph, **kwargs) 499 | 500 | def orient_undirected_graph(self, data, umg, **kwargs): 501 | """ Orient the undirected graph using GNN and apply CGNN to improve the graph 502 | 503 | :param data: data 504 | :param umg: undirected acyclic graph 505 | :return: directed acyclic graph 506 | """ 507 | 508 | warnings.warn("The pairwise GNN model is computed on each edge of the UMG " 509 | "to initialize the model and start CGNN with a DAG") 510 | gnn = GNN(backend=self.backend, **kwargs) 511 | dag = gnn.orient_graph(data, umg, **kwargs) # Pairwise method 512 | return self.orient_directed_graph(data, dag, **kwargs) 513 | --------------------------------------------------------------------------------