├── .gitignore ├── LICENSE ├── README.md ├── __init__.py ├── configs ├── cogsl_config.yaml ├── dagnn_config.yaml ├── gcn_config.yaml ├── gen_config.yaml ├── gprgnn_config.yaml ├── grcn_config.yaml ├── gsr_config.yaml ├── gtn_config.yaml ├── han_config.yaml ├── hesgsl_config.yaml ├── hgpsl_config.yaml ├── hgsl_config.yaml ├── idgl_config.yaml ├── lds_config.yaml ├── mlp_config.yaml ├── neuralsparse_config.yaml ├── nodeformer_config.yaml ├── prognn_config.yaml ├── ptdnet_config.yaml ├── slaps_config.yaml ├── stable_config.yaml ├── sublime_config.yaml └── vibgsl_config.yaml ├── data ├── __init__.py ├── dataset.py ├── graph_dataset.py └── hetero_dataset.py ├── encoder ├── __init__.py ├── dagnn.py ├── gat.py ├── gcn.py ├── gin.py ├── gprgnn.py ├── graphsage.py ├── metamodule.py ├── mlp.py └── nodeformer_conv.py ├── eval ├── __init__.py └── eval_cls.py ├── experiment.py ├── generate_attack.py ├── hyperparams └── idgl_hyper.yaml ├── learner ├── __init__.py ├── attention_learner.py ├── base_learner.py ├── full_param_learner.py ├── gnn_learner.py ├── hadamard_prod_learner.py ├── mlp_learner.py └── sparsification_learner.py ├── logo.jpeg ├── main.py ├── metric.py ├── model ├── __init__.py ├── base_gsl.py ├── baselines.py ├── cogsl.py ├── gen.py ├── grcn.py ├── gsr.py ├── gtn.py ├── han.py ├── hesgsl.py ├── hgpsl.py ├── hgsl.py ├── idgl.py ├── lds.py ├── neuralsparse.py ├── nodeformer.py ├── prognn.py ├── ptdnet.py ├── slaps.py ├── stable.py ├── sublime.py └── vibgsl.py ├── pipeline.png ├── processor.py ├── pyproject.toml ├── setup.cfg ├── setup.py └── utils.py /.gitignore: -------------------------------------------------------------------------------- 1 | out/ 2 | *__pycache__ 3 | .vscode/ 4 | .idea/ 5 | test* 6 | results* 7 | dist/ 8 | *.egg-info/ 9 | temp* 10 | *.pkl 11 | *.npz 12 | *.npy 13 | *.allx 14 | *.graph 15 | *.index 16 | *.ally 17 | *.tx 18 | *.ty 19 | *.x 20 | *.y 21 | gslb_data/IMDB-BINARY_3673667b/tu_IMDB-BINARY.bin 22 | gslb_data/IMDB-BINARY_3673667b/IMDB-BINARY/IMDB-BINARY_graph_labels.txt 23 | gslb_data/IMDB-BINARY_3673667b/IMDB-BINARY/IMDB-BINARY_graph_indicator.txt 24 | gslb_data/IMDB-BINARY_3673667b/IMDB-BINARY/IMDB-BINARY_A.txt 25 | gslb_data/IMDB-BINARY.zip 26 | gslb_data/ENZYMES.zip 27 | gslb_data/ENZYMES_67bfdeff/ENZYMES/ENZYMES_A.txt 28 | gslb_data/ENZYMES_67bfdeff/ENZYMES/ENZYMES_graph_labels.txt 29 | gslb_data/ENZYMES_67bfdeff/ENZYMES/ENZYMES_graph_indicator.txt 30 | gslb_data/ENZYMES_67bfdeff/ENZYMES/ENZYMES_node_attributes.txt 31 | gslb_data/ENZYMES_67bfdeff/ENZYMES/ENZYMES_node_labels.txt 32 | gslb_data/ENZYMES_67bfdeff/ENZYMES/README.txt 33 | gslb_data/ENZYMES_67bfdeff/tu_ENZYMES.bin 34 | gslb_data/Mutagenicity.zip 35 | gslb_data/Mutagenicity_0de0f646/Mutagenicity/Mutagenicity_A.txt 36 | gslb_data/Mutagenicity_0de0f646/Mutagenicity/Mutagenicity_edge_labels.txt 37 | gslb_data/Mutagenicity_0de0f646/Mutagenicity/Mutagenicity_graph_indicator.txt 38 | gslb_data/Mutagenicity_0de0f646/Mutagenicity/Mutagenicity_graph_labels.txt 39 | *.txt 40 | *.bin 41 | *.zip 42 | *.txt~ 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 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 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | logo 3 |

4 | 5 | # Graph Structure Learning Benchmark (GSLB) 6 | 7 | GSLB is a Graph Structure Learning (GSL) library and benchmark based on [DGL](https://github.com/dmlc/dgl) and [PyTorch](https://pytorch.org/). We integrate diverse datasets and state-of-the-art GSL models. 8 | 9 | ## 📔 What is Graph Structure Learning? 10 | 11 |

12 | pipeline 13 |

14 | 15 | Graph Structure Learning (GSL) aims to optimize both the parameters of Graph Neural Networks (GNNs) and the computation graph structure simultaneously. GSL methods start with input features and an optimal initial graph structure. Its corresponding computation graph is iteratively refined through a structure learning module. With the refined computation graph ,GNNs are used to generate graph representations. Then parameters of the GNNs and the structure modeling module are jointly updated, either simultaneously or alternatively, util a preset stopping condition is satisfied. 16 | 17 | If you want to explore more information about GSL, please refer to our [Paper](https://arxiv.org/abs/2310.05174), [survey](https://www.researchgate.net/profile/Yanqiao-Zhu/publication/349787551_Deep_Graph_Structure_Learning_for_Robust_Representations_A_Survey/links/6137188538818c2eaf885a3f/Deep-Graph-Structure-Learning-for-Robust-Representations-A-Survey.pdf), and [paper collection](https://github.com/GSL-Benchmark/Awesome-Graph-Structure-Learning). 18 | 19 | ## 🚀 Get Start 20 | 21 | ### Requirements 22 | 23 | GSLB needs the following requirements to be satisfied beforehand: 24 | 25 | * Python 3.8+ 26 | * PyTorch 1.13 27 | * DGL 1.1+ 28 | * Scipy 1.9+ 29 | * Scikit-learn 30 | * Numpy 31 | * NetworkX 32 | * ogb 33 | * tqdm 34 | * easydict 35 | * PyYAML 36 | * DeepRobust 37 | 38 | ### Installation via PyPI 39 | 40 | To install GSLB with [`pip`](https://pip.pypa.io/en/stable/), simply run: 41 | 42 | ``` 43 | pip install GSLB 44 | ``` 45 | 46 | Then, you can import `GSL` from your current environment. 47 | 48 | ## Usage 49 | 50 | If you want to quickly run an existing GSL model on a graph dataset: 51 | 52 | ```python 53 | python main.py --dataset dataset_name --model model_name --num_trails --gpu_num 0 --use_knn --k 5 --use_mettack --sparse --metric acc --ptb_rate 0. --drop_rate 0. --add_rate 0. 54 | ``` 55 | 56 | Optional arguments: 57 | 58 | ``--dataset`` : the name of graph dataset 59 | 60 | ``--model`` : the name of GSL model 61 | 62 | ``--ntrail`` : repetition count of experiments 63 | 64 | ``--use_knn`` : whether to use knn graph instead of the original graph 65 | 66 | ``--k`` : the number of the nearest neighbors 67 | 68 | ``--drop_rate`` : the probability of randomly edge deletion 69 | 70 | ``--add_rate`` : the probability of randomly edge addition 71 | 72 | ``--mask_feat_rate`` : the probability of randomly mask features 73 | 74 | ``--use_mettack`` : whether to use the structure after being attacked by mettack 75 | 76 | ``--ptb_rate`` : the perturbation rate 77 | 78 | ``--metric`` : the evaluation metric 79 | 80 | ``--gpu_num`` : the selected GPU number 81 | 82 | *Example: Train GRCN on Cora dataset, with the evaluation metric is accuracy.* 83 | 84 | ``` 85 | python main.py --dataset cora --model GRCN --metric acc 86 | ``` 87 | 88 | *If you want to quickly generate a perturbed graph by Mettack:* 89 | 90 | ``` 91 | python generate_attack.py --dataset cora --ptb_rate 0.05 92 | ``` 93 | 94 | Step 1: Load datasets 95 | 96 | ```python 97 | from GSL.data import * 98 | 99 | # load a homophilic or heterophilic graph dataset 100 | data = Dataset(root='/tmp/', name='cora') 101 | 102 | # load a perturbed graph dataset 103 | data = Dataset(root='/tmp/', name='cora', use_mettack=True, ptb_rate=0.05) 104 | 105 | # load a heterogeneous graph dataset 106 | data = HeteroDataset(root='/tmp/', name='acm') 107 | 108 | # load a graph-level dataset 109 | data = GraphDataset(root='/tmp/', name='IMDB-BINARY', model='GCN') 110 | ``` 111 | 112 | Step 2: Initialize the GSL model 113 | 114 | ```python 115 | from GSL.model import * 116 | from GSL.utils import accuracy, macro_f1, micro_f1 117 | 118 | model_name = 'GRCN' 119 | metric = 'acc' 120 | device = torch.device("cuda" if torch.cuda.is_available() else "cpu") 121 | 122 | # the hyper-parameters are recorded in config 123 | config_path = './configs/{}_config.yaml'.format(model_name.lower()) 124 | 125 | # select a evaluation metric 126 | eval_metric = { 127 | 'acc': accuracy, 128 | 'macro-f1': macro_f1, 129 | 'micro-f1': micro_f1 130 | }[metric] 131 | 132 | model = GRCN(data.num_feat, data.num_class, evel_metric, 133 | config_path, dataset_name, device) 134 | ``` 135 | 136 | Step 3: Train GSL model 137 | 138 | ```python 139 | model.fit(data) 140 | ``` 141 | 142 | ## 🧩 Implementation Algorithms 143 | 144 | Currently, we have implemented the following GSL algorithms: 145 | 146 | | Algorithm | Conference/Journal | Category | Paper | Code | 147 | | --------- | ------------------ | -------- | ----- | ---- | 148 | | LDS | ICML 2019 | Homogeneous GSL | [Learning Discrete Structures for Graph Neural Networks](http://proceedings.mlr.press/v97/franceschi19a/franceschi19a.pdf) | [Link](https://github.com/lucfra/LDS-GNN) | 149 | | GRCN | ECML PKDD 2020 | Homogeneous GSL | [Graph-Revised Convolutional Network](https://arxiv.org/pdf/1911.07123.pdf) | [Link](https://github.com/PlusRoss/GRCN) | 150 | | ProGNN | KDD 202 | Homogeneous GSL | [Graph structure learning for robust graph neural networks](https://dl.acm.org/doi/pdf/10.1145/3394486.3403049) | [Link](https://github.com/ChandlerBang/Pro-GNN) | 151 | | IDGL | NeurIPS 2020 | Homogeneous GSL | [Iterative Deep Graph Learning for Graph Neural Networks: Better and Robust Node Embeddings](https://proceedings.neurips.cc/paper/2020/file/e05c7ba4e087beea9410929698dc41a6-Paper.pdf) | [Link](https://github.com/hugochan/IDGL) | 152 | | GEN | WWW 2021 | Homogeneous GSL | [Graph Structure Estimation Neural Networks](https://dl.acm.org/doi/pdf/10.1145/3442381.3449952?casa_token=Ac8pftvrgv0AAAAA:Ka_mklQVpQmYfhNVB-r66cf6fFsCdy8jyVKGFvzC1q5Ko5DbQQqci_3vopigN0jzTDlWiL8L8Q) | [Link](https://github.com/BUPT-GAMMA/Graph-Structure-Estimation-Neural-Networks) | 153 | | CoGSL | WWW 2022 | Homogeneous GSL | [Compact Graph Structure Learning via Mutual Information Compression](https://dl.acm.org/doi/pdf/10.1145/3485447.3512206?casa_token=lyWPk8kyFzwAAAAA:HLmbgpzrKe17LbnQqNh2zI_6WOvNgm_VqfNAgEoqLSXR7rRm_Bzro1oNzETTQb63W9vcVlijNw) | [Link](https://github.com/liun-online/CoGSL) | 154 | | SLAPS | NeurIPS 2021 | Homogeneous GSL | [SLAPS: Self-Supervision Improves Structure Learning for Graph Neural Networks](https://proceedings.neurips.cc/paper/2021/file/bf499a12e998d178afd964adf64a60cb-Paper.pdf) | [Link](https://github.com/BorealisAI/SLAPS-GNN) | 155 | | SUBLIME | WWW 2022 | Homogeneous GSL | [Towards Unsupervised Deep Graph Structure Learning](https://dl.acm.org/doi/pdf/10.1145/3485447.3512186?casa_token=445ECOqpVh4AAAAA:3ZBlGDSLFxrhwN0zEUdGFMpB4DslsI4h-rFLvI3cWHNzNsx6k-4m2t-NiDLubRvw1tBLaziISw) | [Link](https://github.com/GRAND-Lab/SUBLIME) | 156 | | STABLE | KDD 2022 | Homogeneous GSL | [Reliable Representations Make A Stronger Defender: Unsupervised Structure Refinement for Robust GNN](https://dl.acm.org/doi/abs/10.1145/3534678.3539484) | [Link](https://github.com/likuanppd/STABLE) | 157 | | NodeFormer | NeurIPS 2022 | Homogeneous GSL | [NodeFormer: A Scalable Graph Structure Learning Transformer for Node Classification](https://openreview.net/forum?id=sMezXGG5So) | [Link](https://github.com/qitianwu/NodeFormer) | 158 | | HES-GSL | TNNLS 2023 | Homogeneous GSL | [Homophily-Enhanced Self-Supervision for Graph Structure Learning: Insights and Directions](https://ieeexplore.ieee.org/abstract/document/10106110) | [Link](https://github.com/LirongWu/Homophily-Enhanced-Self-supervision) | 159 | | GSR | WSDM 2023 | Homogeneous GSL | [Self-Supervised Graph Structure Refinement for Graph Neural Networks](https://dl.acm.org/doi/abs/10.1145/3539597.3570455) | [Link](https://github.com/andyjzhao/WSDM23-GSR) | 160 | | GTN | NeurIPS 2020 | Heterogeneous GSL | [Graph Transformer Networks](https://proceedings.neurips.cc/paper_files/paper/2019/file/9d63484abb477c97640154d40595a3bb-Paper.pdf) | [Link](https://github.com/seongjunyun/Graph_Transformer_Networks) | 161 | | HGSL | AAAI 2021 | Heterogeneous GSL | [Heterogeneous Graph Structure Learning for Graph Neural Networks](https://ojs.aaai.org/index.php/AAAI/article/view/16600) | [Link](https://github.com/AndyJZhao/HGSL) | 162 | | HGP-SL | AAAI 2020 | Graph-level GSL | [Hierarchical Graph Pooling with Structure Learning](https://arxiv.org/abs/1911.05954) | [Link](https://github.com/cszhangzhen/HGP-SL) | 163 | | VIB-GSL | AAAI 2022 | Graph-level GSL | [Graph Structure Learning with Variational Information Bottleneck](https://ojs.aaai.org/index.php/AAAI/article/view/20335) | [Link](https://github.com/RingBDStack/VIB-GSL) | 164 | 165 | 166 | ## Cite Us 167 | 168 | Feel free to cite this work if you find it useful to you! 169 | ``` 170 | @article{li2023gslb, 171 | title={GSLB: The Graph Structure Learning Benchmark}, 172 | author={Li, Zhixun and Wang, Liang and Sun, Xin and Luo, Yifan and Zhu, Yanqiao and Chen, Dingshuo and Luo, Yingtao and Zhou, Xiangxin and Liu, Qiang and Wu, Shu and others}, 173 | journal={arXiv preprint arXiv:2310.05174}, 174 | year={2023} 175 | } 176 | ``` 177 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | import GSL.learner 2 | import GSL.metric 3 | import GSL.processor 4 | import GSL.utils 5 | 6 | __all__ = [ 7 | 'learner', 8 | 'metric', 9 | 'processor', 10 | 'utils' 11 | ] -------------------------------------------------------------------------------- /configs/cogsl_config.yaml: -------------------------------------------------------------------------------- 1 | Default: 2 | seed: 0 3 | adj_hop: 1 4 | 5 | cora: 6 | big: False 7 | dataset: cora 8 | batch: 0 9 | gpu: 2 10 | seed: 3 11 | name_view1: v1_adj 12 | name_view2: v2_diff 13 | indice_view1: v1_2 14 | indice_view2: v2_40 15 | cls_hid_1: 16 16 | com_lambda_v1: 0.1 17 | com_lambda_v2: 0.1 18 | gen_hid: 32 19 | lam: 0.5 20 | alpha: 0.1 21 | mi_hid_1: 128 22 | cls_lr: 0.01 23 | cls_weight_decay: 0.0005 24 | cls_dropout: 0.5 25 | ve_lr: 0.001 26 | ve_weight_decay: 0.0 27 | ve_dropout: 0.2 28 | mi_lr: 0.01 29 | mi_weight_decay: 0 30 | main_epoch: 200 31 | inner_ne_epoch: 5 32 | inner_cls_epoch: 5 33 | inner_mi_epoch: 10 34 | temp_r: 0.0001 35 | cls_coe: 0.3 36 | mi_coe: 0.3 37 | tau: 0.8 38 | add: False 39 | dele: False 40 | ptb_feat: False 41 | ratio: 0.0 42 | flag: 1 43 | 44 | citeseer: 45 | big: False 46 | dataset: citeseer 47 | batch: 0 48 | gpu: 2 49 | seed: 3 50 | name_view1: v1_adj 51 | name_view2: v2_diff 52 | indice_view1: v1_2 53 | indice_view2: v2_40 54 | cls_hid_1: 16 55 | com_lambda_v1: 0.1 56 | com_lambda_v2: 0.1 57 | gen_hid: 32 58 | lam: 0.5 59 | alpha: 0.1 60 | mi_hid_1: 128 61 | cls_lr: 0.01 62 | cls_weight_decay: 0.0005 63 | cls_dropout: 0.5 64 | ve_lr: 0.001 65 | ve_weight_decay: 0.0 66 | ve_dropout: 0.2 67 | mi_lr: 0.01 68 | mi_weight_decay: 0 69 | main_epoch: 200 70 | inner_ne_epoch: 5 71 | inner_cls_epoch: 5 72 | inner_mi_epoch: 10 73 | temp_r: 0.0001 74 | cls_coe: 0.3 75 | mi_coe: 0.3 76 | tau: 0.8 77 | add: False 78 | dele: False 79 | ptb_feat: False 80 | ratio: 0.0 81 | flag: 1 82 | 83 | pubmed: 84 | big: False 85 | dataset: pubmed 86 | batch: 0 87 | gpu: 2 88 | seed: 3 89 | name_view1: v1_adj 90 | name_view2: v2_diff 91 | indice_view1: v1_2 92 | indice_view2: v2_10 93 | cls_hid_1: 16 94 | com_lambda_v1: 0.1 95 | com_lambda_v2: 0.1 96 | gen_hid: 32 97 | lam: 0.5 98 | alpha: 0.1 99 | mi_hid_1: 128 100 | cls_lr: 0.01 101 | cls_weight_decay: 0.0005 102 | cls_dropout: 0.5 103 | ve_lr: 0.001 104 | ve_weight_decay: 0.0 105 | ve_dropout: 0.2 106 | mi_lr: 0.01 107 | mi_weight_decay: 0 108 | main_epoch: 200 109 | inner_ne_epoch: 5 110 | inner_cls_epoch: 5 111 | inner_mi_epoch: 10 112 | temp_r: 0.0001 113 | cls_coe: 0.3 114 | mi_coe: 0.3 115 | tau: 0.8 116 | add: False 117 | dele: False 118 | ptb_feat: False 119 | ratio: 0.0 120 | flag: 1 121 | 122 | cornell: 123 | big: False 124 | dataset: cornell 125 | batch: 0 126 | gpu: 2 127 | seed: 3 128 | name_view1: v1_adj 129 | name_view2: v2_diff 130 | indice_view1: v1_2 131 | indice_view2: v2_40 132 | cls_hid_1: 16 133 | com_lambda_v1: 0.1 134 | com_lambda_v2: 0.1 135 | gen_hid: 32 136 | lam: 0.5 137 | alpha: 0.1 138 | mi_hid_1: 128 139 | cls_lr: 0.01 140 | cls_weight_decay: 0.0005 141 | cls_dropout: 0.5 142 | ve_lr: 0.001 143 | ve_weight_decay: 0.0 144 | ve_dropout: 0.2 145 | mi_lr: 0.01 146 | mi_weight_decay: 0 147 | main_epoch: 200 148 | inner_ne_epoch: 5 149 | inner_cls_epoch: 5 150 | inner_mi_epoch: 10 151 | temp_r: 0.0001 152 | cls_coe: 0.3 153 | mi_coe: 0.3 154 | tau: 0.8 155 | add: False 156 | dele: False 157 | ptb_feat: False 158 | ratio: 0.0 159 | flag: 1 160 | 161 | actor: 162 | big: False 163 | dataset: actor 164 | batch: 0 165 | gpu: 2 166 | seed: 3 167 | name_view1: v1_adj 168 | name_view2: v2_diff 169 | indice_view1: v1_2 170 | indice_view2: v2_40 171 | cls_hid_1: 16 172 | com_lambda_v1: 0.1 173 | com_lambda_v2: 0.1 174 | gen_hid: 32 175 | lam: 0.5 176 | alpha: 0.1 177 | mi_hid_1: 128 178 | cls_lr: 0.01 179 | cls_weight_decay: 0.0005 180 | cls_dropout: 0.5 181 | ve_lr: 0.001 182 | ve_weight_decay: 0.0 183 | ve_dropout: 0.2 184 | mi_lr: 0.01 185 | mi_weight_decay: 0 186 | main_epoch: 200 187 | inner_ne_epoch: 5 188 | inner_cls_epoch: 5 189 | inner_mi_epoch: 10 190 | temp_r: 0.0001 191 | cls_coe: 0.3 192 | mi_coe: 0.3 193 | tau: 0.8 194 | add: False 195 | dele: False 196 | ptb_feat: False 197 | ratio: 0.0 198 | flag: 1 199 | 200 | texas: 201 | big: False 202 | dataset: texas 203 | batch: 0 204 | gpu: 2 205 | seed: 3 206 | name_view1: v1_adj 207 | name_view2: v2_diff 208 | indice_view1: v1_2 209 | indice_view2: v2_40 210 | cls_hid_1: 16 211 | com_lambda_v1: 0.1 212 | com_lambda_v2: 0.1 213 | gen_hid: 32 214 | lam: 0.5 215 | alpha: 0.1 216 | mi_hid_1: 128 217 | cls_lr: 0.01 218 | cls_weight_decay: 0.0005 219 | cls_dropout: 0.5 220 | ve_lr: 0.001 221 | ve_weight_decay: 0.0 222 | ve_dropout: 0.2 223 | mi_lr: 0.01 224 | mi_weight_decay: 0 225 | main_epoch: 200 226 | inner_ne_epoch: 5 227 | inner_cls_epoch: 5 228 | inner_mi_epoch: 10 229 | temp_r: 0.0001 230 | cls_coe: 0.3 231 | mi_coe: 0.3 232 | tau: 0.8 233 | add: False 234 | dele: False 235 | ptb_feat: False 236 | ratio: 0.0 237 | flag: 1 238 | 239 | wisconsin: 240 | big: False 241 | dataset: wisconsin 242 | batch: 0 243 | gpu: 2 244 | seed: 3 245 | name_view1: v1_adj 246 | name_view2: v2_diff 247 | indice_view1: v1_2 248 | indice_view2: v2_40 249 | cls_hid_1: 16 250 | com_lambda_v1: 0.1 251 | com_lambda_v2: 0.1 252 | gen_hid: 32 253 | lam: 0.5 254 | alpha: 0.1 255 | mi_hid_1: 128 256 | cls_lr: 0.01 257 | cls_weight_decay: 0.0005 258 | cls_dropout: 0.5 259 | ve_lr: 0.001 260 | ve_weight_decay: 0.0 261 | ve_dropout: 0.2 262 | mi_lr: 0.01 263 | mi_weight_decay: 0 264 | main_epoch: 200 265 | inner_ne_epoch: 5 266 | inner_cls_epoch: 5 267 | inner_mi_epoch: 10 268 | temp_r: 0.0001 269 | cls_coe: 0.3 270 | mi_coe: 0.3 271 | tau: 0.8 272 | add: False 273 | dele: False 274 | ptb_feat: False 275 | ratio: 0.0 276 | flag: 1 277 | 278 | chameleon: 279 | big: False 280 | dataset: chameleon 281 | batch: 0 282 | gpu: 2 283 | seed: 3 284 | name_view1: v1_adj 285 | name_view2: v2_diff 286 | indice_view1: v1_2 287 | indice_view2: v2_40 288 | cls_hid_1: 16 289 | com_lambda_v1: 0.1 290 | com_lambda_v2: 0.1 291 | gen_hid: 32 292 | lam: 0.5 293 | alpha: 0.1 294 | mi_hid_1: 128 295 | cls_lr: 0.01 296 | cls_weight_decay: 0.0005 297 | cls_dropout: 0.5 298 | ve_lr: 0.001 299 | ve_weight_decay: 0.0 300 | ve_dropout: 0.2 301 | mi_lr: 0.01 302 | mi_weight_decay: 0 303 | main_epoch: 200 304 | inner_ne_epoch: 5 305 | inner_cls_epoch: 5 306 | inner_mi_epoch: 10 307 | temp_r: 0.0001 308 | cls_coe: 0.3 309 | mi_coe: 0.3 310 | tau: 0.8 311 | add: False 312 | dele: False 313 | ptb_feat: False 314 | ratio: 0.0 315 | flag: 1 316 | 317 | squirrel: 318 | big: False 319 | dataset: squirrel 320 | batch: 0 321 | gpu: 2 322 | seed: 3 323 | name_view1: v1_adj 324 | name_view2: v2_diff 325 | indice_view1: v1_2 326 | indice_view2: v2_40 327 | cls_hid_1: 16 328 | com_lambda_v1: 0.1 329 | com_lambda_v2: 0.1 330 | gen_hid: 32 331 | lam: 0.5 332 | alpha: 0.1 333 | mi_hid_1: 128 334 | cls_lr: 0.01 335 | cls_weight_decay: 0.0005 336 | cls_dropout: 0.5 337 | ve_lr: 0.001 338 | ve_weight_decay: 0.0 339 | ve_dropout: 0.2 340 | mi_lr: 0.01 341 | mi_weight_decay: 0 342 | main_epoch: 200 343 | inner_ne_epoch: 5 344 | inner_cls_epoch: 5 345 | inner_mi_epoch: 10 346 | temp_r: 0.0001 347 | cls_coe: 0.3 348 | mi_coe: 0.3 349 | tau: 0.8 350 | add: False 351 | dele: False 352 | ptb_feat: False 353 | ratio: 0.0 354 | flag: 1 -------------------------------------------------------------------------------- /configs/dagnn_config.yaml: -------------------------------------------------------------------------------- 1 | Default: 2 | epochs: 1000 3 | lr: 0.01 4 | weight_decay: 0.005 5 | hidden: 64 6 | dropout: 0.5 7 | K: 10 8 | 9 | cora: 10 | dropout: 0.8 11 | 12 | citeseer: 13 | weight_decay: 0.02 14 | 15 | pubmed: 16 | dropout: 0.8 -------------------------------------------------------------------------------- /configs/gcn_config.yaml: -------------------------------------------------------------------------------- 1 | Default: 2 | seed: 0 3 | hidden: 32 4 | sparse: False 5 | lr: 0.001 6 | weight_decay: 0 7 | epochs: 300 8 | dropout: 0.5 9 | 10 | cora: 11 | seed: 0 12 | hidden: 32 13 | sparse: False 14 | lr: 0.005 15 | weight_decay: 0.001 16 | epochs: 300 17 | dropout: 0.5 18 | 19 | citeseer: 20 | seed: 0 21 | hidden: 32 22 | sparse: False 23 | lr: 0.005 24 | weight_decay: 0.001 25 | epochs: 300 26 | dropout: 0.5 27 | 28 | pubmed: 29 | seed: 0 30 | hidden: 32 31 | sparse: False 32 | lr: 0.005 33 | weight_decay: 0.0005 34 | epochs: 300 35 | dropout: 0.5 36 | 37 | ogbn-arxiv: 38 | seed: 0 39 | hidden: 256 40 | sparse: True 41 | lr: 0.01 42 | weight_decay: 0 43 | epochs: 500 44 | dropout: 0.5 -------------------------------------------------------------------------------- /configs/gen_config.yaml: -------------------------------------------------------------------------------- 1 | Default: 2 | base: 'gcn' 3 | seed: 42 4 | lr: 0.01 5 | weight_decay: 5.0e-4 6 | hidden: 32 7 | dropout: 0.5 8 | activation: 'relu' 9 | epoch: 200 10 | iter: 30 11 | k: 9 12 | threshold: 0.6 13 | tolerance: 0.01 14 | debug: True 15 | cora: 16 | base: 'gcn' 17 | seed: 42 18 | lr: 0.01 19 | weight_decay: 5.0e-4 20 | hidden: 16 21 | dropout: 0.5 22 | activation: 'relu' 23 | epoch: 200 24 | iter: 30 25 | k: 9 26 | threshold: 0.5 27 | tolerance: 0.01 28 | debug: True 29 | citeseer: 30 | base: 'gcn' 31 | seed: 9 32 | lr: 0.01 33 | weight_decay: 5.0e-4 34 | hidden: 16 35 | dropout: 0.5 36 | activation: 'relu' 37 | epoch: 200 38 | iter: 30 39 | k: 9 40 | threshold: 0.5 41 | tolerance: 0.01 42 | debug: True -------------------------------------------------------------------------------- /configs/gprgnn_config.yaml: -------------------------------------------------------------------------------- 1 | Default: 2 | epochs: 1000 3 | lr: 0.002 4 | weight_decay: 0.0005 5 | hidden: 64 6 | dropout: 0.5 7 | K: 10 8 | alpha: 0.1 9 | dprate: 0.5 10 | Init: PPR 11 | ppnp: GPR_prop 12 | Gamma: None 13 | 14 | cora: 15 | lr: 0.01 16 | alpha: 0.01 17 | 18 | citeseer: 19 | lr: 0.01 20 | alpha: 0.1 21 | 22 | pubmed: 23 | lr: 0.05 24 | alpha: 0.2 25 | 26 | cornell: 27 | lr: 0.05 28 | alpha: 0.9 29 | 30 | texas: 31 | lr: 0.05 32 | alpha: 1.0 -------------------------------------------------------------------------------- /configs/grcn_config.yaml: -------------------------------------------------------------------------------- 1 | Default: 2 | sparse: True 3 | hid_graph: "100:10" 4 | nhid: 32 5 | F: relu 6 | F_graph: tanh 7 | dropout: 0.5 8 | 9 | cora: 10 | compl_param: "150" 11 | normalize: True 12 | reduce: knn 13 | lr: 0.005 14 | weight_decay: 0.005 15 | lr_graph: 0.001 16 | weight_decay_graph: 0. 17 | num_epochs: 300 18 | 19 | citeseer: 20 | compl_param: "300" 21 | normalize: True 22 | reduce: knn 23 | lr: 0.005 24 | weight_decay: 0.005 25 | lr_graph: 0.001 26 | weight_decay_graph: 0. 27 | num_epochs: 200 28 | 29 | pubmed: 30 | compl_param: "1" 31 | normalize: False 32 | reduce: knn 33 | lr: 5.0e-3 34 | weight_decay: 5.0e-3 35 | lr_graph: 0. 36 | weight_decay_graph: 0. 37 | num_epochs: 300 38 | layertype: dense -------------------------------------------------------------------------------- /configs/gsr_config.yaml: -------------------------------------------------------------------------------- 1 | Default: 2 | gnn_model: GCN 3 | activation: Relu 4 | semb: dw 5 | prt_lr: 0.001 6 | p_epochs: 50 7 | p_batch_size: 512 8 | p_schedule_step: 500 9 | poly_decay_lr: True 10 | fan_out: '20_40' 11 | num_workers: 0 12 | decoder_layer: 2 13 | decoder_n_hidden: 48 14 | intra_weight: 0.75 15 | seed: 0 16 | 17 | momentum_factor: 0.999 18 | nce_k: 16382 19 | nce_t: 0.07 20 | pre_dropout: 0.0 21 | 22 | 23 | cos_batch_size: 10000 24 | 25 | fsim_norm: True 26 | fsim_weight: 0.0 27 | rm_ratio: 0.0 28 | add_ratio: 0.25 29 | 30 | cla_batch_size: 5000 31 | lr: 0.01 32 | dropout: 0.5 33 | n_hidden: 256 34 | 35 | weight_decay: 5.e-4 36 | early_stop: 100 37 | epochs: 1000 38 | 39 | train_percentage: 0 40 | gpu: 0 41 | -------------------------------------------------------------------------------- /configs/gtn_config.yaml: -------------------------------------------------------------------------------- 1 | Default: 2 | early_stop: 20 3 | model: 'GTN' 4 | weight_decay: 0.001 5 | num_layers: 1 6 | is_norm: 1 7 | identity: 1 8 | 9 | acm: 10 | dataset: 'acm' 11 | num_layers: 2 12 | epochs: 100 13 | lr: 0.005 14 | num_channels: 2 15 | hid_dim: 128 16 | identity: 1 17 | 18 | dblp: 19 | dataset: 'dblp' 20 | num_layers: 3 21 | epochs: 100 22 | lr: 0.005 23 | num_channels: 2 24 | hid_dim: 128 25 | identity: 1 26 | 27 | yelp: 28 | dataset: 'yelp' 29 | num_layers: 1 30 | epochs: 100 31 | lr: 0.005 32 | num_channels: 2 33 | hid_dim: 128 34 | identity: 1 35 | 36 | imdb: 37 | dataset: 'imdb' 38 | num_layers: 2 39 | epochs: 150 40 | lr: 0.005 41 | num_channels: 2 42 | hid_dim: 128 43 | identity: 1 -------------------------------------------------------------------------------- /configs/han_config.yaml: -------------------------------------------------------------------------------- 1 | Default: 2 | early_stop: 100 3 | model: 'HAN' 4 | weight_decay: 0.001 5 | 6 | acm: 7 | dataset: 'acm' 8 | dropout: 0.46 9 | hid_dim: 128 10 | lr: 0.01 11 | epochs: 200 12 | early_stop: 100 13 | weight_decay: 0.001 14 | 15 | #dblp: 16 | # dataset: 'dblp' 17 | # dropout: 0.46 18 | # hid_dim: 128 19 | # lr: 0.01 20 | # epochs: 200 21 | # early_stop: 100 22 | # weight_decay: 0.001 23 | # 24 | # 25 | #yelp: 26 | # dataset: 'yelp' 27 | # dropout: 0.46 28 | # hid_dim: 128 29 | # lr: 0.01 30 | # epochs: 200 31 | # early_stop: 100 32 | # weight_decay: 0.001 33 | # 34 | # 35 | #imdb: 36 | # dataset: 'imdb' 37 | # dropout: 0.46 38 | # hid_dim: 128 39 | # lr: 0.01 40 | # epochs: 200 41 | # early_stop: 100 42 | # weight_decay: 0.001 43 | -------------------------------------------------------------------------------- /configs/hesgsl_config.yaml: -------------------------------------------------------------------------------- 1 | Default: 2 | dataset: cora 3 | seed: 0 4 | nlayers: 2 5 | hid_dim_cla: 32 6 | hid_dim_dae: 1024 7 | mlp_dim: 1433 8 | k: 30 9 | ratio: 10 10 | scale: 15 11 | num_hop: 10 12 | alpha: 8 13 | beta: 1 14 | num_epochs: 2000 15 | epochs_pre: 400 16 | epochs_hom: 100 17 | lr: 0.001 18 | weight_decay: 0.0005 19 | dropout_cla: 0.6 20 | dropout_adj: 0.25 21 | sparse: False 22 | lr_cla: 0.01 23 | patience: 200 24 | eval_step: 10 25 | 26 | cora: 27 | dataset: cora 28 | 29 | citeseer: 30 | dataset: citeseer 31 | nlayers: 2 32 | hid_dim_cla: 32 33 | hid_dim_dae: 1024 34 | mlp_dim: 3703 35 | k: 30 36 | ratio: 10 37 | scale: 10 38 | num_hop: 1 39 | alpha: 8 40 | beta: 1 41 | num_epochs: 2000 42 | epochs_pre: 400 43 | epochs_hom: 100 44 | lr: 0.001 45 | weight_decay: 0.0005 46 | dropout_cla: 0.5 47 | dropout_adj: 0.2 48 | sparse: False 49 | lr_cla: 0.01 50 | patience: 200 51 | eval_step: 10 52 | 53 | pubmed: 54 | dataset: pubmed 55 | nlayers: 2 56 | hid_dim_cla: 64 57 | hid_dim_dae: 512 58 | mlp_dim: 500 59 | k: 20 60 | ratio: 5 61 | scale: 5 62 | num_hop: 10 63 | alpha: 5 64 | beta: 1 65 | num_epochs: 2000 66 | epochs_pre: 400 67 | epochs_hom: 100 68 | lr: 0.01 69 | weight_decay: 0.0005 70 | dropout_cla: 0.4 71 | dropout_adj: 0.3 72 | sparse: True 73 | lr_cla: 0.01 74 | patience: 200 75 | eval_step: 10 76 | 77 | ogbn-arxiv: 78 | dataset: ogbn-arxiv 79 | nlayers: 2 80 | hid_dim_cla: 256 81 | hid_dim_dae: 512 82 | mlp_dim: 128 83 | k: 20 84 | ratio: 100 85 | scale: 3 86 | num_hop: 3 87 | alpha: 10 88 | beta: 1 89 | num_epochs: 2000 90 | epochs_pre: 0 91 | epochs_hom: 300 92 | lr: 0.001 93 | weight_decay: 0. 94 | dropout_cla: 0.4 95 | dropout_adj: 0.5 96 | sparse: True 97 | lr_cla: 0.01 98 | patience: 200 99 | eval_step: 10 100 | 101 | cornell: 102 | dataset: cornell 103 | nlayers: 2 104 | hid_dim_cla: 128 105 | hid_dim_dae: 1024 106 | mlp_dim: 1703 107 | k: 20 108 | ratio: 20 109 | scale: 15 110 | num_hop: 8 111 | alpha: 8 112 | beta: 1 113 | num_epochs: 2000 114 | epochs_pre: 200 115 | epochs_hom: 100 116 | lr: 0.001 117 | weight_decay: 0.0005 118 | dropout_cla: 0.6 119 | dropout_adj: 0.3 120 | sparse: False 121 | lr_cla: 0.01 122 | patience: 200 123 | eval_step: 10 124 | 125 | texas: 126 | dataset: texas 127 | nlayers: 2 128 | hid_dim_cla: 16 129 | hid_dim_dae: 1024 130 | mlp_dim: 1703 131 | k: 5 132 | ratio: 10 133 | scale: 5 134 | num_hop: 5 135 | alpha: 1 136 | beta: 1 137 | num_epochs: 2000 138 | epochs_pre: 400 139 | epochs_hom: 100 140 | lr: 0.001 141 | weight_decay: 0.0005 142 | dropout_cla: 0.6 143 | dropout_adj: 0.1 144 | sparse: False 145 | lr_cla: 0.01 146 | patience: 200 147 | eval_step: 10 148 | 149 | wisconsin: 150 | dataset: wisconsin 151 | nlayers: 2 152 | hid_dim_cla: 32 153 | hid_dim_dae: 1024 154 | mlp_dim: 1703 155 | k: 3 156 | ratio: 5 157 | scale: 8 158 | num_hop: 3 159 | alpha: 1 160 | beta: 1 161 | num_epochs: 2000 162 | epochs_pre: 400 163 | epochs_hom: 100 164 | lr: 0.001 165 | weight_decay: 0.0005 166 | dropout_cla: 0.6 167 | dropout_adj: 0.25 168 | sparse: False 169 | lr_cla: 0.01 170 | patience: 200 171 | eval_step: 10 172 | 173 | actor: 174 | dataset: actor 175 | nlayers: 2 176 | hid_dim_cla: 32 177 | hid_dim_dae: 256 178 | mlp_dim: 932 179 | k: 30 180 | ratio: 5 181 | scale: 10 182 | num_hop: 3 183 | alpha: 10 184 | beta: 1 185 | num_epochs: 2000 186 | epochs_pre: 400 187 | epochs_hom: 100 188 | lr: 0.005 189 | weight_decay: 0.0005 190 | dropout_cla: 0.6 191 | dropout_adj: 0.2 192 | sparse: False 193 | lr_cla: 0.01 194 | patience: 200 195 | eval_step: 10 196 | -------------------------------------------------------------------------------- /configs/hgpsl_config.yaml: -------------------------------------------------------------------------------- 1 | Default: 2 | seed: 0 3 | folds: 10 4 | epochs: 200 5 | batch_size: 80 6 | test_batch_size: 60 7 | sample: True 8 | lr: 1.0e-3 9 | weight_decay: 1.0e-3 10 | pool_ratio: 0.5 11 | num_layers: 3 12 | dropout: 0 13 | lamb: 1.0 14 | patience: 100 15 | eval_step: 1 16 | hidden_dim: 512 17 | sparse_attn: True 18 | sl: True 19 | backbone: 'GIN' 20 | use_log_soft: True 21 | loss_type: 'Nll' 22 | 23 | IMDB-BINARY: 24 | batch_size: 256 25 | hidden_dim: 64 26 | lr: 1.0e-3 27 | dropout: 0 28 | weight_decay: 0 29 | 30 | REDDIT-BINARY: 31 | batch_size: 256 32 | hidden_dim: 64 33 | lr: 1.0e-3 34 | dropout: 0 35 | weight_decay: 0 36 | 37 | COLLAB: 38 | batch_size: 1024 39 | hidden_dim: 64 40 | lr: 1.0e-3 41 | dropout: 0 42 | weight_decay: 0 -------------------------------------------------------------------------------- /configs/hgsl_config.yaml: -------------------------------------------------------------------------------- 1 | Default: 2 | seed: 0 3 | dataset: 'acm' 4 | lr: 0.01 5 | conv_method: 'gcn' 6 | num_head: 2 7 | early_stop: 80 8 | adj_norm_order: 1 9 | feat_norm: -1 10 | emb_dim: 64 11 | com_feat_dim: 16 12 | weight_decay: 5e-4 13 | model: 'HGSL' 14 | epochs: 200 15 | exp_name: 'debug' 16 | save_weights: False 17 | 18 | acm: 19 | alpha: 1 20 | dropout: 0 21 | fgd_th: 0.8 22 | fgh_th: 0.2 23 | sem_th: 0.6 24 | mp_list: ['psp', 'pap', 'pspap'] 25 | data_type: 'pas' 26 | relation_list: 'p-a+a-p+p-s+s-p' 27 | 28 | dblp: 29 | alpha: 4.5 30 | dropout: 0.2 31 | fgd_th: 0.99 32 | fgh_th: 0.99 33 | sem_th: 0.4 34 | mp_list: ['apcpa'] 35 | data_type: 'apc' 36 | relation_list: 'p-a+a-p+p-c+c-p' 37 | 38 | yelp: 39 | alpha: 0.5 40 | dropout: 0.2 41 | fgd_th: 0.8 42 | fgh_th: 0.1 43 | sem_th: 0.2 44 | mp_list: ['bub', 'bsb', 'bublb', 'bubsb'] 45 | data_type: 'busl' 46 | relation_list: 'b-u+u-b+b-s+s-b+b-l+l-b' 47 | 48 | imdb: 49 | alpha: 1 50 | dropout: 0 51 | fgd_th: 0.8 52 | fgh_th: 0.2 53 | sem_th: 0.6 54 | mp_list: [] 55 | data_type: 'mda' 56 | relation_list: 'm-d+d-m+m-a+a-m' -------------------------------------------------------------------------------- /configs/lds_config.yaml: -------------------------------------------------------------------------------- 1 | Default: 2 | seed: 0 3 | patience: 20 4 | outer_loop_max_epochs: 400 5 | inner_loop_max_epochs: 400 6 | hyper_gradient_interval: 5 7 | hidden_size: 16 8 | dropout: 0.5 9 | gcn_optimizer_learning_rate: 0.01 10 | gcn_weight_decay: 0.0005 11 | graph_model: "lds" 12 | smoothness_factor: 0.0 13 | disconnection_factor: 0.0 14 | sparsity_factor: 0.0 15 | n_samples_empirical_mean: 16 16 | activation_last: 'log_softmax' 17 | normalize_adj: True 18 | 19 | 20 | cora: 21 | seed: 0 22 | patience: 20 23 | outer_loop_max_epochs: 400 24 | inner_loop_max_epochs: 400 25 | hyper_gradient_interval: 5 26 | hidden_size: 128 27 | dropout: 0.5 28 | gcn_optimizer_learning_rate: 0.01 29 | gcn_weight_decay: 0.0005 30 | graph_model: "lds" 31 | smoothness_factor: 0.0 32 | disconnection_factor: 0.0 33 | sparsity_factor: 0.0 34 | n_samples_empirical_mean: 16 35 | activation_last: 'log_softmax' 36 | normalize_adj: True 37 | 38 | 39 | citeseer: 40 | seed: 0 41 | patience: 20 42 | outer_loop_max_epochs: 400 43 | inner_loop_max_epochs: 400 44 | hyper_gradient_interval: 5 45 | hidden_size: 64 46 | dropout: 0.5 47 | gcn_optimizer_learning_rate: 0.01 48 | gcn_weight_decay: 0.0005 49 | graph_model: "lds" 50 | smoothness_factor: 0.0 51 | disconnection_factor: 0.0 52 | sparsity_factor: 0.0 53 | n_samples_empirical_mean: 16 54 | activation_last: 'log_softmax' 55 | normalize_adj: True 56 | 57 | 58 | cornell: 59 | seed: 0 60 | patience: 20 61 | outer_loop_max_epochs: 400 62 | inner_loop_max_epochs: 400 63 | hyper_gradient_interval: 5 64 | hidden_size: 128 65 | dropout: 0.5 66 | gcn_optimizer_learning_rate: 0.01 67 | gcn_weight_decay: 0.0005 68 | graph_model: "lds" 69 | smoothness_factor: 0.0 70 | disconnection_factor: 0.0 71 | sparsity_factor: 0.0 72 | n_samples_empirical_mean: 16 73 | activation_last: 'log_softmax' 74 | normalize_adj: True 75 | 76 | 77 | texas: 78 | seed: 0 79 | patience: 20 80 | outer_loop_max_epochs: 400 81 | inner_loop_max_epochs: 400 82 | hyper_gradient_interval: 5 83 | hidden_size: 64 84 | dropout: 0.5 85 | gcn_optimizer_learning_rate: 0.01 86 | gcn_weight_decay: 0.0005 87 | graph_model: "lds" 88 | smoothness_factor: 0.0 89 | disconnection_factor: 0.0 90 | sparsity_factor: 0.0 91 | n_samples_empirical_mean: 16 92 | activation_last: 'log_softmax' 93 | normalize_adj: True 94 | 95 | wisconsin: 96 | seed: 0 97 | patience: 20 98 | outer_loop_max_epochs: 400 99 | inner_loop_max_epochs: 400 100 | hyper_gradient_interval: 5 101 | hidden_size: 64 102 | dropout: 0.5 103 | gcn_optimizer_learning_rate: 0.01 104 | gcn_weight_decay: 0.0005 105 | graph_model: "lds" 106 | smoothness_factor: 0.0 107 | disconnection_factor: 0.0 108 | sparsity_factor: 0.0 109 | n_samples_empirical_mean: 16 110 | activation_last: 'log_softmax' 111 | normalize_adj: True 112 | 113 | actor: 114 | seed: 0 115 | patience: 20 116 | outer_loop_max_epochs: 400 117 | inner_loop_max_epochs: 400 118 | hyper_gradient_interval: 5 119 | hidden_size: 64 120 | dropout: 0.5 121 | gcn_optimizer_learning_rate: 0.01 122 | gcn_weight_decay: 0.0005 123 | graph_model: "lds" 124 | smoothness_factor: 0.0 125 | disconnection_factor: 0.0 126 | sparsity_factor: 0.0 127 | n_samples_empirical_mean: 16 128 | activation_last: 'log_softmax' 129 | normalize_adj: True 130 | -------------------------------------------------------------------------------- /configs/mlp_config.yaml: -------------------------------------------------------------------------------- 1 | Default: 2 | hidden: 32 3 | lr: 0.01 4 | weight_decay: 0.0001 5 | epochs: 500 6 | 7 | cora: 8 | hideen: 32 9 | lr: 0.01 10 | weight_decay: 0.001 11 | epochs: 500 -------------------------------------------------------------------------------- /configs/neuralsparse_config.yaml: -------------------------------------------------------------------------------- 1 | Default: 2 | dataset: cora 3 | 4 | Cora: 5 | dataset: cora 6 | seed: 0 7 | hidden: 32 8 | epochs: 3000 9 | temp_N: 50 10 | temp_r: 0.001 11 | k: 5 12 | hidden_channel: 128 13 | dropout: 0.5 14 | num_layers: 2 15 | lr: 0.001 16 | weight_decay: 0.001 17 | patience: 100 -------------------------------------------------------------------------------- /configs/nodeformer_config.yaml: -------------------------------------------------------------------------------- 1 | Default: 2 | seed: 9 3 | num_layers: 2 4 | num_heads: 4 5 | kernel_transformation: softmax 6 | nb_random_features: 30 7 | rb_trans: sigmoid 8 | use_residual: True 9 | use_gumbel: True 10 | use_bn: True 11 | use_jk: False 12 | use_edge_loss: True 13 | use_act: False 14 | tau: 0.25 15 | 16 | cora: 17 | sparse: False 18 | hidden_dim: 32 19 | dropout: 0.0 20 | epochs: 500 21 | lr: 0.01 22 | weight_decay: 5.0e-4 23 | eval_step: 1 24 | lamda: 1.0 25 | rb_order: 3 26 | nb_gumbel_sample: 10 27 | 28 | citeseer: 29 | sparse: False 30 | hidden_dim: 32 31 | dropout: 0.5 32 | epochs: 500 33 | lr: 0.01 34 | weight_decay: 5.0e-3 35 | eval_step: 1 36 | lamda: 1.0 37 | rb_order: 3 38 | tau: 0.25 39 | nb_gumbel_sample: 10 40 | 41 | pubmed: 42 | sparse: False 43 | hidden_dim: 32 44 | dropout: 0.3 45 | epochs: 1000 46 | lr: 0.001 47 | weight_decay: 5.0e-4 48 | eval_step: 1 49 | lamda: 1.0 50 | rb_order: 2 51 | tau: 0.25 52 | nb_gumbel_sample: 10 53 | 54 | amazon-computers: 55 | sparse: False 56 | hidden_dim: 64 57 | dropout: 0.2 58 | epochs: 5000 59 | lr: 0.0005 60 | weight_decay: 5.0e-4 61 | eval_step: 10 62 | lamda: 0.5 63 | rb_order: 0 64 | tau: 0.25 65 | nb_gumbel_sample: 10 66 | nb_random_features: 30 67 | use_jk: True 68 | use_act: False 69 | 70 | amazon-photo: 71 | sparse: False 72 | hidden_dim: 64 73 | dropout: 0.2 74 | epochs: 500 75 | lr: 0.003 76 | weight_decay: 0.0005 77 | eval_step: 1 78 | lamda: 0.5 79 | rb_order: 1 80 | tau: 0.25 81 | nb_gumbel_sample: 10 82 | 83 | cornell: 84 | sparse: False 85 | hidden_dim: 128 86 | dropout: 0.5 87 | epochs: 100 88 | lr: 0.01 89 | weight_decay: 5.0e-4 90 | eval_step: 1 91 | lamda: 0.01 92 | rb_order: 1 93 | nb_gumbel_sample: 10 94 | 95 | texas: 96 | sparse: False 97 | hidden_dim: 128 98 | dropout: 0.5 99 | epochs: 50 100 | lr: 0.01 101 | weight_decay: 5.0e-4 102 | eval_step: 1 103 | lamda: 1 104 | rb_order: 1 105 | nb_gumbel_sample: 10 106 | 107 | wisconsin: 108 | sparse: False 109 | hidden_dim: 128 110 | dropout: 0.5 111 | epochs: 500 112 | lr: 0.01 113 | weight_decay: 5.0e-4 114 | eval_step: 1 115 | lamda: 1 116 | rb_order: 1 117 | nb_gumbel_sample: 10 118 | 119 | actor: 120 | sparse: False 121 | hidden_dim: 128 122 | dropout: 0.5 123 | epochs: 500 124 | lr: 0.01 125 | weight_decay: 5.0e-4 126 | eval_step: 1 127 | lamda: 1 128 | rb_order: 1 129 | nb_gumbel_sample: 10 130 | 131 | chameleon: 132 | sparse: False 133 | hidden_dim: 128 134 | dropout: 0.5 135 | epochs: 150 136 | lr: 0.005 137 | weight_decay: 5.0e-4 138 | eval_step: 1 139 | lamda: 1 140 | rb_order: 1 141 | nb_gumbel_sample: 10 142 | 143 | squirrel: 144 | sparse: False 145 | hidden_dim: 128 146 | dropout: 0.5 147 | epochs: 100 148 | lr: 0.005 149 | weight_decay: 5.0e-4 150 | eval_step: 1 151 | lamda: 1 152 | rb_order: 1 153 | nb_gumbel_sample: 10 154 | 155 | acm: 156 | sparse: False 157 | hidden_dim: 32 158 | dropout: 0.0 159 | epochs: 500 160 | lr: 0.01 161 | weight_decay: 5.0e-4 162 | eval_step: 1 163 | lamda: 1.0 164 | rb_order: 2 165 | nb_gumbel_sample: 10 -------------------------------------------------------------------------------- /configs/prognn_config.yaml: -------------------------------------------------------------------------------- 1 | Default: 2 | alpha: 0.0005 3 | beta: 1.5 4 | cuda: true 5 | debug: false 6 | dropout: 0.5 7 | epochs: 400 8 | gamma: 1 9 | hidden: 32 10 | inner_steps: 2 11 | lambda_: 0 12 | lr: 0.01 13 | lr_adj: 0.01 14 | outer_steps: 1 15 | phi: 0 16 | seed: 42 17 | symmetric: false 18 | weight_decay: 0.0005 19 | cora: 20 | alpha: 0.0005 21 | beta: 1.5 22 | cuda: true 23 | debug: false 24 | dropout: 0.5 25 | epochs: 400 26 | gamma: 1 27 | hidden: 16 28 | inner_steps: 2 29 | lambda_: 0 30 | lr: 0.01 31 | lr_adj: 0.01 32 | outer_steps: 1 33 | phi: 0 34 | seed: 42 35 | symmetric: false 36 | weight_decay: 0.0005 37 | Pubmed: 38 | alpha: 0.0005 39 | beta: 1.5 40 | cuda: true 41 | debug: false 42 | dropout: 0.5 43 | epochs: 400 44 | gamma: 1 45 | hidden: 32 46 | inner_steps: 2 47 | lambda_: 0 48 | lr: 0.01 49 | lr_adj: 0.01 50 | outer_steps: 1 51 | phi: 0 52 | seed: 42 53 | symmetric: false 54 | weight_decay: 0.0005 -------------------------------------------------------------------------------- /configs/ptdnet_config.yaml: -------------------------------------------------------------------------------- 1 | Default: 2 | dataset: cora 3 | seed: 0 4 | sparse: False 5 | hiddens: 256 6 | conv_bias: False 7 | denoise_hidden_1: 16 8 | denoise_hidden_2: 0 9 | weight_decay: 5.0e-4 10 | lr: 0.001 11 | epochs: 1000 12 | init_temperature: 2.0 13 | temperature_decay: 0.99 14 | outL: 3 15 | L: 1 16 | dropout: 0.5 17 | gamma: -0.0 18 | zeta: 1.01 19 | lambda1: 0.1 20 | lambda3: 0.01 21 | coff_consis: 0.01 22 | k_svd: 1 23 | patience: 100 24 | 25 | Cora: 26 | dataset: cora 27 | hiddens: 128 28 | 29 | Citeseer: 30 | dataset: citeseer 31 | hiddens: 128 32 | weight_decay: 1.0e-3 -------------------------------------------------------------------------------- /configs/slaps_config.yaml: -------------------------------------------------------------------------------- 1 | Default: 2 | epochs: 200 3 | epochs_adj: 2000 4 | lr: 0.001 5 | lr_adj: 0.01 6 | w_decay: 0.0005 7 | w_decay_adj: 0.0 8 | hidden: 32 9 | hidden_adj: 512 10 | dropout1: 0.5 11 | dropout2: 0.5 12 | dropout_adj1: 0.25 13 | dropout_adj2: 0.25 14 | dataset: cora 15 | nlayers: 2 16 | nlayers_adj: 2 17 | patience: 10 18 | k: 20 19 | ratio: 20 20 | epoch_d: 5 21 | lambda_: 0.1 22 | nr: 5 23 | knn_metric: cosine 24 | model: end2end 25 | i: 6 26 | non_linearity: elu 27 | mlp_act: relu 28 | normalization: sym 29 | mlp_h: 50 30 | learner: MLP # Graph Structure Learner, choices: ['FP', 'MLP'] 31 | sparse: 0 32 | noise: mask 33 | loss: mse 34 | 35 | cora: 36 | dataset: cora 37 | epochs_adj: 2000 38 | lr: 0.01 39 | lr_adj: 0.001 40 | w_decay: 0.0005 41 | nlayers: 2 42 | nlayers_adj: 2 43 | hidden: 32 44 | hidden_adj: 512 45 | dropout1: 0.5 46 | dropout2: 0.5 47 | dropout_adj1: 0.25 48 | dropout_adj2: 0.5 49 | k: 20 50 | lambda_: 10.0 51 | nr: 5 52 | ratio: 10 53 | model: end2end 54 | sparse: 0 55 | learner: MLP 56 | non_linearity: relu 57 | mlp_h: 1433 58 | mlp_act: relu 59 | epoch_d: 5 60 | 61 | citeseer: 62 | dataset: citeseer 63 | epochs_adj: 2000 64 | lr: 0.01 65 | lr_adj: 0.001 66 | w_decay: 0.0005 67 | nlayers: 2 68 | nlayers_adj: 2 69 | hidden: 32 70 | hidden_adj: 1024 71 | dropout1: 0.5 72 | dropout2: 0.5 73 | dropout_adj1: 0.25 74 | dropout_adj2: 0.5 75 | k: 30 76 | lambda_: 10.0 77 | nr: 5 78 | ratio: 10 79 | model: end2end 80 | sparse: 0 81 | learner: MLP 82 | non_linearity: relu 83 | mlp_act: relu 84 | mlp_h: 3703 85 | epoch_d: 5 86 | 87 | pubmed: 88 | dataset: pubmed 89 | epochs_adj: 2000 90 | lr: 0.01 91 | lr_adj: 0.01 92 | w_decay: 0.0005 93 | nlayers: 2 94 | nlayers_adj: 2 95 | hidden: 32 96 | hidden_adj: 128 97 | dropout1: 0.5 98 | dropout2: 0.5 99 | dropout_adj1: 0.5 100 | dropout_adj2: 0.5 101 | k: 15 102 | lambda_: 10.0 103 | nr: 5 104 | ratio: 20 105 | model: end2end 106 | learner: MLP 107 | non_linearity: relu 108 | mlp_h: 500 109 | mlp_act: relu 110 | epoch_d: 5 111 | sparse: 1 112 | 113 | ogbn-arxiv: 114 | dataset: ogbn-arxiv 115 | epochs_adj: 2000 116 | lr: 0.01 117 | lr_adj: 0.001 118 | w_decay: 0.0 119 | nlayers: 2 120 | nlayers_adj: 2 121 | hidden: 256 122 | hidden_adj: 256 123 | dropout1: 0.5 124 | dropout2: 0.5 125 | dropout_adj1: 0.25 126 | dropout_adj2: 0.5 127 | k: 15 128 | lambda_: 10.0 129 | nr: 5 130 | ratio: 100 131 | model: end2end 132 | learner: MLP 133 | non_linearity: relu 134 | mlp_h: 128 135 | mlp_act: relu 136 | epoch_d: 2001 137 | sparse: 1 138 | loss: mse 139 | noise: mask 140 | 141 | cornell: 142 | dataset: cornell 143 | epochs_adj: 2000 144 | lr: 0.01 145 | lr_adj: 0.01 146 | w_decay: 0.0005 147 | nlayers: 2 148 | nlayers_adj: 2 149 | hidden: 32 150 | hidden_adj: 512 151 | dropout1: 0.5 152 | dropout2: 0.5 153 | dropout_adj1: 0.5 154 | dropout_adj2: 0.25 155 | k: 30 156 | lambda_: 10.0 157 | nr: 5 158 | ratio: 10 159 | model: end2end 160 | sparse: 0 161 | learner: FP 162 | non_linearity: elu 163 | epoch_d: 5 164 | 165 | actor: 166 | dataset: actor 167 | epochs_adj: 2000 168 | lr: 0.01 169 | lr_adj: 0.001 170 | w_decay: 0.0005 171 | nlayers: 2 172 | nlayers_adj: 2 173 | hidden: 32 174 | hidden_adj: 512 175 | dropout1: 0.5 176 | dropout2: 0.5 177 | dropout_adj1: 0.25 178 | dropout_adj2: 0.5 179 | k: 20 180 | lambda_: 10.0 181 | nr: 5 182 | ratio: 10 183 | model: end2end 184 | sparse: 0 185 | learner: MLP 186 | non_linearity: relu 187 | mlp_h: 932 188 | mlp_act: relu 189 | epoch_d: 5 190 | 191 | texas: 192 | dataset: texas 193 | epochs_adj: 2000 194 | lr: 0.01 195 | lr_adj: 0.01 196 | w_decay: 0.0005 197 | nlayers: 2 198 | nlayers_adj: 2 199 | hidden: 32 200 | hidden_adj: 512 201 | dropout1: 0.5 202 | dropout2: 0.5 203 | dropout_adj1: 0.5 204 | dropout_adj2: 0.25 205 | k: 30 206 | lambda_: 10.0 207 | nr: 5 208 | ratio: 10 209 | model: end2end 210 | sparse: 0 211 | learner: FP 212 | non_linearity: elu 213 | epoch_d: 5 214 | 215 | wisconsin: 216 | dataset: wisconsin 217 | epochs_adj: 2000 218 | lr: 0.01 219 | lr_adj: 0.01 220 | w_decay: 0.0005 221 | nlayers: 2 222 | nlayers_adj: 2 223 | hidden: 32 224 | hidden_adj: 512 225 | dropout1: 0.5 226 | dropout2: 0.5 227 | dropout_adj1: 0.5 228 | dropout_adj2: 0.25 229 | k: 30 230 | lambda_: 10.0 231 | nr: 5 232 | ratio: 10 233 | model: end2end 234 | sparse: 0 235 | learner: FP 236 | non_linearity: elu 237 | epoch_d: 5 238 | 239 | chameleon: 240 | dataset: chameleon 241 | epochs_adj: 2000 242 | lr: 0.01 243 | lr_adj: 0.001 244 | w_decay: 0.0005 245 | nlayers: 2 246 | nlayers_adj: 2 247 | hidden: 32 248 | hidden_adj: 512 249 | dropout1: 0.5 250 | dropout2: 0.5 251 | dropout_adj1: 0.25 252 | dropout_adj2: 0.5 253 | k: 20 254 | lambda_: 10.0 255 | nr: 5 256 | ratio: 10 257 | model: end2end 258 | sparse: 0 259 | learner: MLP 260 | non_linearity: relu 261 | mlp_h: 1433 262 | mlp_act: relu 263 | epoch_d: 5 264 | 265 | squirrel: 266 | dataset: squirrel 267 | epochs_adj: 2000 268 | lr: 0.01 269 | lr_adj: 0.001 270 | w_decay: 0.0005 271 | nlayers: 2 272 | nlayers_adj: 2 273 | hidden: 32 274 | hidden_adj: 512 275 | dropout1: 0.5 276 | dropout2: 0.5 277 | dropout_adj1: 0.25 278 | dropout_adj2: 0.5 279 | k: 20 280 | lambda_: 10.0 281 | nr: 5 282 | ratio: 10 283 | model: end2end 284 | sparse: 0 285 | learner: MLP 286 | non_linearity: relu 287 | mlp_h: 1433 288 | mlp_act: relu 289 | epoch_d: 5 -------------------------------------------------------------------------------- /configs/stable_config.yaml: -------------------------------------------------------------------------------- 1 | Default: 2 | dataset: cora 3 | jt: 0.03 4 | threshold: 1 5 | recover_percent: 0.2 6 | beta: 2 7 | nb_epochs: 10000 8 | cos: 0.1 9 | k: 5 10 | alpha: -0.3 11 | sparse: False 12 | num_hidden_eval: 16 13 | num_layers_eval: 2 14 | dropout_eval: 0.5 15 | dropedge_rate_eval: 0 16 | lr_cls: 0.01 17 | weight_decay_cls: 0.0005 18 | num_epochs_cls: 200 19 | seed: 1 20 | 21 | Cora-FP: 22 | dataset: cora 23 | jt: 0.03 24 | threshold: 1 25 | recover_percent: 0.2 26 | beta: 2 27 | nb_epochs: 10000 28 | cos: 0.1 29 | k: 5 30 | alpha: -0.3 31 | sparse: False 32 | num_hidden_eval: 16 33 | num_layers_eval: 2 34 | dropout_eval: 0.5 35 | dropedge_rate_eval: 0 36 | lr_cls: 0.01 37 | weight_decay_cls: 0.0005 38 | num_epochs_cls: 200 39 | seed: 1 40 | lr: 0.01 41 | weight_decay: 0.0005 -------------------------------------------------------------------------------- /configs/sublime_config.yaml: -------------------------------------------------------------------------------- 1 | Default: 2 | seed: 1 3 | dataset: cora 4 | lr: 0.01 5 | lr_cls: 0.001 6 | num_hidden: 512 7 | num_hidden_eval: 32 8 | num_rep_dim: 256 9 | num_proj_dim: 256 10 | activation: relu 11 | base_model: GCNConv 12 | num_layers: 2 13 | num_layers_eval: 2 14 | num_epochs: 4000 15 | num_epochs_cls: 200 16 | weight_decay: 0 17 | weight_decay_cls: 0.0005 18 | mode: structure_refinement 19 | sparse: False 20 | k: 30 21 | dropout: 0.5 22 | dropout_eval: 0.5 23 | dropedge_rate: 0.5 24 | dropedge_rate_eval: 0.75 25 | tau: 0.9999 26 | c: 0 27 | learner: mlp 28 | maskfeat_rate_anchor: 0.6 29 | maskfeat_rate_learner: 0.7 30 | contrast_batch_size: 0 31 | 32 | cora: 33 | lr: 0.01 34 | lr_cls: 0.001 35 | num_hidden: 512 36 | num_hidden_eval: 32 37 | num_rep_dim: 256 38 | num_proj_dim: 256 39 | activation: relu 40 | base_model: GCNConv 41 | num_layers: 2 42 | num_layers_eval: 2 43 | num_epochs: 4000 44 | num_epochs_cls: 200 45 | weight_decay: 0 46 | weight_decay_cls: 0.0005 47 | mode: structure_refinement 48 | sparse: False 49 | k: 30 50 | dropout: 0.5 51 | dropout_eval: 0.5 52 | dropedge_rate: 0.5 53 | dropedge_rate_eval: 0.75 54 | tau: 0.9999 55 | c: 0 56 | learner: full 57 | maskfeat_rate_anchor: 0.6 58 | maskfeat_rate_learner: 0.7 59 | contrast_batch_size: 0 60 | eval_step: 50 61 | 62 | citeseer: 63 | seed: 1 64 | dataset: citeseer 65 | lr: 0.001 66 | lr_cls: 0.001 67 | num_hidden: 512 68 | num_hidden_eval: 32 69 | num_rep_dim: 256 70 | num_proj_dim: 256 71 | activation: tanh 72 | base_model: GCNConv 73 | num_layers: 2 74 | num_layers_eval: 2 75 | num_epochs: 1000 76 | num_epochs_cls: 200 77 | weight_decay: 0 78 | weight_decay_cls: 0.05 79 | mode: structure_refinement 80 | sparse: False 81 | k: 20 82 | dropout: 0.5 83 | dropout_eval: 0.5 84 | dropedge_rate: 0.25 85 | dropedge_rate_eval: 0.5 86 | tau: 0.9999 87 | c: 0 88 | learner: att 89 | maskfeat_rate_anchor: 0.8 90 | maskfeat_rate_learner: 0.6 91 | contrast_batch_size: 0 92 | 93 | pubmed: 94 | seed: 1 95 | dataset: pubmed 96 | lr: 0.001 97 | lr_cls: 0.01 98 | num_hidden: 128 99 | num_hidden_eval: 32 100 | num_rep_dim: 64 101 | num_proj_dim: 64 102 | activation: relu 103 | base_model: GCNConv 104 | num_layers: 2 105 | num_layers_eval: 2 106 | num_epochs: 1500 107 | num_epochs_cls: 200 108 | weight_decay: 0 109 | weight_decay_cls: 0.0005 110 | mode: structure_refinement 111 | sparse: True 112 | k: 10 113 | dropout: 0.5 114 | dropout_eval: 0.5 115 | dropedge_rate: 0.5 116 | dropedge_rate_eval: 0.25 117 | tau: 0.9999 118 | c: 50 119 | learner: mlp 120 | maskfeat_rate_anchor: 0.4 121 | maskfeat_rate_learner: 0.4 122 | contrast_batch_size: 2000 -------------------------------------------------------------------------------- /configs/vibgsl_config.yaml: -------------------------------------------------------------------------------- 1 | Default: 2 | seed: 12345 3 | backbone: 'GCN' 4 | graph_type: 'prob' 5 | graph_metric_type: 'mlp' 6 | repar: True 7 | num_layers: 2 8 | hidden_dim: 16 9 | folds: 10 10 | epochs: 200 11 | lr: 0.001 12 | lr_decay_factor: 0.5 13 | lr_decay_step_size: 50 14 | weight_decay: 5*10**-5 15 | batch_size: 100 16 | test_batch_size: 100 17 | beta: 0.00001 18 | IB_size: 16 19 | num_per: 16 20 | feature_denoise: False 21 | top_k: 10 22 | epsilon: 0.3 23 | graph_skip_conn: 0.0 24 | graph_include_self: True 25 | 26 | reddit-b: 27 | epochs: 200 28 | feature_denoise: False 29 | 30 | imdb-b: 31 | epochs: 200 32 | feature_denoise: False 33 | 34 | imdb-m: 35 | epochs: 200 36 | feature_denoise: False 37 | 38 | collab: 39 | epochs: 200 40 | feature_denoise: False -------------------------------------------------------------------------------- /data/__init__.py: -------------------------------------------------------------------------------- 1 | from .dataset import Dataset 2 | from .hetero_dataset import HeteroDataset 3 | from .graph_dataset import GraphDataset 4 | 5 | __all__ = ['Dataset', 'HeteroDataset', 'GraphDataset'] -------------------------------------------------------------------------------- /data/graph_dataset.py: -------------------------------------------------------------------------------- 1 | import os.path as osp 2 | import numpy as np 3 | import dgl 4 | import torch 5 | import torch.nn.functional as F 6 | from dgl.data import TUDataset 7 | 8 | 9 | class GraphDataset: 10 | def __init__(self, root, name): 11 | self.root = osp.expanduser(osp.normpath(root)) 12 | dict_dataset_name = { 13 | 'imdb-b': 'IMDB-BINARY', 14 | 'imdb-m': 'IMDB-MULTI', 15 | 'collab': 'COLLAB', 16 | 'reddit-b': 'REDDIT-BINARY', 17 | } 18 | self.name = dict_dataset_name[name] 19 | 20 | assert self.name in [ 21 | "IMDB-BINARY", "REDDIT-BINARY", "COLLAB", "IMDB-MULTI", 22 | ], ( 23 | "Currently only support IMDB-BINARY, REDDIT-BINARY, COLLAB, IMDB-MULTI" 24 | ) 25 | 26 | self.dgl_dataset = self.load_data() 27 | self.graphs, self.labels = zip(*[self.dgl_dataset[i] for i in range(len(self.dgl_dataset))]) 28 | self.labels = torch.tensor(self.labels) 29 | self.num_feat = self.dgl_dataset[0][0].ndata['feat'].shape[1] 30 | self.num_class = self.dgl_dataset.num_labels 31 | 32 | def __len__(self): 33 | return len(self.dgl_dataset) 34 | 35 | def load_data(self): 36 | # TODO: to_simple will be conducted before every access, which results in 37 | # the computed features being overwritten. Maybe we could abstract this 38 | # function to a dgl.transform class. 39 | dataset = TUDataset(self.name, raw_dir=self.root) 40 | # dataset = TUDataset(self.name, raw_dir=self.root, transform=dgl.to_simple) 41 | graph, _ = dataset[0] 42 | 43 | if "feat" not in graph.ndata: 44 | max_degree = 0 45 | degs = [] 46 | for g, _ in dataset: 47 | degs += [g.in_degrees().to(torch.long)] 48 | max_degree = max(max_degree, degs[-1].max().item()) 49 | 50 | if max_degree < 1000: 51 | # use one-hot degree embedding as node features 52 | for g, _ in dataset: 53 | deg = g.in_degrees() 54 | g.ndata['feat'] = F.one_hot(deg, num_classes=max_degree+1).to(torch.float) 55 | else: 56 | deg = torch.cat(degs, dim=0).to(torch.float) 57 | mean, std = deg.mean().item(), deg.std().item() 58 | for g, _ in dataset: 59 | deg = g.in_degrees().to(torch.float) 60 | deg = (deg - mean) / std 61 | g.ndata['feat'] = deg.view(-1, 1) 62 | 63 | return dataset 64 | 65 | 66 | 67 | if __name__ == "__main__": 68 | 69 | # graph classification dataset 70 | data_path = osp.join(osp.expanduser('~'), 'datasets') 71 | dataset = GraphDataset(root=data_path, name="IMDB-BINARY") 72 | -------------------------------------------------------------------------------- /encoder/__init__.py: -------------------------------------------------------------------------------- 1 | from .gat import GAT, GAT_dgl 2 | from .gcn import GCN, GCNConv, GCNConv_dgl, SparseDropout, GraphEncoder, GCNConv_diag, GCN_dgl, MetaDenseGCN 3 | from .gin import GIN 4 | from .graphsage import GraphSAGE 5 | from .mlp import MLP 6 | from .nodeformer_conv import NodeFormerConv 7 | from .gprgnn import GPRGNN, GPR_prop 8 | from .dagnn import DAGNN 9 | 10 | __all__ = [ 11 | "GAT", 12 | "GAT_dgl", 13 | "GCN", 14 | "GCNConv", 15 | "GCNConv_dgl", 16 | "SparseDropout", 17 | "GraphEncoder", 18 | "GCNConv_diag", 19 | "GCN_dgl", 20 | "GIN", 21 | "GraphSAGE", 22 | "MLP", 23 | "NodeFormerConv", 24 | "GPR_prop", 25 | "GPRGNN", 26 | "DAGNN", 27 | "MetaDenseGCN" 28 | ] 29 | 30 | classes = __all__ -------------------------------------------------------------------------------- /encoder/dagnn.py: -------------------------------------------------------------------------------- 1 | import torch 2 | from torch import nn 3 | from torch.nn import functional as F, Parameter 4 | import dgl.function as fn 5 | 6 | 7 | class DAGNNConv(nn.Module): 8 | def __init__(self, in_dim, k): 9 | super(DAGNNConv, self).__init__() 10 | 11 | self.s = Parameter(torch.FloatTensor(in_dim, 1)) 12 | self.k = k 13 | 14 | self.reset_parameters() 15 | 16 | def reset_parameters(self): 17 | gain = nn.init.calculate_gain("sigmoid") 18 | nn.init.xavier_uniform_(self.s, gain=gain) 19 | 20 | def forward(self, graph, feats): 21 | with graph.local_scope(): 22 | results = [feats] 23 | 24 | degs = graph.in_degrees().float() 25 | norm = torch.pow(degs, -0.5) 26 | norm = norm.to(feats.device).unsqueeze(1) 27 | 28 | for _ in range(self.k): 29 | feats = feats * norm 30 | graph.ndata["h"] = feats 31 | graph.update_all(fn.copy_u("h", "m"), fn.sum("m", "h")) 32 | feats = graph.ndata["h"] 33 | feats = feats * norm 34 | results.append(feats) 35 | 36 | H = torch.stack(results, dim=1) 37 | S = F.sigmoid(torch.matmul(H, self.s)) 38 | S = S.permute(0, 2, 1) 39 | H = torch.matmul(S, H).squeeze() 40 | 41 | return H 42 | 43 | 44 | class MLPLayer(nn.Module): 45 | def __init__(self, in_dim, out_dim, bias=True, activation=None, dropout=0): 46 | super(MLPLayer, self).__init__() 47 | 48 | self.linear = nn.Linear(in_dim, out_dim, bias=bias) 49 | self.activation = activation 50 | self.dropout = nn.Dropout(dropout) 51 | self.reset_parameters() 52 | 53 | def reset_parameters(self): 54 | gain = 1.0 55 | if self.activation is F.relu: 56 | gain = nn.init.calculate_gain("relu") 57 | nn.init.xavier_uniform_(self.linear.weight, gain=gain) 58 | if self.linear.bias is not None: 59 | nn.init.zeros_(self.linear.bias) 60 | 61 | def forward(self, feats): 62 | feats = self.dropout(feats) 63 | feats = self.linear(feats) 64 | if self.activation: 65 | feats = self.activation(feats) 66 | 67 | return feats 68 | 69 | 70 | class DAGNN(nn.Module): 71 | def __init__( 72 | self, 73 | k, 74 | in_dim, 75 | hid_dim, 76 | out_dim, 77 | bias=True, 78 | activation=F.relu, 79 | dropout=0, 80 | ): 81 | super(DAGNN, self).__init__() 82 | self.mlp = nn.ModuleList() 83 | self.mlp.append( 84 | MLPLayer( 85 | in_dim=in_dim, 86 | out_dim=hid_dim, 87 | bias=bias, 88 | activation=activation, 89 | dropout=dropout, 90 | ) 91 | ) 92 | self.mlp.append( 93 | MLPLayer( 94 | in_dim=hid_dim, 95 | out_dim=out_dim, 96 | bias=bias, 97 | activation=None, 98 | dropout=dropout, 99 | ) 100 | ) 101 | self.dagnn = DAGNNConv(in_dim=out_dim, k=k) 102 | 103 | def forward(self, graph, feats): 104 | for layer in self.mlp: 105 | feats = layer(feats) 106 | feats = self.dagnn(graph, feats) 107 | return feats -------------------------------------------------------------------------------- /encoder/gat.py: -------------------------------------------------------------------------------- 1 | import torch 2 | import torch.nn as nn 3 | import torch.nn.functional as F 4 | from dgl.nn.pytorch.conv import GATConv 5 | 6 | 7 | class GraphAttentionLayer(nn.Module): 8 | """ 9 | Simple GAT layer, similar to https://arxiv.org/abs/1710.10903 10 | """ 11 | 12 | def __init__(self, in_features, out_features, dropout, alpha=0.2, concat=True): 13 | super(GraphAttentionLayer, self).__init__() 14 | self.dropout = dropout 15 | self.in_features = in_features 16 | self.out_features = out_features 17 | self.alpha = alpha 18 | self.concat = concat 19 | 20 | self.W = nn.Parameter(torch.zeros(size=(in_features, out_features))) 21 | nn.init.xavier_uniform_(self.W.data, gain=1.414) 22 | 23 | self.a1 = nn.Parameter(torch.zeros(size=(out_features, 1))) 24 | nn.init.xavier_uniform_(self.a1.data, gain=1.414) 25 | self.a2 = nn.Parameter(torch.zeros(size=(out_features, 1))) 26 | nn.init.xavier_uniform_(self.a2.data, gain=1.414) 27 | 28 | self.leakyrelu = nn.LeakyReLU(self.alpha) 29 | 30 | def forward(self, input, adj): 31 | h = torch.mm(input, self.W) 32 | N = h.size()[0] 33 | 34 | a_input1 = torch.matmul(h, self.a1) 35 | a_input2 = torch.matmul(h, self.a2) 36 | e = self.leakyrelu(a_input1 + a_input2.transpose(-1, -2)) 37 | 38 | zero_vec = -9e15 * torch.ones_like(e) 39 | attention = torch.where(adj > 0, e, zero_vec) 40 | attention = F.softmax(attention, dim=1) 41 | attention = F.dropout(attention, self.dropout, training=self.training) 42 | h_prime = torch.matmul(attention, h) 43 | 44 | if self.concat: 45 | return F.elu(h_prime) 46 | else: 47 | return h_prime 48 | 49 | def __repr__(self): 50 | return ( 51 | self.__class__.__name__ 52 | + " (" 53 | + str(self.in_features) 54 | + " -> " 55 | + str(self.out_features) 56 | + ")" 57 | ) 58 | 59 | 60 | class SpecialSpmmFunction(torch.autograd.Function): 61 | """Special function for only sparse region backpropataion layer.""" 62 | @staticmethod 63 | def forward(ctx, indices, values, shape, b): 64 | assert indices.requires_grad == False 65 | a = torch.sparse_coo_tensor(indices, values, shape) 66 | ctx.save_for_backward(a, b) 67 | ctx.N = shape[0] 68 | return torch.matmul(a, b) 69 | 70 | @staticmethod 71 | def backward(ctx, grad_output): 72 | a, b = ctx.saved_tensors 73 | grad_values = grad_b = None 74 | if ctx.needs_input_grad[1]: 75 | grad_a_dense = grad_output.matmul(b.t()) 76 | edge_idx = a._indices()[0, :] * ctx.N + a._indices()[1, :] 77 | grad_values = grad_a_dense.view(-1)[edge_idx] 78 | if ctx.needs_input_grad[3]: 79 | grad_b = a.t().matmul(grad_output) 80 | return None, grad_values, None, grad_b 81 | 82 | 83 | class SpecialSpmm(nn.Module): 84 | def forward(self, indices, values, shape, b): 85 | return SpecialSpmmFunction.apply(indices, values, shape, b) 86 | 87 | 88 | class SpGraphAttentionLayer(nn.Module): 89 | """ 90 | Sparse version GAT layer, similar to https://arxiv.org/abs/1710.10903 91 | """ 92 | 93 | def __init__(self, in_features, out_features, dropout, alpha, concat=True): 94 | super(SpGraphAttentionLayer, self).__init__() 95 | self.in_features = in_features 96 | self.out_features = out_features 97 | self.alpha = alpha 98 | self.concat = concat 99 | 100 | self.W = nn.Parameter(torch.zeros(size=(in_features, out_features))) 101 | nn.init.xavier_normal_(self.W.data, gain=1.414) 102 | 103 | self.a = nn.Parameter(torch.zeros(size=(1, 2*out_features))) 104 | nn.init.xavier_normal_(self.a.data, gain=1.414) 105 | 106 | self.dropout = nn.Dropout(dropout) 107 | self.leakyrelu = nn.LeakyReLU(self.alpha) 108 | self.special_spmm = SpecialSpmm() 109 | 110 | def forward(self, input, adj): 111 | dv = 'cuda' if input.is_cuda else 'cpu' 112 | 113 | N = input.size()[0] 114 | edge = adj.coalesce().indices() 115 | 116 | h = torch.mm(input, self.W) 117 | # h: N x out 118 | assert not torch.isnan(h).any() 119 | 120 | # Self-attention on the nodes - Shared attention mechanism 121 | edge_h = torch.cat((h[edge[0, :], :], h[edge[1, :], :]), dim=1).t() 122 | # edge: 2*D x E 123 | 124 | edge_e = torch.exp(-self.leakyrelu(self.a.mm(edge_h).squeeze())) 125 | assert not torch.isnan(edge_e).any() 126 | # edge_e: E 127 | 128 | e_rowsum = self.special_spmm(edge, edge_e, torch.Size([N, N]), torch.ones(size=(N,1), device=dv)) 129 | # e_rowsum: N x 1 130 | 131 | edge_e = self.dropout(edge_e) 132 | # edge_e: E 133 | 134 | h_prime = self.special_spmm(edge, edge_e, torch.Size([N, N]), h) 135 | assert not torch.isnan(h_prime).any() 136 | # h_prime: N x out 137 | 138 | h_prime = h_prime.div(e_rowsum) 139 | # h_prime: N x out 140 | assert not torch.isnan(h_prime).any() 141 | 142 | if self.concat: 143 | # if this layer is not last layer, 144 | return F.elu(h_prime) 145 | else: 146 | # if this layer is last layer, 147 | return h_prime 148 | 149 | def __repr__(self): 150 | return self.__class__.__name__ + ' (' + str(self.in_features) + ' -> ' + str(self.out_features) + ')' 151 | 152 | 153 | class GAT(nn.Module): 154 | def __init__(self, nfeat, nhid, nclass, dropout, alpha, nheads, sparse=False): 155 | super(GAT, self).__init__() 156 | self.dropout = dropout 157 | self.sparse = sparse 158 | 159 | # def __init__(self, in_features, out_features, dropout, alpha, concat=True): 160 | 161 | if sparse: 162 | self.attentions = [ 163 | SpGraphAttentionLayer(nfeat, nhid, dropout=dropout, alpha=alpha, concat=True) 164 | for _ in range(nheads) 165 | ] 166 | for i, attention in enumerate(self.attentions): 167 | self.add_module('attention_{}'.format(i), attention) 168 | 169 | self.out_att = SpGraphAttentionLayer( 170 | nhid * nheads, nclass, dropout=dropout, alpha=alpha, concat=False 171 | ) 172 | else: 173 | self.attentions = [ 174 | GraphAttentionLayer(nfeat, nhid, dropout=dropout, alpha=alpha, concat=True) 175 | for _ in range(nheads) 176 | ] 177 | for i, attention in enumerate(self.attentions): 178 | self.add_module("attention_{}".format(i), attention) 179 | self.out_att = GraphAttentionLayer( 180 | nhid * nheads, nclass, dropout=dropout, alpha=alpha, concat=False 181 | ) 182 | 183 | def forward(self, x, adj): 184 | x = F.dropout(x, self.dropout, training=self.training) 185 | x = torch.cat([att(x, adj) for att in self.attentions], dim=1) 186 | x = F.dropout(x, self.dropout, training=self.training) 187 | x = F.elu(self.out_att(x, adj)) 188 | return x 189 | 190 | 191 | class GAT_dgl(nn.Module): 192 | def __init__(self, nfeat, nhid, nclass, dropout, alpha, nheads): 193 | super(GAT_dgl, self).__init__() 194 | 195 | self.dropout = dropout 196 | self.gat_layers = nn.ModuleList() 197 | # two-layer GAT 198 | self.gat_layers.append( 199 | GATConv(nfeat, nhid, nheads, feat_drop=dropout, attn_drop=dropout, activation=torch.nn.LeakyReLU(alpha)) 200 | ) 201 | self.gat_layers.append( 202 | GATConv(nhid * nheads, nclass, 1, feat_drop=dropout, attn_drop=dropout, activation=None) 203 | ) 204 | 205 | def forward(self, x, adj): 206 | for i, layer in enumerate(self.gat_layers): 207 | x = layer(adj, x) 208 | if i == 1: # last layer 209 | x = x.mean(1) 210 | else: # other layer(s) 211 | x = x.flatten(1) 212 | return x 213 | 214 | def reset_parameters(self): 215 | for layer in self.gat_layers: 216 | layer.reset_parameters() -------------------------------------------------------------------------------- /encoder/gin.py: -------------------------------------------------------------------------------- 1 | import torch 2 | import torch.nn as nn 3 | import torch.nn.functional as F 4 | from dgl.nn.pytorch.conv import GINConv 5 | 6 | 7 | class MLP(nn.Module): 8 | """Construct two-layer MLP-type aggreator for GIN model""" 9 | 10 | def __init__(self, input_dim, hidden_dim, output_dim): 11 | super().__init__() 12 | self.linears = nn.ModuleList() 13 | # two-layer MLP 14 | self.linears.append(nn.Linear(input_dim, hidden_dim, bias=False)) 15 | self.linears.append(nn.Linear(hidden_dim, output_dim, bias=False)) 16 | self.batch_norm = nn.BatchNorm1d(output_dim) 17 | 18 | def forward(self, x): 19 | h = F.relu(self.linears[0](x)) 20 | h = F.relu(self.linears[1](h)) 21 | h = self.batch_norm(h) 22 | return h 23 | 24 | def reset_parameters(self): 25 | for linear in self.linears: 26 | nn.init.xavier_normal_(linear.weight) 27 | self.batch_norm.reset_parameters() 28 | 29 | 30 | class GIN(nn.Module): 31 | def __init__(self, input_dim, hidden_dim, output_dim, num_layers): 32 | super().__init__() 33 | self.ginlayers = nn.ModuleList() 34 | for layer in range(num_layers): 35 | if layer == 0: 36 | mlp = MLP(input_dim, hidden_dim, hidden_dim) 37 | elif layer == num_layers - 1: 38 | mlp = MLP(hidden_dim, hidden_dim, output_dim) 39 | else: 40 | mlp = MLP(hidden_dim, hidden_dim, hidden_dim) 41 | self.ginlayers.append( 42 | GINConv(mlp, learn_eps=True) 43 | ) 44 | # self.batch_norms = nn.ModuleList() 45 | # self.batch_norms.append(nn.BatchNorm1d(hidden_dim)) 46 | 47 | def reset_parameters(self): 48 | for layer in self.ginlayers: 49 | layer.apply_func.reset_parameters() 50 | layer.eps.data.fill_(0.) 51 | 52 | def forward(self, x, adj): 53 | for layer in self.ginlayers: 54 | x = layer(adj, x) 55 | # h = self.batch_norms[i](h) 56 | # x = F.relu(x) 57 | return x 58 | -------------------------------------------------------------------------------- /encoder/gprgnn.py: -------------------------------------------------------------------------------- 1 | import torch 2 | import torch.nn.functional as F 3 | import numpy as np 4 | from torch.nn import Parameter 5 | 6 | from torch.nn import Linear 7 | import dgl.function as fn 8 | 9 | 10 | class GPR_prop(torch.nn.Module): 11 | ''' 12 | propagation class for GPR_GNN 13 | ''' 14 | def __init__(self, K, alpha, Init, Gamma=None, bias=True, **kwargs): 15 | super(GPR_prop, self).__init__(**kwargs) 16 | self.K = K 17 | self.Init = Init 18 | self.alpha = alpha 19 | self.Gamma = Gamma 20 | 21 | assert Init in ['SGC', 'PPR', 'NPPR', 'Random', 'WS'] 22 | if Init == 'SGC': 23 | # SGC-like, note that in this case, alpha has to be a integer. It means where the peak at when initializing GPR weights. 24 | TEMP = 0.0*np.ones(K+1) 25 | TEMP[alpha] = 1.0 26 | elif Init == 'PPR': 27 | # PPR-like 28 | TEMP = alpha*(1-alpha)**np.arange(K+1) 29 | TEMP[-1] = (1-alpha)**K 30 | elif Init == 'NPPR': 31 | # Negative PPR 32 | TEMP = (alpha)**np.arange(K+1) 33 | TEMP = TEMP/np.sum(np.abs(TEMP)) 34 | elif Init == 'Random': 35 | # Random 36 | bound = np.sqrt(3/(K+1)) 37 | TEMP = np.random.uniform(-bound, bound, K+1) 38 | TEMP = TEMP/np.sum(np.abs(TEMP)) 39 | elif Init == 'WS': 40 | # Specify Gamma 41 | TEMP = Gamma 42 | 43 | self.temp = Parameter(torch.tensor(TEMP)) 44 | 45 | def reset_parameters(self): 46 | torch.nn.init.zeros_(self.temp) 47 | if self.Init == 'SGC': 48 | self.temp.data[self.alpha]= 1.0 49 | elif self.Init == 'PPR': 50 | for k in range(self.K+1): 51 | self.temp.data[k] = self.alpha*(1-self.alpha)**k 52 | self.temp.data[-1] = (1-self.alpha)**self.K 53 | elif self.Init == 'NPPR': 54 | for k in range(self.K+1): 55 | self.temp.data[k] = self.alpha**k 56 | self.temp.data = self.temp.data/torch.sum(torch.abs(self.temp.data)) 57 | elif self.Init == 'Random': 58 | bound = np.sqrt(3/(self.K+1)) 59 | torch.nn.init.uniform_(self.temp,-bound,bound) 60 | self.temp.data = self.temp.data/torch.sum(torch.abs(self.temp.data)) 61 | elif self.Init == 'WS': 62 | self.temp.data = self.Gamma 63 | 64 | def forward(self, graph, feats): 65 | with graph.local_scope(): 66 | degs = graph.in_degrees().float() 67 | norm = torch.pow(degs, -0.5) 68 | norm = norm.to(feats.device).unsqueeze(1) 69 | 70 | hidden = feats*(self.temp[0]) 71 | for k in range(self.K): 72 | feats = feats * norm 73 | graph.ndata["h"] = feats 74 | graph.update_all(fn.copy_u("h", "m"), fn.sum("m", "h")) 75 | feats = graph.ndata["h"] 76 | feats = feats * norm 77 | gamma = self.temp[k+1] 78 | hidden = hidden + gamma*feats 79 | 80 | return hidden 81 | 82 | def __repr__(self): 83 | return '{}(K={}, temp={})'.format(self.__class__.__name__, self.K, 84 | self.temp) 85 | 86 | 87 | class GPRGNN(torch.nn.Module): 88 | def __init__(self, num_feat, num_class, hidden, ppnp, K, alpha, Init, Gamma, dprate, dropout): 89 | super(GPRGNN, self).__init__() 90 | self.lin1 = Linear(num_feat, hidden) 91 | self.lin2 = Linear(hidden, num_class) 92 | 93 | if ppnp == 'GPR_prop': 94 | self.prop1 = GPR_prop(K, alpha, Init, Gamma) 95 | 96 | self.Init = Init 97 | self.dprate = dprate 98 | self.dropout = dropout 99 | 100 | def reset_parameters(self): 101 | self.prop1.reset_parameters() 102 | 103 | def forward(self, graph, features): 104 | 105 | x = F.dropout(features, p=self.dropout, training=self.training) 106 | x = F.relu(self.lin1(x)) 107 | x = F.dropout(x, p=self.dropout, training=self.training) 108 | x = self.lin2(x) 109 | 110 | if self.dprate == 0.0: 111 | x = self.prop1(graph, x) 112 | return F.log_softmax(x, dim=1) 113 | else: 114 | x = F.dropout(x, p=self.dprate, training=self.training) 115 | x = self.prop1(graph, x) 116 | return F.log_softmax(x, dim=1) -------------------------------------------------------------------------------- /encoder/graphsage.py: -------------------------------------------------------------------------------- 1 | import torch 2 | import torch.nn as nn 3 | import torch.nn.functional as F 4 | 5 | 6 | class GraphSAGE(nn.Module): 7 | """https://github.com/dmlc/dgl/blob/master/examples/pytorch/graphsage/train_full.py""" 8 | 9 | def __init__( 10 | self, 11 | in_feats, 12 | n_hidden, 13 | n_classes, 14 | n_layers, 15 | activation, 16 | dropout, 17 | aggregator_type, 18 | ): 19 | super(GraphSAGE, self).__init__() 20 | from dgl.nn.pytorch.conv import SAGEConv 21 | 22 | self.layers = nn.ModuleList() 23 | self.dropout = nn.Dropout(dropout) 24 | self.activation = activation 25 | 26 | # input layer 27 | self.layers.append(SAGEConv(in_feats, n_hidden, aggregator_type)) 28 | # hidden layers 29 | for i in range(n_layers - 1): 30 | self.layers.append(SAGEConv(n_hidden, n_hidden, aggregator_type)) 31 | # output layer 32 | self.layers.append( 33 | SAGEConv(n_hidden, n_classes, aggregator_type) 34 | ) # activation None 35 | 36 | def forward(self, graph, inputs): 37 | h = self.dropout(inputs) 38 | for l, layer in enumerate(self.layers): 39 | h = layer(graph, h) 40 | if l != len(self.layers) - 1: 41 | h = self.activation(h) 42 | h = self.dropout(h) 43 | return h 44 | -------------------------------------------------------------------------------- /encoder/metamodule.py: -------------------------------------------------------------------------------- 1 | import torch 2 | from collections import OrderedDict 3 | import torch.nn as nn 4 | import torch.nn.functional as F 5 | 6 | class MetaModule(nn.Module): 7 | """ 8 | Base class for PyTorch meta-learning modules. These modules accept an 9 | additional argument `params` in their `forward` method. 10 | 11 | Notes 12 | ----- 13 | Objects inherited from `MetaModule` are fully compatible with PyTorch 14 | modules from `torch.nn.Module`. The argument `params` is a dictionary of 15 | tensors, with full support of the computation graph (for differentiation). 16 | """ 17 | def meta_named_parameters(self, prefix='', recurse=True): 18 | gen = self._named_members( 19 | lambda module: module._parameters.items() 20 | if isinstance(module, MetaModule) else [], 21 | prefix=prefix, recurse=recurse) 22 | for elem in gen: 23 | yield elem 24 | 25 | def meta_parameters(self, recurse=True): 26 | for name, param in self.meta_named_parameters(recurse=recurse): 27 | yield param 28 | 29 | 30 | class MetaLinear(nn.Linear, MetaModule): 31 | __doc__ = nn.Linear.__doc__ 32 | 33 | def forward(self, input, params=None): 34 | if params is None: 35 | params = OrderedDict(self.named_parameters()) 36 | bias = params.get('bias', None) 37 | return F.linear(input, params['weight'], bias) 38 | 39 | 40 | import re 41 | 42 | def get_subdict(dictionary, key=None): 43 | if dictionary is None: 44 | return None 45 | if (key is None) or (key == ''): 46 | return dictionary 47 | key_re = re.compile(r'^{0}\.(.+)'.format(re.escape(key))) 48 | return OrderedDict((key_re.sub(r'\1', k), value) for (k, value) 49 | in dictionary.items() if key_re.match(k) is not None) -------------------------------------------------------------------------------- /encoder/mlp.py: -------------------------------------------------------------------------------- 1 | import torch 2 | import torch.nn as nn 3 | 4 | 5 | class MLP(nn.Module): 6 | def __init__(self, n_layer, input_dim, output_dim, n_hidden, dropout=0.5, activation=torch.nn.ReLU()): 7 | super(MLP, self).__init__() 8 | self.layers = nn.ModuleList() 9 | # input layer 10 | self.layers.append(nn.Linear(input_dim, n_hidden)) 11 | # hidden layers 12 | for i in range(n_layer - 2): 13 | self.layers.append(nn.Linear(n_hidden, n_hidden)) 14 | # output layer 15 | self.layers.append(nn.Linear(n_hidden, output_dim)) 16 | self.dropout = nn.Dropout(p=dropout) 17 | self.activation = activation 18 | return 19 | 20 | def forward(self, input): 21 | h = input 22 | for i, layer in enumerate(self.layers): 23 | if i != 0: 24 | h = self.dropout(h) 25 | # h = F.relu(layer(h)) 26 | h = self.activation(layer(h)) 27 | return h -------------------------------------------------------------------------------- /eval/__init__.py: -------------------------------------------------------------------------------- 1 | from .eval_cls import ClsEvaluator -------------------------------------------------------------------------------- /eval/eval_cls.py: -------------------------------------------------------------------------------- 1 | import torch 2 | import copy 3 | import torch.nn.functional as F 4 | from GSL.encoder import GCN 5 | from GSL.utils import accuracy 6 | 7 | 8 | class ClsEvaluator: 9 | def __init__(self, model, config, nfeat, nclass, device): 10 | self.config = config 11 | 12 | if model == "GCN": 13 | self.model = GCN(nfeat, self.config.num_hidden_eval, nclass, self.config.num_layers_eval, 14 | self.config.dropout_eval, self.config.dropedge_rate_eval, self.config.sparse) 15 | self.model = self.model.to(device) 16 | else: 17 | raise Exception("We don't support the GNN model") 18 | 19 | def compute_loss_acc(self, model, features, adj, mask, labels): 20 | logits = model(features, adj) 21 | logp = F.log_softmax(logits, 1) 22 | loss = F.nll_loss(logp[mask], labels[mask], reduction='mean') 23 | acc = accuracy(logits[mask], labels[mask]) 24 | return loss, acc 25 | 26 | 27 | def __call__(self, features, adj, train_mask, val_mask, test_mask, labels): 28 | if self.config.sparse: 29 | f_adj = copy.deepcopy(adj) 30 | f_adj.edata['w'] = adj.edata['w'].detach() 31 | else: 32 | f_adj = adj.detach() 33 | # self.model.init_para() 34 | optimizer = torch.optim.Adam(self.model.parameters(), lr=self.config.lr_cls, weight_decay=self.config.weight_decay_cls) 35 | 36 | best_val = 0 37 | best_model = None 38 | 39 | for epoch in range(1, self.config.num_epochs_cls+1): 40 | self.model.train() 41 | loss_train, _ = self.compute_loss_acc(self.model, features, f_adj, train_mask, labels) 42 | 43 | optimizer.zero_grad() 44 | loss_train.backward() 45 | optimizer.step() 46 | 47 | if epoch % 10 == 0: 48 | self.model.eval() 49 | loss_val, acc_val = self.compute_loss_acc(self.model, features, f_adj, val_mask, labels) 50 | 51 | if acc_val > best_val: 52 | best_val = acc_val 53 | best_model = copy.deepcopy(self.model) 54 | 55 | best_model.eval() 56 | _, acc_test = self.compute_loss_acc(best_model, features, f_adj, test_mask, labels) 57 | 58 | return { 59 | 'Acc_val': best_val.item(), 60 | 'Acc_test': acc_test.item() 61 | } 62 | -------------------------------------------------------------------------------- /experiment.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pickle 3 | from contextlib import redirect_stdout 4 | 5 | import numpy as np 6 | import torch 7 | import yaml 8 | from easydict import EasyDict as edict 9 | # from hyperopt import Trials, fmin, hp, tpe 10 | 11 | from GSL.data import * 12 | from GSL.encoder import * 13 | from GSL.model import * 14 | from GSL.utils import (accuracy, get_knn_graph, macro_f1, micro_f1, 15 | random_add_edge, random_drop_edge, seed_everything, 16 | sparse_mx_to_torch_sparse_tensor, feature_mask, apply_feature_mask) 17 | 18 | 19 | class Experiment(object): 20 | def __init__(self, model_name, 21 | dataset, ntrail, 22 | data_path, config_path, 23 | sparse: bool = False, 24 | metric: str = 'acc', 25 | gpu_num: int = 0, 26 | use_knn: bool = False, 27 | k: int = 5, 28 | drop_rate: float = 0., 29 | add_rate: float = 0., 30 | mask_feat_rate: float = 0., 31 | use_mettack: bool = False, 32 | ptb_rate: float = 0.05 33 | ): 34 | 35 | self.sparse = sparse 36 | self.ntrail = ntrail 37 | self.metric = metric 38 | self.config_path = config_path 39 | self.device = torch.device("cuda:"+str(gpu_num) if torch.cuda.is_available() else "cpu") 40 | 41 | self.model_name = model_name 42 | self.dataset_name = dataset.lower() 43 | self.model_dict = { 44 | 'GCN': GCN_Trainer, 'MLP': MLP_Trainer, 'GPRGNN': GPRGNN_Trainer, 'DAGNN': DAGNN_Trainer, 'GRCN': GRCN, 'ProGNN': ProGNN, 'IDGL': IDGL, 45 | 'SLAPS': SLAPS, 'SUBLIME': SUBLIME, 'STABLE': STABLE, 'GTN': GTN, 'HGSL': HGSL, 'CoGSL': CoGSL, 'GEN': GEN, 46 | 'HGPSL': HGPSL, 'VIBGSL': VIBGSL, 'HESGSL': HESGSL, 'GSR': GSR, 'NodeFormer': NodeFormer, 'HAN': HAN, 'LDS': LDS 47 | } 48 | 49 | # Load graph datasets 50 | if self.dataset_name in ['cora', 'citeseer', 'pubmed', 'ogbn-arxiv', 51 | 'cornell', 'texas', 'wisconsin', 'actor']: 52 | self.data = Dataset(root=data_path, name=self.dataset_name, use_mettack=use_mettack, ptb_rate=ptb_rate) 53 | elif self.dataset_name in ['acm', 'dblp', 'yelp', 'imdb']: 54 | self.data = HeteroDataset(root=data_path, name=self.dataset_name) 55 | elif self.dataset_name in ['imdb-b', 'imdb-m', 'collab', 'reddit-b', 'mutag', 56 | 'proteins', 'peptides-func', 'peptides-struct']: 57 | self.data = GraphDataset(root=data_path, name=self.dataset_name) 58 | 59 | if isinstance(self.data, Dataset): 60 | # Modify graph structures 61 | adj = self.data.adj 62 | features = self.data.features 63 | 64 | mask = feature_mask(features, mask_feat_rate) 65 | apply_feature_mask(features, mask) 66 | 67 | # Randomly drop edges 68 | if drop_rate > 0: 69 | adj = random_drop_edge(adj, drop_rate) 70 | 71 | # Randomly add edges 72 | if add_rate > 0: 73 | adj = random_add_edge(adj, add_rate) 74 | 75 | # Use knn graph instead of the original structure 76 | if use_knn: 77 | adj = get_knn_graph(features, k, self.dataset_name) 78 | 79 | # Sparse or notyao 80 | if not self.sparse: 81 | self.data.adj = torch.from_numpy(adj.todense()).to(torch.float) 82 | else: 83 | self.data.adj = sparse_mx_to_torch_sparse_tensor(adj) 84 | 85 | if isinstance(self.data, GraphDataset): 86 | self.data = self.data.dgl_dataset 87 | else: 88 | self.data = self.data.to(self.device) 89 | 90 | # Select the metric of evaluation 91 | self.eval_metric = { 92 | 'acc': accuracy, 93 | 'macro-f1': macro_f1, 94 | 'micro-f1': micro_f1, 95 | }[metric] 96 | 97 | def run(self, params=None): 98 | """ 99 | Run the experiment 100 | """ 101 | test_results = [] 102 | num_feat, num_class = self.data.num_feat, self.data.num_class 103 | 104 | for i in range(self.ntrail): 105 | 106 | seed_everything(i) 107 | 108 | # Initialize the GSL model 109 | if self.model_name in ['SLAPS', 'CoGSL', 'HGSL', 'GTN', 'HAN']: 110 | model = self.model_dict[self.model_name](num_feat, num_class, self.eval_metric, 111 | self.config_path, self.dataset_name, self.device, self.data) # TODO modify the config according to the search space 112 | else: 113 | model = self.model_dict[self.model_name](num_feat, num_class, self.eval_metric, 114 | self.config_path, self.dataset_name, self.device, params) 115 | self.model = model.to(self.device) 116 | 117 | 118 | # Structure Learning 119 | self.model.fit(self.data, split_num=i) 120 | 121 | result = self.model.best_result 122 | test_results.append(result) 123 | print('Run: {} | Test result: {}'.format(i+1, result)) 124 | 125 | # TODO: support multiple metrics 126 | exp_info = '------------------------------------------------------------------------------\n' \ 127 | 'Experimental settings: \n' \ 128 | 'Model: {} | Dataset: {} | Metric: {} \n' \ 129 | 'Result: {} \n' \ 130 | 'Final test result: {} +- {} \n' \ 131 | '------------------------------------------------------------------------------'.\ 132 | format(self.model_name, self.dataset_name, self.metric, 133 | test_results, np.mean(test_results), np.std(test_results)) 134 | print(exp_info) 135 | return np.mean(test_results) 136 | 137 | def objective(self, params): 138 | with redirect_stdout(open(os.devnull, "w")): 139 | return {'loss': -self.run(params), 'status': 'ok'} 140 | 141 | def hp_search(self, hyperspace_path): 142 | with open(hyperspace_path, "r") as fin: 143 | raw_text = fin.read() 144 | raw_space = edict(yaml.safe_load(raw_text)) 145 | 146 | space_hyperopt = {} 147 | for key, config in raw_space.items(): 148 | if config.type == 'choice': 149 | space_hyperopt[key] = hp.choice(key, config.values) 150 | 151 | elif config.type == 'uniform': 152 | space_hyperopt[key] = hp.uniform(key, config.bounds[0], config.bounds[1]) 153 | 154 | # 可以扩展其他类型 155 | print(space_hyperopt) 156 | trials = Trials() 157 | best = fmin(self.objective, space_hyperopt, algo=tpe.suggest, max_evals=100, trials=trials) 158 | print(trials.best_trial) 159 | 160 | with open('trails.pkl', 'wb') as f: 161 | pickle.dump(trials, f) 162 | -------------------------------------------------------------------------------- /generate_attack.py: -------------------------------------------------------------------------------- 1 | import torch 2 | import numpy as np 3 | import torch.nn.functional as F 4 | import torch.optim as optim 5 | from deeprobust.graph.defense import GCN 6 | from deeprobust.graph.global_attack import MetaApprox, Metattack 7 | from deeprobust.graph.utils import * 8 | from deeprobust.graph.data import Dataset 9 | import argparse 10 | import time 11 | import os.path as osp 12 | 13 | parser = argparse.ArgumentParser() 14 | parser.add_argument('--no_cuda', action='store_true', 15 | help='Disables CUDA training.') 16 | parser.add_argument('--gpu_num', type=int, default=0, 17 | help='The selected GPU number.') 18 | parser.add_argument('--seed', type=int, default=int(time.time()), help='Random seed.') 19 | parser.add_argument('--epochs', type=int, default=200, 20 | help='Number of epochs to train.') 21 | parser.add_argument('--lr', type=float, default=0.01, 22 | help='Initial learning rate.') 23 | parser.add_argument('--weight_decay', type=float, default=5e-4, 24 | help='Weight decay (L2 loss on parameters).') 25 | parser.add_argument('--hidden', type=int, default=16, 26 | help='Number of hidden units.') 27 | parser.add_argument('--dropout', type=float, default=0.5, 28 | help='Dropout rate (1 - keep probability).') 29 | parser.add_argument('--dataset', type=str, default='cora', choices=['cora', 'citeseer', 'polblogs'], help='dataset') 30 | parser.add_argument('--ptb_rate', type=float, default=0.05, help='pertubation rate') 31 | parser.add_argument('--model', type=str, default='Meta-Self', 32 | choices=['Meta-Self', 'A-Meta-Self', 'Meta-Train', 'A-Meta-Train'], help='model variant') 33 | 34 | args = parser.parse_args() 35 | 36 | device = torch.device("cuda:{}".format(args.gpu_num) if not args.no_cuda else "cpu") 37 | 38 | np.random.seed(args.seed) 39 | torch.manual_seed(args.seed) 40 | if device != 'cpu': 41 | torch.cuda.manual_seed(args.seed) 42 | 43 | data_path = osp.join(osp.expanduser('~'), 'datasets') 44 | 45 | data = Dataset(root='/tmp/', name=args.dataset, setting='nettack') 46 | adj, features, labels = data.adj, data.features, data.labels 47 | idx_train, idx_val, idx_test = data.idx_train, data.idx_val, data.idx_test 48 | idx_unlabeled = np.union1d(idx_val, idx_test) 49 | 50 | perturbations = int(args.ptb_rate * (adj.sum()//2)) 51 | adj, features, labels = preprocess(adj, features, labels, preprocess_adj=False) 52 | 53 | # Setup Surrogate Model 54 | surrogate = GCN(nfeat=features.shape[1], nclass=labels.max().item()+1, nhid=16, 55 | dropout=0.5, with_relu=False, with_bias=True, weight_decay=5e-4, device=device) 56 | 57 | surrogate = surrogate.to(device) 58 | surrogate.fit(features, adj, labels, idx_train) 59 | 60 | # Setup Attack Model 61 | if 'Self' in args.model: 62 | lambda_ = 0 63 | if 'Train' in args.model: 64 | lambda_ = 1 65 | if 'Both' in args.model: 66 | lambda_ = 0.5 67 | 68 | if 'A' in args.model: 69 | model = MetaApprox(model=surrogate, nnodes=adj.shape[0], feature_shape=features.shape, attack_structure=True, attack_features=False, device=device, lambda_=lambda_) 70 | 71 | else: 72 | model = Metattack(model=surrogate, nnodes=adj.shape[0], feature_shape=features.shape, attack_structure=True, attack_features=False, device=device, lambda_=lambda_) 73 | 74 | model = model.to(device) 75 | 76 | def test(adj): 77 | ''' test on GCN ''' 78 | 79 | # adj = normalize_adj_tensor(adj) 80 | gcn = GCN(nfeat=features.shape[1], 81 | nhid=args.hidden, 82 | nclass=labels.max().item() + 1, 83 | dropout=args.dropout, device=device) 84 | gcn = gcn.to(device) 85 | # gcn.fit(features, adj, labels, idx_train) # train without model picking 86 | gcn.fit(features, adj, labels, idx_train, idx_val) # train with validation model picking 87 | output = gcn.output.cpu() 88 | loss_test = F.nll_loss(output[idx_test], labels[idx_test]) 89 | acc_test = accuracy(output[idx_test], labels[idx_test]) 90 | print("Test set results:", 91 | "loss= {:.4f}".format(loss_test.item()), 92 | "accuracy= {:.4f}".format(acc_test.item())) 93 | 94 | return acc_test.item() 95 | 96 | def main(): 97 | model.attack(features, adj, labels, idx_train, idx_unlabeled, perturbations, ll_constraint=False) 98 | print('=== testing GCN on modified graph ===') 99 | modified_adj = model.modified_adj 100 | torch.save(modified_adj.cpu().to_sparse(), osp.join(data_path, "%s_%s_%s.pt" % ('mettack', args.dataset, args.ptb_rate))) 101 | np.save(osp.join(data_path, "%s_%s_%s_idx_train.npy" % ('mettack', args.dataset, args.ptb_rate)), idx_train) 102 | np.save(osp.join(data_path, "%s_%s_%s_idx_val.npy" % ('mettack', args.dataset, args.ptb_rate)), idx_val) 103 | np.save(osp.join(data_path, "%s_%s_%s_idx_test.npy" % ('mettack', args.dataset, args.ptb_rate)), idx_test) 104 | 105 | # test(modified_adj) 106 | 107 | 108 | if __name__ == '__main__': 109 | main() 110 | -------------------------------------------------------------------------------- /hyperparams/idgl_hyper.yaml: -------------------------------------------------------------------------------- 1 | learning_rate: 2 | type: choice 3 | values: [1.e-4, 1.e-3, 1.e-2] 4 | 5 | graph_skip_conn: 6 | type: uniform 7 | bounds: [0.5, 0.9] 8 | -------------------------------------------------------------------------------- /learner/__init__.py: -------------------------------------------------------------------------------- 1 | from .base_learner import BaseLearner 2 | from .attention_learner import AttLearner 3 | from .full_param_learner import FullParam 4 | from .gnn_learner import GNNLearner 5 | from .hadamard_prod_learner import KHeadHPLearner 6 | from .mlp_learner import MLPLearner 7 | from .sparsification_learner import SpLearner 8 | 9 | __all__ = [ 10 | "BaseLearner", 11 | "FullParam", 12 | "AttLearner", 13 | "MLPLearner", 14 | "KHeadHPLearner", 15 | "GNNLearner", 16 | "SpLearner" 17 | ] 18 | 19 | classes = __all__ 20 | -------------------------------------------------------------------------------- /learner/attention_learner.py: -------------------------------------------------------------------------------- 1 | import torch 2 | import torch.nn as nn 3 | import dgl 4 | 5 | from GSL.learner import BaseLearner 6 | from GSL.metric import * 7 | from GSL.processor import * 8 | from GSL.utils import knn_fast 9 | 10 | 11 | class Attentive(nn.Module): 12 | def __init__(self, size): 13 | super(Attentive, self).__init__() 14 | self.w = nn.Parameter(torch.ones(size)) 15 | 16 | def forward(self, x): 17 | return x @ torch.diag(self.w) 18 | 19 | 20 | class AttLearner(BaseLearner): 21 | """Attentive Learner""" 22 | def __init__(self, metric, processors, nlayers, size, activation, sparse): 23 | """ 24 | nlayers: int 25 | Number of attention layers 26 | act: str 27 | Activation Function 28 | """ 29 | super(AttLearner, self).__init__(metric, processors) 30 | self.nlayers = nlayers 31 | self.layers = nn.ModuleList() 32 | for _ in range(self.nlayers): 33 | self.layers.append(Attentive(size)) 34 | self.activation = activation 35 | self.sparse = sparse 36 | 37 | def internal_forward(self, x): 38 | for i, layer in enumerate(self.layers): 39 | x = layer(x) 40 | if i != (self.nlayers - 1): 41 | x = self.activation(x) 42 | return x 43 | 44 | def forward(self, features): 45 | if self.sparse: 46 | z = self.internal_forward(features) 47 | # TODO: knn should be moved to processor 48 | rows, cols, values = knn_fast(z, self.k, 1000) 49 | rows_ = torch.cat((rows, cols)) 50 | cols_ = torch.cat((cols, rows)) 51 | values_ = torch.cat((values, values)) 52 | values_ = self.activation(values_) 53 | adj = dgl.graph((rows_, cols_), num_nodes=features.shape[0], device='cuda') 54 | adj.edata['w'] = values_ 55 | return adj 56 | else: 57 | z = self.internal_forward(features) 58 | z = F.normalize(z, dim=1, p=2) 59 | similarities = self.metric(z) 60 | for processor in self.processors: 61 | similarities = processor(similarities) 62 | similarities = F.relu(similarities) 63 | return similarities 64 | 65 | 66 | if __name__ == '__main__': 67 | metric = CosineSimilarity() 68 | processors = [KNNSparsify(3), NonLinearize(non_linearity='relu'), Symmetrize(), Normalize(mode='sym')] 69 | activation = nn.ReLU 70 | att_learner = AttLearner(metric, processors, 1, 16, activation) 71 | x = torch.rand(8, 16) 72 | adj = att_learner(x) 73 | print(adj) -------------------------------------------------------------------------------- /learner/base_learner.py: -------------------------------------------------------------------------------- 1 | import torch 2 | import torch.nn as nn 3 | 4 | class BaseLearner(nn.Module): 5 | """Abstract base class for graph learner""" 6 | def __init__(self, metric, processors): 7 | super(BaseLearner, self).__init__() 8 | 9 | self.metric = metric 10 | self.processors = processors -------------------------------------------------------------------------------- /learner/full_param_learner.py: -------------------------------------------------------------------------------- 1 | import torch.nn as nn 2 | import torch.nn.functional as F 3 | 4 | from GSL.learner import BaseLearner 5 | 6 | 7 | class FullParam(BaseLearner): 8 | """Full graph parameterization learner""" 9 | 10 | def __init__( 11 | self, 12 | metric=None, 13 | processors=None, 14 | features=None, 15 | sparse=False, 16 | non_linearize=None, 17 | adj=None, 18 | ): 19 | super(FullParam, self).__init__(metric, processors) 20 | self.sparse = sparse 21 | self.non_linearize = non_linearize 22 | if adj is None: 23 | adj = self.metric(features) 24 | if processors: 25 | for processor in self.processors: 26 | adj = processor(adj) 27 | self.adj = nn.Parameter(adj) 28 | 29 | def forward(self, h): 30 | # note: non_linearize should be conducted in every forward 31 | adj = self.non_linearize(self.adj) 32 | return adj 33 | -------------------------------------------------------------------------------- /learner/gnn_learner.py: -------------------------------------------------------------------------------- 1 | import torch 2 | import dgl 3 | import os.path as osp 4 | import torch.nn as nn 5 | import torch.nn.functional as F 6 | from GSL.encoder import GCNConv_dgl, GCNConv 7 | from GSL.learner import BaseLearner 8 | from GSL.utils import knn_fast, top_k 9 | from GSL.metric import * 10 | from GSL.processor import * 11 | from GSL.data import Dataset 12 | 13 | class GNNLearner(BaseLearner): 14 | def __init__(self, metric, processors, adj, nlayers, num_feature, hidden, 15 | activation, sparse, k, base_model='GCN', bias=False): 16 | super(GNNLearner, self).__init__(metric, processors) 17 | 18 | self.nlayers = nlayers 19 | if sparse: 20 | self.base_model = {'GCN': GCNConv_dgl}[base_model] 21 | self.layers = nn.ModuleList() 22 | if nlayers == 1: 23 | self.layers.append(self.base_model(num_feature, hidden)) 24 | else: 25 | self.layers.append(self.base_model(num_feature, hidden)) 26 | for _ in range(nlayers - 2): 27 | self.layers.append(self.base_model(hidden, hidden)) 28 | self.layers.append(self.base_model(hidden, hidden)) 29 | else: 30 | self.base_model = {'GCN': GCNConv}[base_model] 31 | self.layers = nn.ModuleList() 32 | if nlayers == 1: 33 | self.layers.append(self.base_model(num_feature, hidden, bias=bias, activation=activation)) 34 | else: 35 | self.layers.append(self.base_model(num_feature, hidden, bias=bias, activation=activation)) 36 | for _ in range(nlayers - 2): 37 | self.layers.append(self.base_model(hidden, hidden, bias=bias, activation=activation)) 38 | self.layers.append(self.base_model(hidden, hidden, bias=bias, activation=activation)) 39 | 40 | self.activation = activation 41 | self.dim = hidden 42 | self.sparse = sparse 43 | self.k = k 44 | self.adj = adj 45 | self.param_init() 46 | 47 | def param_init(self): 48 | for layer in self.layers: 49 | layer.weight = nn.Parameter(torch.eye(self.dim)) 50 | 51 | def internal_forward(self, h): 52 | for i, layer in enumerate(self.layers): 53 | if self.sparse: 54 | h = layer(self.adj, h) 55 | else: 56 | h = layer(h, self.adj) 57 | if i != (len(self.layers) - 1): 58 | h = self.activation(h) 59 | return h 60 | 61 | def forward(self, features, v_indices=None): 62 | # TODO: knn sparsify should be a post-processor 63 | if self.sparse: 64 | embeddings = self.internal_forward(features) 65 | rows, cols, values = knn_fast(embeddings, self.k, 1000) 66 | rows_ = torch.cat((rows, cols)) 67 | cols_ = torch.cat((cols, rows)) 68 | values_ = torch.cat((values, values)) 69 | values_ = self.processors(values_) 70 | adj = dgl.graph((rows_, cols_), num_nodes=features.shape[0], device='cuda') 71 | adj.edata['w'] = values_ 72 | return adj 73 | else: 74 | embeddings = self.internal_forward(features) 75 | # embeddings = F.normalize(embeddings, dim=1, p=2) 76 | if v_indices is not None: 77 | similarities = self.metric(embeddings, v_indices) 78 | else: 79 | similarities = self.metric(embeddings) 80 | for processor in self.processors: 81 | similarities = processor(similarities) 82 | return similarities 83 | 84 | 85 | if __name__ == "__main__": 86 | data_path = osp.join(osp.expanduser('~'), 'datasets') 87 | data = Dataset(root=data_path, name='cora') 88 | adj = data.adj 89 | 90 | metric = CosineSimilarity() 91 | processors = [NonLinearize(non_linearity='relu', alpha=1)] 92 | graph_learner = GNNLearner(metric, processors, adj, 2, 32, F.relu, False, 30, 'GCN') 93 | -------------------------------------------------------------------------------- /learner/hadamard_prod_learner.py: -------------------------------------------------------------------------------- 1 | import torch 2 | import torch.nn as nn 3 | from GSL.utils import knn_fast 4 | import torch.nn.functional as F 5 | import dgl 6 | from GSL.learner import BaseLearner 7 | 8 | 9 | class HadamardProduct(nn.Module): 10 | def __init__(self, dim): 11 | super(HadamardProduct, self).__init__() 12 | self.weight = nn.Parameter(torch.FloatTensor(1, dim)) 13 | nn.init.xavier_uniform_(self.weight) 14 | 15 | def forward(self, h): 16 | return h * self.weight 17 | 18 | 19 | class KHeadHPLearner(BaseLearner): 20 | def __init__(self, metric, processors, dim, num_heads, sparse=False): 21 | super().__init__(metric=metric, processors=processors) 22 | 23 | self.dim = dim 24 | self.num_heads = num_heads 25 | self.sparse = sparse 26 | 27 | self.hp_heads = nn.ModuleList() 28 | for i in range(num_heads): 29 | self.hp_heads.append(HadamardProduct(dim)) 30 | 31 | def internal_forward(self, x): 32 | for i, layer in enumerate(self.layers): 33 | x = layer(x) 34 | if i != self.nlayers - 1: 35 | x = self.activation(x) 36 | return x 37 | 38 | def forward(self, left_features, right_feature=None): 39 | """ 40 | Note: The right_feature is used to computer a non-square adjacent matrix, such as relation subgraph 41 | of heterogeneous graph. 42 | """ 43 | if self.sparse: 44 | raise NameError('We dont support the sparse version yet') 45 | else: 46 | if torch.sum(left_features) == 0 or torch.sum(right_feature) == 0: 47 | return torch.zeros((left_features.shape[0], right_feature.shape[0])) 48 | s = torch.zeros((left_features.shape[0], right_feature.shape[0])).to(left_features.device) 49 | zero_lines = torch.nonzero(torch.sum(left_features, 1) == 0) 50 | # The ReLU function will generate zero lines, which lead to the nan (devided by zero) problem. 51 | if len(zero_lines) > 0: 52 | left_features[zero_lines, :] += 1e-8 53 | 54 | # metric 55 | for i in range(self.num_heads): 56 | weighted_left_h = self.hp_heads[i](left_features) 57 | weighted_right_h = self.hp_heads[i](right_feature) 58 | s += self.metric(weighted_left_h, weighted_right_h) 59 | s /= self.num_heads 60 | 61 | # processor 62 | for processor in self.processors: 63 | s = processor(s) 64 | 65 | return s -------------------------------------------------------------------------------- /learner/mlp_learner.py: -------------------------------------------------------------------------------- 1 | import torch 2 | import torch.nn as nn 3 | from GSL.utils import knn_fast 4 | import torch.nn.functional as F 5 | import dgl 6 | 7 | from GSL.learner import BaseLearner 8 | 9 | class MLPLearner(BaseLearner): 10 | """Multi-Layer Perception learner""" 11 | def __init__(self, metric, processors, nlayers, in_dim, hidden_dim, activation, sparse, k=None): 12 | super().__init__(metric=metric, processors=processors) 13 | 14 | self.nlayers = nlayers 15 | self.layers = nn.ModuleList() 16 | self.layers.append(nn.Linear(in_dim, hidden_dim)) 17 | for _ in range(1, nlayers): 18 | self.layers.append(nn.Linear(hidden_dim, hidden_dim)) 19 | 20 | self.in_dim = in_dim 21 | self.hidden_dim = hidden_dim 22 | self.param_init() 23 | self.activation = activation 24 | self.sparse = sparse 25 | self.k = k 26 | 27 | def param_init(self): 28 | for layer in self.layers: 29 | # layer.reset_parameters() 30 | layer.weight = nn.Parameter(torch.eye(self.in_dim)) 31 | 32 | def internal_forward(self, x): 33 | for i, layer in enumerate(self.layers): 34 | x = layer(x) 35 | if i != self.nlayers - 1: 36 | x = self.activation(x) 37 | return x 38 | 39 | def forward(self, features): 40 | if self.sparse: 41 | z = self.internal_forward(features) 42 | # TODO: knn should be moved to processor 43 | rows, cols, values = knn_fast(z, self.k, 1000) 44 | rows_ = torch.cat((rows, cols)) 45 | cols_ = torch.cat((cols, rows)) 46 | values_ = torch.cat((values, values)) 47 | values_ = self.activation(values_) 48 | adj = dgl.graph((rows_, cols_), num_nodes=features.shape[0], device=features.device) 49 | adj.edata['w'] = values_ 50 | return adj 51 | else: 52 | z = self.internal_forward(features) 53 | z = F.normalize(z, dim=1, p=2) 54 | similarities = self.metric(z) 55 | for processor in self.processors: 56 | similarities = processor(similarities) 57 | similarities = F.relu(similarities) 58 | return similarities -------------------------------------------------------------------------------- /learner/sparsification_learner.py: -------------------------------------------------------------------------------- 1 | from GSL.learner import BaseLearner 2 | import torch 3 | import torch.nn as nn 4 | import torch.nn.functional as F 5 | 6 | eps = 1e-8 7 | 8 | 9 | class SpLearner(BaseLearner): 10 | """Sparsification learner""" 11 | def __init__(self, nlayers, in_dim, hidden, activation, k, weight=True, metric=None, processors=None): 12 | super().__init__(metric=metric, processors=processors) 13 | 14 | self.nlayers = nlayers 15 | self.layers = nn.ModuleList() 16 | self.layers.append(nn.Linear(in_dim, hidden)) 17 | for _ in range(nlayers - 2): 18 | self.layers.append(nn.Linear(hidden, hidden)) 19 | self.layers.append(nn.Linear(hidden, 1)) 20 | 21 | self.param_init() 22 | self.activation = activation 23 | self.k = k 24 | self.weight = weight 25 | 26 | def param_init(self): 27 | for layer in self.layers: 28 | nn.init.xavier_uniform_(layer.weight) 29 | 30 | def internal_forward(self, x): 31 | for i, layer in enumerate(self.layers): 32 | x = layer(x) 33 | if i != self.nlayers - 1: 34 | x = self.activation(x) 35 | return x 36 | 37 | def gumbel_softmax_sample(self, logits, temperature, adj, training): 38 | """Draw a sample from the Gumbel-Softmax distribution""" 39 | r = self.sample_gumble(logits.coalesce().values().shape) 40 | if training is not None: 41 | values = torch.log(logits.coalesce().values()) + r.to(logits.device) 42 | else: 43 | values = torch.log(logits.coalesce().values()) 44 | values /= temperature 45 | y = torch.sparse_coo_tensor(indices=adj.coalesce().indices(), values=values, size=adj.shape) 46 | return torch.sparse.softmax(y, dim=1) 47 | 48 | def sample_gumble(self, shape): 49 | """Sample from Gumbel(0, 1)""" 50 | U = torch.rand(shape) 51 | return -torch.log(-torch.log(U + eps) + eps) 52 | 53 | def forward(self, features, adj, temperature, training=None): 54 | indices = adj.coalesce().indices().t() 55 | values = adj.coalesce().values() 56 | 57 | f1_features = torch.index_select(features, 0, indices[:, 0]) 58 | f2_features = torch.index_select(features, 0, indices[:, 1]) 59 | auv = torch.unsqueeze(values, -1) 60 | temp = torch.cat([f1_features, f2_features, auv], -1) 61 | 62 | temp = self.internal_forward(temp) 63 | z = torch.reshape(temp, [-1]) 64 | 65 | z_matrix = torch.sparse_coo_tensor(indices=indices.t(), values=z, size=adj.shape) 66 | pi = torch.sparse.softmax(z_matrix, dim=1) 67 | 68 | y = self.gumbel_softmax_sample(pi, temperature, adj, training) 69 | y_dense = y.to_dense() 70 | 71 | top_k_v, top_k_i = torch.topk(y_dense, self.k) 72 | kth = torch.min(top_k_v, -1)[0] + eps 73 | kth = kth.unsqueeze(-1) 74 | kth = torch.tile(kth, [1, kth.shape[0]]) 75 | mask2 = y_dense >= kth 76 | mask2 = mask2.to(torch.float32) 77 | row_sum = mask2.sum(-1) 78 | 79 | dense_support = mask2 80 | 81 | if self.weight: 82 | dense_support = torch.mul(y_dense, mask2) 83 | else: 84 | print('no gradient bug here!') 85 | exit() 86 | 87 | # norm 88 | dense_support = dense_support + torch.eye(adj.shape[0]).to(dense_support.device) 89 | 90 | rowsum = torch.sum(dense_support, -1) + 1e-6 91 | d_inv_sqrt = torch.reshape(torch.pow(rowsum, -0.5), [-1]) 92 | d_mat_inv_sqer = torch.diag(d_inv_sqrt) 93 | ad = torch.matmul(dense_support, d_mat_inv_sqer) 94 | adt = ad.t() 95 | dadt = torch.matmul(adt, d_mat_inv_sqer) 96 | support = dadt 97 | 98 | return support -------------------------------------------------------------------------------- /logo.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSL-Benchmark/GSLB/375cc1c20314360a927527f8d8528773fe1b0abc/logo.jpeg -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import os.path as osp 3 | 4 | from GSL.experiment import Experiment 5 | 6 | if __name__ == '__main__': 7 | 8 | parser = argparse.ArgumentParser() 9 | parser.add_argument('--dataset', type=str, default='cora', 10 | help='the name of graph dataset') 11 | parser.add_argument('--model', type=str, default='GRCN', 12 | help='the name of GSL model') 13 | parser.add_argument('--ntrail', type=int, default=1, 14 | help='repetition count of experiments') 15 | parser.add_argument('--use_knn', action='store_true', 16 | help='whether to use knn graph instead of the original structure') 17 | parser.add_argument('--k', type=int, default=5, help='the number of nearest neighbors') 18 | parser.add_argument('--drop_rate', type=float, default=0., 19 | help='the probability of randomly drop edges') 20 | parser.add_argument('--add_rate', type=float, default=0., 21 | help='the probability of randomly add edges') 22 | parser.add_argument('--mask_feat_rate', type=float, default=0., 23 | help='the probability of randomly mask features') 24 | parser.add_argument('--use_mettack', action='store_true', 25 | help='whether to use the structure after being attacked by mettack') 26 | parser.add_argument('--ptb_rate', type=float, default=0., 27 | help='the perturbation rate') 28 | parser.add_argument('--metric', type=str, default='acc', 29 | help='the evaluation metric') 30 | parser.add_argument('--sparse', action='store_true', 31 | help='whether to use sparse version') 32 | parser.add_argument('--gpu_num', type=int, default=0, 33 | help='the selected GPU number') 34 | args = parser.parse_args() 35 | 36 | data_path = osp.join(osp.expanduser('~'), 'datasets') 37 | config_path = './configs/{}_config.yaml'.format(args.model.lower()) 38 | 39 | exp = Experiment(model_name=args.model, dataset=args.dataset, ntrail=args.ntrail, 40 | data_path=data_path, config_path=config_path, metric=args.metric, sparse=args.sparse, 41 | use_knn=args.use_knn, k=args.k, drop_rate=args.drop_rate, gpu_num=args.gpu_num, 42 | add_rate=args.add_rate, use_mettack=args.use_mettack, ptb_rate=args.ptb_rate, mask_feat_rate=args.mask_feat_rate) 43 | exp.run() 44 | # exp.hp_search("hyperparams/idgl_hyper.yaml") -------------------------------------------------------------------------------- /metric.py: -------------------------------------------------------------------------------- 1 | import torch 2 | import torch.nn as nn 3 | import torch.nn.functional as F 4 | 5 | 6 | def cal_similarity_graph(node_embeddings, right_node_embedding=None): 7 | if right_node_embedding is None: 8 | similarity_graph = torch.mm(node_embeddings, node_embeddings.t()) 9 | else: 10 | similarity_graph = torch.mm(node_embeddings, right_node_embedding.t()) 11 | return similarity_graph 12 | 13 | 14 | class InnerProductSimilarity: 15 | def __init__(self): 16 | super(InnerProductSimilarity, self).__init__() 17 | 18 | def __call__(self, embeddings, right_embeddings=None): 19 | similarities = cal_similarity_graph(embeddings, right_embeddings) 20 | return similarities 21 | 22 | 23 | class CosineSimilarity: 24 | def __init__(self): 25 | super(CosineSimilarity, self).__init__() 26 | 27 | def __call__(self, embeddings, right_embeddings=None): 28 | if right_embeddings is None: 29 | embeddings = F.normalize(embeddings, dim=1, p=2) 30 | similarities = cal_similarity_graph(embeddings) 31 | else: 32 | embeddings = F.normalize(embeddings, dim=1, p=2) 33 | right_embeddings = F.normalize(right_embeddings, dim=1, p=2) 34 | similarities = cal_similarity_graph(embeddings, right_embeddings) 35 | return similarities 36 | 37 | 38 | class WeightedCosine(nn.Module): 39 | def __init__(self, input_size, num_pers): 40 | super().__init__() 41 | self.weight_tensor = torch.Tensor(num_pers, input_size) 42 | self.weight_tensor = nn.Parameter(nn.init.xavier_uniform_(self.weight_tensor)) 43 | 44 | def __call__(self, embeddings): 45 | expand_weight_tensor = self.weight_tensor.unsqueeze(1) 46 | if len(embeddings.shape) == 3: 47 | expand_weight_tensor = expand_weight_tensor.unsqueeze(1) 48 | 49 | embeddings_fc = embeddings.unsqueeze(0) * expand_weight_tensor 50 | embeddings_norm = F.normalize(embeddings_fc, p=2, dim=-1) 51 | attention = torch.matmul( 52 | embeddings_norm, embeddings_norm.transpose(-1, -2) 53 | ).mean(0) 54 | return attention 55 | 56 | 57 | class MLPRefineSimilarity(nn.Module): 58 | def __init__(self, hid, dropout): 59 | super(MLPRefineSimilarity, self).__init__() 60 | self.gen_mlp = nn.Linear(2 * hid, 1) 61 | self.dropout = nn.Dropout(dropout) 62 | 63 | def __call__(self, embeddings, v_indices): 64 | num_node = embeddings.shape[0] 65 | f1 = embeddings[v_indices[0]] 66 | f2 = embeddings[v_indices[1]] 67 | ff = torch.cat([f1, f2], dim=-1) 68 | temp = self.gen_mlp(self.dropout(ff)).reshape(-1) 69 | z_matrix = torch.sparse.FloatTensor(v_indices, temp, (num_node, num_node)).to_dense() 70 | return z_matrix 71 | 72 | # class Minkowski: 73 | -------------------------------------------------------------------------------- /model/__init__.py: -------------------------------------------------------------------------------- 1 | from .base_gsl import BaseModel 2 | from .gen import GEN 3 | from .idgl import IDGL 4 | from .prognn import ProGNN 5 | from .ptdnet import PTDNet 6 | from .slaps import SLAPS 7 | from .stable import STABLE 8 | from .sublime import SUBLIME 9 | from .vibgsl import VIBGSL 10 | from .cogsl import CoGSL 11 | from .neuralsparse import NeuralSparse 12 | from .grcn import GRCN 13 | from .hgpsl import HGPSL 14 | from .gtn import GTN 15 | from .hgsl import HGSL 16 | from .hesgsl import HESGSL 17 | from .baselines import GCN_Trainer, MLP_Trainer, GPRGNN_Trainer, DAGNN_Trainer 18 | from .gsr import GSR 19 | from .nodeformer import NodeFormer 20 | from .han import HAN 21 | from .lds import LDS 22 | from .lds import LDS 23 | 24 | __all__ = [ 25 | 'BaseModel', 26 | 'IDGL', 27 | 'SLAPS', 28 | 'SUBLIME', 29 | 'STABLE', 30 | 'VIBGSL', 31 | 'PTDNet', 32 | 'GEN', 33 | 'ProGNN', 34 | 'CoGSL', 35 | 'NeuralSparse', 36 | 'GRCN', 37 | 'HGPSL', 38 | 'GTN', 39 | 'HGSL', 40 | 'HESGSL', 41 | 'GCN_Trainer', 42 | 'MLP_Trainer', 43 | 'GPRGNN_Trainer', 44 | 'DAGNN_Trainer', 45 | 'GSR', 46 | 'NodeFormer', 47 | 'HAN', 48 | 'LDS' 49 | ] 50 | -------------------------------------------------------------------------------- /model/base_gsl.py: -------------------------------------------------------------------------------- 1 | import easydict 2 | import numpy as np 3 | import torch.nn as nn 4 | import yaml 5 | 6 | from GSL.learner import * 7 | 8 | 9 | class BaseModel(nn.Module): 10 | ''' 11 | Abstract base class for graph structure learning models. 12 | ''' 13 | def __init__(self, num_feat, num_class, metric, config_path, dataset, device, params=None): 14 | super(BaseModel, self).__init__() 15 | self.num_feat = num_feat 16 | self.num_class = num_class 17 | self.metric = metric 18 | self.device = device 19 | self.load_config(config_path, dataset, params) 20 | self.config.device = device 21 | 22 | def check_adj(self, adj): 23 | """ 24 | Check if the modified adjacency is symmetric and unweighted. 25 | """ 26 | assert np.abs(adj - adj.T).sum() == 0, "Input graph is not symmetric" 27 | assert adj.tocsr().max() == 1, "Max value should be 1!" 28 | assert adj.tocsr().min() == 0, "Min value should be 0!" 29 | 30 | def load_config(self, config_path, dataset, params): 31 | """ 32 | Load the hyper-parameters required for the models. 33 | """ 34 | with open(config_path, "r") as fin: 35 | raw_text = fin.read() 36 | configs = [easydict.EasyDict(yaml.safe_load(raw_text))] 37 | 38 | all_config = configs[0] 39 | config = all_config.Default 40 | dataset_config = all_config.get(dataset, None) 41 | if dataset_config is not None: 42 | config.update(dataset_config) 43 | self.config = config 44 | self.config.dataset = dataset 45 | self.load_hypers(params) 46 | 47 | def load_hypers(self, params): 48 | if params is None: 49 | return 50 | for key, value in params.items(): 51 | self.config[key] = value 52 | -------------------------------------------------------------------------------- /model/gen.py: -------------------------------------------------------------------------------- 1 | import time 2 | from collections import Counter 3 | from copy import deepcopy 4 | 5 | import numpy as np 6 | import torch 7 | import torch.nn.functional as F 8 | import torch.optim as optim 9 | from sklearn.metrics.pairwise import cosine_similarity as cos 10 | 11 | from GSL.encoder import GCN 12 | from GSL.metric import CosineSimilarity 13 | from GSL.model import BaseModel 14 | from GSL.processor import KNNSparsify 15 | from GSL.utils import accuracy, get_homophily, prob_to_adj 16 | 17 | 18 | class GEN(BaseModel): 19 | def __init__(self, num_features, num_classes, metric, config_path, dataset_name, device): 20 | super(GEN, self).__init__(num_features, num_classes, metric, config_path, dataset_name, device) 21 | # self.knn = KNNGraphFromFeature( 22 | # k=self.config["k"], metric="minkowski", i=self.config["i"] 23 | # ) 24 | self.config.update({"num_feat": num_features, "num_class": num_classes}) 25 | self.base_model = GCN( 26 | self.config.num_feat, 27 | self.config.hidden, 28 | self.config.num_class, 29 | num_layers=2, 30 | dropout=self.config.dropout, 31 | dropout_adj=0.0, 32 | sparse=False, 33 | activation_last="log_softmax", 34 | ) 35 | self.best_acc_val = 0.0 36 | self.iter = 0 37 | self.k = self.config.k 38 | 39 | def knn(self, feature): 40 | adj = np.zeros((self.num_node, self.num_node), dtype=np.int64) 41 | dist = cos(feature.detach().cpu().numpy()) 42 | col = np.argpartition(dist, -(self.config.k + 1), axis=1)[:,-(self.config.k + 1):].flatten() 43 | adj[np.arange(self.num_node).repeat(self.config.k + 1), col] = 1 44 | return adj 45 | # metric = CosineSimilarity() 46 | # processor = KNNSparsify(self.k) 47 | # dist = metric(feature) 48 | # adj_float = processor(dist).detach().cpu().numpy() 49 | # return (adj_float > 0).astype(np.int64) 50 | 51 | def fit(self, dataset, split_num=0): 52 | adj, features, labels = dataset.adj.clone(), dataset.features.clone(), dataset.labels 53 | if dataset.name in ['cornell', 'texas', 'wisconsin', 'actor']: 54 | train_mask = dataset.train_masks[split_num % 10] 55 | val_mask = dataset.val_masks[split_num % 10] 56 | test_mask = dataset.test_masks[split_num % 10] 57 | else: 58 | train_mask, val_mask, test_mask = dataset.train_mask, dataset.val_mask, dataset.test_mask 59 | 60 | if adj.is_sparse: 61 | indices = adj.coalesce().indices() 62 | values = adj.coalesce().values() 63 | shape = adj.coalesce().shape 64 | num_nodes = features.shape[0] 65 | loop_edge_index = torch.stack([torch.arange(num_nodes), torch.arange(num_nodes)]).to(adj.device) 66 | loop_edge_index = torch.cat([indices, loop_edge_index], dim=1) 67 | loop_values = torch.ones(num_nodes).to(adj.device) 68 | loop_values = torch.cat([values, loop_values], dim=0) 69 | adj = torch.sparse_coo_tensor(indices=loop_edge_index, values=loop_values, size=shape) 70 | else: 71 | adj += torch.eye(adj.shape[0]).to(self.device) 72 | # adj = adj.to_sparse() 73 | 74 | self.num_class = self.config["num_class"] 75 | self.num_node = features.shape[0] 76 | 77 | homophily = get_homophily(label=labels, adj=adj) 78 | estimator = EstimateAdj( 79 | features, 80 | labels, 81 | adj, 82 | torch.where(train_mask)[0], 83 | self.num_class, 84 | self.num_node, 85 | homophily, 86 | ) 87 | 88 | t_total = time.time() 89 | for iter in range(self.config["iter"]): 90 | self.train_base_model(features, adj, labels, train_mask, val_mask, iter) 91 | 92 | estimator.reset_obs() 93 | estimator.update_obs(self.knn(features)) 94 | estimator.update_obs(self.knn(self.hidden_output)) 95 | estimator.update_obs(self.knn(self.output)) 96 | self.iter += 1 97 | alpha, beta, O, Q, iterations = estimator.EM( 98 | self.output.max(1)[1].detach().cpu().numpy(), self.config["tolerance"] 99 | ) 100 | adj = prob_to_adj(Q, self.config["threshold"]).to(self.device) 101 | 102 | print( 103 | "***********************************************************************************************" 104 | ) 105 | print("Optimization Finished!") 106 | print( 107 | "Total time:{:.4f}s".format(time.time() - t_total), 108 | "Best validation accuracy:{:.4f}".format(self.best_acc_val), 109 | "EM iterations:{:04d}\n".format(iterations), 110 | ) 111 | self.best_result = self.test(features, labels, test_mask) 112 | 113 | def train_base_model(self, features, adj, labels, train_mask, val_mask, iter): 114 | best_acc_val = 0 115 | optimizer = optim.Adam( 116 | self.base_model.parameters(), 117 | lr=self.config["lr"], 118 | weight_decay=self.config["weight_decay"], 119 | ) 120 | 121 | t = time.time() 122 | for epoch in range(self.config["epoch"]): 123 | self.base_model.train() 124 | optimizer.zero_grad() 125 | 126 | hidden_output, output = self.base_model(features, adj, return_hidden=True) 127 | loss_train = F.nll_loss(output[train_mask], labels[train_mask]) 128 | acc_train = accuracy(output[train_mask], labels[train_mask]) 129 | loss_train.backward() 130 | optimizer.step() 131 | 132 | # Evaluate valset performance (deactivate dropout) 133 | self.base_model.eval() 134 | hidden_output, output = self.base_model(features, adj, return_hidden=True) 135 | 136 | loss_val = F.nll_loss(output[val_mask], labels[val_mask]) 137 | acc_val = accuracy(output[val_mask], labels[val_mask]) 138 | 139 | if acc_val > best_acc_val: 140 | best_acc_val = acc_val 141 | if acc_val > self.best_acc_val: 142 | self.best_acc_val = acc_val 143 | self.best_graph = adj 144 | self.hidden_output = hidden_output 145 | self.output = output 146 | self.weights = deepcopy(self.base_model.state_dict()) 147 | if self.config["debug"]: 148 | print( 149 | "=== Saving current graph/base_model, best_acc_val:%.4f" 150 | % self.best_acc_val.item() 151 | ) 152 | 153 | if self.config["debug"]: 154 | if epoch % 1 == 0: 155 | print( 156 | "Epoch {:04d}".format(epoch + 1), 157 | "loss_train:{:.4f}".format(loss_train.item()), 158 | "acc_train:{:.4f}".format(acc_train.item()), 159 | "loss_val:{:.4f}".format(loss_val.item()), 160 | "acc_val:{:.4f}".format(acc_val.item()), 161 | "time:{:.4f}s".format(time.time() - t), 162 | ) 163 | 164 | print( 165 | "Iteration {:04d}".format(iter), 166 | "acc_val:{:.4f}".format(best_acc_val.item()), 167 | ) 168 | 169 | def test(self, features, labels, mask_test, hetero=False): 170 | """Evaluate the performance on testset.""" 171 | print("=== Testing ===") 172 | print("Picking the best model according to validation performance") 173 | 174 | self.base_model.load_state_dict(self.weights) 175 | self.base_model.eval() 176 | hidden_output, output = self.base_model( 177 | features, self.best_graph, return_hidden=True 178 | ) 179 | loss_test = F.nll_loss(output[mask_test], labels[mask_test]) 180 | if hetero: 181 | pred = torch.argmax(output, dim=1) 182 | return torch_f1_score(pred[mask_test], labels[mask_test]) 183 | else: 184 | acc_test = accuracy(output[mask_test], labels[mask_test]) 185 | print( 186 | "Testset results: ", 187 | "loss={:.4f}".format(loss_test.item()), 188 | "accuracy={:.4f}".format(acc_test.item()), 189 | ) 190 | return acc_test.item() 191 | 192 | 193 | class EstimateAdj: 194 | def __init__( 195 | self, features, labels, adj, idx_train, num_class, num_node, homophily 196 | ): 197 | self.num_class = num_class 198 | self.num_node = num_node 199 | self.idx_train = idx_train.cpu().numpy() 200 | self.label = labels.cpu().numpy() 201 | self.adj = adj.cpu().numpy() 202 | 203 | self.output = None 204 | self.iterations = 0 205 | 206 | self.homophily = homophily 207 | 208 | def reset_obs(self): 209 | self.N = 0 210 | self.E = np.zeros((self.num_node, self.num_node), dtype=np.int64) 211 | 212 | def update_obs(self, output): 213 | self.E += output 214 | self.N += 1 215 | 216 | def revise_pred(self): 217 | for j in range(len(self.idx_train)): 218 | self.output[self.idx_train[j]] = self.label[self.idx_train[j]] 219 | 220 | def E_step(self, Q): 221 | """Run the Expectation(E) step of the EM algorithm. 222 | Parameters 223 | ---------- 224 | Q: 225 | The current estimation that each edge is actually present (numpy.array) 226 | 227 | Returns 228 | ---------- 229 | alpha: 230 | The estimation of true-positive rate (float) 231 | beta: 232 | The estimation of false-positive rate (float) 233 | O: 234 | The estimation of network model parameters (numpy.array) 235 | """ 236 | # Temporary variables to hold the numerators and denominators of alpha and beta 237 | an = Q * self.E 238 | an = np.triu(an, 1).sum() 239 | bn = (1 - Q) * self.E 240 | bn = np.triu(bn, 1).sum() 241 | ad = Q * self.N 242 | ad = np.triu(ad, 1).sum() 243 | bd = (1 - Q) * self.N 244 | bd = np.triu(bd, 1).sum() 245 | 246 | # Calculate alpha, beta 247 | alpha = an * 1.0 / (ad) 248 | beta = bn * 1.0 / (bd) 249 | 250 | O = np.zeros((self.num_class, self.num_class)) 251 | 252 | n = [] 253 | counter = Counter(self.output) 254 | for i in range(self.num_class): 255 | n.append(counter[i]) 256 | 257 | a = self.output.repeat(self.num_node).reshape(self.num_node, -1) 258 | for j in range(self.num_class): 259 | c = a == j 260 | for i in range(j + 1): 261 | b = a == i 262 | O[i, j] = np.triu((b & c.T) * Q, 1).sum() 263 | if i == j: 264 | O[j, j] = 2.0 / (n[j] * (n[j] - 1)) * O[j, j] 265 | else: 266 | O[i, j] = 1.0 / (n[i] * n[j]) * O[i, j] 267 | return (alpha, beta, O) 268 | 269 | def M_step(self, alpha, beta, O): 270 | """Run the Maximization(M) step of the EM algorithm.""" 271 | O += O.T - np.diag(O.diagonal()) 272 | 273 | row = self.output.repeat(self.num_node) 274 | col = np.tile(self.output, self.num_node) 275 | tmp = O[row, col].reshape(self.num_node, -1) 276 | 277 | p1 = tmp * np.power(alpha, self.E) * np.power(1 - alpha, self.N - self.E) 278 | p2 = (1 - tmp) * np.power(beta, self.E) * np.power(1 - beta, self.N - self.E) 279 | Q = p1 * 1.0 / (p1 + p2 * 1.0) 280 | return Q 281 | 282 | def EM(self, output, tolerance=0.000001): 283 | """Run the complete EM algorithm. 284 | Parameters 285 | ---------- 286 | tolerance: 287 | Determine the tolerance in the variantions of alpha, beta and O, which is acceptable to stop iterating (float) 288 | seed: 289 | seed for np.random.seed (int) 290 | 291 | Returns 292 | ---------- 293 | iterations: 294 | The number of iterations to achieve the tolerance on the parameters (int) 295 | """ 296 | # Record previous values to confirm convergence 297 | alpha_p = 0 298 | beta_p = 0 299 | 300 | self.output = output 301 | self.revise_pred() 302 | 303 | # Do an initial E-step with random alpha, beta and O 304 | # Beta must be smaller than alpha 305 | beta, alpha = np.sort(np.random.rand(2)) 306 | O = np.triu(np.random.rand(self.num_class, self.num_class)) 307 | 308 | # Calculate initial Q 309 | Q = self.M_step(alpha, beta, O) 310 | 311 | while abs(alpha_p - alpha) > tolerance or abs(beta_p - beta) > tolerance: 312 | alpha_p = alpha 313 | beta_p = beta 314 | alpha, beta, O = self.E_step(Q) 315 | Q = self.M_step(alpha, beta, O) 316 | self.iterations += 1 317 | 318 | if self.homophily > 0.5: 319 | Q += self.adj 320 | return (alpha, beta, O, Q, self.iterations) 321 | -------------------------------------------------------------------------------- /model/grcn.py: -------------------------------------------------------------------------------- 1 | from GSL.model import BaseModel 2 | from GSL.encoder.gcn import GCNConv, GCNConv_diag 3 | from GSL.metric import InnerProductSimilarity 4 | from GSL.utils import accuracy 5 | 6 | import numpy as np 7 | from scipy.sparse import coo_matrix 8 | import torch 9 | import torch.nn.functional as F 10 | from tqdm import tqdm 11 | 12 | EOS = 1e-10 13 | 14 | 15 | class GRCN(BaseModel): 16 | 17 | """Graph-Revised Convolutional Network (ECML-PKDD 2020)""" 18 | 19 | def __init__(self, num_features, num_classes, metric, config_path, dataset_name, device): 20 | super(GRCN, self).__init__(num_features, num_classes, metric, config_path, dataset_name, device) 21 | self.device = device 22 | self.num_features = num_features 23 | self.graph_nhid = int(self.config.hid_graph.split(":")[0]) 24 | self.graph_nhid2 = int(self.config.hid_graph.split(":")[1]) 25 | self.nhid = self.config.nhid 26 | self.conv1 = GCNConv(self.num_features, self.nhid) 27 | self.conv2 = GCNConv(self.nhid, num_classes) 28 | 29 | if self.config.sparse: 30 | self.conv_graph = GCNConv_diag(self.num_features, self.device) 31 | self.conv_graph2 = GCNConv_diag(self.num_features, self.device) 32 | else: 33 | self.conv_graph = GCNConv(self.num_features, self.graph_nhid) 34 | self.conv_graph2 = GCNConv(self.graph_nhid, self.graph_nhid2) 35 | 36 | self.F = ({'relu': F.relu, 'prelu': F.prelu, 'tanh': torch.tanh})[self.config.F] 37 | self.F_graph = ({'relu': F.relu, 'prelu': F.prelu, 'tanh': torch.tanh})[self.config.F_graph] 38 | self.dropout = self.config.dropout 39 | self.K = self.config.compl_param.split(":")[0] 40 | self.mask = None 41 | self.Adj_new = None 42 | self._normalize = self.config.normalize 43 | self.reduce = self.config.reduce 44 | self.sparse = self.config.sparse 45 | self.norm_mode = "sym" 46 | self.config = self.config 47 | 48 | def init_para(self): 49 | self.conv1.init_para() 50 | self.conv2.init_para() 51 | self.conv_graph.init_para() 52 | self.conv_graph2.init_para() 53 | 54 | def graph_parameters(self): 55 | return list(self.conv_graph.parameters()) + list(self.conv_graph2.parameters()) 56 | 57 | def base_parameters(self): 58 | return list(self.conv1.parameters()) + list(self.conv2.parameters()) 59 | 60 | def normalize(self, adj, mode="sym"): 61 | if not self.sparse: 62 | if mode == "sym": 63 | inv_sqrt_degree = 1. / (torch.sqrt(adj.sum(dim=1, keepdim=False)) + EOS) 64 | return inv_sqrt_degree[:, None] * adj * inv_sqrt_degree[None, :] 65 | elif mode == "row": 66 | inv_degree = 1. / (adj.sum(dim=1, keepdim=False) + EOS) 67 | return inv_degree[:, None] * adj 68 | else: 69 | exit("wrong norm mode") 70 | else: 71 | adj = adj.coalesce() 72 | if mode == "sym": 73 | inv_sqrt_degree = 1. / (torch.sqrt(torch.sparse.sum(adj, dim=1).values()) + EOS) 74 | D_value = inv_sqrt_degree[adj.indices()[0]] * inv_sqrt_degree[adj.indices()[1]] 75 | elif mode == "row": 76 | inv_degree = 1. / (torch.sparse.sum(adj, dim=1).values() + EOS) 77 | D_value = inv_degree[adj.indices()[0]] 78 | else: 79 | exit("wrong norm mode") 80 | new_values = adj.values() * D_value 81 | return torch.sparse_coo_tensor(adj.indices(), new_values, adj.size()).to(self.device) 82 | 83 | def _node_embeddings(self, features, adj): 84 | norm_adj = self.normalize(adj, self.norm_mode) 85 | node_embeddings = self.F_graph(self.conv_graph(features, norm_adj, self.sparse)) 86 | node_embeddings = self.conv_graph2(node_embeddings, norm_adj, self.sparse) 87 | if self._normalize: 88 | node_embeddings = F.normalize(node_embeddings, dim=1, p=2) 89 | return node_embeddings 90 | 91 | def _sparse_graph(self, raw_graph, K): 92 | if self.reduce == 'knn': 93 | values, indices = raw_graph.topk(k=int(K), dim=-1) 94 | assert torch.sum(torch.isnan(values)) == 0 95 | assert torch.max(indices) < raw_graph.shape[1] 96 | if not self.sparse: 97 | self.mask = torch.zeros(raw_graph.shape).to(self.device) 98 | self.mask[torch.arange(raw_graph.shape[0]).view(-1,1), indices] = 1. 99 | self.mask[indices, torch.arange(raw_graph.shape[1]).view(-1,1)] = 1. 100 | else: 101 | inds = torch.stack([torch.arange(raw_graph.shape[0]).view(-1,1).expand(-1,int(K)).contiguous().view(1,-1)[0].to(self.device), 102 | indices.view(1,-1)[0]]) 103 | inds = torch.cat([inds, torch.stack([inds[1], inds[0]])], dim=1) 104 | values = torch.cat([values.view(1,-1)[0], values.view(1,-1)[0]]) 105 | return inds, values 106 | else: 107 | exit("wrong sparsification method") 108 | self.mask.requires_grad = False 109 | sparse_graph = raw_graph * self.mask 110 | return sparse_graph 111 | 112 | def feedforward(self, features, adj): 113 | adj.requires_grad = False 114 | node_embeddings = self._node_embeddings(features, adj) 115 | 116 | metric = InnerProductSimilarity() 117 | adj_new = metric(node_embeddings[:, :int(self.num_features/2)], node_embeddings[:, :int(self.num_features/2)]) 118 | adj_new += metric(node_embeddings[:, int(self.num_features/2):], node_embeddings[:, int(self.num_features/2):]) 119 | 120 | if not self.sparse: 121 | adj_new = self._sparse_graph(adj_new, self.K) 122 | adj_new = self.normalize(adj + adj_new, self.norm_mode) 123 | else: 124 | adj_new_indices, adj_new_values = self._sparse_graph(adj_new, self.K) 125 | new_inds = torch.cat([adj.coalesce().indices(), adj_new_indices], dim=1) 126 | new_values = torch.cat([adj.coalesce().values(), adj_new_values]) 127 | adj_new = torch.sparse.FloatTensor(new_inds, new_values, adj.size()).to(self.device) 128 | adj_new = self.normalize(adj_new, self.norm_mode) 129 | 130 | x = self.conv1(features, adj_new, sparse=self.sparse) 131 | x = F.dropout(self.F(x), training=self.training, p=self.dropout) 132 | x = self.conv2(x, adj_new, sparse=self.sparse) 133 | 134 | return F.log_softmax(x, dim=1) 135 | 136 | def test(self, features, adj, labels, mask): 137 | with torch.no_grad(): 138 | output = self.feedforward(features, adj) 139 | result = self.metric(output[mask], labels[mask]) 140 | return result.item() 141 | 142 | def fit(self, dataset, split_num=0): 143 | adj, features, labels = dataset.adj.copy(), dataset.features.clone().to(self.device), dataset.labels.to(self.device) 144 | if dataset.name in ['cornell', 'texas', 'wisconsin', 'actor']: 145 | train_mask = dataset.train_masks[split_num % 10] 146 | val_mask = dataset.val_masks[split_num % 10] 147 | test_mask = dataset.test_masks[split_num % 10] 148 | else: 149 | train_mask, val_mask, test_mask = dataset.train_mask, dataset.val_mask, dataset.test_mask 150 | 151 | # if self.sparse: ?? 152 | 153 | # if adj.is_sparse: 154 | # indices = adj.coalesce().indices() 155 | # values = adj.coalesce().values() 156 | # shape = adj.coalesce().shape 157 | # num_nodes = features.shape[0] 158 | # loop_edge_index = torch.stack([torch.arange(num_nodes), torch.arange(num_nodes)]).to(adj.device) 159 | # loop_edge_index = torch.cat([indices, loop_edge_index], dim=1) 160 | # loop_values = torch.ones(num_nodes).to(adj.device) 161 | # loop_values = torch.cat([values, loop_values], dim=0) 162 | # adj = torch.sparse_coo_tensor(indices=loop_edge_index, values=loop_values, size=shape) 163 | # else: 164 | # adj += torch.eye(adj.shape[0]).to(self.device) 165 | # adj = adj.to_sparse() 166 | 167 | coo = coo_matrix(adj) 168 | values = coo.data 169 | indices = np.vstack((coo.row, coo.col)) 170 | i = torch.LongTensor(indices) 171 | v = torch.FloatTensor(values) 172 | shape = coo.shape 173 | adj = torch.sparse_coo_tensor(i, v, torch.Size(shape)).to(self.device) 174 | 175 | optimizer_base = torch.optim.Adam(self.base_parameters(), lr=self.config.lr, weight_decay=self.config.weight_decay) 176 | optimizer_graph = torch.optim.Adam(self.graph_parameters(), lr=self.config.lr_graph, weight_decay=self.config.weight_decay_graph) 177 | 178 | best_val_result = 0 179 | for epoch in range(self.config.num_epochs): 180 | optimizer_base.zero_grad() 181 | optimizer_graph.zero_grad() 182 | 183 | output = self.feedforward(features, adj) 184 | 185 | loss = F.nll_loss(output[train_mask], labels[train_mask]) 186 | loss.backward(retain_graph=False) 187 | 188 | optimizer_base.step() 189 | optimizer_graph.step() 190 | 191 | if epoch % 10 == 0: 192 | train_result = self.test(features, adj, labels, train_mask) 193 | val_result = self.test(features, adj, labels, val_mask) 194 | test_result = self.test(features, adj, labels, test_mask) 195 | if val_result > best_val_result: 196 | best_val_result = val_result 197 | self.best_result = test_result 198 | 199 | print(f'Epoch: {epoch: 02d}, ' 200 | f'Loss: {loss:.4f}, ' 201 | f'Train: {100 * train_result:.2f}%, ' 202 | f'Valid: {100 * val_result:.2f}%, ' 203 | f'Test: {100 * test_result:.2f}%') -------------------------------------------------------------------------------- /model/hesgsl.py: -------------------------------------------------------------------------------- 1 | import math 2 | import torch 3 | import torch.nn as nn 4 | import torch.nn.functional as F 5 | import dgl 6 | import dgl.function as fn 7 | 8 | from GSL.model import BaseModel 9 | from GSL.utils import knn_fast, top_k 10 | from GSL.encoder import GCNConv_dgl, GCNConv, GCN, GCN_dgl 11 | 12 | def get_random_mask(features, r, scale, dataset): 13 | 14 | if dataset == 'ogbn-arxiv' or dataset == 'minist' or dataset == 'cifar10' or dataset == 'fashionmnist': 15 | probs = torch.full(features.shape, 1 / r) 16 | mask = torch.bernoulli(probs) 17 | return mask 18 | 19 | nones = torch.sum(features > 0.0).float() 20 | nzeros = features.shape[0] * features.shape[1] - nones 21 | pzeros = nones / nzeros / r * scale 22 | 23 | probs = torch.zeros(features.shape).to(features.device) 24 | probs[features == 0.0] = pzeros 25 | probs[features > 0.0] = 1 / r 26 | 27 | mask = torch.bernoulli(probs) 28 | 29 | return mask 30 | 31 | 32 | def get_homophily(adj, labels, dgl_=False): 33 | if dgl_: 34 | src, dst = adj.edges() 35 | else: 36 | src, dst = adj.detach().nonzero().t() 37 | homophily_ratio = 1.0 * torch.sum((labels[src] == labels[dst])) / src.shape[0] 38 | 39 | return homophily_ratio 40 | 41 | 42 | class HESGSL(BaseModel): 43 | """ 44 | Homophily-Enhanced Self-Supervision for Graph Structure Learning: Insights and Directions (TNNLS 2023') 45 | """ 46 | def __init__(self, num_features, num_classes, metric, config_path, dataset_name, device): 47 | super(HESGSL, self).__init__(num_features, num_classes, metric, config_path, dataset_name, device) 48 | self.nclass = num_classes 49 | self.hid_dim_dae = self.config.hid_dim_dae 50 | self.hid_dim_cla = self.config.hid_dim_cla 51 | self.nlayers = self.config.nlayers 52 | self.dropout_cla = self.config.dropout_cla 53 | self.dropout_adj = self.config.dropout_adj 54 | self.mlp_dim = self.config.mlp_dim 55 | self.k = self.config.k 56 | self.sparse = self.config.sparse 57 | self.lr = self.config.lr 58 | self.lr_cla = self.config.lr_cla 59 | self.weight_decay = self.config.weight_decay 60 | self.num_epochs = self.config.num_epochs 61 | self.epochs_pre = self.config.epochs_pre 62 | self.epochs_hom = self.config.epochs_hom 63 | self.dataset = self.config.dataset 64 | self.ratio = self.config.ratio 65 | self.scale = self.config.scale 66 | self.num_hop = self.config.num_hop 67 | self.alpha = self.config.alpha 68 | self.beta = self.config.beta 69 | self.patience = self.config.patience 70 | self.eval_step = self.config.eval_step 71 | 72 | self.model_dae = GCN_DAE(num_features, self.hid_dim_dae, num_features, self.nlayers, self.dropout_cla, self.dropout_adj, self.mlp_dim, self.k, self.sparse).to(device) 73 | if self.sparse: 74 | self.model_cla = GCN_dgl(num_features, self.hid_dim_cla, num_classes, self.nlayers, self.dropout_cla, self.dropout_adj) 75 | else: 76 | self.model_cla = GCN(num_features, self.hid_dim_cla, num_classes, self.nlayers, self.dropout_cla, self.dropout_adj, self.sparse) 77 | 78 | self.optimizer_dat = torch.optim.Adam(self.model_dae.parameters(), lr=self.lr, weight_decay=float(0.0)) 79 | self.optimizer_cla = torch.optim.Adam(self.model_cla.parameters(), lr=self.lr_cla, weight_decay=self.weight_decay) 80 | 81 | def get_loss_reconstruction(self, features, mask): 82 | if self.dataset == 'ogbn-arxiv' or self.dataset == 'minist' or self.dataset == 'cifar10' or self.dataset == 'fashionmnist': 83 | masked_features = features * (1 - mask) 84 | logits, adj = self.model_dae(features, masked_features) 85 | 86 | indices = mask > 0 87 | loss = F.mse_loss(logits[indices], features[indices], reduction='mean') 88 | 89 | return loss, adj 90 | 91 | logits, adj = self.model_dae(features, features) 92 | 93 | indices = mask > 0 94 | loss = F.binary_cross_entropy_with_logits(logits[indices], features[indices], reduction='mean') 95 | 96 | return loss, adj 97 | 98 | def get_loss_classification(self, features, adj, mask, labels): 99 | 100 | logits = self.model_cla(features, adj) 101 | logits = F.log_softmax(logits, 1) 102 | 103 | loss = F.nll_loss(logits[mask], labels[mask], reduction='mean') 104 | return loss 105 | 106 | def fit(self, dataset, split_num=0): 107 | adj, features, labels = dataset.adj.clone(), dataset.features.clone(), dataset.labels 108 | if dataset.name in ['cornell', 'texas', 'wisconsin', 'actor']: 109 | train_mask = dataset.train_masks[split_num % 10] 110 | val_mask = dataset.val_masks[split_num % 10] 111 | test_mask = dataset.test_masks[split_num % 10] 112 | else: 113 | train_mask, val_mask, test_mask = dataset.train_mask, dataset.val_mask, dataset.test_mask 114 | 115 | best_val, best_test = float('-inf'), 0 116 | hom_ratio_val = 0 117 | p = 0 118 | for epoch in range(1, self.num_epochs + 1): 119 | self.model_dae.train() 120 | self.model_cla.train() 121 | 122 | self.optimizer_dat.zero_grad() 123 | self.optimizer_cla.zero_grad() 124 | 125 | mask = get_random_mask(features, self.ratio, self.scale, self.dataset).to(self.device) 126 | 127 | if epoch < self.epochs_pre: 128 | loss_dae, adj = self.get_loss_reconstruction(features, mask) 129 | loss_cla = torch.tensor(0).to(self.device) 130 | loss_hom = torch.tensor(0).to(self.device) 131 | elif epoch < self.epochs_pre + self.epochs_hom: 132 | loss_dae, adj = self.get_loss_reconstruction(features, mask) 133 | loss_cla = self.get_loss_classification(features, adj, train_mask, labels) 134 | loss_hom = torch.tensor(0).to(self.device) 135 | else: 136 | loss_dae, adj = self.get_loss_reconstruction(features, mask) 137 | loss_cla = self.get_loss_classification(features, adj, train_mask, labels) 138 | loss_hom = self.model_dae.get_loss_homophily(adj, logits, labels, train_mask, self.nclass, self.num_hop, self.sparse) 139 | 140 | loss = loss_dae * self.alpha + loss_cla + loss_hom * self.beta 141 | loss.backward() 142 | 143 | self.optimizer_dat.step() 144 | self.optimizer_cla.step() 145 | 146 | self.model_dae.eval() 147 | self.model_cla.eval() 148 | adj = self.model_dae.get_adj(features) 149 | unnorm_adj = self.model_dae.get_unnorm_adj(features) 150 | logits = self.model_cla(features, adj) 151 | 152 | train_result = self.metric(logits[train_mask], labels[train_mask]) 153 | val_result = self.metric(logits[val_mask], labels[val_mask]) 154 | test_result = self.metric(logits[test_mask], labels[test_mask]) 155 | hom_ratio = get_homophily(adj, labels, self.sparse).item() 156 | 157 | if epoch >= self.epochs_pre: 158 | if val_result > best_val: 159 | best_val = val_result 160 | best_test = test_result 161 | hom_ratio_val = hom_ratio 162 | p = 0 163 | torch.save(unnorm_adj, 'HESGSL_Cora.pt') 164 | else: 165 | p += 1 166 | if p >= self.patience: 167 | print("Early stopping!") 168 | break 169 | 170 | if epoch % self.eval_step == 0: 171 | print(f'Epoch: {epoch: 02d}, ' 172 | f'Loss: {loss:.4f}, ' 173 | f'Homophily: {hom_ratio:.2f}', 174 | f'Train: {100 * train_result:.2f}%, ' 175 | f'Valid: {100 * val_result:.2f}%, ' 176 | f'Test: {100 * test_result:.2f}%') 177 | 178 | print('Best Test Result: ', best_test.item()) 179 | self.best_result = best_test.item() 180 | 181 | # GSL.learner.mlp_learner.py 182 | class GSL(nn.Module): 183 | def __init__(self, in_dim, hid_dim, out_dim, nlayers, k, sparse): 184 | super(GSL, self).__init__() 185 | 186 | self.layers = nn.ModuleList() 187 | self.layers.append(nn.Linear(in_dim, hid_dim)) 188 | for _ in range(nlayers - 2): 189 | self.layers.append(nn.Linear(hid_dim, hid_dim)) 190 | self.layers.append(nn.Linear(hid_dim, out_dim)) 191 | 192 | self.k = k 193 | self.sparse = sparse 194 | self.in_dim = in_dim 195 | self.mlp_knn_init() 196 | 197 | def mlp_knn_init(self): 198 | for layer in self.layers: 199 | layer.weight = nn.Parameter(torch.eye(self.in_dim)) 200 | 201 | def forward(self, h): 202 | for i, layer in enumerate(self.layers): 203 | h = layer(h) 204 | if i != (len(self.layers) - 1): 205 | h = F.relu(h) 206 | 207 | if self.sparse == 1: 208 | rows, cols, values = knn_fast(h, self.k, 1000) 209 | 210 | rows_ = torch.cat((rows, cols)) 211 | cols_ = torch.cat((cols, rows)) 212 | values_ = F.relu(torch.cat((values, values))) 213 | 214 | adj = dgl.graph((rows_, cols_), num_nodes=h.shape[0], device=h.device) 215 | adj.edata['w'] = values_ 216 | else: 217 | embeddings = F.normalize(h, dim=1, p=2) 218 | adj = torch.mm(embeddings, embeddings.t()) 219 | 220 | adj = top_k(adj, self.k + 1) 221 | adj = F.relu(adj) 222 | 223 | return adj 224 | 225 | 226 | class GCN_DAE(nn.Module): 227 | def __init__(self, in_dim, hid_dim, out_dim, nlayers, dropout_cla, dropout_adj, mlp_dim, k, sparse): 228 | super(GCN_DAE, self).__init__() 229 | 230 | self.layers = nn.ModuleList() 231 | if sparse == 1: 232 | self.layers.append(GCNConv_dgl(in_dim, hid_dim)) 233 | for i in range(nlayers - 2): 234 | self.layers.append(GCNConv_dgl(hid_dim, hid_dim)) 235 | self.layers.append(GCNConv_dgl(hid_dim, out_dim)) 236 | 237 | self.dropout_cla = dropout_cla 238 | self.dropout_adj = dropout_adj 239 | 240 | else: 241 | self.layers = nn.ModuleList() 242 | self.layers.append(GCNConv(in_dim, hid_dim)) 243 | for i in range(nlayers - 2): 244 | self.layers.append(GCNConv(hid_dim, hid_dim)) 245 | self.layers.append(GCNConv(hid_dim, out_dim)) 246 | 247 | self.dropout_cla = dropout_cla 248 | self.dropout_adj = nn.Dropout(p=dropout_adj) 249 | 250 | self.sparse = sparse 251 | self.graph_generator = GSL(in_dim, math.floor(math.sqrt(in_dim * mlp_dim)), mlp_dim, nlayers, k, sparse) 252 | 253 | def get_adj(self, features): 254 | if self.sparse == 1: 255 | return self.graph_generator(features) 256 | else: 257 | adj = self.graph_generator(features) 258 | adj = (adj + adj.T) / 2 259 | 260 | inv_sqrt_degree = 1. / (torch.sqrt(adj.sum(dim=1, keepdim=False)) + 1e-10) 261 | return inv_sqrt_degree[:, None] * adj * inv_sqrt_degree[None, :] 262 | 263 | def get_unnorm_adj(self, features): 264 | if self.sparse == 1: 265 | return self.graph_generator(features) 266 | else: 267 | adj = self.graph_generator(features) 268 | adj = (adj + adj.T) / 2 269 | return adj 270 | 271 | def forward(self, features, x): 272 | adj = self.get_adj(features) 273 | if self.sparse == 1: 274 | adj_dropout = adj 275 | adj_dropout.edata['w'] = F.dropout(adj_dropout.edata['w'], p=self.dropout_adj, training=self.training) 276 | else: 277 | adj_dropout = self.dropout_adj(adj) 278 | 279 | for i, conv in enumerate(self.layers[:-1]): 280 | x = conv(x, adj_dropout) 281 | x = F.relu(x) 282 | x = F.dropout(x, p=self.dropout_cla, training=self.training) 283 | x = self.layers[-1](x, adj_dropout) 284 | 285 | return x, adj 286 | 287 | def get_loss_homophily(self, g, logits, labels, train_mask, nclasses, num_hop, sparse): 288 | 289 | logits = torch.argmax(logits, dim=-1, keepdim=True) 290 | logits[train_mask, 0] = labels[train_mask] 291 | 292 | preds = torch.zeros(logits.shape[0], nclasses).to(labels.device) 293 | preds = preds.scatter(1, logits, 1).detach() 294 | 295 | if sparse == 1: 296 | g.ndata['l'] = preds 297 | for _ in range(num_hop): 298 | g.update_all(fn.u_mul_e('l', 'w', 'm'), fn.sum(msg='m', out='l')) 299 | q_dist = F.log_softmax(g.ndata['l'], dim=-1) 300 | else: 301 | q_dist = preds 302 | for _ in range(num_hop): 303 | q_dist = torch.matmul(g, q_dist) 304 | q_dist = F.log_softmax(q_dist, dim=-1) 305 | 306 | loss_hom = F.kl_div(q_dist, preds) 307 | 308 | return loss_hom -------------------------------------------------------------------------------- /model/hgsl.py: -------------------------------------------------------------------------------- 1 | import time 2 | from GSL.model import BaseModel 3 | from GSL.utils import * 4 | from GSL.learner import * 5 | from GSL.encoder import * 6 | from GSL.metric import * 7 | from GSL.processor import * 8 | import torch 9 | import torch.nn as nn 10 | 11 | 12 | class HGSL(BaseModel): 13 | def __init__(self, num_features, num_classes, metric, config_path, dataset_name, device, data): 14 | super(HGSL, self).__init__(num_features, num_classes, metric, config_path, dataset_name, device) 15 | metadata = data.metadata 16 | self.ti, self.ri, self.types, self.ud_rels = metadata['t_info'], metadata['r_info']\ 17 | , metadata['types'], metadata['undirected_relations'] 18 | feat_dim, mp_emb_dim, n_class = metadata['n_feat'], metadata['n_meta_path_emb'], metadata['n_class'] 19 | self.non_linear = nn.ReLU() 20 | # ! Graph Structure Learning 21 | MD = nn.ModuleDict 22 | self.fgg_direct, self.fgg_left, self.fgg_right, self.fg_agg, self.sgg_gen, self.sg_agg, self.overall_g_agg = \ 23 | MD({}), MD({}), MD({}), MD({}), MD({}), MD({}), MD({}) 24 | # Feature encoder 25 | self.encoder = MD(dict(zip(metadata['types'], [nn.Linear(feat_dim, self.config.com_feat_dim) 26 | for _ in metadata['types']]))) 27 | 28 | for r in metadata['undirected_relations']: 29 | # ! Feature Graph Generator 30 | self.fgg_direct[r] = KHeadHPLearner(CosineSimilarity(), [ThresholdSparsify(self.config.fgd_th)], self.config.com_feat_dim, self.config.num_head) 31 | self.fgg_left[r] = KHeadHPLearner(CosineSimilarity(), [ThresholdSparsify(self.config.fgh_th)], feat_dim, self.config.num_head) 32 | self.fgg_right[r] = KHeadHPLearner(CosineSimilarity(), [ThresholdSparsify(self.config.fgh_th)], feat_dim, self.config.num_head) 33 | self.fg_agg[r] = GraphChannelAttLayer(3) # 3 = 1 (first-order/direct) + 2 (second-order) 34 | 35 | # ! Semantic Graph Generator 36 | self.sgg_gen[r] = MD(dict( 37 | zip(self.config.mp_list, [KHeadHPLearner(CosineSimilarity(), [ThresholdSparsify(self.config.sem_th)], mp_emb_dim, self.config.num_head) for _ in self.config.mp_list]))) 38 | self.sg_agg[r] = GraphChannelAttLayer(len(self.config.mp_list)) 39 | 40 | # ! Overall Graph Generator 41 | if len(data.meta_path_emb) > 0: 42 | self.overall_g_agg[r] = GraphChannelAttLayer(3, [1, 1, 10]) # 3 = feat-graph + sem-graph + ori_graph 43 | else: 44 | self.overall_g_agg[r] = GraphChannelAttLayer(2, [1, 1, 10]) # 2 = feat-graph + ori_graph 45 | 46 | # ! Graph Convolution 47 | if self.config.conv_method == 'gcn': 48 | self.GCN = GCN(in_channels=feat_dim, hidden_channels=self.config.emb_dim, out_channels=n_class, num_layers=2, 49 | dropout=self.config.dropout, dropout_adj=0., sparse=False, activation_last='log_softmax', 50 | conv_bias=True) 51 | self.norm_order = self.config.adj_norm_order 52 | self.stopper = None 53 | 54 | def fit(self, data): 55 | adj, features, labels = data.adj, data.features, data.labels 56 | train_idx, val_idx, test_idx = data.train_idx, data.val_idx, data.test_idx 57 | meta_path_emb = data.meta_path_emb 58 | optimizer = torch.optim.Adam( 59 | self.parameters(), lr=self.config.lr, weight_decay=float(self.config.weight_decay)) 60 | self.stopper = EarlyStopping(patience=self.config.early_stop) 61 | cla_loss = torch.nn.NLLLoss() 62 | 63 | dur = [] 64 | # w_list = [] 65 | for epoch in range(self.config.epochs): 66 | # ! Train 67 | t0 = time.time() 68 | self.train() 69 | logits, adj_new = self.forward(features, adj, meta_path_emb) 70 | train_f1, train_mif1 = self.eval_logits(logits, train_idx, labels[train_idx]) 71 | 72 | l_pred = cla_loss(logits[train_idx], labels[train_idx]) 73 | l_reg = self.config.alpha * torch.norm(adj, 1) 74 | loss = l_pred + l_reg 75 | optimizer.zero_grad() 76 | loss.backward() 77 | optimizer.step() 78 | 79 | # ! Valid 80 | self.eval() 81 | with torch.no_grad(): 82 | logits = self.GCN(features, adj_new) 83 | val_f1, val_mif1 = self.eval_logits(logits, val_idx, labels[val_idx]) 84 | dur.append(time.time() - t0) 85 | print( 86 | f"Epoch {epoch:05d} | Time(s) {np.mean(dur):.4f} | Loss {loss.item():.4f} | TrainF1 {train_f1:.4f} | ValF1 {val_f1:.4f}") 87 | 88 | if self.config.early_stop > 0: 89 | if self.stopper.step(val_f1, self, epoch): 90 | print(f'Early stopped, loading model from epoch-{self.stopper.best_epoch}') 91 | break 92 | 93 | if self.config.early_stop > 0: 94 | self.load_state_dict(self.stopper.best_weight) 95 | self.test(adj, features, labels, val_idx, test_idx, meta_path_emb) 96 | 97 | def test(self, adj, features, labels, val_idx, test_idx, meta_path_emb): 98 | with torch.no_grad(): 99 | logits, _ = self.forward(features, adj, meta_path_emb) 100 | test_f1, test_mif1 = self.eval_logits(logits, test_idx, labels[test_idx]) 101 | val_f1, val_mif1 = self.eval_logits(logits, val_idx, labels[val_idx]) 102 | res = {} 103 | if self.stopper != None: 104 | res.update({'test_f1': f'{test_f1:.4f}', 'test_mif1': f'{test_mif1:.4f}', 105 | 'val_f1': f'{val_f1:.4f}', 'val_mif1': f'{val_mif1:.4f}', 106 | 'best_epoch': self.stopper.best_epoch}) 107 | else: 108 | res.update({'test_f1': f'{test_f1:.4f}', 'test_mif1': f'{test_mif1:.4f}', 109 | 'val_f1': f'{val_f1:.4f}', 'val_mif1': f'{val_mif1:.4f}'}) 110 | res_dict = {'res': res} 111 | print(f'results:{res_dict}') 112 | self.best_result = test_f1.item() 113 | 114 | def eval_logits(self, logits, target_x, target_y): 115 | pred_y = torch.argmax(logits[target_x], dim=1) 116 | return macro_f1(pred_y, target_y, n_class=logits.shape[1]), micro_f1(pred_y, target_y, n_class=logits.shape[1]) 117 | 118 | def forward(self, features, adj_ori, mp_emb): 119 | def get_rel_mat(mat, r): 120 | return mat[self.ri[r][0]:self.ri[r][1], self.ri[r][2]:self.ri[r][3]] 121 | 122 | def get_type_rows(mat, type): 123 | return mat[self.ti[type]['ind'], :] 124 | 125 | def gen_g_via_feat(graph_gen_func, mat, r): 126 | return graph_gen_func(get_type_rows(mat, r[0]), get_type_rows(mat, r[-1])) 127 | 128 | # ! Heterogeneous Feature Mapping 129 | com_feat_mat = torch.cat([self.non_linear( 130 | self.encoder[t](features[self.ti[t]['ind']])) for t in self.types]) 131 | 132 | # ! Heterogeneous Graph Generation 133 | new_adj = torch.zeros_like(adj_ori).to(self.device) 134 | for r in self.ud_rels: 135 | ori_g = get_rel_mat(adj_ori, r) 136 | # ! Feature Graph Generation 137 | fg_direct = gen_g_via_feat(self.fgg_direct[r], com_feat_mat, r) 138 | 139 | fmat_l, fmat_r = features[self.ti[r[0]]['ind']], features[self.ti[r[-1]]['ind']] 140 | sim_l, sim_r = self.fgg_left[r](fmat_l, fmat_l), self.fgg_right[r](fmat_r, fmat_r) 141 | fg_left, fg_right = sim_l.mm(ori_g), sim_r.mm(ori_g.t()).t() 142 | 143 | feat_g = self.fg_agg[r]([fg_direct, fg_left, fg_right]) 144 | 145 | # ! Semantic Graph Generation 146 | sem_g_list = [gen_g_via_feat(self.sgg_gen[r][mp], mp_emb[mp], r) for mp in mp_emb] 147 | if len(sem_g_list) > 0: 148 | sem_g = self.sg_agg[r](sem_g_list) 149 | # ! Overall Graph 150 | # Update relation sub-matixs 151 | new_adj[self.ri[r][0]:self.ri[r][1], self.ri[r][2]:self.ri[r][3]] = \ 152 | self.overall_g_agg[r]([feat_g, sem_g, ori_g]) # update edge e.g. AP 153 | else: 154 | new_adj[self.ri[r][0]:self.ri[r][1], self.ri[r][2]:self.ri[r][3]] = \ 155 | self.overall_g_agg[r]([feat_g, ori_g]) # update edge e.g. AP 156 | 157 | 158 | new_adj += new_adj.clone().t() # sysmetric 159 | # ! Aggregate 160 | new_adj = F.normalize(new_adj, dim=0, p=self.norm_order) 161 | logits = self.GCN(features, new_adj) 162 | return logits, new_adj 163 | 164 | 165 | class GraphChannelAttLayer(nn.Module): 166 | """ 167 | Fuse a multi-channel graph to a single-channel graph with attention. 168 | """ 169 | def __init__(self, num_channel, weights=None): 170 | super(GraphChannelAttLayer, self).__init__() 171 | self.weight = nn.Parameter(torch.Tensor(num_channel, 1, 1)) 172 | nn.init.constant_(self.weight, 0.1) # equal weight 173 | 174 | def forward(self, adj_list): 175 | adj_list = torch.stack(adj_list) 176 | # Row normalization of all graphs generated 177 | adj_list = F.normalize(adj_list, dim=1, p=1) 178 | # Hadamard product + summation -> Conv 179 | return torch.sum(adj_list * F.softmax(self.weight, dim=0), dim=0) 180 | -------------------------------------------------------------------------------- /model/neuralsparse.py: -------------------------------------------------------------------------------- 1 | from GSL.model import BaseModel 2 | from GSL.learner import SpLearner 3 | from GSL.encoder import GCN 4 | from GSL.utils import accuracy 5 | import torch 6 | import torch.nn as nn 7 | import torch.nn.functional as F 8 | import scipy.sparse as sp 9 | import numpy as np 10 | from tqdm import tqdm 11 | 12 | 13 | class NeuralSparse(BaseModel): 14 | """ 15 | Robust Graph Representation Learning via Neural Sparsification (ICML 2020) 16 | """ 17 | def __init__(self, config, device, num_features, num_classes): 18 | super(NeuralSparse, self).__init__(device=device) 19 | self.config = config 20 | self.learner = SpLearner(nlayers=2, in_dim=2*num_features+1, hidden=config.hidden, activation=nn.ReLU(), k=config.k) 21 | self.gnn = GCN(in_channels=num_features, 22 | hidden_channels=config.hidden_channel, 23 | out_channels=num_classes, 24 | num_layers=config.num_layers, 25 | dropout=config.dropout, 26 | dropout_adj=0, 27 | sparse=False) 28 | self.optimizer = torch.optim.Adam(list(self.learner.parameters())+list(self.gnn.parameters()), lr=config.lr, weight_decay=config.weight_decay) 29 | 30 | def test(self, features, adj, labels, mask): 31 | self.learner.eval() 32 | self.gnn.eval() 33 | with torch.no_grad(): 34 | support = self.learner(features, adj, 1.0, False) 35 | output = self.gnn(features, support) 36 | acc = accuracy(output[mask], labels[mask]).item() 37 | return acc 38 | 39 | 40 | def fit(self, features, adj, labels, train_mask, val_mask, test_mask): 41 | features = features.to(self.device) 42 | labels = labels.to(self.device) 43 | train_mask = train_mask.to(self.device) 44 | val_mask = val_mask.to(self.device) 45 | test_mask = test_mask.to(self.device) 46 | adj = torch.tensor(adj).to(self.device) 47 | 48 | indices = torch.nonzero(adj) 49 | values = adj[indices[:, 0], indices[:, 1]] 50 | shape = adj.shape 51 | 52 | adj = torch.sparse_coo_tensor(indices.t(), values, torch.Size(shape)).to(torch.float32) 53 | 54 | best_val_acc, best_test_acc = 0, 0 55 | with tqdm(total=self.config.epochs, desc='(NeuralSparse)', 56 | bar_format='{l_bar}{bar}| {n_fmt}/{total_fmt} [{elapsed}<{remaining}{postfix}]') as pbar: 57 | for epoch in range(self.config.epochs): 58 | self.learner.train() 59 | self.gnn.train() 60 | if epoch % self.config.temp_N == 0: 61 | decay_temp = np.exp(-1*self.config.temp_r*epoch) 62 | temp = max(0.05, decay_temp) 63 | support = self.learner(features, adj, temp, True) 64 | output = self.gnn(features, support) 65 | loss_train = F.cross_entropy(output[train_mask], labels[train_mask]) 66 | 67 | self.optimizer.zero_grad() 68 | loss_train.backward() 69 | self.optimizer.step() 70 | 71 | val_acc = self.test(features, adj, labels, val_mask) 72 | test_acc = self.test(features, adj, labels, test_mask) 73 | if val_acc > best_val_acc: 74 | best_val_acc = val_acc 75 | best_test_acc = test_acc 76 | patience = 0 77 | else: 78 | patience += 1 79 | 80 | if patience > self.config.patience: 81 | print("Early stopping...") 82 | break 83 | 84 | pbar.set_postfix({'Epoch': epoch+1, 'Loss': loss_train.item(), 'Acc_val': val_acc, 'Acc_test': test_acc}) 85 | pbar.update(1) 86 | print("Best Test Accuracy: ", best_test_acc) -------------------------------------------------------------------------------- /model/nodeformer.py: -------------------------------------------------------------------------------- 1 | from GSL.model import BaseModel 2 | from GSL.processor import AddEye 3 | from GSL.encoder import NodeFormerConv 4 | from GSL.utils import dense_adj_to_edge_index 5 | 6 | import torch 7 | import torch.nn.functional as F 8 | import torch.nn as nn 9 | 10 | 11 | class NodeFormer(BaseModel): 12 | """NodeFormer: A Scalable Graph Structure Learning Transformer for Node Classification (NeurIPS 2022)""" 13 | def __init__(self, num_features, num_classes, metric, config_path, dataset_name, device, params): 14 | super(NodeFormer, self).__init__(num_features, num_classes, metric, config_path, dataset_name, device, params) 15 | 16 | # hyper-parameters of model initialization 17 | self.hidden_dim = self.config.hidden_dim 18 | self.num_layers = self.config.num_layers 19 | self.use_jk = self.config.use_jk 20 | self.dropout = self.config.dropout 21 | self.activation = F.elu 22 | self.use_bn = self.config.use_bn 23 | self.use_residual = self.config.use_residual 24 | self.use_act = self.config.use_act 25 | self.use_edge_loss = self.config.use_edge_loss 26 | self.num_heads = self.config.num_heads 27 | self.kernel_transformation = self.config.kernel_transformation 28 | self.nb_random_features = self.config.nb_random_features 29 | self.use_gumbel = self.config.use_gumbel 30 | self.nb_gumbel_sample = self.config.nb_gumbel_sample 31 | self.rb_trans = self.config.rb_trans 32 | 33 | # hyper-parameters of model training 34 | self.epochs = self.config.epochs 35 | self.lr = self.config.lr 36 | self.weight_decay = self.config.weight_decay 37 | self.eval_step = self.config.eval_step 38 | self.lamda = self.config.lamda 39 | self.rb_order = self.config.rb_order 40 | self.tau = self.config.tau 41 | 42 | self.convs = nn.ModuleList() 43 | self.fcs = nn.ModuleList() 44 | self.fcs.append(nn.Linear(num_features, self.hidden_dim)) 45 | self.bns = nn.ModuleList() 46 | self.bns.append(nn.LayerNorm(self.hidden_dim)) 47 | for i in range(self.num_layers): 48 | self.convs.append(NodeFormerConv(self.hidden_dim, self.hidden_dim, num_heads=self.num_heads, 49 | kernel_transformation=self.kernel_transformation, 50 | nb_random_features=self.nb_random_features, 51 | use_gumbel=self.use_gumbel, 52 | nb_gumbel_sample=self.nb_gumbel_sample, 53 | rb_order=self.rb_order, 54 | rb_trans=self.rb_trans, 55 | use_edge_loss=self.use_edge_loss)) 56 | self.bns.append(nn.LayerNorm(self.hidden_dim)) 57 | 58 | if self.use_jk: 59 | self.fcs.append(nn.Linear(self.hidden_dim * self.num_layers + self.hidden_dim, num_classes)) 60 | else: 61 | self.fcs.append(nn.Linear(self.hidden_dim, num_classes)) 62 | 63 | def reset_parameters(self): 64 | for conv in self.convs: 65 | conv.reset_parameters() 66 | for bn in self.bns: 67 | bn.reset_parameters() 68 | for fc in self.fcs: 69 | fc.reset_parameters() 70 | 71 | def model_parameters(self): 72 | params = list() 73 | params += list(self.convs.parameters()) 74 | params += list(self.bns.parameters()) 75 | params += list(self.fcs.parameters()) 76 | return params 77 | 78 | def feedforward(self, x, adjs, tau=1.0): 79 | x = x.unsqueeze(0) # [B, N, H, D], B=1 denotes number of graph 80 | layer_ = [] 81 | link_loss_ = [] 82 | z = self.fcs[0](x) 83 | if self.use_bn: 84 | z = self.bns[0](z) 85 | z = self.activation(z) 86 | z = F.dropout(z, p=self.dropout, training=self.training) 87 | layer_.append(z) 88 | 89 | for i, conv in enumerate(self.convs): 90 | if self.use_edge_loss: 91 | z, link_loss = conv(z, adjs, tau) 92 | link_loss_.append(link_loss) 93 | else: 94 | z = conv(z, adjs, tau) 95 | if self.use_residual: 96 | z += layer_[i] 97 | if self.use_bn: 98 | z = self.bns[i+1](z) 99 | if self.use_act: 100 | z = self.activation(z) 101 | z = F.dropout(z, p=self.dropout, training=self.training) 102 | layer_.append(z) 103 | 104 | if self.use_jk: # use jk connection for each layer 105 | z = torch.cat(layer_, dim=-1) 106 | 107 | x_out = self.fcs[-1](z).squeeze(0) 108 | 109 | if self.use_edge_loss: 110 | return x_out, link_loss_ 111 | else: 112 | return x_out 113 | 114 | def test(self, features, adjs, labels, mask): 115 | self.eval() 116 | with torch.no_grad(): 117 | out, _ = self.feedforward(features, adjs, self.tau) 118 | acc = self.metric(out[mask], labels[mask], self.num_class) 119 | return acc 120 | 121 | @staticmethod 122 | def adj_mul(adj_i, adj, N): 123 | adj_i_sp = torch.sparse_coo_tensor(adj_i, torch.ones(adj_i.shape[1], dtype=torch.float).to(adj.device), (N, N)) 124 | adj_sp = torch.sparse_coo_tensor(adj, torch.ones(adj.shape[1], dtype=torch.float).to(adj.device), (N, N)) 125 | adj_j = torch.sparse.mm(adj_i_sp, adj_sp) 126 | adj_j = adj_j.coalesce().indices() 127 | return adj_j 128 | 129 | def to(self, device): 130 | self.convs.to(device) 131 | self.fcs.to(device) 132 | self.bns.to(device) 133 | return self 134 | 135 | def fit(self, dataset, split_num=0): 136 | adj, features, labels = dataset.adj.clone(), dataset.features.clone(), dataset.labels 137 | if dataset.name in ['cornell', 'texas', 'wisconsin', 'actor']: 138 | train_mask = dataset.train_masks[split_num % 10] 139 | val_mask = dataset.val_masks[split_num % 10] 140 | test_mask = dataset.test_masks[split_num % 10] 141 | else: 142 | train_mask, val_mask, test_mask = dataset.train_mask, dataset.val_mask, dataset.test_mask 143 | 144 | 145 | num_nodes = features.shape[0] 146 | adjs = [] 147 | adj = AddEye()(adj) 148 | adj = dense_adj_to_edge_index(adj) 149 | adjs.append(adj) 150 | for i in range(self.rb_order - 1): 151 | adj = self.adj_mul(adj, adj, num_nodes) 152 | adjs.append(adj) 153 | 154 | criterion = nn.NLLLoss() 155 | optimizer = torch.optim.Adam(self.model_parameters(), lr=self.lr, weight_decay=self.weight_decay) 156 | 157 | best_test, best_val = 0, float('-inf') 158 | 159 | for epoch in range(self.epochs): 160 | self.train() 161 | optimizer.zero_grad() 162 | 163 | out, link_loss_ = self.feedforward(features, adjs, self.tau) 164 | out = F.log_softmax(out, dim=1) 165 | loss = criterion(out[train_mask], labels[train_mask]) 166 | loss -= self.lamda * sum(link_loss_) / len(link_loss_) 167 | 168 | loss.backward() 169 | optimizer.step() 170 | 171 | if epoch % self.eval_step == 0: 172 | train_result = self.test(features, adjs, labels, train_mask) 173 | val_result = self.test(features, adjs, labels, val_mask) 174 | test_result = self.test(features, adjs, labels, test_mask) 175 | 176 | if val_result > best_val: 177 | best_val = val_result 178 | best_test = test_result 179 | 180 | print(f'Epoch: {epoch: 02d}, ' 181 | f'Loss: {loss:.4f}, ' 182 | f'Train: {100 * train_result:.2f}%, ' 183 | f'Valid: {100 * val_result:.2f}%, ' 184 | f'Test: {100 * test_result:.2f}%') 185 | self.best_result = best_test.item() 186 | -------------------------------------------------------------------------------- /model/ptdnet.py: -------------------------------------------------------------------------------- 1 | from GSL.model import BaseModel 2 | from GSL.encoder import GCNConv, GCNConv_dgl 3 | from GSL.utils import accuracy 4 | import numpy as np 5 | import torch.nn as nn 6 | import torch.nn.functional as F 7 | import torch 8 | from scipy.sparse import csc_matrix 9 | from scipy.sparse.linalg import svds 10 | 11 | eps = 1e-7 12 | SVD_PI = True 13 | 14 | 15 | class PTDNet(BaseModel): 16 | """ 17 | Learning to Drop: Robust Graph Neural Network via Topological Denoising (WSDM 2021) 18 | """ 19 | def __init__(self, config, device, in_dim, out_dim): 20 | super(PTDNet, self).__init__(device=device) 21 | self.config = config 22 | self.sparse = config.sparse 23 | self.in_dim = in_dim 24 | self.out_dim = out_dim 25 | hiddens = config.hiddens 26 | 27 | self.layers = nn.ModuleList() 28 | 29 | try: 30 | hiddens = [int(s) for s in str(hiddens).split('-')] 31 | except: 32 | hiddens = [config.hidden1] 33 | 34 | nhiddens = len(hiddens) 35 | 36 | if self.sparse: 37 | self.layers.append(GCNConv_dgl(self.in_dim, hiddens[0])) 38 | for _ in range(1, nhiddens): 39 | self.layers.append(GCNConv_dgl(hiddens[-1], hiddens[_])) 40 | self.layers.append(GCNConv_dgl(hiddens[-1], self.out_dim)) 41 | else: 42 | self.layers.append(GCNConv(self.in_dim, hiddens[0])) 43 | for i in range(1, nhiddens): 44 | self.layers.append(GCNConv(hiddens[-1], hiddens[_])) 45 | self.layers.append(GCNConv(hiddens[-1], self.out_dim)) 46 | 47 | hidden_1 = config.denoise_hidden_1 48 | hidden_2 = config.denoise_hidden_2 49 | self.edge_weights = [] 50 | self.nblayers = nn.ModuleList() 51 | self.selflayers = nn.ModuleList() 52 | 53 | self.attentions = nn.ModuleList() 54 | self.attentions.append(nn.ModuleList()) 55 | for _ in hiddens: 56 | self.attentions.append(nn.ModuleList()) 57 | 58 | self.nblayers.append(nn.Linear(self.in_dim, hidden_1)) 59 | self.selflayers.append(nn.Linear(self.in_dim, hidden_1)) 60 | 61 | if hidden_2 > 0: 62 | self.attentions[0].append(nn.Linear(hidden_1*2, hidden_2)) 63 | 64 | self.attentions[0].append(nn.Linear(hidden_1*2, 1)) 65 | 66 | for i in range(1, len(self.attentions)): 67 | self.nblayers.append(nn.Linear(hiddens[i-1], hidden_1)) 68 | self.selflayers.append(nn.Linear(hiddens[i-1], hidden_1)) 69 | 70 | if hidden_2>0: 71 | self.attentions[i].append(nn.Linear(hidden_1*2, hidden_2)) 72 | 73 | self.attentions[i].append(nn.Linear(hidden_1*2, 1)) 74 | 75 | def get_attention(self, input1, input2, l=0, training=False): 76 | 77 | nb_layer = self.nblayers[l] 78 | selflayer = self.selflayers[l] 79 | net = self.attentions[l] 80 | 81 | input1 = F.relu(nb_layer(input1)) 82 | if training: 83 | input1 = F.dropout(input1, p=self.config.dropout) 84 | input2 = F.relu(selflayer(input2)) 85 | if training: 86 | input2 = F.dropout(input2, p=self.config.dropout) 87 | 88 | input10 = torch.cat([input1, input2], dim=1) 89 | for _layer in net: 90 | input10 = _layer(input10) 91 | if training: 92 | input10 = F.dropout(input10, p=self.config.dropout) 93 | weight10 = input10 94 | return weight10 95 | 96 | def hard_concrete_sample(self, log_alpha, beta=1.0, training=True): 97 | gamma = self.config.gamma 98 | zeta = self.config.zeta 99 | 100 | if training: 101 | debug_var = eps 102 | bias = 0.0 103 | random_noise = bias+torch.empty_like(log_alpha).uniform_(debug_var, 1.0 - debug_var).to(torch.float32) 104 | gate_inputs = torch.log(random_noise) - torch.log(1.0 - random_noise) 105 | gate_inputs = (gate_inputs + log_alpha) / beta 106 | gate_inputs = torch.sigmoid(gate_inputs) 107 | else: 108 | gate_inputs = torch.sigmoid(log_alpha) 109 | 110 | stretched_values = gate_inputs * (zeta - gamma) + gamma 111 | cliped = torch.clamp(stretched_values, min=0.0, max=1.0) 112 | return cliped 113 | 114 | def l0_norm(self, log_alpha, beta): 115 | gamma = self.config.gamma 116 | zeta = self.config.zeta 117 | temp = torch.tensor(-gamma / zeta).to(self.device) 118 | reg_per_weight = torch.sigmoid(log_alpha - beta * torch.log(temp).to(torch.float32)) 119 | return torch.mean(reg_per_weight) 120 | 121 | def lossl0(self, temperature): 122 | l0_loss = torch.zeros([], dtype=torch.float32).to(self.device) 123 | for weight in self.edge_weights: 124 | l0_loss += self.l0_norm(weight, temperature) 125 | self.edge_weights = [] 126 | return l0_loss 127 | 128 | def nuclear(self): 129 | nuclear_loss = torch.zeros([], dtype=torch.float32) 130 | values = [] 131 | if self.config.lambda3 == 0: 132 | return 0 133 | for mask in self.maskes: 134 | mask = torch.squeeze(mask) 135 | support = torch.sparse_coo_tensor(indices=self.indices.t(), 136 | values=mask, 137 | size=self.adj_mat.shape) 138 | support_dense = support.to_dense() 139 | support_trans = support_dense.t() 140 | 141 | AA = torch.matmul(support_trans, support_dense) 142 | if SVD_PI: 143 | row_ind = self.indices[:, 0].cpu().numpy() 144 | col_ind = self.indices[:, 1].cpu().numpy() 145 | support_csc = csc_matrix((mask.detach().cpu().numpy(), (row_ind, col_ind))) 146 | k = self.config.k_svd 147 | u, s, vh = svds(support_csc, k=k) 148 | 149 | u = torch.tensor(u.copy()) 150 | s = torch.tensor(s.copy()) 151 | vh = torch.tensor(vh.copy()) 152 | 153 | for i in range(k): 154 | vi = torch.unsqueeze(vh[i], -1).to(self.device) 155 | for ite in range(1): 156 | vi = torch.matmul(AA, vi) 157 | vi_norm = torch.linalg.norm(vi) 158 | vi = vi / vi_norm 159 | 160 | vmv = torch.matmul(vi.t(), torch.matmul(AA, vi)) 161 | vv = torch.matmul(vi.t(), vi) 162 | 163 | t_vi = torch.sqrt(torch.abs(vmv / vv)) 164 | values.append(t_vi) 165 | 166 | if k > 1: 167 | AA_minus = torch.matmul(AA, torch.matmul(vi, torch.transpose(vi))) 168 | AA = AA - AA_minus 169 | else: 170 | trace = torch.linalg.trace(AA) 171 | values.append(torch.sum(trace)) 172 | 173 | nuclear_loss = torch.sum(torch.stack(values, dim=0), dim=0) 174 | 175 | return nuclear_loss 176 | 177 | def feedforward(self, inputs, training=None): 178 | if training: 179 | temperature = inputs 180 | else: 181 | temperature = 1.0 182 | 183 | self.edge_maskes = [] 184 | self.maskes = [] 185 | layer_index = 0 186 | x = self.features 187 | 188 | for i in range(len(self.layers)): 189 | xs = [] 190 | layer = self.layers[i] 191 | for l in range(self.config.L): 192 | f1_features = torch.index_select(x, 0, self.row) 193 | f2_features = torch.index_select(x, 0, self.col) 194 | weight = self.get_attention(f1_features, f2_features, l=layer_index, training=True) 195 | mask = self.hard_concrete_sample(weight, temperature, True) 196 | self.edge_weights.append(weight) 197 | self.maskes.append(mask) 198 | mask = torch.squeeze(mask) 199 | adj = torch.sparse_coo_tensor(indices=self.indices.t(), 200 | values=mask, 201 | size=self.adj_mat.shape) 202 | # norm 203 | adj = torch.eye(self.node_num, dtype=torch.float32).to_sparse().to(self.device) + adj 204 | 205 | row = adj.coalesce().indices().t()[:, 0] 206 | col = adj.coalesce().indices().t()[:, 1] 207 | 208 | rowsum = torch.sparse.sum(adj, dim=-1).to_dense() 209 | d_inv_sqrt = torch.reshape(torch.pow(rowsum, -0.5), [-1]) 210 | d_inv_sqrt = torch.clamp(d_inv_sqrt, 0, 10.0) 211 | row_inv_sqrt = torch.index_select(d_inv_sqrt, 0, row) 212 | col_inv_sqrt = torch.index_select(d_inv_sqrt, 0, col) 213 | values = torch.mul(adj.coalesce().values(), row_inv_sqrt) 214 | values = torch.mul(values, col_inv_sqrt) 215 | 216 | support = torch.sparse_coo_tensor(indices=adj.coalesce().indices(), 217 | values = values, 218 | size=adj.shape).to_dense().to(torch.float32) 219 | nextx = layer(x, support) 220 | if i != len(self.layers)-1: 221 | nextx = F.relu(nextx) 222 | xs.append(nextx) 223 | x = torch.mean(torch.stack(xs, dim=0), dim=0) 224 | layer_index += 1 225 | return x 226 | 227 | def compute_loss(self, preds, temperature, labels, train_mask): 228 | all_preds = torch.cat(preds, dim=0) 229 | mean_preds = torch.mean(torch.stack(preds, dim=0), dim=0) 230 | mean_preds = torch.squeeze(mean_preds, dim=0) 231 | diff = mean_preds - all_preds 232 | 233 | consistency_loss = F.mse_loss(diff, torch.zeros_like(diff)) 234 | 235 | cross_loss = self.criterion(mean_preds[train_mask], labels[train_mask]) 236 | lossl0 = self.lossl0(temperature) 237 | nuclear = self.nuclear() 238 | loss = cross_loss + self.config.lambda1*lossl0 + self.config.lambda3*nuclear + self.config.coff_consis*consistency_loss 239 | return loss 240 | 241 | def test(self, labels, mask): 242 | with torch.no_grad(): 243 | output = self.feedforward(None, False) 244 | acc = accuracy(output[mask], labels[mask]).item() 245 | return acc 246 | 247 | 248 | def fit(self, features, adj, labels, train_mask, val_mask, test_mask): 249 | self.optimizer = torch.optim.Adam(self.parameters(), weight_decay=self.config.weight_decay, lr=self.config.lr) 250 | self.criterion = nn.CrossEntropyLoss() 251 | labels = labels.to(self.device) 252 | 253 | self.adj = adj.to(self.device) 254 | self.features = features.to(self.device) 255 | self.indices = adj.coalesce().indices().t().to(self.device) 256 | self.node_num = features.shape[0] 257 | self.values = adj.coalesce().values() 258 | self.shape = adj.shape 259 | self.row = self.indices[:, 0] 260 | self.col = self.indices[:, 1] 261 | self.adj_mat = adj 262 | 263 | best_val_result, best_test_result = float('-inf'), 0 264 | patience = 0 265 | for epoch in range(self.config.epochs): 266 | temperature = max(0.05, self.config.init_temperature * pow(self.config.temperature_decay, epoch)) 267 | preds = [] 268 | 269 | for _ in range(self.config.outL): 270 | output = self.feedforward(temperature, True) 271 | preds.append(torch.unsqueeze(output, 0)) 272 | 273 | loss = self.compute_loss(preds, temperature, labels, train_mask) 274 | 275 | self.optimizer.zero_grad() 276 | loss.backward() 277 | self.optimizer.step() 278 | 279 | train_result = self.test(labels, train_mask) 280 | val_result = self.test(labels, val_mask) 281 | test_result = self.test(labels, test_mask) 282 | if val_result > best_val_result: 283 | best_val_result = val_result 284 | best_test_result = test_result 285 | patience = 0 286 | else: 287 | patience += 1 288 | 289 | print(f'Epoch: {epoch: 02d}, ' 290 | f'Loss: {loss.item():.4f}, ' 291 | f'Train: {100 * train_result:.2f}%, ' 292 | f'Valid: {100 * val_result:.2f}%, ' 293 | f'Test: {100 * test_result:.2f}%') 294 | 295 | if patience > self.config.patience: 296 | print("Early stopping...") 297 | break 298 | 299 | print("Best Test Accuracy: ", best_test_result) -------------------------------------------------------------------------------- /model/slaps.py: -------------------------------------------------------------------------------- 1 | from GSL.model import BaseModel 2 | from GSL.utils import * 3 | from GSL.learner import * 4 | from GSL.encoder import * 5 | from GSL.metric import * 6 | from GSL.processor import * 7 | import math 8 | import torch 9 | import torch.nn as nn 10 | 11 | class SLAPS(BaseModel): 12 | def __init__(self, num_features, num_classes, metric, config_path, dataset_name, device, data): 13 | super(SLAPS, self).__init__(num_features, num_classes, metric, config_path, dataset_name, device) 14 | 15 | self.features = data.features 16 | #self.features = features 17 | self.Adj = None 18 | self.device = device 19 | 20 | self.model1 = GCN_DAE(config=self.config, nlayers=self.config.nlayers_adj, in_dim=num_features, hidden_dim=self.config.hidden_adj, 21 | num_classes=num_features, 22 | dropout=self.config.dropout1, dropout_adj=self.config.dropout_adj1, 23 | features=self.features.cpu(), k=self.config.k, knn_metric=self.config.knn_metric, i_=self.config.i, 24 | non_linearity=self.config.non_linearity, normalization=self.config.normalization, 25 | mlp_h=self.config.mlp_h, 26 | learner=self.config.learner, sparse=self.config.sparse, 27 | mlp_act=self.config.mlp_act).to(self.device) 28 | self.model2 = GCN_C(in_channels=num_features, hidden_channels=self.config.hidden, out_channels=num_classes, 29 | num_layers=self.config.nlayers, dropout=self.config.dropout2, 30 | dropout_adj=self.config.dropout_adj2, 31 | sparse=self.config.sparse).to(self.device) 32 | 33 | def get_loss_masked_features(self, model, features, mask, ogb, noise, loss_t): 34 | 35 | features = features.to(self.device) 36 | 37 | if ogb: 38 | if noise == 'mask': 39 | masked_features = features * (1 - mask) 40 | elif noise == "normal": 41 | noise = torch.normal(0.0, 1.0, size=features.shape).to(self.device) 42 | masked_features = features + (noise * mask) 43 | 44 | logits, Adj = model(features, masked_features) 45 | indices = mask > 0 46 | 47 | if loss_t == 'bce': 48 | features_sign = torch.sign(features).to(self.device) * 0.5 + 0.5 49 | loss = F.binary_cross_entropy_with_logits(logits[indices], features_sign[indices], reduction='mean') 50 | elif loss_t == 'mse': 51 | loss = F.mse_loss(logits[indices], features[indices], reduction='mean') 52 | else: 53 | masked_features = features * (1 - mask) 54 | logits, Adj = model(features, masked_features) 55 | indices = mask > 0 56 | loss = F.binary_cross_entropy_with_logits(logits[indices], features[indices], reduction='mean') 57 | return loss, Adj 58 | 59 | def get_loss_learnable_adj(self, model, mask, features, labels, Adj): 60 | 61 | features = features.to(self.device) 62 | mask = mask.to(self.device) 63 | labels = labels.to(self.device) 64 | 65 | logits = model(features, Adj) 66 | logp = F.log_softmax(logits, 1) 67 | loss = F.nll_loss(logp[mask], labels[mask], reduction='mean') 68 | accu = accuracy(logp[mask], labels[mask]) 69 | return loss, accu 70 | 71 | def fit(self, data, split_num=0): 72 | features, labels = data.features, data.labels 73 | if data.name in ['cornell', 'texas', 'wisconsin', 'actor']: 74 | train_mask = data.train_masks[split_num % 10] 75 | val_mask = data.val_masks[split_num % 10] 76 | test_mask = data.test_masks[split_num % 10] 77 | else: 78 | train_mask, val_mask, test_mask = data.train_mask, data.val_mask, data.test_mask 79 | 80 | optimizer1 = torch.optim.Adam(self.model1.parameters(), lr=self.config.lr_adj, weight_decay=self.config.w_decay_adj) 81 | optimizer2 = torch.optim.Adam(self.model2.parameters(), lr=self.config.lr, weight_decay=self.config.w_decay) 82 | 83 | best_val_result = 0.0 84 | 85 | for epoch in range(1, self.config.epochs_adj + 1): 86 | self.model1.train() 87 | self.model2.train() 88 | 89 | optimizer1.zero_grad() 90 | optimizer2.zero_grad() 91 | 92 | if self.config.dataset.startswith('ogb') or self.config.dataset in ["wine", "digits", "breast_cancer"]: 93 | mask = get_random_mask_ogb(features, self.config.ratio).to(self.device) 94 | ogb = True 95 | elif self.config.dataset == "20news10": 96 | mask = get_random_mask(features, self.config.ratio, self.config.nr).to(self.device) 97 | ogb = True 98 | else: 99 | mask = get_random_mask(features, self.config.ratio, self.config.nr).to(self.device) 100 | ogb = False 101 | 102 | if epoch < self.config.epochs_adj // self.config.epoch_d: 103 | self.model2.eval() 104 | loss1, Adj = self.get_loss_masked_features(self.model1, features, mask, ogb, self.config.noise, self.config.loss) 105 | loss2 = torch.tensor(0).to(self.device) 106 | else: 107 | loss1, Adj = self.get_loss_masked_features(self.model1, features, mask, ogb, self.config.noise, self.config.loss) 108 | loss2, train_result = self.get_loss_learnable_adj(self.model2, train_mask, features, labels, Adj) 109 | 110 | loss = loss1 * self.config.lambda_ + loss2 111 | loss.backward() 112 | optimizer1.step() 113 | optimizer2.step() 114 | 115 | if epoch >= self.config.epochs_adj // self.config.epoch_d and epoch % 1 == 0: 116 | val_result = self.test(val_mask, features, labels, Adj) 117 | test_result = self.test(test_mask, features, labels, Adj) 118 | if val_result > best_val_result: 119 | # best_weight = deepcopy(self.state_dict()) 120 | best_val_result = val_result 121 | self.best_result = test_result 122 | self.Adj = Adj 123 | print(f'Epoch: {epoch: 02d}, ' 124 | f'Loss: {loss:.4f}, ' 125 | f'Train: {100 * train_result.item():.2f}%, ' 126 | f'Valid: {100 * val_result:.2f}%, ' 127 | f'Test: {100 * test_result:.2f}%') 128 | 129 | def test(self, test_mask, features, labels, adj): 130 | with torch.no_grad(): 131 | self.model1.eval() 132 | self.model2.eval() 133 | test_loss_, test_accu_ = self.get_loss_learnable_adj(self.model2, test_mask, features, labels, adj) 134 | return test_accu_.item() 135 | 136 | class GCN_DAE(nn.Module): 137 | def __init__(self, config, nlayers, in_dim, hidden_dim, num_classes, dropout, dropout_adj, features, k, knn_metric, i_, 138 | non_linearity, normalization, mlp_h, learner, sparse, mlp_act): 139 | super(GCN_DAE, self).__init__() 140 | 141 | self.layers = nn.ModuleList() 142 | 143 | if sparse: 144 | self.layers.append(GCNConv_dgl(in_dim, hidden_dim)) 145 | for _ in range(nlayers - 2): 146 | self.layers.append(GCNConv_dgl(hidden_dim, hidden_dim)) 147 | self.layers.append(GCNConv_dgl(hidden_dim, num_classes)) 148 | 149 | else: 150 | self.layers.append(GCNConv(in_dim, hidden_dim)) 151 | for i in range(nlayers - 2): 152 | self.layers.append(GCNConv(hidden_dim, hidden_dim)) 153 | self.layers.append(GCNConv(hidden_dim, num_classes)) 154 | 155 | self.dropout = dropout 156 | self.dropout_adj = nn.Dropout(p=dropout_adj) 157 | self.dropout_adj_p = dropout_adj 158 | self.k = k 159 | self.knn_metric = knn_metric 160 | self.i = i_ 161 | self.non_linearity = non_linearity 162 | self.normalization = normalization 163 | self.nnodes = features.shape[0] 164 | self.mlp_h = mlp_h 165 | self.sparse = sparse 166 | 167 | print('Graph Structure Learner: {}'.format(learner)) 168 | if learner == 'FP': 169 | metric = CosineSimilarity() 170 | processors = [KNNSparsify(k), Discretize(), LinearTransform(config.i)] 171 | non_linearize = NonLinearize(config.non_linearity, alpha=config.i) 172 | self.graph_gen = FullParam(metric, processors, features, sparse, non_linearize) 173 | elif learner == 'MLP': 174 | metric = CosineSimilarity() 175 | processors = [KNNSparsify(k)] 176 | activation = ({'relu': F.relu, 'prelu': F.prelu, 'tanh': F.tanh})[mlp_act] 177 | self.graph_gen = MLPLearner(metric, processors, 2, features.shape[1], 178 | math.floor(math.sqrt(features.shape[1] * self.mlp_h)), activation, sparse, k=k) 179 | 180 | def get_adj(self, h): 181 | Adj_ = self.graph_gen(h) 182 | if not self.sparse: 183 | Adj_ = symmetrize(Adj_) 184 | Adj_ = normalize(Adj_, self.normalization, self.sparse) 185 | return Adj_ 186 | 187 | def forward(self, features, x): # x corresponds to masked_fearures 188 | Adj_ = self.get_adj(features) 189 | if self.sparse: 190 | Adj = Adj_ 191 | Adj.edata['w'] = F.dropout(Adj.edata['w'], p=self.dropout_adj_p, training=self.training) 192 | else: 193 | Adj = self.dropout_adj(Adj_) 194 | for i, conv in enumerate(self.layers[:-1]): 195 | x = conv(x, Adj) 196 | x = F.relu(x) 197 | x = F.dropout(x, p=self.dropout, training=self.training) 198 | x = self.layers[-1](x, Adj) 199 | return x, Adj_ 200 | 201 | 202 | 203 | class GCN_C(nn.Module): 204 | 205 | def __init__(self, in_channels, hidden_channels, out_channels, num_layers, dropout, dropout_adj, sparse): 206 | super(GCN_C, self).__init__() 207 | 208 | self.layers = nn.ModuleList() 209 | 210 | if sparse: 211 | self.layers.append(GCNConv_dgl(in_channels, hidden_channels)) 212 | for _ in range(num_layers - 2): 213 | self.layers.append(GCNConv_dgl(hidden_channels, hidden_channels)) 214 | self.layers.append(GCNConv_dgl(hidden_channels, out_channels)) 215 | else: 216 | self.layers.append(GCNConv(in_channels, hidden_channels)) 217 | for i in range(num_layers - 2): 218 | self.layers.append(GCNConv(hidden_channels, hidden_channels)) 219 | self.layers.append(GCNConv(hidden_channels, out_channels)) 220 | 221 | self.dropout = dropout 222 | self.dropout_adj = nn.Dropout(p=dropout_adj) 223 | self.dropout_adj_p = dropout_adj 224 | self.sparse = sparse 225 | 226 | def forward(self, x, adj_t): 227 | 228 | if self.sparse: 229 | Adj = adj_t 230 | Adj.edata['w'] = F.dropout(Adj.edata['w'], p=self.dropout_adj_p, training=self.training) 231 | else: 232 | Adj = self.dropout_adj(adj_t) 233 | 234 | for i, conv in enumerate(self.layers[:-1]): 235 | x = conv(x, Adj) 236 | x = F.relu(x) 237 | x = F.dropout(x, p=self.dropout, training=self.training) 238 | x = self.layers[-1](x, Adj) 239 | return x -------------------------------------------------------------------------------- /model/sublime.py: -------------------------------------------------------------------------------- 1 | from GSL.model import BaseModel 2 | from GSL.utils import * 3 | from GSL.learner import * 4 | from GSL.encoder import * 5 | from GSL.metric import * 6 | from GSL.processor import * 7 | from GSL.eval import ClsEvaluator 8 | import torch 9 | import copy 10 | import torch.nn as nn 11 | import torch.nn.functional as F 12 | from scipy.sparse import coo_matrix 13 | 14 | #test git push 15 | 16 | class SUBLIME(BaseModel): 17 | ''' 18 | Towards Unsupervised Deep Graph Structure Learning (WWW 2022') 19 | ''' 20 | # def __init__(self, num_features, num_classes, metric, config_path, dataset_name, device, params): 21 | # super(SUBLIME, self).__init__(num_features, num_classes, metric, config_path, dataset_name, device, params) 22 | def __init__(self, num_features, num_classes, metric, config_path, dataset_name, device): 23 | super(SUBLIME, self).__init__(num_features, num_classes, metric, config_path, dataset_name, device) 24 | self.num_features = num_features 25 | self.num_classes = num_classes 26 | self.device = device 27 | 28 | def fit(self, dataset, split_num=0): 29 | adj, features, labels = dataset.adj.copy(), dataset.features.clone(), dataset.labels 30 | if dataset.name in ['cornell', 'texas', 'wisconsin', 'actor']: 31 | train_mask = dataset.train_masks[split_num % 10] 32 | val_mask = dataset.val_masks[split_num % 10] 33 | test_mask = dataset.test_masks[split_num % 10] 34 | else: 35 | train_mask, val_mask, test_mask = dataset.train_mask, dataset.val_mask, dataset.test_mask 36 | 37 | 38 | if self.config.mode == 'structure_inference': 39 | if self.config.sparse: 40 | anchor_adj_raw = torch_sparse_eye(features.shape[0]) 41 | else: 42 | anchor_adj_raw = torch.eye(features.shape[0]) 43 | elif self.config.mode == 'structure_refinement': 44 | # if self.config.sparse: 45 | # anchor_adj_raw = adj 46 | # else: 47 | # anchor_adj_raw = torch.from_numpy(adj) 48 | #anchor_adj_raw = adj 49 | coo = coo_matrix(adj) 50 | values = coo.data 51 | indices = np.vstack((coo.row, coo.col)) 52 | i = torch.LongTensor(indices) 53 | v = torch.FloatTensor(values) 54 | shape = coo.shape 55 | adj = torch.sparse_coo_tensor(i, v, torch.Size(shape)).to(self.device) 56 | anchor_adj_raw = adj.to_dense() 57 | 58 | anchor_adj = normalize(anchor_adj_raw, 'sym', self.config['sparse']) 59 | 60 | if self.config.sparse: 61 | anchor_adj_torch_sparse = copy.deepcopy(anchor_adj) 62 | anchor_adj = torch_sparse_to_dgl_graph(anchor_adj) 63 | 64 | activation = ({'relu': F.relu, 'prelu': F.prelu, 'tanh': F.tanh})[self.config.activation] 65 | 66 | #TODO: Graph Learner Initialize 67 | metric = CosineSimilarity() 68 | knnsparsify = KNNSparsify(self.config.k, discrete=True, self_loop=False) 69 | linear_trans = LinearTransform(6) 70 | processors = [knnsparsify, linear_trans] 71 | nonlinear = NonLinearize(non_linearity='elu') 72 | if self.config.learner == 'mlp': 73 | graph_learner = MLPLearner(metric, processors, 2, features.shape[1], features.shape[1], activation, self.config.sparse, self.config.k) 74 | elif self.config.learner == 'full': 75 | graph_learner = FullParam(metric, processors, features.cpu(), self.config.sparse, nonlinear) 76 | elif self.config.learner == 'gnn': 77 | graph_learner = GNNLearner(metric, processors, 2, features.shape[1], features.shape[1], activation) 78 | elif self.config.learner == 'att': 79 | graph_learner = AttLearner(metric, processors, 2, features.shape[1], activation, self.config.sparse) 80 | 81 | model = GCL(nlayers=self.config.num_layers, in_dim=self.num_features, hidden_dim=self.config.num_hidden, 82 | emb_dim=self.config.num_rep_dim, proj_dim=self.config.num_proj_dim, 83 | dropout=self.config.dropout, dropout_adj=self.config.dropedge_rate, sparse=self.config.sparse) 84 | 85 | optimizer_cl = torch.optim.Adam(model.parameters(), lr=self.config.lr, weight_decay=self.config.weight_decay) 86 | optimizer_learner = torch.optim.Adam(graph_learner.parameters(), lr=self.config.lr, weight_decay=self.config.weight_decay) 87 | 88 | model = model.to(self.device) 89 | graph_learner = graph_learner.to(self.device) 90 | train_mask = train_mask.to(self.device) 91 | val_mask = val_mask.to(self.device) 92 | test_mask = test_mask.to(self.device) 93 | features = features.to(self.device) 94 | labels = labels.to(self.device) 95 | if not self.config.sparse: 96 | anchor_adj = anchor_adj.to(self.device) 97 | 98 | best_val_result = 0 99 | for epoch in range(self.config.num_epochs): 100 | # Training 101 | model.train() 102 | graph_learner.train() 103 | loss, Adj = self.loss_gcl(model, graph_learner, features, anchor_adj) 104 | 105 | optimizer_cl.zero_grad() 106 | optimizer_learner.zero_grad() 107 | loss.backward() 108 | optimizer_cl.step() 109 | optimizer_learner.step() 110 | 111 | # Structure Bootstrapping 112 | if (1 - self.config.tau) and (self.config.c == 0 or epoch % self.config.c == 0): 113 | if self.config.sparse: 114 | learned_adj_torch_sparse = dgl_graph_to_torch_sparse(Adj) 115 | anchor_adj_torch_sparse = anchor_adj_torch_sparse * self.config.tau \ 116 | + learned_adj_torch_sparse * (1 - self.config.tau) 117 | anchor_adj = torch_sparse_to_dgl_graph(anchor_adj_torch_sparse) 118 | else: 119 | anchor_adj = anchor_adj * self.config.tau + Adj.detach() * (1 - self.config.tau) 120 | 121 | # Evaluate 122 | if epoch % self.config.eval_step == 0: 123 | model.eval() 124 | graph_learner.eval() 125 | f_adj = Adj 126 | 127 | ClsEval = ClsEvaluator('GCN', self.config, self.num_features, self.num_classes, self.device) 128 | result = ClsEval(features, f_adj, train_mask, \ 129 | val_mask, test_mask, labels) 130 | val_result, test_result = result['Acc_val'], result['Acc_test'] 131 | if val_result > best_val_result: 132 | self.best_result = test_result 133 | best_val_result = val_result 134 | self.Adj = anchor_adj 135 | 136 | print(f'Epoch: {epoch: 02d}, ' 137 | f'Loss: {loss:.4f}, ' 138 | f'Valid: {100 * val_result:.2f}%, ' 139 | f'Test: {100 * test_result:.2f}%') 140 | 141 | 142 | 143 | def loss_gcl(self, model, graph_learner, features, anchor_adj): 144 | 145 | # view 1: anchor graph 146 | if self.config.maskfeat_rate_anchor: 147 | mask_v1, _ = get_feat_mask(features, self.config.maskfeat_rate_anchor) 148 | features_v1 = features * (1 - mask_v1) 149 | else: 150 | features_v1 = copy.deepcopy(features) 151 | 152 | z1, _ = model(features_v1, anchor_adj, 'anchor') 153 | 154 | # view 2: learned graph 155 | if self.config.maskfeat_rate_learner: 156 | mask, _ = get_feat_mask(features, self.config.maskfeat_rate_learner) 157 | features_v2 = features * (1 - mask) 158 | else: 159 | features_v2 = copy.deepcopy(features) 160 | 161 | learned_adj = graph_learner(features) 162 | if not self.config.sparse: 163 | learned_adj = symmetrize(learned_adj) 164 | learned_adj = normalize(learned_adj, 'sym', self.config.sparse) 165 | 166 | z2, _ = model(features_v2, learned_adj, 'learner') 167 | 168 | # compute loss 169 | if self.config.contrast_batch_size: 170 | node_idxs = list(range(features.shape[0])) 171 | # random.shuffle(node_idxs) 172 | batches = split_batch(node_idxs, self.config.contrast_batch_size) 173 | loss = 0 174 | for batch in batches: 175 | weight = len(batch) / features.shape[0] 176 | loss += model.calc_loss(z1[batch], z2[batch]) * weight 177 | else: 178 | loss = model.calc_loss(z1, z2) 179 | 180 | return loss, learned_adj 181 | 182 | 183 | class GCL(nn.Module): 184 | def __init__(self, nlayers, in_dim, hidden_dim, emb_dim, proj_dim, dropout, dropout_adj, sparse): 185 | super(GCL, self).__init__() 186 | 187 | self.encoder = GraphEncoder(nlayers, in_dim, hidden_dim, emb_dim, proj_dim, dropout, dropout_adj, sparse) 188 | 189 | def forward(self, x, Adj_, branch=None): 190 | z, embedding = self.encoder(x, Adj_, branch) 191 | return z, embedding 192 | 193 | @staticmethod 194 | def calc_loss(x, x_aug, temperature=0.2, sym=True): 195 | batch_size, _ = x.size() 196 | x_abs = x.norm(dim=1) 197 | x_aug_abs = x_aug.norm(dim=1) 198 | 199 | sim_matrix = torch.einsum('ik,jk->ij', x, x_aug) / torch.einsum('i,j->ij', x_abs, x_aug_abs) 200 | sim_matrix = torch.exp(sim_matrix / temperature) 201 | pos_sim = sim_matrix[range(batch_size), range(batch_size)] 202 | if sym: 203 | loss_0 = pos_sim / (sim_matrix.sum(dim=0) - pos_sim) 204 | loss_1 = pos_sim / (sim_matrix.sum(dim=1) - pos_sim) 205 | 206 | loss_0 = - torch.log(loss_0).mean() 207 | loss_1 = - torch.log(loss_1).mean() 208 | loss = (loss_0 + loss_1) / 2.0 209 | return loss 210 | else: 211 | loss_1 = pos_sim / (sim_matrix.sum(dim=1) - pos_sim) 212 | loss_1 = - torch.log(loss_1).mean() 213 | return loss_1 -------------------------------------------------------------------------------- /pipeline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSL-Benchmark/GSLB/375cc1c20314360a927527f8d8528773fe1b0abc/pipeline.png -------------------------------------------------------------------------------- /processor.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import torch 3 | import torch.nn.functional as F 4 | from sklearn.neighbors import kneighbors_graph 5 | from torch.distributions.relaxed_bernoulli import RelaxedBernoulli 6 | 7 | 8 | class KNNSparsify: 9 | def __init__(self, k, discrete=False, self_loop=True): 10 | super(KNNSparsify, self).__init__() 11 | self.k = k 12 | self.discrete = discrete 13 | self.self_loop = self_loop 14 | 15 | def __call__(self, adj): 16 | _, indices = adj.topk(k=int(self.k+1), dim=-1) 17 | assert torch.max(indices) < adj.shape[1] 18 | mask = torch.zeros(adj.shape).to(adj.device) 19 | mask[torch.arange(adj.shape[0]).view(-1, 1), indices] = 1. 20 | 21 | mask.requires_grad = False 22 | if self.discrete: 23 | sparse_adj = mask.to(torch.float) 24 | else: 25 | sparse_adj = adj * mask 26 | 27 | if not self.self_loop: 28 | sparse_adj.fill_diagonal_(0) 29 | return sparse_adj 30 | 31 | 32 | class ThresholdSparsify: 33 | def __init__(self, threshold): 34 | super(ThresholdSparsify, self).__init__() 35 | self.threshold = threshold 36 | 37 | def __call__(self, adj): 38 | return torch.where(adj < self.threshold, torch.zeros_like(adj), adj) 39 | 40 | 41 | class ProbabilitySparsify: 42 | def __init__(self, temperature=0.1): 43 | self.temperature = temperature 44 | 45 | def __call__(self, prob): 46 | prob = torch.clamp(prob, 0.01, 0.99) 47 | adj = RelaxedBernoulli(temperature=torch.Tensor([self.temperature]).to(prob.device), 48 | probs=prob).rsample() 49 | eps = 0.5 50 | mask = (adj > eps).detach().float() 51 | adj = adj * mask + 0.0 * (1 - mask) 52 | return adj 53 | 54 | 55 | class Discretize: 56 | def __init__(self): 57 | super(Discretize, self).__init__() 58 | 59 | def __call__(self, adj): 60 | adj[adj != 0] = 1.0 61 | return adj 62 | 63 | 64 | class AddEye: 65 | def __init__(self): 66 | super(AddEye, self).__init__() 67 | 68 | def __call__(self, adj): 69 | adj += torch.eye(adj.shape[0]).to(adj.device) 70 | return adj 71 | 72 | 73 | class LinearTransform: 74 | def __init__(self, alpha): 75 | super(LinearTransform, self).__init__() 76 | self.alpha = alpha 77 | 78 | def __call__(self, adj): 79 | adj = adj * self.alpha - self.alpha 80 | return adj 81 | 82 | 83 | class NonLinearize: 84 | def __init__(self, non_linearity='relu', alpha=1.0): 85 | super(NonLinearize, self).__init__() 86 | self.non_linearity = non_linearity 87 | self.alpha = alpha 88 | 89 | def __call__(self, adj): 90 | if self.non_linearity == 'elu': 91 | return F.elu(adj) + 1 92 | elif self.non_linearity == 'relu': 93 | return F.relu(adj) 94 | elif self.non_linearity == 'none': 95 | return adj 96 | else: 97 | raise NameError('We dont support the non-linearity yet') 98 | 99 | 100 | class Symmetrize: 101 | def __init__(self): 102 | super(Symmetrize, self).__init__() 103 | 104 | def __call__(self, adj): 105 | return (adj + adj.T) / 2 106 | 107 | 108 | class Normalize: 109 | def __init__(self, mode='sym', eos=1e-10): 110 | super(Normalize, self).__init__() 111 | self.mode = mode 112 | self.EOS = eos 113 | 114 | def __call__(self, adj): 115 | if self.mode == "sym": 116 | inv_sqrt_degree = 1. / (torch.sqrt(adj.sum(dim=1, keepdim=False)) + self.EOS) 117 | return inv_sqrt_degree[:, None] * adj * inv_sqrt_degree[None, :] 118 | elif self.mode == "row": 119 | inv_degree = 1. / (adj.sum(dim=1, keepdim=False) + self.EOS) 120 | return inv_degree[:, None] * adj 121 | elif self.mode == "row_softmax": 122 | return F.softmax(adj, dim=1) 123 | elif self.mode == "row_softmax_sparse": 124 | return torch.sparse.softmax(adj.to_sparse(), dim=1).to_dense() 125 | else: 126 | raise Exception('We dont support the normalization mode') -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools", "wheel"] 3 | build-backend = "setuptools.build_meta" -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = GSLB 3 | version = 0.1.0 4 | author = Liang Wang 5 | author_email = wangliang.leon20@gmail.com 6 | description = GSLB: Graph Structure Learning Benchmark 7 | long_description = file: README.md 8 | long_description_content_type = text/markdown 9 | url = https://github.com/GSL-Benchmark/GSLB 10 | project_urls = 11 | Bug Tracker = https://github.com/GSL-Benchmark/GSLB/issues 12 | classifiers = 13 | Programming Language :: Python :: 3 14 | License :: OSI Approved :: MIT License 15 | Operating System :: OS Independent 16 | keywords = 17 | PyTorch 18 | Graph-Neural-Networks 19 | Graph-Structure-Learning 20 | 21 | [options] 22 | package_dir = 23 | = . 24 | packages = find: 25 | python_requires = >=3.8 26 | install_requires = 27 | torch >= 1.13 28 | dgl >= 1.1 29 | numpy 30 | scipy 31 | networkx 32 | scikit-learn 33 | tqdm 34 | ogb 35 | easydict 36 | PyYAML 37 | 38 | [options.packages.find] 39 | where = . -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup() --------------------------------------------------------------------------------