| import numpy as np |
| import torch |
| import logging |
| import pytz |
| import random |
| import os |
| import yaml |
| import shutil |
| from datetime import datetime |
| try: |
| from ogb.nodeproppred import Evaluator |
| except ImportError: |
| Evaluator = None |
| try: |
| from dgl import function as fn |
| except ImportError: |
| fn = None |
|
|
| CPF_data = ["cora", "citeseer", "pubmed", "a-computer", "a-photo"] |
| OGB_data = ["ogbn-arxiv", "ogbn-products"] |
| NonHom_data = ["pokec", "penn94"] |
| BGNN_data = ["house_class", "vk_class"] |
| CORE_ELEMENTS = {"5", "6", "7", "8", "14", "15", "16"} |
| CBDICT = { |
| '1_0_1_0_0_0': 34, |
| '5_-1_4_0_0_0': 1, |
| '5_-1_4_0_1_0': 2, |
| '5_0_3_0_0_0': 11, |
| '5_0_3_0_1_0': 8, |
| '6_-1_2_0_0_0': 1, |
| '6_0_2_0_0_0': 511, |
| '6_0_2_0_1_0': 3, |
| '6_0_3_0_0_0': 7687, |
| '6_0_3_0_1_0': 3068, |
| '6_0_3_1_1_0': 75098, |
| '6_0_4_0_0_0': 26727, |
| '6_0_4_0_1_0': 20629, |
| '6_1_3_0_0_0': 1, |
| '6_1_3_1_1_0': 1, |
| '7_-1_3_0_0_0': 14, |
| '7_-1_3_0_1_0': 1, |
| '7_-1_3_1_1_0': 1, |
| '7_-1_4_0_0_0': 1, |
| '7_0_2_0_0_0': 310, |
| '7_0_3_0_0_0': 5858, |
| '7_0_3_0_1_0': 2597, |
| '7_0_3_1_1_0': 8356, |
| '7_0_4_0_0_0': 1043, |
| '7_0_4_0_1_0': 1333, |
| '7_1_2_0_0_0': 14, |
| '7_1_3_0_0_0': 236, |
| '7_1_3_0_1_0': 8, |
| '7_1_3_1_1_0': 80, |
| '7_1_4_0_0_0': 92, |
| '7_1_4_0_1_0': 51, |
| '8_-1_3_0_0_0': 280, |
| '8_-1_4_0_0_0': 42, |
| '8_0_3_0_0_0': 15342, |
| '8_0_3_0_1_0': 676, |
| '8_0_3_1_1_0': 705, |
| '8_0_4_0_0_0': 2287, |
| '8_0_4_0_1_0': 789, |
| '8_1_3_0_1_0': 1, |
| '8_1_3_1_1_0': 1, |
| '9_0_4_0_0_0': 2589, |
| '14_0_4_0_0_0': 8, |
| '14_0_4_0_1_0': 1, |
| '15_0_3_0_0_0': 1, |
| '15_0_3_1_1_0': 1, |
| '15_0_4_0_0_0': 137, |
| '15_0_4_0_1_0': 9, |
| '15_0_6_0_0_0': 1, |
| '15_0_6_0_1_0': 1, |
| '15_1_4_0_0_0': 2, |
| '15_1_4_0_1_0': 1, |
| '16_-1_3_0_0_0': 1, |
| '16_-1_4_0_0_0': 1, |
| '16_0_3_0_0_0': 105, |
| '16_0_3_0_1_0': 1, |
| '16_0_3_1_1_0': 714, |
| '16_0_4_0_0_0': 1157, |
| '16_0_4_0_1_0': 225, |
| '16_0_6_0_0_0': 1, |
| '16_0_6_0_1_0': 1, |
| '16_0_7_0_0_0': 2, |
| '16_1_3_0_0_0': 1, |
| '16_1_3_1_1_0': 2, |
| '16_1_4_0_0_0': 17, |
| '16_1_4_0_1_0': 5, |
| '17_0_4_0_0_0': 1415, |
| '34_0_3_0_0_0': 1, |
| '34_0_3_1_1_0': 3, |
| '34_0_4_0_0_0': 5, |
| '34_0_4_0_1_0': 1, |
| '34_1_3_1_1_0': 1, |
| '34_1_4_0_0_0': 1, |
| '35_0_4_0_0_0': 296, |
| '53_0_4_0_0_0': 48, |
| '53_1_4_0_0_0': 1, |
| '53_1_4_0_1_0': 1, |
| } |
|
|
|
|
|
|
| def set_seed(seed): |
| torch.manual_seed(seed) |
| np.random.seed(seed) |
| random.seed(seed) |
| torch.backends.cudnn.benchmark = False |
| torch.backends.cudnn.deterministic = True |
| if torch.cuda.is_available(): |
| torch.cuda.manual_seed_all(seed) |
|
|
|
|
| def get_training_config(config_path, model_name, dataset): |
| with open(config_path, "r") as conf: |
| full_config = yaml.load(conf, Loader=yaml.FullLoader) |
| dataset_specific_config = full_config["global"] |
| model_specific_config = full_config[dataset][model_name] |
|
|
| if model_specific_config is not None: |
| specific_config = dict(dataset_specific_config, **model_specific_config) |
| else: |
| specific_config = dataset_specific_config |
|
|
| specific_config["model_name"] = model_name |
| return specific_config |
|
|
|
|
| def check_writable(path, overwrite=True): |
| if not os.path.exists(path): |
| os.makedirs(path) |
| elif overwrite: |
| shutil.rmtree(path) |
| os.makedirs(path) |
| else: |
| pass |
|
|
|
|
| def check_readable(path): |
| if not os.path.exists(path): |
| raise ValueError(f"No such file or directory! {path}") |
|
|
|
|
| def timetz(*args): |
| tz = pytz.timezone("US/Pacific") |
| return datetime.now(tz).timetuple() |
|
|
| def get_logger(filename, console_log=False, log_level=logging.INFO): |
| logger = logging.getLogger(f"logger_{filename}") |
| logger.propagate = False |
| logger.setLevel(log_level) |
|
|
| |
| if not logger.handlers: |
| file_handler = logging.FileHandler(filename, mode="a") |
| formatter = logging.Formatter("%(asctime)s: %(message)s", datefmt="%b%d %H-%M-%S") |
| file_handler.setFormatter(formatter) |
| logger.addHandler(file_handler) |
|
|
| if console_log: |
| console_handler = logging.StreamHandler() |
| console_handler.setFormatter(formatter) |
| logger.addHandler(console_handler) |
|
|
| return logger |
|
|
|
|
|
|
| def idx_split(idx, ratio, seed=0, train_or_infer=None): |
| """ |
| randomly split idx into two portions with ratio% elements and (1 - ratio)% elements |
| """ |
| set_seed(seed) |
| n = len(idx) |
| cut = int(n * ratio) |
| |
| if train_or_infer == "train": |
| idx_idx_shuffle = torch.randperm(n) |
| idx1_idx, idx2_idx = idx_idx_shuffle[:cut], idx_idx_shuffle[cut:] |
| elif train_or_infer == "infer": |
| idx_idx_list = list(range(n)) |
| idx1_idx, idx2_idx = idx_idx_list[:cut], idx_idx_list[cut:] |
| idx1, idx2 = idx[idx1_idx], idx[idx2_idx] |
| |
| return idx1, idx2 |
|
|
|
|
| def graph_split(idx_train, idx_val, idx_test, rate, seed, train_or_infer): |
| """ |
| Args: |
| The original setting was transductive. Full graph is observed, and idx_train takes up a small portion. |
| Split the graph by further divide idx_test into [idx_test_tran, idx_test_ind]. |
| rate = idx_test_ind : idx_test (how much test to hide for the inductive evaluation) |
| |
| Ex. Ogbn-products |
| loaded : train : val : test = 8 : 2 : 90, rate = 0.2 |
| after split: train : val : test_tran : test_ind = 8 : 2 : 72 : 18 |
| |
| Return: |
| Indices start with 'obs_' correspond to the node indices within the observed subgraph, |
| where as indices start directly with 'idx_' correspond to the node indices in the original graph |
| """ |
| idx_test_ind, idx_test_tran = idx_split(idx_test, rate, seed, train_or_infer) |
|
|
| idx_obs = torch.cat([idx_train, idx_val]) |
| N1, N2 = idx_train.shape[0], idx_val.shape[0] |
| obs_idx_all = torch.arange(idx_obs.shape[0]) |
| obs_idx_train = obs_idx_all[:N1] |
| obs_idx_val = obs_idx_all[N1 : N1 + N2] |
| obs_idx_test = idx_test |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| idx_test_ind = torch.tensor(list(range(N1 + N2 + N2, N1 + N2 + N2 + N2 + 1))) |
| return obs_idx_train, obs_idx_val, obs_idx_test, obs_idx_all, idx_test_ind |
|
|
|
|
| def get_evaluator(dataset): |
| if dataset in CPF_data + NonHom_data + BGNN_data: |
|
|
| def evaluator(out, labels): |
| pred = out.argmax(1) |
| return pred.eq(labels).float().mean().item() |
|
|
| elif dataset in OGB_data: |
| ogb_evaluator = Evaluator(dataset) |
|
|
| def evaluator(out, labels): |
| pred = out.argmax(1, keepdim=True) |
| input_dict = {"y_true": labels.unsqueeze(1), "y_pred": pred} |
| return ogb_evaluator.eval(input_dict)["acc"] |
|
|
| else: |
| raise ValueError("Unknown dataset") |
|
|
| return evaluator |
|
|
|
|
| def get_evaluator(dataset): |
| def evaluator(out, labels): |
| pred = out.argmax(1) |
| return pred.eq(labels).float().mean().item() |
|
|
| return evaluator |
|
|
|
|
| def compute_min_cut_loss(g, out): |
| out = out.to("cpu") |
| g = g.to("cpu") |
| S = out.exp() |
| A = g.adj().to_dense() |
| D = g.in_degrees().float().diag() |
| print(S.device, A.device, D.device) |
| min_cut = ( |
| torch.matmul(torch.matmul(S.transpose(1, 0), A), S).trace() |
| / torch.matmul(torch.matmul(S.transpose(1, 0), D), S).trace() |
| ) |
| return min_cut.item() |
|
|
|
|
| def feature_prop(feats, g, k): |
| """ |
| Augment node feature by propagating the node features within k-hop neighborhood. |
| The propagation is done in the SGC fashion, i.e. hop by hop and symmetrically normalized by node degrees. |
| """ |
| assert feats.shape[0] == g.num_nodes() |
|
|
| degs = g.in_degrees().float().clamp(min=1) |
| norm = torch.pow(degs, -0.5).unsqueeze(1) |
|
|
| |
| for _ in range(k): |
| feats = feats * norm |
| g.ndata["h"] = feats |
| g.update_all(fn.copy_u("h", "m"), fn.sum("m", "h")) |
| feats = g.ndata.pop("h") |
| feats = feats * norm |
|
|
| return feats |
|
|