From 6d244521a19a6e4a6ef7e4a2356cc29ab1cf9917 Mon Sep 17 00:00:00 2001
From: toooooodo <1042178105@qq.com>
Date: Mon, 16 Oct 2023 14:19:55 +0800
Subject: [PATCH] init
---
.gitignore | 12 +
LICENSE | 21 ++
README.md | 100 +++++
args_parse.py | 47 +++
configs/GOODHIV/base.yaml | 8 +
configs/GOODHIV/scaffold/base.yaml | 11 +
configs/GOODHIV/scaffold/concept/ERM.yaml | 13 +
configs/GOODHIV/scaffold/concept/base.yaml | 6 +
configs/GOODHIV/scaffold/covariate/ERM.yaml | 13 +
configs/GOODHIV/scaffold/covariate/base.yaml | 6 +
configs/GOODHIV/size/base.yaml | 11 +
configs/GOODHIV/size/concept/ERM.yaml | 13 +
configs/GOODHIV/size/concept/base.yaml | 6 +
configs/GOODHIV/size/covariate/ERM.yaml | 13 +
configs/GOODHIV/size/covariate/base.yaml | 6 +
configs/GOODPCBA/base.yaml | 10 +
configs/GOODPCBA/scaffold/base.yaml | 10 +
configs/GOODPCBA/scaffold/concept/ERM.yaml | 12 +
configs/GOODPCBA/scaffold/concept/base.yaml | 4 +
configs/GOODPCBA/scaffold/covariate/ERM.yaml | 12 +
configs/GOODPCBA/scaffold/covariate/base.yaml | 4 +
configs/GOODPCBA/size/base.yaml | 10 +
configs/GOODPCBA/size/concept/ERM.yaml | 12 +
configs/GOODPCBA/size/concept/base.yaml | 4 +
configs/GOODPCBA/size/covariate/ERM.yaml | 12 +
configs/GOODPCBA/size/covariate/base.yaml | 4 +
configs/GOODZINC/base.yaml | 10 +
configs/GOODZINC/scaffold/base.yaml | 11 +
configs/GOODZINC/scaffold/concept/ERM.yaml | 12 +
configs/GOODZINC/scaffold/concept/base.yaml | 4 +
configs/GOODZINC/scaffold/covariate/ERM.yaml | 12 +
configs/GOODZINC/scaffold/covariate/base.yaml | 4 +
configs/GOODZINC/size/base.yaml | 11 +
configs/GOODZINC/size/concept/ERM.yaml | 12 +
configs/GOODZINC/size/concept/base.yaml | 4 +
configs/GOODZINC/size/covariate/ERM.yaml | 12 +
configs/GOODZINC/size/covariate/base.yaml | 4 +
configs/base.yaml | 33 ++
dataset/__init__.py | 1 +
dataset/drugdataset.py | 115 ++++++
dataset/smiles2graph.py | 172 +++++++++
eval.py | 121 ++++++
exputils.py | 176 +++++++++
models/__init__.py | 1 +
models/gnnconv.py | 356 ++++++++++++++++++
models/model.py | 147 ++++++++
resources/framework.png | Bin 0 -> 289412 bytes
run.py | 203 ++++++++++
48 files changed, 1801 insertions(+)
create mode 100644 .gitignore
create mode 100644 LICENSE
create mode 100644 README.md
create mode 100644 args_parse.py
create mode 100644 configs/GOODHIV/base.yaml
create mode 100644 configs/GOODHIV/scaffold/base.yaml
create mode 100644 configs/GOODHIV/scaffold/concept/ERM.yaml
create mode 100644 configs/GOODHIV/scaffold/concept/base.yaml
create mode 100644 configs/GOODHIV/scaffold/covariate/ERM.yaml
create mode 100644 configs/GOODHIV/scaffold/covariate/base.yaml
create mode 100644 configs/GOODHIV/size/base.yaml
create mode 100644 configs/GOODHIV/size/concept/ERM.yaml
create mode 100644 configs/GOODHIV/size/concept/base.yaml
create mode 100644 configs/GOODHIV/size/covariate/ERM.yaml
create mode 100644 configs/GOODHIV/size/covariate/base.yaml
create mode 100644 configs/GOODPCBA/base.yaml
create mode 100644 configs/GOODPCBA/scaffold/base.yaml
create mode 100644 configs/GOODPCBA/scaffold/concept/ERM.yaml
create mode 100644 configs/GOODPCBA/scaffold/concept/base.yaml
create mode 100644 configs/GOODPCBA/scaffold/covariate/ERM.yaml
create mode 100644 configs/GOODPCBA/scaffold/covariate/base.yaml
create mode 100644 configs/GOODPCBA/size/base.yaml
create mode 100644 configs/GOODPCBA/size/concept/ERM.yaml
create mode 100644 configs/GOODPCBA/size/concept/base.yaml
create mode 100644 configs/GOODPCBA/size/covariate/ERM.yaml
create mode 100644 configs/GOODPCBA/size/covariate/base.yaml
create mode 100644 configs/GOODZINC/base.yaml
create mode 100644 configs/GOODZINC/scaffold/base.yaml
create mode 100644 configs/GOODZINC/scaffold/concept/ERM.yaml
create mode 100644 configs/GOODZINC/scaffold/concept/base.yaml
create mode 100644 configs/GOODZINC/scaffold/covariate/ERM.yaml
create mode 100644 configs/GOODZINC/scaffold/covariate/base.yaml
create mode 100644 configs/GOODZINC/size/base.yaml
create mode 100644 configs/GOODZINC/size/concept/ERM.yaml
create mode 100644 configs/GOODZINC/size/concept/base.yaml
create mode 100644 configs/GOODZINC/size/covariate/ERM.yaml
create mode 100644 configs/GOODZINC/size/covariate/base.yaml
create mode 100644 configs/base.yaml
create mode 100644 dataset/__init__.py
create mode 100644 dataset/drugdataset.py
create mode 100644 dataset/smiles2graph.py
create mode 100644 eval.py
create mode 100644 exputils.py
create mode 100644 models/__init__.py
create mode 100644 models/gnnconv.py
create mode 100644 models/model.py
create mode 100644 resources/framework.png
create mode 100644 run.py
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..882901f
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,12 @@
+dump/
+data/
+ogb-data/
+__pycache__/
+script/
+drugood-data*/
+drugood-data*
+ogb-data
+data
+*.out
+*.tar.gz
+checkpoint
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..5547bb5
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2023 todoooooo
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..8affe0b
--- /dev/null
+++ b/README.md
@@ -0,0 +1,100 @@
+# Learning Invariant Molecular Representation in Latent Discrete Space
+This repository is the official implementation of our paper:
+
+**Learning Invariant Molecular Representation in Latent Discrete Space**
+
+_Xiang Zhuang, Qiang Zhang*, Keyan Ding, Yatao Bian, Xiao Wang, Jingsong Lv, Hongyang Chen, Huajun Chen* (* denotes correspondence)_
+
+Advances in Neural Information Processing Systems (NeurIPS) 2023
+
+
+
+
+## Environment
+To run the code successfully, the following dependencies need to be installed:
+```
+Python 3.8
+torch 1.10.1
+torch_geometric 2.0.4
+torch_scatter 2.0.9
+torch_cluster 1.6.0
+torch_sparse 0.6.13
+torch_spline_conv 1.2.1
+rdkit_pypi 2022.9.5
+vector_quantize_pytorch 1.0.7
+ogb 1.3.6
+```
+
+This repo is also depended on `GOOD` and `DrugOOD`, please follow the installation methods provided for each package:
+- GOOD (Version 1.1.1)
+ - Repository: https://github.com/divelab/GOOD/
+ - Installation: Please follow the instructions provided in the repository to install.
+- DrugOOD (Version 0.0.1)
+ - Repository: https://github.com/tencent-ailab/DrugOOD
+ - Installation: Please follow the instructions provided in the repository to install.
+
+## Data
+The data used in the experiments can be downloaded from the following sources:
+
+1. GOOD
+ - [GOODPCBA](https://drive.google.com/file/d/1WGieOjtgNXtGoO6o1EGhKrZj0zWU7AJl/view?usp=sharing)
+ - [GOODHIV](https://drive.google.com/file/d/1CoOqYCuLObnG5M0D8a2P2NyL61WjbCzo/view?usp=sharing)
+ - [GOODZINC](https://drive.google.com/file/d/1CHR0I1JcNoBqrqFicAZVKU3213hbsEPZ/view?usp=sharing)
+ - Extract the downloaded files and save the contents in the `data` directory.
+2. DrugOOD
+ - download from [link](https://drive.google.com/drive/folders/19EAVkhJg0AgMx7X-bXGOhD4ENLfxJMWC).
+ - Extract the downloaded file and save the contents in the `drugood-data-chembl30` directory.
+
+An example of the folder hierarchy after adding the data files:
+```
+├── data
+│ ├── GOODHIV
+│ ├── GOODPCBA
+│ ├── GOODZINC
+├── drugood-data-chembl30
+│ ├── lbap_core_ec50_assay.json
+│ └── ...
+├── models
+│ ├── model.py
+│ └── ...
+├── run.py
+└── README.md
+```
+## Running Script
+#### Training
+```
+python run.py --dataset GOODZINC --domain scaffold --shift concept --num_e 4000 --bs 256 --gamma 0.5 --inv_w 0.01 --reg_w 0.5 --gpu 0 --exp_name ZINC --exp_id scaffold-concept
+```
+Running parameters and descriptions are as follows:
+| Parameter | Description | Choices |
+| --- | --- | --- |
+| dataset | name of dataset | `GOODHIV`, `GOODZINC`, `GOODPCBA`, `ic50_assay`, `ic50_scaffold`, `ic50_size`, `ec50_assay`, `ec50_scaffold`, `ec50_size`.|
+| domain | environment-splitting strategy | `scaffold`, `size`. Only need to be specified for datasets in `GOOD`. |
+| shift | type of distribution shift | `covariate`, `concept`. Only need to be specified for datasets in `GOOD`. |
+| num_e | code book size | - |
+| bs | batch size | - |
+| gamma | threshold $\gamma$ | - |
+| inv_w | $\lambda_1$ | - |
+| reg_w | $\lambda_2$ | - |
+| gpu | which GPU to use | - |
+| exp_name | experiment name | - |
+| exp_id | experiment ID | - |
+
+#### Evaluation
+We provide the hyperparameters for the training of each dataset in the Appendix, and provide the corresponding checkpoints in the [release page](https://github.com/HICAI-ZJU/iMoLD/releases).
+```
+python eval.py --dataset GOODZINC --domain scaffold --shift concept --load_path checkpoint/GOODZINC-scaffold-concept.pkl
+```
+The `load_path` parameter specifies the path to load the checkpoint.
+
+## Citation
+If you use or extend our work, please cite the paper as follows:
+
+```bibtex
+@InProceedings{zhuang2023learning,
+ title={Learning Invariant Molecular Representation in Latent Discrete Space},
+ author={Xiang Zhuang and Qiang Zhang and Keyan Ding and Yatao Bian and Xiao Wang and Jingsong Lv and Hongyang Chen and Huajun Chen},
+ booktile={Advances in Neural Information Processing Systems},
+ year={2023}
+}
+```
\ No newline at end of file
diff --git a/args_parse.py b/args_parse.py
new file mode 100644
index 0000000..d8a5a2c
--- /dev/null
+++ b/args_parse.py
@@ -0,0 +1,47 @@
+import argparse
+
+
+def args_parser():
+ parser = argparse.ArgumentParser()
+ # exp
+ parser.add_argument("--exp_name", default="run", type=str,
+ help="Experiment name")
+ parser.add_argument("--dump_path", default="dump/", type=str,
+ help="Experiment dump path")
+ parser.add_argument("--exp_id", default="", type=str,
+ help="Experiment ID")
+ parser.add_argument("--gpu", default='0', type=str)
+ parser.add_argument("--random_seed", default=0, type=int)
+ parser.add_argument("--load_path", default=None, type=str)
+
+ # dataset
+ parser.add_argument("--data_root", default='data', type=str)
+ parser.add_argument("--config_path", default='configs', type=str)
+ parser.add_argument("--dataset", default='GOODHIV', type=str)
+ parser.add_argument("--domain", default='scaffold', type=str)
+ parser.add_argument("--shift", default='covariate', type=str)
+
+ # VQ
+ parser.add_argument("--num_e", default=4000, type=int)
+ parser.add_argument("--commitment_weight", default=0.1, type=float)
+
+ # Encoder
+ parser.add_argument("--emb_dim", default=128, type=int)
+ parser.add_argument("--layer", default=4, type=int)
+ parser.add_argument("--dropout", default=0.5, type=float)
+ parser.add_argument("--gnn_type", default='gin', type=str, choices=['gcn', 'gin'])
+ parser.add_argument("--pooling_type", default='mean', type=str)
+
+ # Model
+ parser.add_argument("--inv_w", default=0.01, type=float)
+ parser.add_argument("--reg_w", default=0.5, type=float)
+ parser.add_argument("--gamma", default=0.9, type=float)
+
+ # Training
+ parser.add_argument("--lr", default=0.001, type=float)
+ parser.add_argument("--bs", default=128, type=int)
+ parser.add_argument("--epoch", default=200, type=int)
+
+ args = parser.parse_args()
+
+ return args
diff --git a/configs/GOODHIV/base.yaml b/configs/GOODHIV/base.yaml
new file mode 100644
index 0000000..a152a5b
--- /dev/null
+++ b/configs/GOODHIV/base.yaml
@@ -0,0 +1,8 @@
+includes:
+ - ../base.yaml
+model:
+ model_layer: 3
+ global_pool: mean
+# train:
+# num_steps: 10
+# mile_stones: [150]
\ No newline at end of file
diff --git a/configs/GOODHIV/scaffold/base.yaml b/configs/GOODHIV/scaffold/base.yaml
new file mode 100644
index 0000000..d004719
--- /dev/null
+++ b/configs/GOODHIV/scaffold/base.yaml
@@ -0,0 +1,11 @@
+includes:
+ - ../base.yaml
+dataset:
+ dataset_name: GOODHIV
+ domain: scaffold
+train:
+ # max_epoch: 200
+ train_bs: 32
+ val_bs: 256
+ test_bs: 256
+ # weight_decay: 0
\ No newline at end of file
diff --git a/configs/GOODHIV/scaffold/concept/ERM.yaml b/configs/GOODHIV/scaffold/concept/ERM.yaml
new file mode 100644
index 0000000..618e6af
--- /dev/null
+++ b/configs/GOODHIV/scaffold/concept/ERM.yaml
@@ -0,0 +1,13 @@
+includes:
+ - base.yaml
+model:
+ model_name: vGIN
+ood:
+ ood_alg: ERM
+ ood_param: -1.0
+train:
+ max_epoch: 300
+ lr: 0.001
+ weight_decay: 0.0
+log_file: lb_sweeping
+num_workers: 0
diff --git a/configs/GOODHIV/scaffold/concept/base.yaml b/configs/GOODHIV/scaffold/concept/base.yaml
new file mode 100644
index 0000000..18290e4
--- /dev/null
+++ b/configs/GOODHIV/scaffold/concept/base.yaml
@@ -0,0 +1,6 @@
+includes:
+ - ../base.yaml
+dataset:
+ shift_type: concept
+model:
+ model_name: vGIN
\ No newline at end of file
diff --git a/configs/GOODHIV/scaffold/covariate/ERM.yaml b/configs/GOODHIV/scaffold/covariate/ERM.yaml
new file mode 100644
index 0000000..618e6af
--- /dev/null
+++ b/configs/GOODHIV/scaffold/covariate/ERM.yaml
@@ -0,0 +1,13 @@
+includes:
+ - base.yaml
+model:
+ model_name: vGIN
+ood:
+ ood_alg: ERM
+ ood_param: -1.0
+train:
+ max_epoch: 300
+ lr: 0.001
+ weight_decay: 0.0
+log_file: lb_sweeping
+num_workers: 0
diff --git a/configs/GOODHIV/scaffold/covariate/base.yaml b/configs/GOODHIV/scaffold/covariate/base.yaml
new file mode 100644
index 0000000..ffef5ad
--- /dev/null
+++ b/configs/GOODHIV/scaffold/covariate/base.yaml
@@ -0,0 +1,6 @@
+includes:
+ - ../base.yaml
+dataset:
+ shift_type: covariate
+model:
+ model_name: vGIN
\ No newline at end of file
diff --git a/configs/GOODHIV/size/base.yaml b/configs/GOODHIV/size/base.yaml
new file mode 100644
index 0000000..f06360e
--- /dev/null
+++ b/configs/GOODHIV/size/base.yaml
@@ -0,0 +1,11 @@
+includes:
+ - ../base.yaml
+dataset:
+ dataset_name: GOODHIV
+ domain: size
+train:
+ # max_epoch: 200
+ train_bs: 32
+ val_bs: 256
+ test_bs: 256
+ # weight_decay: 0
\ No newline at end of file
diff --git a/configs/GOODHIV/size/concept/ERM.yaml b/configs/GOODHIV/size/concept/ERM.yaml
new file mode 100644
index 0000000..618e6af
--- /dev/null
+++ b/configs/GOODHIV/size/concept/ERM.yaml
@@ -0,0 +1,13 @@
+includes:
+ - base.yaml
+model:
+ model_name: vGIN
+ood:
+ ood_alg: ERM
+ ood_param: -1.0
+train:
+ max_epoch: 300
+ lr: 0.001
+ weight_decay: 0.0
+log_file: lb_sweeping
+num_workers: 0
diff --git a/configs/GOODHIV/size/concept/base.yaml b/configs/GOODHIV/size/concept/base.yaml
new file mode 100644
index 0000000..18290e4
--- /dev/null
+++ b/configs/GOODHIV/size/concept/base.yaml
@@ -0,0 +1,6 @@
+includes:
+ - ../base.yaml
+dataset:
+ shift_type: concept
+model:
+ model_name: vGIN
\ No newline at end of file
diff --git a/configs/GOODHIV/size/covariate/ERM.yaml b/configs/GOODHIV/size/covariate/ERM.yaml
new file mode 100644
index 0000000..13b18d5
--- /dev/null
+++ b/configs/GOODHIV/size/covariate/ERM.yaml
@@ -0,0 +1,13 @@
+includes:
+ - base.yaml
+model:
+ model_name: vGIN
+ood:
+ ood_alg: ERM
+ ood_param: -1.0
+train:
+ max_epoch: 200
+ lr: 0.001
+ weight_decay: 0.0
+log_file: lb_sweeping
+num_workers: 0
diff --git a/configs/GOODHIV/size/covariate/base.yaml b/configs/GOODHIV/size/covariate/base.yaml
new file mode 100644
index 0000000..ffef5ad
--- /dev/null
+++ b/configs/GOODHIV/size/covariate/base.yaml
@@ -0,0 +1,6 @@
+includes:
+ - ../base.yaml
+dataset:
+ shift_type: covariate
+model:
+ model_name: vGIN
\ No newline at end of file
diff --git a/configs/GOODPCBA/base.yaml b/configs/GOODPCBA/base.yaml
new file mode 100644
index 0000000..13481aa
--- /dev/null
+++ b/configs/GOODPCBA/base.yaml
@@ -0,0 +1,10 @@
+includes:
+ - ../base.yaml
+model:
+ model_layer: 5
+ global_pool: mean
+ model_name: vGIN
+train:
+ # num_steps: 10
+ test_bs: 128
+ # mile_stones: [150]
\ No newline at end of file
diff --git a/configs/GOODPCBA/scaffold/base.yaml b/configs/GOODPCBA/scaffold/base.yaml
new file mode 100644
index 0000000..269ae28
--- /dev/null
+++ b/configs/GOODPCBA/scaffold/base.yaml
@@ -0,0 +1,10 @@
+includes:
+ - ../base.yaml
+dataset:
+ dataset_name: GOODPCBA
+ domain: scaffold
+train:
+ # max_epoch: 200
+ train_bs: 32
+ val_bs: 128
+ # weight_decay: 0
\ No newline at end of file
diff --git a/configs/GOODPCBA/scaffold/concept/ERM.yaml b/configs/GOODPCBA/scaffold/concept/ERM.yaml
new file mode 100644
index 0000000..96f3655
--- /dev/null
+++ b/configs/GOODPCBA/scaffold/concept/ERM.yaml
@@ -0,0 +1,12 @@
+includes:
+ - base.yaml
+model:
+ model_name: vGIN
+ood:
+ ood_alg: ERM
+ ood_param: -1.
+train:
+ max_epoch: 200
+ lr: 1e-3
+ mile_stones: [150]
+
diff --git a/configs/GOODPCBA/scaffold/concept/base.yaml b/configs/GOODPCBA/scaffold/concept/base.yaml
new file mode 100644
index 0000000..3fb650e
--- /dev/null
+++ b/configs/GOODPCBA/scaffold/concept/base.yaml
@@ -0,0 +1,4 @@
+includes:
+ - ../base.yaml
+dataset:
+ shift_type: concept
\ No newline at end of file
diff --git a/configs/GOODPCBA/scaffold/covariate/ERM.yaml b/configs/GOODPCBA/scaffold/covariate/ERM.yaml
new file mode 100644
index 0000000..96f3655
--- /dev/null
+++ b/configs/GOODPCBA/scaffold/covariate/ERM.yaml
@@ -0,0 +1,12 @@
+includes:
+ - base.yaml
+model:
+ model_name: vGIN
+ood:
+ ood_alg: ERM
+ ood_param: -1.
+train:
+ max_epoch: 200
+ lr: 1e-3
+ mile_stones: [150]
+
diff --git a/configs/GOODPCBA/scaffold/covariate/base.yaml b/configs/GOODPCBA/scaffold/covariate/base.yaml
new file mode 100644
index 0000000..8804c14
--- /dev/null
+++ b/configs/GOODPCBA/scaffold/covariate/base.yaml
@@ -0,0 +1,4 @@
+includes:
+ - ../base.yaml
+dataset:
+ shift_type: covariate
\ No newline at end of file
diff --git a/configs/GOODPCBA/size/base.yaml b/configs/GOODPCBA/size/base.yaml
new file mode 100644
index 0000000..ad34209
--- /dev/null
+++ b/configs/GOODPCBA/size/base.yaml
@@ -0,0 +1,10 @@
+includes:
+ - ../base.yaml
+dataset:
+ dataset_name: GOODPCBA
+ domain: size
+train:
+ # max_epoch: 200
+ train_bs: 32
+ val_bs: 128
+ # weight_decay: 0
\ No newline at end of file
diff --git a/configs/GOODPCBA/size/concept/ERM.yaml b/configs/GOODPCBA/size/concept/ERM.yaml
new file mode 100644
index 0000000..96f3655
--- /dev/null
+++ b/configs/GOODPCBA/size/concept/ERM.yaml
@@ -0,0 +1,12 @@
+includes:
+ - base.yaml
+model:
+ model_name: vGIN
+ood:
+ ood_alg: ERM
+ ood_param: -1.
+train:
+ max_epoch: 200
+ lr: 1e-3
+ mile_stones: [150]
+
diff --git a/configs/GOODPCBA/size/concept/base.yaml b/configs/GOODPCBA/size/concept/base.yaml
new file mode 100644
index 0000000..3fb650e
--- /dev/null
+++ b/configs/GOODPCBA/size/concept/base.yaml
@@ -0,0 +1,4 @@
+includes:
+ - ../base.yaml
+dataset:
+ shift_type: concept
\ No newline at end of file
diff --git a/configs/GOODPCBA/size/covariate/ERM.yaml b/configs/GOODPCBA/size/covariate/ERM.yaml
new file mode 100644
index 0000000..96f3655
--- /dev/null
+++ b/configs/GOODPCBA/size/covariate/ERM.yaml
@@ -0,0 +1,12 @@
+includes:
+ - base.yaml
+model:
+ model_name: vGIN
+ood:
+ ood_alg: ERM
+ ood_param: -1.
+train:
+ max_epoch: 200
+ lr: 1e-3
+ mile_stones: [150]
+
diff --git a/configs/GOODPCBA/size/covariate/base.yaml b/configs/GOODPCBA/size/covariate/base.yaml
new file mode 100644
index 0000000..8804c14
--- /dev/null
+++ b/configs/GOODPCBA/size/covariate/base.yaml
@@ -0,0 +1,4 @@
+includes:
+ - ../base.yaml
+dataset:
+ shift_type: covariate
\ No newline at end of file
diff --git a/configs/GOODZINC/base.yaml b/configs/GOODZINC/base.yaml
new file mode 100644
index 0000000..7d5a0a8
--- /dev/null
+++ b/configs/GOODZINC/base.yaml
@@ -0,0 +1,10 @@
+includes:
+ - ../base.yaml
+model:
+ model_layer: 3
+ global_pool: mean
+ model_name: vGIN
+
+# train:
+# num_steps: 10
+# mile_stones: [150]
\ No newline at end of file
diff --git a/configs/GOODZINC/scaffold/base.yaml b/configs/GOODZINC/scaffold/base.yaml
new file mode 100644
index 0000000..fb2b308
--- /dev/null
+++ b/configs/GOODZINC/scaffold/base.yaml
@@ -0,0 +1,11 @@
+includes:
+ - ../base.yaml
+dataset:
+ dataset_name: GOODZINC
+ domain: scaffold
+train:
+ # max_epoch: 200
+ train_bs: 32
+ val_bs: 128
+ test_bs: 128
+ # weight_decay: 0
\ No newline at end of file
diff --git a/configs/GOODZINC/scaffold/concept/ERM.yaml b/configs/GOODZINC/scaffold/concept/ERM.yaml
new file mode 100644
index 0000000..96f3655
--- /dev/null
+++ b/configs/GOODZINC/scaffold/concept/ERM.yaml
@@ -0,0 +1,12 @@
+includes:
+ - base.yaml
+model:
+ model_name: vGIN
+ood:
+ ood_alg: ERM
+ ood_param: -1.
+train:
+ max_epoch: 200
+ lr: 1e-3
+ mile_stones: [150]
+
diff --git a/configs/GOODZINC/scaffold/concept/base.yaml b/configs/GOODZINC/scaffold/concept/base.yaml
new file mode 100644
index 0000000..3fb650e
--- /dev/null
+++ b/configs/GOODZINC/scaffold/concept/base.yaml
@@ -0,0 +1,4 @@
+includes:
+ - ../base.yaml
+dataset:
+ shift_type: concept
\ No newline at end of file
diff --git a/configs/GOODZINC/scaffold/covariate/ERM.yaml b/configs/GOODZINC/scaffold/covariate/ERM.yaml
new file mode 100644
index 0000000..96f3655
--- /dev/null
+++ b/configs/GOODZINC/scaffold/covariate/ERM.yaml
@@ -0,0 +1,12 @@
+includes:
+ - base.yaml
+model:
+ model_name: vGIN
+ood:
+ ood_alg: ERM
+ ood_param: -1.
+train:
+ max_epoch: 200
+ lr: 1e-3
+ mile_stones: [150]
+
diff --git a/configs/GOODZINC/scaffold/covariate/base.yaml b/configs/GOODZINC/scaffold/covariate/base.yaml
new file mode 100644
index 0000000..8804c14
--- /dev/null
+++ b/configs/GOODZINC/scaffold/covariate/base.yaml
@@ -0,0 +1,4 @@
+includes:
+ - ../base.yaml
+dataset:
+ shift_type: covariate
\ No newline at end of file
diff --git a/configs/GOODZINC/size/base.yaml b/configs/GOODZINC/size/base.yaml
new file mode 100644
index 0000000..85c3378
--- /dev/null
+++ b/configs/GOODZINC/size/base.yaml
@@ -0,0 +1,11 @@
+includes:
+ - ../base.yaml
+dataset:
+ dataset_name: GOODZINC
+ domain: size
+train:
+ # max_epoch: 200
+ train_bs: 32
+ val_bs: 128
+ test_bs: 128
+ # weight_decay: 0
\ No newline at end of file
diff --git a/configs/GOODZINC/size/concept/ERM.yaml b/configs/GOODZINC/size/concept/ERM.yaml
new file mode 100644
index 0000000..96f3655
--- /dev/null
+++ b/configs/GOODZINC/size/concept/ERM.yaml
@@ -0,0 +1,12 @@
+includes:
+ - base.yaml
+model:
+ model_name: vGIN
+ood:
+ ood_alg: ERM
+ ood_param: -1.
+train:
+ max_epoch: 200
+ lr: 1e-3
+ mile_stones: [150]
+
diff --git a/configs/GOODZINC/size/concept/base.yaml b/configs/GOODZINC/size/concept/base.yaml
new file mode 100644
index 0000000..3fb650e
--- /dev/null
+++ b/configs/GOODZINC/size/concept/base.yaml
@@ -0,0 +1,4 @@
+includes:
+ - ../base.yaml
+dataset:
+ shift_type: concept
\ No newline at end of file
diff --git a/configs/GOODZINC/size/covariate/ERM.yaml b/configs/GOODZINC/size/covariate/ERM.yaml
new file mode 100644
index 0000000..96f3655
--- /dev/null
+++ b/configs/GOODZINC/size/covariate/ERM.yaml
@@ -0,0 +1,12 @@
+includes:
+ - base.yaml
+model:
+ model_name: vGIN
+ood:
+ ood_alg: ERM
+ ood_param: -1.
+train:
+ max_epoch: 200
+ lr: 1e-3
+ mile_stones: [150]
+
diff --git a/configs/GOODZINC/size/covariate/base.yaml b/configs/GOODZINC/size/covariate/base.yaml
new file mode 100644
index 0000000..8804c14
--- /dev/null
+++ b/configs/GOODZINC/size/covariate/base.yaml
@@ -0,0 +1,4 @@
+includes:
+ - ../base.yaml
+dataset:
+ shift_type: covariate
\ No newline at end of file
diff --git a/configs/base.yaml b/configs/base.yaml
new file mode 100644
index 0000000..5d553b3
--- /dev/null
+++ b/configs/base.yaml
@@ -0,0 +1,33 @@
+# task: train
+random_seed: 123
+# exp_round: null
+# log_file: default
+# gpu_idx: 0
+# ckpt_root: null
+# ckpt_dir: null
+# save_tag: null
+# pytest: False
+# pipeline: Pipeline
+num_workers: 1
+# train:
+# weight_decay: 0.
+ # save_gap: 10
+ # tr_ctn: False
+ # ctn_epoch: 0
+ # epoch: 0
+ # alpha: 0.2 # parameter for DANN
+ # stage_stones: []
+model:
+ dim_hidden: 300
+ dim_ffn: 300
+ dropout_rate: 0.5
+dataset:
+ dataloader_name: BaseDataLoader
+ # dataset_root: null
+ generate: False
+ # dim_node: null
+ # dim_edge: null
+ # num_classes: null
+ # num_envs: null
+# ood:
+# extra_param: null
\ No newline at end of file
diff --git a/dataset/__init__.py b/dataset/__init__.py
new file mode 100644
index 0000000..63a1198
--- /dev/null
+++ b/dataset/__init__.py
@@ -0,0 +1 @@
+from .drugdataset import DrugOODDataset
diff --git a/dataset/drugdataset.py b/dataset/drugdataset.py
new file mode 100644
index 0000000..f194249
--- /dev/null
+++ b/dataset/drugdataset.py
@@ -0,0 +1,115 @@
+import os
+import os.path as osp
+import json
+import pickle
+
+
+import torch
+from torch_geometric.data import InMemoryDataset, Data, DataLoader
+from rdkit import Chem
+from tqdm import tqdm
+
+from .smiles2graph import smile2graph4drugood
+
+
+
+class DrugOODDataset(InMemoryDataset):
+ def __init__(self, name, version='chembl30', type='lbap', root='data', drugood_root='drugood-data',
+ transform=None, pre_transform=None, pre_filter=None):
+ self.name = name
+ self.root = root
+ # self.dir_name = '_'.join(name.split('-'))
+ self.drugood_root = drugood_root
+ self.version = version
+ self.type = type
+ super(DrugOODDataset, self).__init__(root, transform, pre_transform, pre_filter)
+ self.data, self.slices = torch.load(self.processed_paths[0])
+ self.data_cfg = pickle.load(open(self.processed_paths[1], 'rb'))
+ self.data_statistics = pickle.load(open(self.processed_paths[2], 'rb'))
+ self.train_index, self.valid_index, self.test_index = pickle.load(open(self.processed_paths[3], 'rb'))
+ self.num_tasks = 1
+
+ @property
+ def raw_dir(self):
+ # return osp.join(self.ogb_root, self.dir_name, 'mapping')
+ # return self.drugood_root
+ return self.drugood_root + '-' + self.version
+
+ @property
+ def raw_file_names(self):
+ # return 'lbap_core_' + self.name + '.json'
+ return f'{self.type}_core_{self.name}.json'
+ # return 'mol.csv.gz'
+ # return f'{self.names[self.name][2]}.csv'
+
+ @property
+ def processed_dir(self):
+ # return osp.join(self.root, self.name, f'{self.decomp}-processed')
+ # return osp.join(self.root, self.dir_name, f'{self.decomp}-processed')
+ # return osp.join(self.root, f'{self.name}-{self.version}')
+ return osp.join(self.root, f'{self.type}-{self.name}-{self.version}')
+
+ @property
+ def processed_file_names(self):
+ return 'data.pt', 'cfg.pt', 'statistics.pt', 'split.pt'
+
+ def __subprocess(self, datalist):
+ processed_data = []
+ for datapoint in tqdm(datalist):
+ # ['smiles', 'reg_label', 'assay_id', 'cls_label', 'domain_id']
+ smiles = datapoint['smiles']
+ x, edge_index, edge_attr = smile2graph4drugood(smiles)
+ y = torch.tensor([datapoint['cls_label']]).unsqueeze(0)
+ if self.type == 'lbap':
+ data = Data(x=x, edge_index=edge_index, edge_attr=edge_attr, y=y, smiles=smiles,
+ reg_label=datapoint['reg_label'], assay_id=datapoint['assay_id'],
+ domain_id=datapoint['domain_id'])
+ else:
+ protein = datapoint['protein']
+ data = Data(x=x, edge_index=edge_index, edge_attr=edge_attr, y=y, smiles=smiles, protein=protein,
+ reg_label=datapoint['reg_label'], assay_id=datapoint['assay_id'],
+ domain_id=datapoint['domain_id'])
+
+ data.batch_num_nodes = data.num_nodes
+ # if self.pre_filter is not None and not self.pre_filter(data):
+ # continue
+
+ if self.pre_transform is not None:
+ data = self.pre_transform(data)
+ processed_data.append(data)
+ return processed_data, len(processed_data)
+
+ def process(self):
+ # data_list = []
+ json_data = json.load(open(self.raw_paths[0], 'r', encoding='utf-8'))
+ data_cfg, data_statistics = json_data['cfg'], json_data['statistics']
+ train_data = json_data['split']['train']
+ valid_data = json_data['split']['ood_val']
+ test_data = json_data['split']['ood_test']
+ train_data_list, train_num = self.__subprocess(train_data)
+ valid_data_list, valid_num = self.__subprocess(valid_data)
+ test_data_list, test_num = self.__subprocess(test_data)
+ data_list = train_data_list + valid_data_list + test_data_list
+ train_index = list(range(train_num))
+ valid_index = list(range(train_num, train_num + valid_num))
+ test_index = list(range(train_num + valid_num, train_num + valid_num + test_num))
+ torch.save(self.collate(data_list), self.processed_paths[0])
+ pickle.dump(data_cfg, open(self.processed_paths[1], 'wb'))
+ pickle.dump(data_statistics, open(self.processed_paths[2], 'wb'))
+ pickle.dump([train_index, valid_index, test_index], open(self.processed_paths[3], 'wb'))
+
+
+ def __repr__(self):
+ return '{}({})'.format(self.name, len(self))
+
+
+# if __name__ == '__main__':
+# dataset = DrugOODDataset(name='ic50_assay', root='../data', drugood_root='../drugood-data')
+# # data = json.load(open('../drugood-data/lbap_core_ec50_size.json', 'r', encoding='utf-8'))
+# train_set = dataset[dataset.train_index]
+# test_set = dataset[dataset.test_index]
+# loader = DataLoader(train_set, batch_size=4, shuffle=True)
+# for data in loader:
+# import pdb;
+#
+# pdb.set_trace()
diff --git a/dataset/smiles2graph.py b/dataset/smiles2graph.py
new file mode 100644
index 0000000..fbac01d
--- /dev/null
+++ b/dataset/smiles2graph.py
@@ -0,0 +1,172 @@
+# import dgl
+import numpy as np
+import rdkit
+import torch
+from rdkit import Chem
+
+
+def get_atom_features(atom):
+ # The usage of features is along with the Attentive FP.
+ feature = np.zeros(39)
+
+ # Symbol
+ symbol = atom.GetSymbol()
+ symbol_list = ['B', 'C', 'N', 'O', 'F', 'Si', 'P', 'S', 'Cl', 'As', 'Se', 'Br', 'Te', 'I', 'At']
+ if symbol in symbol_list:
+ loc = symbol_list.index(symbol)
+ feature[loc] = 1
+ else:
+ feature[15] = 1
+
+ # Degree
+ degree = atom.GetDegree()
+ if degree > 5:
+ print("atom degree larger than 5. Please check before featurizing.")
+ raise RuntimeError
+
+ feature[16 + degree] = 1
+
+ # Formal Charge
+ charge = atom.GetFormalCharge()
+ feature[22] = charge
+
+ # radical electrons
+ radelc = atom.GetNumRadicalElectrons()
+ feature[23] = radelc
+
+ # Hybridization
+ hyb = atom.GetHybridization()
+ hybridization_list = [rdkit.Chem.rdchem.HybridizationType.SP,
+ rdkit.Chem.rdchem.HybridizationType.SP2,
+ rdkit.Chem.rdchem.HybridizationType.SP3,
+ rdkit.Chem.rdchem.HybridizationType.SP3D,
+ rdkit.Chem.rdchem.HybridizationType.SP3D2]
+ if hyb in hybridization_list:
+ loc = hybridization_list.index(hyb)
+ feature[loc + 24] = 1
+ else:
+ feature[29] = 1
+
+ # aromaticity
+ if atom.GetIsAromatic():
+ feature[30] = 1
+
+ # hydrogens
+ hs = atom.GetNumImplicitHs()
+ feature[31 + hs] = 1
+
+ # chirality, chirality type
+ if atom.HasProp('_ChiralityPossible'):
+ # TODO what kind of error
+ feature[36] = 1
+
+ try:
+ chi = atom.GetProp('_CIPCode')
+ chi_list = ['R', 'S']
+ loc = chi_list.index(chi)
+ feature[37 + loc] = 1
+ except KeyError:
+ feature[37] = 0
+ feature[38] = 0
+
+ return feature
+
+
+def get_bond_features(bond):
+ feature = np.zeros(10)
+
+ # bond type
+ type = bond.GetBondType()
+ bond_type_list = [rdkit.Chem.rdchem.BondType.SINGLE,
+ rdkit.Chem.rdchem.BondType.DOUBLE,
+ rdkit.Chem.rdchem.BondType.TRIPLE,
+ rdkit.Chem.rdchem.BondType.AROMATIC]
+ if type in bond_type_list:
+ loc = bond_type_list.index(type)
+ feature[0 + loc] = 1
+ else:
+ print("Wrong type of bond. Please check before feturization.")
+ raise RuntimeError
+
+ # conjugation
+ conj = bond.GetIsConjugated()
+ feature[4] = conj
+
+ # ring
+ ring = bond.IsInRing()
+ feature[5] = ring
+
+ # stereo
+ stereo = bond.GetStereo()
+ stereo_list = [rdkit.Chem.rdchem.BondStereo.STEREONONE,
+ rdkit.Chem.rdchem.BondStereo.STEREOANY,
+ rdkit.Chem.rdchem.BondStereo.STEREOZ,
+ rdkit.Chem.rdchem.BondStereo.STEREOE]
+ if stereo in stereo_list:
+ loc = stereo_list.index(stereo)
+ feature[6 + loc] = 1
+ else:
+ print("Wrong stereo type of bond. Please check before featurization.")
+ raise RuntimeError
+
+ return feature
+
+
+def smile2graph4drugood(smile):
+ mol = Chem.MolFromSmiles(smile)
+ # if (mol is None):
+ # return None
+ src = []
+ dst = []
+ atom_feature = []
+ bond_feature = []
+
+ for atom in mol.GetAtoms():
+ one_atom_feature = get_atom_features(atom)
+ atom_feature.append(one_atom_feature)
+ atom_feature = np.array(atom_feature)
+ atom_feature = torch.tensor(atom_feature).float()
+
+ if len(mol.GetBonds()) > 0: # mol has bonds
+ for bond in mol.GetBonds():
+ i = bond.GetBeginAtomIdx()
+ j = bond.GetEndAtomIdx()
+ one_bond_feature = get_bond_features(bond)
+ src.append(i)
+ dst.append(j)
+ bond_feature.append(one_bond_feature)
+ src.append(j)
+ dst.append(i)
+ bond_feature.append(one_bond_feature)
+
+ src = torch.tensor(src).long()
+ dst = torch.tensor(dst).long()
+ bond_feature = np.array(bond_feature)
+ bond_feature = torch.tensor(bond_feature).float()
+ edge_index = torch.vstack([src, dst])
+ # graph_cur_smile = dgl.graph((src, dst), num_nodes=len(mol.GetAtoms()))
+ # graph_cur_smile.ndata['x'] = atom_feature
+ # graph_cur_smile.edata['x'] = bond_feature
+ else:
+ edge_index = torch.empty((2, 0)).long()
+ bond_feature = torch.empty((0, 10)).float()
+
+ return atom_feature, edge_index, bond_feature
+
+
+def featurize_atoms(mol):
+ feats = []
+ for atom in mol.GetAtoms():
+ feats.append(atom.GetAtomicNum())
+ return {'atomic': torch.tensor(feats).reshape(-1).to(torch.int64)}
+
+
+def featurize_bonds(mol):
+ feats = []
+ bond_types = [Chem.rdchem.BondType.SINGLE, Chem.rdchem.BondType.DOUBLE,
+ Chem.rdchem.BondType.TRIPLE, Chem.rdchem.BondType.AROMATIC]
+ for bond in mol.GetBonds():
+ btype = bond_types.index(bond.GetBondType())
+ # One bond between atom u and v corresponds to two edges (u, v) and (v, u)
+ feats.extend([btype, btype])
+ return {'type': torch.tensor(feats).reshape(-1).to(torch.int64)}
diff --git a/eval.py b/eval.py
new file mode 100644
index 0000000..61b552d
--- /dev/null
+++ b/eval.py
@@ -0,0 +1,121 @@
+import os
+import logging
+from tqdm import tqdm
+from munch import Munch, munchify
+
+import torch
+import torch.nn.functional as F
+from torch.utils.tensorboard import SummaryWriter
+from torch_geometric.loader import DataLoader
+import numpy as np
+
+from GOOD import register
+from GOOD.utils.config_reader import load_config
+from GOOD.utils.metric import Metric
+from GOOD.data.dataset_manager import read_meta_info
+from GOOD.utils.evaluation import eval_data_preprocess, eval_score
+from GOOD.utils.train import nan2zero_get_mask
+
+from args_parse import args_parser
+from exputils import initialize_exp, set_seed, get_dump_path, describe_model, save_model, load_model
+from models import MyModel
+from dataset import DrugOODDataset
+
+logger = logging.getLogger()
+
+
+class Runner:
+ def __init__(self, args, logger_path):
+ self.args = args
+ self.device = torch.device(f'cuda')
+
+ if args.dataset.startswith('GOOD'):
+ # for GOOD, load Config
+ cfg_path = os.path.join(args.config_path, args.dataset, args.domain, args.shift, 'base.yaml')
+ cfg, _, _ = load_config(path=cfg_path)
+ cfg = munchify(cfg)
+ cfg.device = self.device
+ dataset, meta_info = register.datasets[cfg.dataset.dataset_name].load(dataset_root=args.data_root,
+ domain=cfg.dataset.domain,
+ shift=cfg.dataset.shift_type,
+ generate=cfg.dataset.generate)
+ read_meta_info(meta_info, cfg)
+ # cfg.dropout
+ # cfg.bs
+ # update dropout & bs
+ cfg.model.dropout_rate = args.dropout
+ cfg.train.train_bs = args.bs
+ cfg.random_seed = args.random_seed
+
+ loader = register.dataloader[cfg.dataset.dataloader_name].setup(dataset, cfg)
+ self.train_loader = loader['train']
+ self.valid_loader = loader['val']
+ self.test_loader = loader['test']
+
+ self.metric = Metric()
+ self.metric.set_score_func(dataset['metric'] if type(dataset) is dict else getattr(dataset, 'metric'))
+ self.metric.set_loss_func(dataset['task'] if type(dataset) is dict else getattr(dataset, 'task'))
+ cfg.metric = self.metric
+ else:
+ # DrugOOD
+ dataset = DrugOODDataset(name=args.dataset, root=args.data_root)
+ self.train_set = dataset[dataset.train_index]
+ self.valid_set = dataset[dataset.valid_index]
+ self.test_set = dataset[dataset.test_index]
+
+ self.train_loader = DataLoader(self.train_set, batch_size=args.bs, shuffle=True, drop_last=True)
+ self.valid_loader = DataLoader(self.valid_set, batch_size=args.bs, shuffle=False)
+ self.test_loader = DataLoader(self.test_set, batch_size=args.bs, shuffle=False)
+ self.metric = Metric()
+ self.metric.set_loss_func(task_name='Binary classification')
+ self.metric.set_score_func(metric_name='ROC-AUC')
+ cfg = Munch()
+ cfg.metric = self.metric
+ cfg.model = Munch()
+ cfg.model.model_level = 'graph'
+
+ self.model = MyModel(args=args, config=cfg).to(self.device)
+ self.model.load_state_dict(load_model(args.load_path, map_location=self.device))
+ self.logger_path = logger_path
+
+ self.cfg = cfg
+
+
+ def run(self):
+ train_score = self.test_step(self.train_loader)
+ val_score = self.test_step(self.valid_loader)
+ test_score = self.test_step(self.test_loader)
+ logger.info(f"TRAIN={train_score:.5f}, VAL={val_score:.5f}, TEST={test_score:.5f}")
+
+
+ @torch.no_grad()
+ def test_step(self, loader):
+
+ self.model.eval()
+ y_pred, y_gt = [], []
+ for data in loader:
+ data = data.to(self.device)
+ logit, _, _, _, _ = self.model(data)
+ mask, _ = nan2zero_get_mask(data, 'None', self.cfg)
+ pred, target = eval_data_preprocess(data.y, logit, mask, self.cfg)
+ y_pred.append(pred)
+ y_gt.append(target)
+
+ score = eval_score(y_pred, y_gt, self.cfg)
+
+ return score
+
+def main():
+ args = args_parser()
+ torch.cuda.set_device(int(args.gpu))
+
+ logger = initialize_exp(args)
+ set_seed(args.random_seed)
+ logger_path = get_dump_path(args)
+
+ runner = Runner(args, logger_path)
+ runner.run()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/exputils.py b/exputils.py
new file mode 100644
index 0000000..4555e4e
--- /dev/null
+++ b/exputils.py
@@ -0,0 +1,176 @@
+import os
+import re
+import sys
+import time
+import json
+import torch
+import pickle
+import random
+import getpass
+import logging
+import argparse
+import subprocess
+import numpy as np
+from datetime import timedelta, date, datetime
+
+
+class LogFormatter:
+ def __init__(self):
+ self.start_time = time.time()
+
+ def format(self, record):
+ elapsed_seconds = round(record.created - self.start_time)
+
+ prefix = "%s - %s - %s" % (
+ record.levelname,
+ time.strftime('%x %X'),
+ timedelta(seconds=elapsed_seconds)
+ )
+ message = record.getMessage()
+ message = message.replace('\n', '\n' + ' ' * (len(prefix) + 3))
+ return "%s - %s" % (prefix, message) if message else ''
+
+
+def create_logger(filepath, rank):
+ """
+ Create a logger.
+ Use a different log file for each process.
+ """
+ # create log formatter
+ log_formatter = LogFormatter()
+
+ # create file handler and set level to debug
+ if filepath is not None:
+ if rank > 0:
+ filepath = '%s-%i' % (filepath, rank)
+ file_handler = logging.FileHandler(filepath, "a", encoding='utf-8')
+ file_handler.setLevel(logging.DEBUG)
+ file_handler.setFormatter(log_formatter)
+
+ # create console handler and set level to info
+ console_handler = logging.StreamHandler()
+ console_handler.setLevel(logging.INFO)
+ console_handler.setFormatter(log_formatter)
+
+ # create logger and set level to debug
+ logger = logging.getLogger()
+ logger.handlers = []
+ logger.setLevel(logging.DEBUG)
+ logger.propagate = False
+ if filepath is not None:
+ logger.addHandler(file_handler)
+ logger.addHandler(console_handler)
+
+ # reset logger elapsed time
+ def reset_time():
+ log_formatter.start_time = time.time()
+
+ logger.reset_time = reset_time
+
+ return logger
+
+
+def initialize_exp(params):
+ """
+ Initialize the experiment:
+ - dump parameters
+ - create a logger
+ """
+ # dump parameters
+ exp_folder = get_dump_path(params)
+ json.dump(vars(params), open(os.path.join(exp_folder, 'params.pkl'), 'w'), indent=4)
+
+ # get running command
+ command = ["python", sys.argv[0]]
+ for x in sys.argv[1:]:
+ if x.startswith('--'):
+ assert '"' not in x and "'" not in x
+ command.append(x)
+ else:
+ assert "'" not in x
+ if re.match('^[a-zA-Z0-9_]+$', x):
+ command.append("%s" % x)
+ else:
+ command.append("'%s'" % x)
+ command = ' '.join(command)
+ params.command = command + ' --exp_id "%s"' % params.exp_id
+
+ # check experiment name
+ assert len(params.exp_name.strip()) > 0
+
+ # create a logger
+ logger = create_logger(os.path.join(exp_folder, 'train.log'), rank=getattr(params, 'global_rank', 0))
+ logger.info("============ Initialized logger ============")
+ logger.info("\n".join("%s: %s" % (k, str(v))
+ for k, v in sorted(dict(vars(params)).items())))
+
+ logger.info("The experiment will be stored in %s\n" % exp_folder)
+ logger.info("Running command: %s" % command)
+ return logger
+
+
+def get_dump_path(params):
+ """
+ Create a directory to store the experiment.
+ """
+ assert len(params.exp_name) > 0
+ assert not params.dump_path in ('', None), \
+ 'Please choose your favorite destination for dump.'
+ dump_path = params.dump_path
+
+ # create the sweep path if it does not exist
+ when = date.today().strftime('%m%d-')
+ sweep_path = os.path.join(dump_path, when + params.exp_name)
+ if not os.path.exists(sweep_path):
+ subprocess.Popen("mkdir -p %s" % sweep_path, shell=True).wait()
+
+ # create an random ID for the job if it is not given in the parameters.
+ if params.exp_id == '':
+ # exp_id = time.strftime('%H-%M-%S')
+ exp_id = datetime.now().strftime('%H-%M-%S.%f')[:-3]
+ exp_id += ''.join(random.sample('abcdefghijklmnopqrstuvwxyz', 3))
+ # chars = 'abcdefghijklmnopqrstuvwxyz0123456789'
+ # while True:
+ # exp_id = ''.join(random.choice(chars) for _ in range(10))
+ # if not os.path.isdir(os.path.join(sweep_path, exp_id)):
+ # break
+ params.exp_id = exp_id
+
+ # create the dump folder / update parameters
+ exp_folder = os.path.join(sweep_path, params.exp_id)
+ if not os.path.isdir(exp_folder):
+ subprocess.Popen("mkdir -p %s" % exp_folder, shell=True).wait()
+ return exp_folder
+
+
+def describe_model(model, path, name='model'):
+ file_path = os.path.join(path, f'{name}.describe')
+ with open(file_path, 'w') as fout:
+ print(model, file=fout)
+
+
+def set_seed(seed):
+ """
+ Freeze every seed for reproducibility.
+ torch.cuda.manual_seed_all is useful when using random generation on GPUs.
+ e.g. torch.cuda.FloatTensor(100).uniform_()
+ """
+ random.seed(seed)
+ np.random.seed(seed)
+ torch.manual_seed(seed)
+ torch.cuda.manual_seed(seed)
+ torch.cuda.manual_seed_all(seed)
+
+
+def save_model(model, save_dir, epoch=None, model_name='model'):
+ model_to_save = model.module if hasattr(model, "module") else model
+ if epoch is None:
+ save_path = os.path.join(save_dir, f'{model_name}.pkl')
+ else:
+ save_path = os.path.join(save_dir, f'{model_name}-{epoch}.pkl')
+ os.makedirs(save_dir, exist_ok=True)
+ torch.save(model_to_save.state_dict(), save_path)
+
+
+def load_model(path, map_location):
+ return torch.load(path, map_location=map_location)
diff --git a/models/__init__.py b/models/__init__.py
new file mode 100644
index 0000000..7d58437
--- /dev/null
+++ b/models/__init__.py
@@ -0,0 +1 @@
+from .model import MyModel
diff --git a/models/gnnconv.py b/models/gnnconv.py
new file mode 100644
index 0000000..ddc88ea
--- /dev/null
+++ b/models/gnnconv.py
@@ -0,0 +1,356 @@
+import torch
+from torch import nn
+from torch_geometric.nn import MessagePassing
+import torch.nn.functional as F
+from torch_geometric.nn import global_mean_pool, global_add_pool
+from ogb.graphproppred.mol_encoder import AtomEncoder, BondEncoder
+from torch_geometric.utils import degree
+
+import math
+
+
+class MLP(nn.Module):
+ """MLP with linear output"""
+
+ def __init__(self, num_layers, input_dim, hidden_dim, output_dim):
+ """MLP layers construction
+
+ Paramters
+ ---------
+ num_layers: int
+ The number of linear layers
+ input_dim: int
+ The dimensionality of input features
+ hidden_dim: int
+ The dimensionality of hidden units at ALL layers
+ output_dim: int
+ The number of classes for prediction
+
+ """
+ super(MLP, self).__init__()
+ self.linear_or_not = True # default is linear model
+ self.num_layers = num_layers
+ self.output_dim = output_dim
+
+ if num_layers < 1:
+ raise ValueError("number of layers should be positive!")
+ elif num_layers == 1:
+ # Linear model
+ self.linear = nn.Linear(input_dim, output_dim)
+ else:
+ # Multi-layer model
+ self.linear_or_not = False
+ self.linears = torch.nn.ModuleList()
+ self.batch_norms = torch.nn.ModuleList()
+
+ self.linears.append(nn.Linear(input_dim, hidden_dim))
+ for _ in range(num_layers - 2):
+ self.linears.append(nn.Linear(hidden_dim, hidden_dim))
+ self.linears.append(nn.Linear(hidden_dim, output_dim))
+
+ for _ in range(num_layers - 1):
+ self.batch_norms.append(nn.BatchNorm1d(hidden_dim))
+
+ def forward(self, x):
+ if self.linear_or_not:
+ # If linear model
+ return self.linear(x)
+ else:
+ # If MLP
+ h = x
+ for i in range(self.num_layers - 1):
+ h = F.relu(self.batch_norms[i](self.linears[i](h)))
+ return self.linears[-1](h)
+
+
+# GIN convolution along the graph structure
+class GINConv(MessagePassing):
+ def __init__(self, emb_dim):
+ '''
+ emb_dim (int): node embedding dimensionality
+ '''
+
+ super(GINConv, self).__init__(aggr="add")
+
+ self.mlp = torch.nn.Sequential(
+ torch.nn.Linear(emb_dim, 2 * emb_dim),
+ torch.nn.BatchNorm1d(2 * emb_dim),
+ torch.nn.ReLU(),
+ torch.nn.Linear(2 * emb_dim, emb_dim)
+ )
+ self.eps = torch.nn.Parameter(torch.Tensor([0]))
+ # if datatype == 'ogb':
+ # self.bond_encoder = BondEncoder(emb_dim=emb_dim)
+ # else:
+ self.bond_encoder = MLP(num_layers=1, input_dim=10, output_dim=emb_dim, hidden_dim=emb_dim)
+
+ def forward(self, x, edge_index, edge_attr):
+ edge_embedding = self.bond_encoder(edge_attr)
+ out = self.mlp(
+ (1 + self.eps) * x +
+ self.propagate(edge_index, x=x, edge_attr=edge_embedding)
+ )
+
+ return out
+
+ def message(self, x_j, edge_attr):
+ return F.relu(x_j + edge_attr)
+
+ def update(self, aggr_out):
+ return aggr_out
+
+
+# GCN convolution along the graph structure
+
+
+class GCNConv(MessagePassing):
+ def __init__(self, emb_dim):
+ super(GCNConv, self).__init__(aggr='add')
+
+ self.linear = torch.nn.Linear(emb_dim, emb_dim)
+ self.root_emb = torch.nn.Embedding(1, emb_dim)
+ # if datatype == 'ogb':
+ # self.bond_encoder = BondEncoder(emb_dim=emb_dim)
+ # else:
+ self.bond_encoder = MLP(num_layers=1, input_dim=10, output_dim=emb_dim, hidden_dim=emb_dim)
+
+ def forward(self, x, edge_index, edge_attr):
+ x = self.linear(x)
+ edge_embedding = self.bond_encoder(edge_attr)
+
+ row, col = edge_index
+
+ # edge_weight = torch.ones((edge_index.size(1), ), device=edge_index.device)
+ deg = degree(row, x.size(0), dtype=x.dtype) + 1
+ deg_inv_sqrt = deg.pow(-0.5)
+ deg_inv_sqrt[deg_inv_sqrt == float('inf')] = 0
+
+ norm = deg_inv_sqrt[row] * deg_inv_sqrt[col]
+
+ return self.propagate(
+ edge_index, x=x, edge_attr=edge_embedding, norm=norm
+ ) + F.relu(x + self.root_emb.weight) * 1. / deg.view(-1, 1)
+
+ def message(self, x_j, edge_attr, norm):
+ return norm.view(-1, 1) * F.relu(x_j + edge_attr)
+
+ def update(self, aggr_out):
+ return aggr_out
+
+
+# GNN to generate node embedding
+class GNN_node(torch.nn.Module):
+ """
+ Output:
+ node representations
+ """
+
+ def __init__(
+ self, num_layer, emb_dim, drop_ratio=0.5,
+ JK="last", residual=False, gnn_type='gin'
+ ):
+ '''
+ emb_dim (int): node embedding dimensionality
+ num_layer (int): number of GNN message passing layers
+
+ '''
+
+ super(GNN_node, self).__init__()
+ self.num_layer = num_layer
+ self.drop_ratio = drop_ratio
+ self.JK = JK
+ # add residual connection or not
+ self.residual = residual
+
+ # if self.num_layer < 2:
+ # raise ValueError("Number of GNN layers must be greater than 1.")
+ # if datatype == 'ogb':
+ # self.atom_encoder = AtomEncoder(emb_dim)
+ # else:
+ self.atom_encoder = MLP(input_dim=39, hidden_dim=emb_dim, output_dim=emb_dim, num_layers=2)
+
+ # List of GNNs
+ self.convs = torch.nn.ModuleList()
+ self.batch_norms = torch.nn.ModuleList()
+
+ for layer in range(num_layer):
+ if gnn_type == 'gin':
+ self.convs.append(GINConv(emb_dim))
+ elif gnn_type == 'gcn':
+ self.convs.append(GCNConv(emb_dim))
+ else:
+ raise ValueError(
+ 'Undefined GNN type called {}'.format(gnn_type))
+
+ self.batch_norms.append(torch.nn.BatchNorm1d(emb_dim))
+
+ def forward(self, *argv):
+
+ # computing input node embedding
+ if len(argv) == 4:
+ x, edge_index, edge_attr, batch = argv[0], argv[1], argv[2], argv[3]
+ elif len(argv) == 1:
+ batched_data = argv[0]
+ x, edge_index = batched_data.x, batched_data.edge_index
+ edge_attr, batch = batched_data.edge_attr, batched_data.batch
+ else:
+ raise ValueError("unmatched number of arguments.")
+
+ h_list = [self.atom_encoder(x)]
+ for layer in range(self.num_layer):
+
+ h = self.convs[layer](h_list[layer], edge_index, edge_attr)
+ h = self.batch_norms[layer](h)
+
+ if layer == self.num_layer - 1:
+ # remove relu for the last layer
+ h = F.dropout(h, self.drop_ratio, training=self.training)
+ # h = h
+ else:
+ h = F.dropout(
+ F.relu(h), self.drop_ratio, training=self.training
+ )
+
+ if self.residual:
+ h += h_list[layer]
+
+ h_list.append(h)
+
+ # Different implementations of Jk-concat
+ if self.JK == "last":
+ node_representation = h_list[-1]
+ elif self.JK == "sum":
+ node_representation = 0
+ for layer in range(self.num_layer + 1):
+ node_representation += h_list[layer]
+
+ return node_representation
+
+
+# Virtual GNN to generate node embedding
+class GNN_node_Virtualnode(torch.nn.Module):
+ """
+ Output:
+ node representations
+ """
+
+ def __init__(
+ self, num_layer, emb_dim, drop_ratio=0.5,
+ JK="last", residual=False, gnn_type='gin'
+ ):
+ '''
+ emb_dim (int): node embedding dimensionality
+ '''
+
+ super(GNN_node_Virtualnode, self).__init__()
+ self.num_layer = num_layer
+ self.drop_ratio = drop_ratio
+ self.JK = JK
+ # add residual connection or not
+ self.residual = residual
+
+ if self.num_layer < 2:
+ raise ValueError("Number of GNN layers must be greater than 1.")
+
+ # self.atom_encoder = AtomEncoder(emb_dim)
+ # if datatype == 'ogb':
+ # self.atom_encoder = AtomEncoder(emb_dim)
+ # else:
+ self.atom_encoder = MLP(input_dim=39, hidden_dim=emb_dim, output_dim=emb_dim, num_layers=2)
+
+
+ # set the initial virtual node embedding to 0.
+ self.virtualnode_embedding = torch.nn.Embedding(1, emb_dim)
+ torch.nn.init.constant_(self.virtualnode_embedding.weight.data, 0)
+
+ # List of GNNs
+ self.convs = torch.nn.ModuleList()
+ # batch norms applied to node embeddings
+ self.batch_norms = torch.nn.ModuleList()
+
+ # List of MLPs to transform virtual node at every layer
+ self.mlp_virtualnode_list = torch.nn.ModuleList()
+
+ for layer in range(num_layer):
+ if gnn_type == 'gin':
+ self.convs.append(GINConv(emb_dim))
+ elif gnn_type == 'gcn':
+ self.convs.append(GCNConv(emb_dim))
+ else:
+ raise ValueError(f'Undefined GNN type called {gnn_type}')
+
+ self.batch_norms.append(torch.nn.BatchNorm1d(emb_dim))
+
+ for layer in range(num_layer - 1):
+ self.mlp_virtualnode_list.append(torch.nn.Sequential(
+ torch.nn.Linear(emb_dim, 2 * emb_dim),
+ torch.nn.BatchNorm1d(2 * emb_dim),
+ torch.nn.ReLU(),
+ torch.nn.Linear(2 * emb_dim, emb_dim),
+ torch.nn.BatchNorm1d(emb_dim),
+ torch.nn.ReLU()
+ ))
+
+ def forward(self, batched_data):
+
+ x, edge_index = batched_data.x, batched_data.edge_index
+ edge_attr, batch = batched_data.edge_attr, batched_data.batch
+ # virtual node embeddings for graphs
+ virtualnode_embedding = self.virtualnode_embedding(torch.zeros(
+ batch[-1].item() + 1).to(edge_index.dtype).to(edge_index.device))
+
+ h_list = [self.atom_encoder(x)]
+ for layer in range(self.num_layer):
+ # add message from virtual nodes to graph nodes
+ h_list[layer] = h_list[layer] + virtualnode_embedding[batch]
+
+ # Message passing among graph nodes
+ h = self.convs[layer](h_list[layer], edge_index, edge_attr)
+
+ h = self.batch_norms[layer](h)
+ if layer == self.num_layer - 1:
+ # remove relu for the last layer
+ h = F.dropout(h, self.drop_ratio, training=self.training)
+ else:
+ h = F.dropout(
+ F.relu(h), self.drop_ratio, training=self.training
+ )
+
+ if self.residual:
+ h = h + h_list[layer]
+
+ h_list.append(h)
+
+ # update the virtual nodes
+ if layer < self.num_layer - 1:
+ # add message from graph nodes to virtual nodes
+ virtualnode_embedding_temp = global_add_pool(
+ h_list[layer], batch) + virtualnode_embedding
+ # transform virtual nodes using MLP
+
+ if self.residual:
+ virtualnode_embedding = virtualnode_embedding + F.dropout(
+ self.mlp_virtualnode_list[layer](
+ virtualnode_embedding_temp
+ ), self.drop_ratio, training=self.training
+ )
+ else:
+ virtualnode_embedding = F.dropout(
+ self.mlp_virtualnode_list[layer](
+ virtualnode_embedding_temp
+ ), self.drop_ratio, training=self.training
+ )
+
+ # Different implementations of Jk-concat
+ if self.JK == "last":
+ node_representation = h_list[-1]
+ elif self.JK == "sum":
+ node_representation = 0
+ for layer in range(self.num_layer + 1):
+ node_representation += h_list[layer]
+
+ return node_representation
+
+
+if __name__ == "__main__":
+ pass
diff --git a/models/model.py b/models/model.py
new file mode 100644
index 0000000..815d127
--- /dev/null
+++ b/models/model.py
@@ -0,0 +1,147 @@
+import torch
+from torch import nn
+import torch.nn.functional as F
+from torch_geometric.nn import global_mean_pool
+from torch_scatter import scatter_add, scatter_mean
+import numpy as np
+
+from GOOD.networks.models.GINs import GINFeatExtractor
+from GOOD.networks.models.GINvirtualnode import vGINFeatExtractor
+
+from vector_quantize_pytorch import VectorQuantize
+# from .vq_update import VectorQuantize
+
+from .gnnconv import GNN_node
+
+
+class Separator(nn.Module):
+ def __init__(self, args, config):
+ super(Separator, self).__init__()
+ if args.dataset.startswith('GOOD'):
+ # GOOD
+ if config.model.model_name == 'GIN':
+ self.r_gnn = GINFeatExtractor(config, without_readout=True)
+ else:
+ self.r_gnn = vGINFeatExtractor(config, without_readout=True)
+ emb_d = config.model.dim_hidden
+ else:
+ self.r_gnn = GNN_node(num_layer=args.layer, emb_dim=args.emb_dim,
+ drop_ratio=args.dropout, gnn_type=args.gnn_type)
+ emb_d = args.emb_dim
+
+ self.separator = nn.Sequential(nn.Linear(emb_d, emb_d * 2),
+ nn.BatchNorm1d(emb_d * 2),
+ nn.ReLU(),
+ nn.Linear(emb_d * 2, emb_d),
+ nn.Sigmoid())
+ self.args = args
+
+ def forward(self, data):
+ if self.args.dataset.startswith('GOOD'):
+ # DrugOOD
+ node_feat = self.r_gnn(data=data)
+ else:
+ # GOOD
+ node_feat = self.r_gnn(data)
+ score = self.separator(node_feat) # [n, d]
+
+ # reg on score
+
+ pos_score_on_node = score.mean(1) # [n]
+ pos_score_on_batch = scatter_add(pos_score_on_node, data.batch, dim=0) # [B]
+ neg_score_on_batch = scatter_add((1 - pos_score_on_node), data.batch, dim=0) # [B]
+ return score, pos_score_on_batch + 1e-8, neg_score_on_batch + 1e-8
+
+
+class DiscreteEncoder(nn.Module):
+ def __init__(self, args, config):
+ super(DiscreteEncoder, self).__init__()
+ self.args = args
+ self.config = config
+ if args.dataset.startswith('GOOD'):
+ emb_dim = config.model.dim_hidden
+ if config.model.model_name == 'GIN':
+ self.gnn = GINFeatExtractor(config, without_readout=True)
+ else:
+ self.gnn = vGINFeatExtractor(config, without_readout=True)
+ self.classifier = nn.Sequential(*(
+ [nn.Linear(emb_dim, config.dataset.num_classes)]
+ ))
+ else:
+ emb_dim = args.emb_dim
+ self.gnn = GNN_node(num_layer=args.layer, emb_dim=args.emb_dim,
+ drop_ratio=args.dropout, gnn_type=args.gnn_type)
+ self.classifier = nn.Sequential(nn.Linear(emb_dim, emb_dim * 2),
+ nn.BatchNorm1d(emb_dim * 2),
+ nn.ReLU(),
+ nn.Dropout(),
+ nn.Linear(emb_dim * 2, 1))
+
+ self.pool = global_mean_pool
+
+ self.vq = VectorQuantize(dim=emb_dim,
+ codebook_size=args.num_e,
+ commitment_weight=args.commitment_weight,
+ decay=0.9)
+
+ self.mix_proj = nn.Sequential(nn.Linear(emb_dim * 2, emb_dim),
+ nn.BatchNorm1d(emb_dim),
+ nn.ReLU(),
+ nn.Dropout(),
+ nn.Linear(emb_dim, emb_dim))
+
+ self.simsiam_proj = nn.Sequential(nn.Linear(emb_dim, emb_dim * 2),
+ nn.BatchNorm1d(emb_dim * 2),
+ nn.ReLU(),
+ nn.Linear(emb_dim * 2, emb_dim))
+
+ def vector_quantize(self, f, vq_model):
+ v_f, indices, v_loss = vq_model(f)
+
+ return v_f, v_loss
+
+ def forward(self, data, score):
+ if self.args.dataset.startswith('GOOD'):
+ # DrugOOD
+ node_feat = self.gnn(data=data)
+ else:
+ # GOOD
+ node_feat = self.gnn(data)
+
+ node_v_feat, cmt_loss = self.vector_quantize(node_feat.unsqueeze(0), self.vq)
+ node_v_feat = node_v_feat.squeeze(0)
+ node_res_feat = node_feat + node_v_feat
+ c_node_feat = node_res_feat * score
+ s_node_feat = node_res_feat * (1 - score)
+
+ c_graph_feat = self.pool(c_node_feat, data.batch)
+ s_graph_feat = self.pool(s_node_feat, data.batch)
+
+ c_logit = self.classifier(c_graph_feat)
+
+ return c_logit, c_graph_feat, s_graph_feat, cmt_loss
+
+
+class MyModel(nn.Module):
+ def __init__(self, args, config):
+ super(MyModel, self).__init__()
+ self.args = args
+ self.config = config
+
+ self.separator = Separator(args, config)
+ self.encoder = DiscreteEncoder(args, config)
+
+ def forward(self, data):
+ score, pos_score, neg_score = self.separator(data)
+ c_logit, c_graph_feat, s_graph_feat, cmt_loss = self.encoder(data, score)
+ # reg on score
+ loss_reg = torch.abs(pos_score / (pos_score + neg_score) - self.args.gamma * torch.ones_like(pos_score)).mean()
+ return c_logit, c_graph_feat, s_graph_feat, cmt_loss, loss_reg
+
+ def mix_cs_proj(self, c_f: torch.Tensor, s_f: torch.Tensor):
+ n = c_f.size(0)
+ perm = np.random.permutation(n)
+ mix_f = torch.cat([c_f, s_f[perm]], dim=-1)
+ proj_mix_f = self.encoder.mix_proj(mix_f)
+ return proj_mix_f
+
diff --git a/resources/framework.png b/resources/framework.png
new file mode 100644
index 0000000000000000000000000000000000000000..29401399a13a06e52806f9b611e78c74c1af29f2
GIT binary patch
literal 289412
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 h
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