From ed2ca6cf1715ddec65a337201676f733aa4b3475 Mon Sep 17 00:00:00 2001 From: schnamo Date: Wed, 5 Jun 2024 15:09:54 +0200 Subject: [PATCH 01/54] create new class for solubility data --- chebai/preprocessing/datasets/solCuration.py | 290 +++++++++++++++++++ 1 file changed, 290 insertions(+) create mode 100644 chebai/preprocessing/datasets/solCuration.py diff --git a/chebai/preprocessing/datasets/solCuration.py b/chebai/preprocessing/datasets/solCuration.py new file mode 100644 index 00000000..ebbcad27 --- /dev/null +++ b/chebai/preprocessing/datasets/solCuration.py @@ -0,0 +1,290 @@ +from tempfile import NamedTemporaryFile, TemporaryDirectory +from urllib import request +import csv +import gzip +import os +import random +import shutil +import zipfile + +from rdkit import Chem +from sklearn.model_selection import GroupShuffleSplit, train_test_split +import numpy as np +import pysmiles +import torch + +from chebai.preprocessing import reader as dr +from chebai.preprocessing.datasets.base import MergedDataset, XYBaseDataModule +from chebai.preprocessing.datasets.chebi import JCIExtendedTokenData +from chebai.preprocessing.datasets.pubchem import Hazardous + + +class SolCuration(XYBaseDataModule): + HEADERS = [ + "NR-AR", + "NR-AR-LBD", + "NR-AhR", + "NR-Aromatase", + "NR-ER", + "NR-ER-LBD", + "NR-PPAR-gamma", + "SR-ARE", + "SR-ATAD5", + "SR-HSE", + "SR-MMP", + "SR-p53", + ] + + @property + def _name(self): + return "SolCuration" + + @property + def label_number(self): + return 12 + + @property + def raw_file_names(self): + return ["solCuration.csv"] + + @property + def processed_file_names(self): + return ["test.pt", "train.pt", "validation.pt"] + + def download(self): + with NamedTemporaryFile("rb") as gout: + request.urlretrieve( + "https://deepchemdata.s3-us-west-1.amazonaws.com/datasets/tox21.csv.gz", + gout.name, + ) + with gzip.open(gout.name) as gfile: + with open(os.path.join(self.raw_dir, "tox21.csv"), "wt") as fout: + fout.write(gfile.read().decode()) + + def setup_processed(self): + print("Create splits") + data = self._load_data_from_file(os.path.join(self.raw_dir, f"tox21.csv")) + groups = np.array([d["group"] for d in data]) + if not all(g is None for g in groups): + split_size = int(len(set(groups)) * self.train_split) + os.makedirs(self.processed_dir, exist_ok=True) + splitter = GroupShuffleSplit(train_size=split_size, n_splits=1) + + train_split_index, temp_split_index = next( + splitter.split(data, groups=groups) + ) + + split_groups = groups[temp_split_index] + + splitter = GroupShuffleSplit( + train_size=int(len(set(split_groups)) * self.train_split), n_splits=1 + ) + test_split_index, validation_split_index = next( + splitter.split(temp_split_index, groups=split_groups) + ) + train_split = [data[i] for i in train_split_index] + test_split = [ + d + for d in (data[temp_split_index[i]] for i in test_split_index) + if d["original"] + ] + validation_split = [ + d + for d in (data[temp_split_index[i]] for i in validation_split_index) + if d["original"] + ] + else: + train_split, test_split = train_test_split( + data, train_size=self.train_split, shuffle=True + ) + test_split, validation_split = train_test_split( + test_split, train_size=0.5, shuffle=True + ) + for k, split in [ + ("test", test_split), + ("train", train_split), + ("validation", validation_split), + ]: + print("transform", k) + torch.save( + split, + os.path.join(self.processed_dir, f"{k}.pt"), + ) + + def setup(self, **kwargs): + if any( + not os.path.isfile(os.path.join(self.raw_dir, f)) + for f in self.raw_file_names + ): + self.download() + if any( + not os.path.isfile(os.path.join(self.processed_dir, f)) + for f in self.processed_file_names + ): + self.setup_processed() + + def _load_dict(self, input_file_path): + with open(input_file_path, "r") as input_file: + reader = csv.DictReader(input_file) + for row in reader: + smiles = row["smiles"] + labels = [ + bool(int(l)) if l else None for l in (row[k] for k in self.HEADERS) + ] + yield dict(features=smiles, labels=labels, ident=row["mol_id"]) + + +class Tox21Challenge(XYBaseDataModule): + HEADERS = [ + "NR-AR", + "NR-AR-LBD", + "NR-AhR", + "NR-Aromatase", + "NR-ER", + "NR-ER-LBD", + "NR-PPAR-gamma", + "SR-ARE", + "SR-ATAD5", + "SR-HSE", + "SR-MMP", + "SR-p53", + ] + + @property + def _name(self): + return "Tox21Chal" + + @property + def label_number(self): + return 12 + + @property + def raw_file_names(self): + return [ + "train.sdf", + "validation.sdf", + "validation.smiles", + "test.smiles", + "test_results.txt", + ] + + @property + def processed_file_names(self): + return ["test.pt", "train.pt", "validation.pt"] + + def download(self): + self._retrieve_file( + "https://tripod.nih.gov/tox21/challenge/download?id=tox21_10k_data_allsdf&sec=", + "train.sdf", + compression="zip", + ) + self._retrieve_file( + "https://tripod.nih.gov/tox21/challenge/download?id=tox21_10k_challenge_testsdf&sec=", + "validation.sdf", + compression="zip", + ) + self._retrieve_file( + "https://tripod.nih.gov/tox21/challenge/download?id=tox21_10k_challenge_scoresmiles&sec=", + "test.smiles", + ) + self._retrieve_file( + "https://tripod.nih.gov/tox21/challenge/download?id=tox21_10k_challenge_scoretxt&sec=", + "test_results.txt", + ) + + def _retrieve_file(self, url, target_file, compression=None): + target_path = os.path.join(self.raw_dir, target_file) + if not os.path.isfile(target_path): + with NamedTemporaryFile("rb") as gout: + if compression is None: + download_path = target_path + else: + download_path = gout.name + request.urlretrieve( + url, + download_path, + ) + if compression == "zip": + td = TemporaryDirectory() + with zipfile.ZipFile(download_path, "r") as zip_ref: + zip_ref.extractall(td.name) + files_in_zip = os.listdir(td.name) + f = files_in_zip[0] + assert len(files_in_zip) == 1 + shutil.move(os.path.join(td.name, f), target_path) + + def _load_data_from_file(self, path): + sdf = Chem.SDMolSupplier(path) + data = [] + for mol in sdf: + if mol is not None: + d = dict( + labels=[ + int(mol.GetProp(h)) if h in mol.GetPropNames() else None + for h in self.HEADERS + ], + ident=[ + mol.GetProp(k) + for k in ("DSSTox_CID", "Compound ID") + if k in mol.GetPropNames() + ][0], + features=Chem.MolToSmiles(mol), + ) + data.append(self.reader.to_data(d)) + return data + + def setup_processed(self): + for k in ("train", "validation"): + d = self._load_data_from_file(os.path.join(self.raw_dir, f"{k}.sdf")) + torch.save(d, os.path.join(self.processed_dir, f"{k}.pt")) + + with open(os.path.join(self.raw_dir, f"test.smiles")) as fin: + next(fin) + test_smiles = dict(reversed(row.strip().split("\t")) for row in fin) + with open(os.path.join(self.raw_dir, f"test_results.txt")) as fin: + headers = next(fin).strip().split("\t") + test_results = { + k["Sample ID"]: [ + int(k[h]) if k[h] != "x" else None for h in self.HEADERS + ] + for k in ( + dict(zip(headers, row.strip().split("\t"))) for row in fin if row + ) + } + test_data = [ + self.reader.to_data( + dict(features=test_smiles[k], labels=test_results[k], ident=k) + ) + for k in test_smiles + ] + torch.save(test_data, os.path.join(self.processed_dir, f"test.pt")) + + def setup(self, **kwargs): + if any( + not os.path.isfile(os.path.join(self.raw_dir, f)) + for f in self.raw_file_names + ): + self.download() + if any( + not os.path.isfile(os.path.join(self.processed_dir, f)) + for f in self.processed_file_names + ): + self.setup_processed() + + def _load_dict(self, input_file_path): + with open(input_file_path, "r") as input_file: + reader = csv.DictReader(input_file) + for row in reader: + smiles = row["smiles"] + labels = [ + bool(int(l)) if l else None for l in (row[k] for k in self.HEADERS) + ] + yield dict(features=smiles, labels=labels, ident=row["mol_id"]) + + +class Tox21ChallengeChem(Tox21Challenge): + READER = dr.ChemDataReader + + +class Tox21MolNetChem(Tox21MolNet): + READER = dr.ChemDataReader From ead30072eed9c2c1a17bc8f7ac5dd79c0b90c476 Mon Sep 17 00:00:00 2001 From: schnamo Date: Wed, 5 Jun 2024 18:01:17 +0200 Subject: [PATCH 02/54] adjusting new class --- chebai/preprocessing/datasets/solCuration.py | 188 ++----------------- 1 file changed, 13 insertions(+), 175 deletions(-) diff --git a/chebai/preprocessing/datasets/solCuration.py b/chebai/preprocessing/datasets/solCuration.py index ebbcad27..d85e9af9 100644 --- a/chebai/preprocessing/datasets/solCuration.py +++ b/chebai/preprocessing/datasets/solCuration.py @@ -21,18 +21,7 @@ class SolCuration(XYBaseDataModule): HEADERS = [ - "NR-AR", - "NR-AR-LBD", - "NR-AhR", - "NR-Aromatase", - "NR-ER", - "NR-ER-LBD", - "NR-PPAR-gamma", - "SR-ARE", - "SR-ATAD5", - "SR-HSE", - "SR-MMP", - "SR-p53", + "logS", ] @property @@ -53,17 +42,18 @@ def processed_file_names(self): def download(self): with NamedTemporaryFile("rb") as gout: + # start with downloading just one part of the dataset, later add the remaining ones request.urlretrieve( - "https://deepchemdata.s3-us-west-1.amazonaws.com/datasets/tox21.csv.gz", + "https://github.com/Mengjintao/SolCuration/blob/master/cure/esol_cure.csv", gout.name, ) - with gzip.open(gout.name) as gfile: - with open(os.path.join(self.raw_dir, "tox21.csv"), "wt") as fout: - fout.write(gfile.read().decode()) + with gout.name as gfile: + with open(os.path.join(self.raw_dir, "solCuration.csv"), "wt") as fout: + fout.write(gfile.read()) def setup_processed(self): print("Create splits") - data = self._load_data_from_file(os.path.join(self.raw_dir, f"tox21.csv")) + data = self._load_data_from_file(os.path.join(self.raw_dir, f"solCuration.csv")) groups = np.array([d["group"] for d in data]) if not all(g is None for g in groups): split_size = int(len(set(groups)) * self.train_split) @@ -124,167 +114,15 @@ def setup(self, **kwargs): self.setup_processed() def _load_dict(self, input_file_path): + i = 0 with open(input_file_path, "r") as input_file: reader = csv.DictReader(input_file) for row in reader: smiles = row["smiles"] - labels = [ - bool(int(l)) if l else None for l in (row[k] for k in self.HEADERS) - ] - yield dict(features=smiles, labels=labels, ident=row["mol_id"]) + labels = row["logS"] + # dataset has no mol_id TODO + yield dict(features=smiles, labels=labels, ident=i) #, ident=row["mol_id"] + i += 1 - -class Tox21Challenge(XYBaseDataModule): - HEADERS = [ - "NR-AR", - "NR-AR-LBD", - "NR-AhR", - "NR-Aromatase", - "NR-ER", - "NR-ER-LBD", - "NR-PPAR-gamma", - "SR-ARE", - "SR-ATAD5", - "SR-HSE", - "SR-MMP", - "SR-p53", - ] - - @property - def _name(self): - return "Tox21Chal" - - @property - def label_number(self): - return 12 - - @property - def raw_file_names(self): - return [ - "train.sdf", - "validation.sdf", - "validation.smiles", - "test.smiles", - "test_results.txt", - ] - - @property - def processed_file_names(self): - return ["test.pt", "train.pt", "validation.pt"] - - def download(self): - self._retrieve_file( - "https://tripod.nih.gov/tox21/challenge/download?id=tox21_10k_data_allsdf&sec=", - "train.sdf", - compression="zip", - ) - self._retrieve_file( - "https://tripod.nih.gov/tox21/challenge/download?id=tox21_10k_challenge_testsdf&sec=", - "validation.sdf", - compression="zip", - ) - self._retrieve_file( - "https://tripod.nih.gov/tox21/challenge/download?id=tox21_10k_challenge_scoresmiles&sec=", - "test.smiles", - ) - self._retrieve_file( - "https://tripod.nih.gov/tox21/challenge/download?id=tox21_10k_challenge_scoretxt&sec=", - "test_results.txt", - ) - - def _retrieve_file(self, url, target_file, compression=None): - target_path = os.path.join(self.raw_dir, target_file) - if not os.path.isfile(target_path): - with NamedTemporaryFile("rb") as gout: - if compression is None: - download_path = target_path - else: - download_path = gout.name - request.urlretrieve( - url, - download_path, - ) - if compression == "zip": - td = TemporaryDirectory() - with zipfile.ZipFile(download_path, "r") as zip_ref: - zip_ref.extractall(td.name) - files_in_zip = os.listdir(td.name) - f = files_in_zip[0] - assert len(files_in_zip) == 1 - shutil.move(os.path.join(td.name, f), target_path) - - def _load_data_from_file(self, path): - sdf = Chem.SDMolSupplier(path) - data = [] - for mol in sdf: - if mol is not None: - d = dict( - labels=[ - int(mol.GetProp(h)) if h in mol.GetPropNames() else None - for h in self.HEADERS - ], - ident=[ - mol.GetProp(k) - for k in ("DSSTox_CID", "Compound ID") - if k in mol.GetPropNames() - ][0], - features=Chem.MolToSmiles(mol), - ) - data.append(self.reader.to_data(d)) - return data - - def setup_processed(self): - for k in ("train", "validation"): - d = self._load_data_from_file(os.path.join(self.raw_dir, f"{k}.sdf")) - torch.save(d, os.path.join(self.processed_dir, f"{k}.pt")) - - with open(os.path.join(self.raw_dir, f"test.smiles")) as fin: - next(fin) - test_smiles = dict(reversed(row.strip().split("\t")) for row in fin) - with open(os.path.join(self.raw_dir, f"test_results.txt")) as fin: - headers = next(fin).strip().split("\t") - test_results = { - k["Sample ID"]: [ - int(k[h]) if k[h] != "x" else None for h in self.HEADERS - ] - for k in ( - dict(zip(headers, row.strip().split("\t"))) for row in fin if row - ) - } - test_data = [ - self.reader.to_data( - dict(features=test_smiles[k], labels=test_results[k], ident=k) - ) - for k in test_smiles - ] - torch.save(test_data, os.path.join(self.processed_dir, f"test.pt")) - - def setup(self, **kwargs): - if any( - not os.path.isfile(os.path.join(self.raw_dir, f)) - for f in self.raw_file_names - ): - self.download() - if any( - not os.path.isfile(os.path.join(self.processed_dir, f)) - for f in self.processed_file_names - ): - self.setup_processed() - - def _load_dict(self, input_file_path): - with open(input_file_path, "r") as input_file: - reader = csv.DictReader(input_file) - for row in reader: - smiles = row["smiles"] - labels = [ - bool(int(l)) if l else None for l in (row[k] for k in self.HEADERS) - ] - yield dict(features=smiles, labels=labels, ident=row["mol_id"]) - - -class Tox21ChallengeChem(Tox21Challenge): - READER = dr.ChemDataReader - - -class Tox21MolNetChem(Tox21MolNet): +class SolubilityCuratedData(SolCuration): READER = dr.ChemDataReader From 5956183d40c5c738fa5052401ea32fcf21ffde98 Mon Sep 17 00:00:00 2001 From: schnamo Date: Thu, 6 Jun 2024 15:06:24 +0200 Subject: [PATCH 03/54] add solubility yml file --- configs/data/solubilityCuration.yml | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 configs/data/solubilityCuration.yml diff --git a/configs/data/solubilityCuration.yml b/configs/data/solubilityCuration.yml new file mode 100644 index 00000000..aee416de --- /dev/null +++ b/configs/data/solubilityCuration.yml @@ -0,0 +1,3 @@ +class_path: chebai.preprocessing.datasets.solCuration.SolubilityCuratedData +init_args: + batch_size: 10 From c3afeeda1488bc63aead42abffa18b67044a8b24 Mon Sep 17 00:00:00 2001 From: schnamo Date: Fri, 7 Jun 2024 23:06:56 +0200 Subject: [PATCH 04/54] adjusting solubility class to correctly download solubility data --- chebai/preprocessing/datasets/solCuration.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/chebai/preprocessing/datasets/solCuration.py b/chebai/preprocessing/datasets/solCuration.py index d85e9af9..b6a694ce 100644 --- a/chebai/preprocessing/datasets/solCuration.py +++ b/chebai/preprocessing/datasets/solCuration.py @@ -41,15 +41,12 @@ def processed_file_names(self): return ["test.pt", "train.pt", "validation.pt"] def download(self): - with NamedTemporaryFile("rb") as gout: # start with downloading just one part of the dataset, later add the remaining ones - request.urlretrieve( - "https://github.com/Mengjintao/SolCuration/blob/master/cure/esol_cure.csv", - gout.name, - ) - with gout.name as gfile: - with open(os.path.join(self.raw_dir, "solCuration.csv"), "wt") as fout: - fout.write(gfile.read()) + with request.urlopen( + "https://raw.githubusercontent.com/Mengjintao/SolCuration/master/cure/esol_cure.csv", + ) as src: + with open(os.path.join(self.raw_dir, "solCuration.csv"), "wb") as dst: + shutil.copyfileobj(src, dst) def setup_processed(self): print("Create splits") From d57b073ab271ce1fbe131a383c8802b655fa876e Mon Sep 17 00:00:00 2001 From: schnamo Date: Fri, 7 Jun 2024 23:54:09 +0200 Subject: [PATCH 05/54] make it compatible with classification problem --- chebai/preprocessing/datasets/solCuration.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/chebai/preprocessing/datasets/solCuration.py b/chebai/preprocessing/datasets/solCuration.py index b6a694ce..108d5022 100644 --- a/chebai/preprocessing/datasets/solCuration.py +++ b/chebai/preprocessing/datasets/solCuration.py @@ -30,7 +30,7 @@ def _name(self): @property def label_number(self): - return 12 + return 2 @property def raw_file_names(self): @@ -116,7 +116,11 @@ def _load_dict(self, input_file_path): reader = csv.DictReader(input_file) for row in reader: smiles = row["smiles"] - labels = row["logS"] + test = float(row["logS"]) + if test > -1: + labels = [0,1] + else: + labels = [1,0] # dataset has no mol_id TODO yield dict(features=smiles, labels=labels, ident=i) #, ident=row["mol_id"] i += 1 From 0faca3107cffc1edc6a2423e15f92fb88341eef7 Mon Sep 17 00:00:00 2001 From: schnamo Date: Fri, 5 Jul 2024 00:07:08 +0200 Subject: [PATCH 06/54] onehotencoding for solubility labels --- chebai/preprocessing/datasets/solCuration.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/chebai/preprocessing/datasets/solCuration.py b/chebai/preprocessing/datasets/solCuration.py index 108d5022..64c59ce2 100644 --- a/chebai/preprocessing/datasets/solCuration.py +++ b/chebai/preprocessing/datasets/solCuration.py @@ -12,6 +12,7 @@ import numpy as np import pysmiles import torch +from sklearn.preprocessing import LabelBinarizer from chebai.preprocessing import reader as dr from chebai.preprocessing.datasets.base import MergedDataset, XYBaseDataModule @@ -111,19 +112,20 @@ def setup(self, **kwargs): self.setup_processed() def _load_dict(self, input_file_path): - i = 0 + smiles_l = [] + labels_l = [] with open(input_file_path, "r") as input_file: reader = csv.DictReader(input_file) for row in reader: - smiles = row["smiles"] - test = float(row["logS"]) - if test > -1: - labels = [0,1] - else: - labels = [1,0] + smiles_l.append(row["smiles"]) + labels_l.append(np.floor(float(row["logS"]))) + # onehotencoding + label_binarizer = LabelBinarizer() + label_binarizer.fit(labels_l) + onehot_label_l = label_binarizer.transform(labels_l) + for i in range(0,len(smiles_l)): # dataset has no mol_id TODO - yield dict(features=smiles, labels=labels, ident=i) #, ident=row["mol_id"] - i += 1 + yield dict(features=smiles_l[i], labels=onehot_label_l[i], ident=i) #, ident=row["mol_id"] class SolubilityCuratedData(SolCuration): READER = dr.ChemDataReader From 4000215a3d29025844e0b4c7012f95cc89b8aed4 Mon Sep 17 00:00:00 2001 From: schnamo Date: Wed, 17 Jul 2024 13:45:23 +0200 Subject: [PATCH 07/54] adjust to regression, add yml files for regression --- chebai/models/base.py | 3 ++- chebai/models/electra.py | 8 +++++-- chebai/preprocessing/collate.py | 3 +++ chebai/preprocessing/datasets/solCuration.py | 22 ++++++++++++++------ chebai/result/pretraining.py | 3 ++- configs/loss/mse.yml | 1 + configs/metrics/mse.yml | 5 +++++ 7 files changed, 35 insertions(+), 10 deletions(-) create mode 100644 configs/loss/mse.yml create mode 100644 configs/metrics/mse.yml diff --git a/chebai/models/base.py b/chebai/models/base.py index b62e1bf8..7324f551 100644 --- a/chebai/models/base.py +++ b/chebai/models/base.py @@ -120,7 +120,8 @@ def _execute(self, batch, batch_idx, metrics, prefix="", log=True, sync_dist=Fal data = self._process_batch(batch, batch_idx) labels = data["labels"] model_output = self(data, **data.get("model_kwargs", dict())) - pr, tar = self._get_prediction_and_labels(data, labels, model_output) + # do I figure out here if solCuration and then set flag to 1? TODO + pr, tar = self._get_prediction_and_labels(data, labels, model_output, 1) d = dict(data=data, labels=labels, output=model_output, preds=pr) if log: if self.criterion is not None: diff --git a/chebai/models/electra.py b/chebai/models/electra.py index 76f2711e..32391a5b 100644 --- a/chebai/models/electra.py +++ b/chebai/models/electra.py @@ -252,7 +252,7 @@ def _process_for_loss(self, model_output, labels, loss_kwargs): labels = labels.float() return model_output["logits"], labels, kwargs_copy - def _get_prediction_and_labels(self, data, labels, model_output): + def _get_prediction_and_labels(self, data, labels, model_output, model_type=0): """ Get the predictions and labels from the model output. Applies a sigmoid to the model output. @@ -270,7 +270,11 @@ def _get_prediction_and_labels(self, data, labels, model_output): if "non_null_labels" in loss_kwargs: n = loss_kwargs["non_null_labels"] d = d[n] - return torch.sigmoid(d), labels.int() if labels is not None else None + # todo: fix this + if model_type == 0: + return torch.sigmoid(d), labels.int() if labels is not None else None + else: + return d, labels if labels is not None else None def forward(self, data, **kwargs): """ diff --git a/chebai/preprocessing/collate.py b/chebai/preprocessing/collate.py index 181b3afd..b91a38cd 100644 --- a/chebai/preprocessing/collate.py +++ b/chebai/preprocessing/collate.py @@ -56,6 +56,9 @@ def __call__(self, data): def process_label_rows(self, labels): return pad_sequence( [ + # todo: fix! + # torch.tensor([row if row is not None else False]) + # for row in labels torch.tensor([v if v is not None else False for v in row]) for row in labels ], diff --git a/chebai/preprocessing/datasets/solCuration.py b/chebai/preprocessing/datasets/solCuration.py index 64c59ce2..acf9350d 100644 --- a/chebai/preprocessing/datasets/solCuration.py +++ b/chebai/preprocessing/datasets/solCuration.py @@ -31,7 +31,7 @@ def _name(self): @property def label_number(self): - return 2 + return 1 @property def raw_file_names(self): @@ -48,6 +48,15 @@ def download(self): ) as src: with open(os.path.join(self.raw_dir, "solCuration.csv"), "wb") as dst: shutil.copyfileobj(src, dst) + # download and combine all the available curated datasets from xxx + # db_sol = ['aqsol','aqua','chembl','esol','kinect','ochem','phys'] + # with open(os.path.join(self.raw_dir, "solCuration.csv"), "ab") as dst: + # for i, db in enumerate(db_sol): + # with request.urlopen(f"https://raw.githubusercontent.com/Mengjintao/SolCuration/master/cure/{db}_cure.csv",) as src: + # if i > 0: + # src.readline() + # shutil.copyfileobj(src, dst) + def setup_processed(self): print("Create splits") @@ -118,14 +127,15 @@ def _load_dict(self, input_file_path): reader = csv.DictReader(input_file) for row in reader: smiles_l.append(row["smiles"]) - labels_l.append(np.floor(float(row["logS"]))) + labels_l.append([float(row["logS"])]) + # labels_l.append(np.floor(float(row["logS"]))) # onehotencoding - label_binarizer = LabelBinarizer() - label_binarizer.fit(labels_l) - onehot_label_l = label_binarizer.transform(labels_l) + # label_binarizer = LabelBinarizer() + # label_binarizer.fit(labels_l) + # onehot_label_l = label_binarizer.transform(labels_l) for i in range(0,len(smiles_l)): # dataset has no mol_id TODO - yield dict(features=smiles_l[i], labels=onehot_label_l[i], ident=i) #, ident=row["mol_id"] + yield dict(features=smiles_l[i], labels=labels_l[i], ident=i) #, ident=row["mol_id"] class SolubilityCuratedData(SolCuration): READER = dr.ChemDataReader diff --git a/chebai/result/pretraining.py b/chebai/result/pretraining.py index 20822d12..2de29d81 100644 --- a/chebai/result/pretraining.py +++ b/chebai/result/pretraining.py @@ -41,8 +41,9 @@ def evaluate_model(logs_base_path, model_filename, data_module): for row in tqdm.tqdm(data_list): processable_data = model._process_batch(collate([row]), 0) model_output = model(processable_data, **processable_data["model_kwargs"]) + # todo fix this preds, labels = model._get_prediction_and_labels( - processable_data, processable_data["labels"], model_output + processable_data, processable_data["labels"], model_output, 1 ) preds_list.append(preds) labels_list.append(labels) diff --git a/configs/loss/mse.yml b/configs/loss/mse.yml new file mode 100644 index 00000000..16fab1c8 --- /dev/null +++ b/configs/loss/mse.yml @@ -0,0 +1 @@ +class_path: torch.nn.MSELoss \ No newline at end of file diff --git a/configs/metrics/mse.yml b/configs/metrics/mse.yml new file mode 100644 index 00000000..1914442e --- /dev/null +++ b/configs/metrics/mse.yml @@ -0,0 +1,5 @@ +class_path: torchmetrics.MetricCollection +init_args: + metrics: + mse: + class_path: torchmetrics.regression.MeanSquaredError \ No newline at end of file From 070918888969155abf53188a648f81db7723f719 Mon Sep 17 00:00:00 2001 From: schnamo Date: Wed, 17 Jul 2024 16:14:55 +0200 Subject: [PATCH 08/54] adjust prediction to regression --- chebai/trainer/CustomTrainer.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/chebai/trainer/CustomTrainer.py b/chebai/trainer/CustomTrainer.py index 647fd617..65ddfd8a 100644 --- a/chebai/trainer/CustomTrainer.py +++ b/chebai/trainer/CustomTrainer.py @@ -51,7 +51,9 @@ def _predict_smiles(self, model: LightningModule, smiles: List[str]): ) features = torch.cat((cls_tokens, x), dim=1) model_output = model({"features": features}) - preds = torch.sigmoid(model_output["logits"]) + # todo: adjust this later with flag + preds = model_output["logits"] + # preds = torch.sigmoid(model_output["logits"]) print(preds.shape) return preds From f8bd06ac691c27a79809b0034b7b859e4db6226c Mon Sep 17 00:00:00 2001 From: schnamo Date: Tue, 23 Jul 2024 10:17:06 +0200 Subject: [PATCH 09/54] refactor code --- chebai/models/base.py | 3 +-- chebai/models/electra.py | 10 +++++--- chebai/preprocessing/collate.py | 3 --- chebai/preprocessing/datasets/solCuration.py | 26 ++++++++++---------- chebai/result/pretraining.py | 2 +- configs/model/electra.yml | 1 + configs/training/solCur_callbacks.yml | 12 +++++++++ 7 files changed, 34 insertions(+), 23 deletions(-) create mode 100644 configs/training/solCur_callbacks.yml diff --git a/chebai/models/base.py b/chebai/models/base.py index 7324f551..b62e1bf8 100644 --- a/chebai/models/base.py +++ b/chebai/models/base.py @@ -120,8 +120,7 @@ def _execute(self, batch, batch_idx, metrics, prefix="", log=True, sync_dist=Fal data = self._process_batch(batch, batch_idx) labels = data["labels"] model_output = self(data, **data.get("model_kwargs", dict())) - # do I figure out here if solCuration and then set flag to 1? TODO - pr, tar = self._get_prediction_and_labels(data, labels, model_output, 1) + pr, tar = self._get_prediction_and_labels(data, labels, model_output) d = dict(data=data, labels=labels, output=model_output, preds=pr) if log: if self.criterion is not None: diff --git a/chebai/models/electra.py b/chebai/models/electra.py index 32391a5b..a5a7fc64 100644 --- a/chebai/models/electra.py +++ b/chebai/models/electra.py @@ -198,7 +198,7 @@ def as_pretrained(self): return self.electra.electra def __init__( - self, config=None, pretrained_checkpoint=None, load_prefix=None, **kwargs + self, config=None, pretrained_checkpoint=None, load_prefix=None, model_type='classification',**kwargs ): # Remove this property in order to prevent it from being stored as a # hyper parameter @@ -252,7 +252,7 @@ def _process_for_loss(self, model_output, labels, loss_kwargs): labels = labels.float() return model_output["logits"], labels, kwargs_copy - def _get_prediction_and_labels(self, data, labels, model_output, model_type=0): + def _get_prediction_and_labels(self, data, labels, model_output): """ Get the predictions and labels from the model output. Applies a sigmoid to the model output. @@ -271,10 +271,12 @@ def _get_prediction_and_labels(self, data, labels, model_output, model_type=0): n = loss_kwargs["non_null_labels"] d = d[n] # todo: fix this - if model_type == 0: + if self.model_type == 'classification': return torch.sigmoid(d), labels.int() if labels is not None else None - else: + elif self.model_type == 'regression': return d, labels if labels is not None else None + else: + raise ValueError('Please specify a valid model type in your model config.') def forward(self, data, **kwargs): """ diff --git a/chebai/preprocessing/collate.py b/chebai/preprocessing/collate.py index b91a38cd..181b3afd 100644 --- a/chebai/preprocessing/collate.py +++ b/chebai/preprocessing/collate.py @@ -56,9 +56,6 @@ def __call__(self, data): def process_label_rows(self, labels): return pad_sequence( [ - # todo: fix! - # torch.tensor([row if row is not None else False]) - # for row in labels torch.tensor([v if v is not None else False for v in row]) for row in labels ], diff --git a/chebai/preprocessing/datasets/solCuration.py b/chebai/preprocessing/datasets/solCuration.py index acf9350d..a3428f84 100644 --- a/chebai/preprocessing/datasets/solCuration.py +++ b/chebai/preprocessing/datasets/solCuration.py @@ -42,20 +42,20 @@ def processed_file_names(self): return ["test.pt", "train.pt", "validation.pt"] def download(self): - # start with downloading just one part of the dataset, later add the remaining ones - with request.urlopen( - "https://raw.githubusercontent.com/Mengjintao/SolCuration/master/cure/esol_cure.csv", - ) as src: - with open(os.path.join(self.raw_dir, "solCuration.csv"), "wb") as dst: - shutil.copyfileobj(src, dst) + # # start with downloading just one part of the dataset, later add the remaining ones + # with request.urlopen( + # "https://raw.githubusercontent.com/Mengjintao/SolCuration/master/cure/esol_cure.csv", + # ) as src: + # with open(os.path.join(self.raw_dir, "solCuration.csv"), "wb") as dst: + # shutil.copyfileobj(src, dst) # download and combine all the available curated datasets from xxx - # db_sol = ['aqsol','aqua','chembl','esol','kinect','ochem','phys'] - # with open(os.path.join(self.raw_dir, "solCuration.csv"), "ab") as dst: - # for i, db in enumerate(db_sol): - # with request.urlopen(f"https://raw.githubusercontent.com/Mengjintao/SolCuration/master/cure/{db}_cure.csv",) as src: - # if i > 0: - # src.readline() - # shutil.copyfileobj(src, dst) + db_sol = ['aqsol','aqua','chembl','esol','kinect','ochem','phys'] + with open(os.path.join(self.raw_dir, "solCuration.csv"), "ab") as dst: + for i, db in enumerate(db_sol): + with request.urlopen(f"https://raw.githubusercontent.com/Mengjintao/SolCuration/master/cure/{db}_cure.csv",) as src: + if i > 0: + src.readline() + shutil.copyfileobj(src, dst) def setup_processed(self): diff --git a/chebai/result/pretraining.py b/chebai/result/pretraining.py index 2de29d81..1c085386 100644 --- a/chebai/result/pretraining.py +++ b/chebai/result/pretraining.py @@ -43,7 +43,7 @@ def evaluate_model(logs_base_path, model_filename, data_module): model_output = model(processable_data, **processable_data["model_kwargs"]) # todo fix this preds, labels = model._get_prediction_and_labels( - processable_data, processable_data["labels"], model_output, 1 + processable_data, processable_data["labels"], model_output ) preds_list.append(preds) labels_list.append(labels) diff --git a/configs/model/electra.yml b/configs/model/electra.yml index c7117b9c..653c47ac 100644 --- a/configs/model/electra.yml +++ b/configs/model/electra.yml @@ -1,5 +1,6 @@ class_path: chebai.models.Electra init_args: + model_type: regression optimizer_kwargs: lr: 1e-3 config: diff --git a/configs/training/solCur_callbacks.yml b/configs/training/solCur_callbacks.yml new file mode 100644 index 00000000..eb221331 --- /dev/null +++ b/configs/training/solCur_callbacks.yml @@ -0,0 +1,12 @@ +- class_path: chebai.callbacks.model_checkpoint.CustomModelCheckpoint + init_args: + monitor: val_mse + mode: 'min' + filename: 'best_{epoch:02d}_{val_loss:.4f}_{val_mse:.4f}' + every_n_epochs: 1 + save_top_k: 3 +- class_path: chebai.callbacks.model_checkpoint.CustomModelCheckpoint + init_args: + filename: 'per_{epoch:02d}_{val_loss:.4f}_{val_mse:.4f}' + every_n_epochs: 25 + save_top_k: -1 From 21fbde4cbdc85625ac9e0e5c3ec464fbe3483e8d Mon Sep 17 00:00:00 2001 From: schnamo Date: Thu, 25 Jul 2024 12:30:55 +0200 Subject: [PATCH 10/54] regression fix, yml files for mae loss --- chebai/models/electra.py | 4 ++-- configs/data/solubilityCuration.yml | 2 +- configs/loss/mae.yml | 1 + configs/training/solCur_callbacks.yml | 6 +++--- 4 files changed, 7 insertions(+), 6 deletions(-) create mode 100644 configs/loss/mae.yml diff --git a/chebai/models/electra.py b/chebai/models/electra.py index a5a7fc64..3024255f 100644 --- a/chebai/models/electra.py +++ b/chebai/models/electra.py @@ -198,7 +198,7 @@ def as_pretrained(self): return self.electra.electra def __init__( - self, config=None, pretrained_checkpoint=None, load_prefix=None, model_type='classification',**kwargs + self, config=None, pretrained_checkpoint=None, load_prefix=None, model_type='classification', **kwargs ): # Remove this property in order to prevent it from being stored as a # hyper parameter @@ -210,6 +210,7 @@ def __init__( config["num_labels"] = self.out_dim self.config = ElectraConfig(**config, output_attentions=True) self.word_dropout = nn.Dropout(config.get("word_dropout", 0)) + self.model_type = model_type in_d = self.config.hidden_size self.output = nn.Sequential( @@ -270,7 +271,6 @@ def _get_prediction_and_labels(self, data, labels, model_output): if "non_null_labels" in loss_kwargs: n = loss_kwargs["non_null_labels"] d = d[n] - # todo: fix this if self.model_type == 'classification': return torch.sigmoid(d), labels.int() if labels is not None else None elif self.model_type == 'regression': diff --git a/configs/data/solubilityCuration.yml b/configs/data/solubilityCuration.yml index aee416de..b7114eac 100644 --- a/configs/data/solubilityCuration.yml +++ b/configs/data/solubilityCuration.yml @@ -1,3 +1,3 @@ class_path: chebai.preprocessing.datasets.solCuration.SolubilityCuratedData init_args: - batch_size: 10 + batch_size: 20 diff --git a/configs/loss/mae.yml b/configs/loss/mae.yml new file mode 100644 index 00000000..75e011be --- /dev/null +++ b/configs/loss/mae.yml @@ -0,0 +1 @@ +class_path: torch.nn.L1Loss \ No newline at end of file diff --git a/configs/training/solCur_callbacks.yml b/configs/training/solCur_callbacks.yml index eb221331..b6ad7158 100644 --- a/configs/training/solCur_callbacks.yml +++ b/configs/training/solCur_callbacks.yml @@ -1,12 +1,12 @@ - class_path: chebai.callbacks.model_checkpoint.CustomModelCheckpoint init_args: - monitor: val_mse + monitor: val_mae mode: 'min' - filename: 'best_{epoch:02d}_{val_loss:.4f}_{val_mse:.4f}' + filename: 'best_{epoch:02d}_{val_loss:.4f}_{val_mae:.4f}' every_n_epochs: 1 save_top_k: 3 - class_path: chebai.callbacks.model_checkpoint.CustomModelCheckpoint init_args: - filename: 'per_{epoch:02d}_{val_loss:.4f}_{val_mse:.4f}' + filename: 'per_{epoch:02d}_{val_loss:.4f}_{val_mae:.4f}' every_n_epochs: 25 save_top_k: -1 From f3bfe0882184cfbfb1d4d53cc8bb31f6baa59b1e Mon Sep 17 00:00:00 2001 From: schnamo Date: Thu, 25 Jul 2024 14:16:34 +0200 Subject: [PATCH 11/54] take out kinect dataset --- chebai/preprocessing/datasets/solCuration.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chebai/preprocessing/datasets/solCuration.py b/chebai/preprocessing/datasets/solCuration.py index a3428f84..66015b74 100644 --- a/chebai/preprocessing/datasets/solCuration.py +++ b/chebai/preprocessing/datasets/solCuration.py @@ -49,7 +49,7 @@ def download(self): # with open(os.path.join(self.raw_dir, "solCuration.csv"), "wb") as dst: # shutil.copyfileobj(src, dst) # download and combine all the available curated datasets from xxx - db_sol = ['aqsol','aqua','chembl','esol','kinect','ochem','phys'] + db_sol = ['aqsol','aqua','chembl','esol','ochem','phys'] with open(os.path.join(self.raw_dir, "solCuration.csv"), "ab") as dst: for i, db in enumerate(db_sol): with request.urlopen(f"https://raw.githubusercontent.com/Mengjintao/SolCuration/master/cure/{db}_cure.csv",) as src: From e26925d34f8f09d3e605c547ca50fb8ed583645b Mon Sep 17 00:00:00 2001 From: schnamo Date: Thu, 25 Jul 2024 18:16:11 +0200 Subject: [PATCH 12/54] adjust learning rate --- configs/model/electra.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/configs/model/electra.yml b/configs/model/electra.yml index 653c47ac..9fab973e 100644 --- a/configs/model/electra.yml +++ b/configs/model/electra.yml @@ -2,7 +2,7 @@ class_path: chebai.models.Electra init_args: model_type: regression optimizer_kwargs: - lr: 1e-3 + lr: 1e-2 config: vocab_size: 1400 max_position_embeddings: 1800 From 0f2f85fe7cfcd491deaa543367d0f204f4ff261f Mon Sep 17 00:00:00 2001 From: schnamo Date: Thu, 12 Dec 2024 14:45:23 +0100 Subject: [PATCH 13/54] adjustments for new solu dataset --- chebai/preprocessing/datasets/solCuration.py | 10 ++++++---- configs/model/electra.yml | 2 +- configs/training/solCur_callbacks.yml | 6 +++--- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/chebai/preprocessing/datasets/solCuration.py b/chebai/preprocessing/datasets/solCuration.py index 66015b74..fa1c1c20 100644 --- a/chebai/preprocessing/datasets/solCuration.py +++ b/chebai/preprocessing/datasets/solCuration.py @@ -127,15 +127,17 @@ def _load_dict(self, input_file_path): reader = csv.DictReader(input_file) for row in reader: smiles_l.append(row["smiles"]) - labels_l.append([float(row["logS"])]) + labels_l.append(float(row["logS"])) # labels_l.append(np.floor(float(row["logS"]))) # onehotencoding # label_binarizer = LabelBinarizer() # label_binarizer.fit(labels_l) # onehot_label_l = label_binarizer.transform(labels_l) - for i in range(0,len(smiles_l)): - # dataset has no mol_id TODO - yield dict(features=smiles_l[i], labels=labels_l[i], ident=i) #, ident=row["mol_id"] + + # normalise data to be between 0 and 1 + labels_norm = [(float(label)-min(labels_l))/(max(labels_l)-min(labels_l)) for label in labels_l] + for i in range(0,len(smiles_l)): + yield dict(features=smiles_l[i], labels=[labels_l[i]], ident=i) class SolubilityCuratedData(SolCuration): READER = dr.ChemDataReader diff --git a/configs/model/electra.yml b/configs/model/electra.yml index 9fab973e..653c47ac 100644 --- a/configs/model/electra.yml +++ b/configs/model/electra.yml @@ -2,7 +2,7 @@ class_path: chebai.models.Electra init_args: model_type: regression optimizer_kwargs: - lr: 1e-2 + lr: 1e-3 config: vocab_size: 1400 max_position_embeddings: 1800 diff --git a/configs/training/solCur_callbacks.yml b/configs/training/solCur_callbacks.yml index b6ad7158..eb221331 100644 --- a/configs/training/solCur_callbacks.yml +++ b/configs/training/solCur_callbacks.yml @@ -1,12 +1,12 @@ - class_path: chebai.callbacks.model_checkpoint.CustomModelCheckpoint init_args: - monitor: val_mae + monitor: val_mse mode: 'min' - filename: 'best_{epoch:02d}_{val_loss:.4f}_{val_mae:.4f}' + filename: 'best_{epoch:02d}_{val_loss:.4f}_{val_mse:.4f}' every_n_epochs: 1 save_top_k: 3 - class_path: chebai.callbacks.model_checkpoint.CustomModelCheckpoint init_args: - filename: 'per_{epoch:02d}_{val_loss:.4f}_{val_mae:.4f}' + filename: 'per_{epoch:02d}_{val_loss:.4f}_{val_mse:.4f}' every_n_epochs: 25 save_top_k: -1 From 0d94b4413e904cf3927813e19e7f373920639529 Mon Sep 17 00:00:00 2001 From: schnamo Date: Thu, 19 Dec 2024 11:09:18 +0100 Subject: [PATCH 14/54] working on evaluation script, addded a bunch of things earlier for solubility regression --- chebai/preprocessing/datasets/solCuration.py | 90 ++++--- chebai/train.py | 1 + configs/data/solubilityCuration.yml | 4 +- configs/metrics/mae.yml | 5 + configs/model/electra.yml | 4 +- configs/training/default_trainer.yml | 2 +- eval_model_regression.ipynb | 233 +++++++++++++++++++ tutorials/eval_model_basic.ipynb | 2 +- 8 files changed, 297 insertions(+), 44 deletions(-) create mode 100644 configs/metrics/mae.yml create mode 100644 eval_model_regression.ipynb diff --git a/chebai/preprocessing/datasets/solCuration.py b/chebai/preprocessing/datasets/solCuration.py index fa1c1c20..a1192401 100644 --- a/chebai/preprocessing/datasets/solCuration.py +++ b/chebai/preprocessing/datasets/solCuration.py @@ -6,6 +6,7 @@ import random import shutil import zipfile +from typing import Dict, Generator, List, Optional from rdkit import Chem from sklearn.model_selection import GroupShuffleSplit, train_test_split @@ -42,14 +43,8 @@ def processed_file_names(self): return ["test.pt", "train.pt", "validation.pt"] def download(self): - # # start with downloading just one part of the dataset, later add the remaining ones - # with request.urlopen( - # "https://raw.githubusercontent.com/Mengjintao/SolCuration/master/cure/esol_cure.csv", - # ) as src: - # with open(os.path.join(self.raw_dir, "solCuration.csv"), "wb") as dst: - # shutil.copyfileobj(src, dst) # download and combine all the available curated datasets from xxx - db_sol = ['aqsol','aqua','chembl','esol','ochem','phys'] + db_sol = ['aqsol','aqua','esol','ochem','phys'] with open(os.path.join(self.raw_dir, "solCuration.csv"), "ab") as dst: for i, db in enumerate(db_sol): with request.urlopen(f"https://raw.githubusercontent.com/Mengjintao/SolCuration/master/cure/{db}_cure.csv",) as src: @@ -61,36 +56,38 @@ def download(self): def setup_processed(self): print("Create splits") data = self._load_data_from_file(os.path.join(self.raw_dir, f"solCuration.csv")) - groups = np.array([d["group"] for d in data]) - if not all(g is None for g in groups): - split_size = int(len(set(groups)) * self.train_split) - os.makedirs(self.processed_dir, exist_ok=True) - splitter = GroupShuffleSplit(train_size=split_size, n_splits=1) - - train_split_index, temp_split_index = next( - splitter.split(data, groups=groups) - ) - - split_groups = groups[temp_split_index] - - splitter = GroupShuffleSplit( - train_size=int(len(set(split_groups)) * self.train_split), n_splits=1 - ) - test_split_index, validation_split_index = next( - splitter.split(temp_split_index, groups=split_groups) - ) - train_split = [data[i] for i in train_split_index] - test_split = [ - d - for d in (data[temp_split_index[i]] for i in test_split_index) - if d["original"] - ] - validation_split = [ - d - for d in (data[temp_split_index[i]] for i in validation_split_index) - if d["original"] - ] - else: + # todo: figure out where the groups are supposed to come from + # groups = np.array([d["group"] for d in data]) + # if not all(g is None for g in groups): + # split_size = int(len(set(groups)) * self.train_split) + # os.makedirs(self.processed_dir, exist_ok=True) + # splitter = GroupShuffleSplit(train_size=split_size, n_splits=1) + + # train_split_index, temp_split_index = next( + # splitter.split(data, groups=groups) + # ) + + # split_groups = groups[temp_split_index] + + # splitter = GroupShuffleSplit( + # train_size=int(len(set(split_groups)) * self.train_split), n_splits=1 + # ) + # test_split_index, validation_split_index = next( + # splitter.split(temp_split_index, groups=split_groups) + # ) + # train_split = [data[i] for i in train_split_index] + # test_split = [ + # d + # for d in (data[temp_split_index[i]] for i in test_split_index) + # if d["original"] + # ] + # validation_split = [ + # d + # for d in (data[temp_split_index[i]] for i in validation_split_index) + # if d["original"] + # ] + # else: + if 0 == 0: train_split, test_split = train_test_split( data, train_size=self.train_split, shuffle=True ) @@ -114,13 +111,25 @@ def setup(self, **kwargs): for f in self.raw_file_names ): self.download() + print([ + not os.path.isfile(os.path.join(self.processed_dir, f)) + for f in self.processed_file_names + ]) if any( not os.path.isfile(os.path.join(self.processed_dir, f)) for f in self.processed_file_names ): self.setup_processed() - def _load_dict(self, input_file_path): + def _load_data_from_file(self, input_file_path: str) -> List[Dict]: + """Loads data from a CSV file. + + Args: + input_file_path (str): Path to the CSV file. + + Returns: + List[Dict]: List of data dictionaries. + """ smiles_l = [] labels_l = [] with open(input_file_path, "r") as input_file: @@ -141,3 +150,8 @@ def _load_dict(self, input_file_path): class SolubilityCuratedData(SolCuration): READER = dr.ChemDataReader + +class SolCurationChem(SolCuration): + """Chemical data reader for the solubility dataset.""" + + READER = dr.ChemDataReader \ No newline at end of file diff --git a/chebai/train.py b/chebai/train.py index 06deec27..afc039cb 100644 --- a/chebai/train.py +++ b/chebai/train.py @@ -132,6 +132,7 @@ def _execute( - train_running_loss (float): Average loss over the data. - f1 (float): Average F1 score over the data. """ + train_running_loss = 0.0 data_size = 0 f1 = 0 diff --git a/configs/data/solubilityCuration.yml b/configs/data/solubilityCuration.yml index b7114eac..7e07f37a 100644 --- a/configs/data/solubilityCuration.yml +++ b/configs/data/solubilityCuration.yml @@ -1,3 +1,3 @@ -class_path: chebai.preprocessing.datasets.solCuration.SolubilityCuratedData +class_path: chebai.preprocessing.datasets.solCuration.SolCurationChem init_args: - batch_size: 20 + batch_size: 32 diff --git a/configs/metrics/mae.yml b/configs/metrics/mae.yml new file mode 100644 index 00000000..323e5fb4 --- /dev/null +++ b/configs/metrics/mae.yml @@ -0,0 +1,5 @@ +class_path: torchmetrics.MetricCollection +init_args: + metrics: + mae: + class_path: torchmetrics.regression.MeanAbsoluteError \ No newline at end of file diff --git a/configs/model/electra.yml b/configs/model/electra.yml index ecae0761..56de72eb 100644 --- a/configs/model/electra.yml +++ b/configs/model/electra.yml @@ -1,9 +1,9 @@ class_path: chebai.models.Electra init_args: - model_type: regression optimizer_kwargs: - lr: 1e-3 + lr: 1e-1 config: + model_type: regression vocab_size: 1400 max_position_embeddings: 1800 num_attention_heads: 8 diff --git a/configs/training/default_trainer.yml b/configs/training/default_trainer.yml index 91aa4244..0ce68a49 100644 --- a/configs/training/default_trainer.yml +++ b/configs/training/default_trainer.yml @@ -1,4 +1,4 @@ -min_epochs: 100 +min_epochs: 20 max_epochs: 100 default_root_dir: &default_root_dir logs logger: csv_logger.yml diff --git a/eval_model_regression.ipynb b/eval_model_regression.ipynb new file mode 100644 index 00000000..faaca9c3 --- /dev/null +++ b/eval_model_regression.ipynb @@ -0,0 +1,233 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "initial_id", + "metadata": { + "ExecuteTime": { + "end_time": "2024-04-02T13:47:31.150545Z", + "start_time": "2024-04-02T13:47:27.181585Z" + } + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/ctumes/Cheb-AI/chebai_env/lib/python3.9/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n", + " from .autonotebook import tqdm as notebook_tqdm\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "cpu\n" + ] + } + ], + "source": [ + "import pandas as pd\n", + "\n", + "from chebai.result.utils import (\n", + " evaluate_model,\n", + " load_results_from_buffer,\n", + ")\n", + "from chebai.result.classification import print_metrics\n", + "from chebai.models.electra import Electra\n", + "from chebai.preprocessing.datasets.solCuration import SolCuration, SolCurationChem\n", + "from chebai.preprocessing.datasets.tox21 import Tox21MolNet\n", + "import os\n", + "import tqdm\n", + "import torch\n", + "import pickle\n", + "\n", + "DEVICE = torch.device(\"cuda:0\" if torch.cuda.is_available() else \"cpu\")\n", + "print(DEVICE)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "bdb5fc6919cf72be", + "metadata": { + "ExecuteTime": { + "end_time": "2024-04-02T13:47:35.484307Z", + "start_time": "2024-04-02T13:47:35.477111Z" + }, + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[False, False, False]\n" + ] + } + ], + "source": [ + "# specify the checkpoint name\n", + "checkpoint_name = \"solFinetuningAGAIN_MSE/version_1/checkpoints/best_epoch=29_val_loss=0.5266_val_mse=10.9920\"\n", + "checkpoint_path = os.path.join(\"logs\", f\"{checkpoint_name}.ckpt\")\n", + "kind = \"test\" # replace with \"train\" / \"validation\" to run on train / validation sets\n", + "buffer_dir = os.path.join(\"results_buffer\", checkpoint_name, kind)\n", + "# make sure to use the same data module and model class that were used during training\n", + "data_module = SolCurationChem()\n", + "# load chebi data if missing and perform dynamic splits\n", + "data_module.prepare_data()\n", + "data_module.setup()\n", + "\n", + "model_class = Electra" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "fa1276b47def696c", + "metadata": { + "ExecuteTime": { + "end_time": "2024-04-02T13:47:38.418564Z", + "start_time": "2024-04-02T13:47:37.861168Z" + } + }, + "outputs": [ + { + "ename": "FileNotFoundError", + "evalue": "[Errno 2] No such file or directory: '/Users/ctumes/python-chebai/logs/best_epoch=29_val_loss=0.5266_val_mse=10.9920.ckpt'", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mFileNotFoundError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[5], line 2\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[38;5;66;03m# evaluates model, stores results in buffer_dir\u001b[39;00m\n\u001b[0;32m----> 2\u001b[0m model \u001b[38;5;241m=\u001b[39m \u001b[43mmodel_class\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mload_from_checkpoint\u001b[49m\u001b[43m(\u001b[49m\u001b[43mcheckpoint_path\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 3\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m buffer_dir \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m:\n\u001b[1;32m 4\u001b[0m preds, labels \u001b[38;5;241m=\u001b[39m evaluate_model(\n\u001b[1;32m 5\u001b[0m model,\n\u001b[1;32m 6\u001b[0m data_module,\n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 11\u001b[0m kind\u001b[38;5;241m=\u001b[39mkind,\n\u001b[1;32m 12\u001b[0m )\n", + "File \u001b[0;32m~/Cheb-AI/chebai_env/lib/python3.9/site-packages/lightning/pytorch/core/module.py:1552\u001b[0m, in \u001b[0;36mLightningModule.load_from_checkpoint\u001b[0;34m(cls, checkpoint_path, map_location, hparams_file, strict, **kwargs)\u001b[0m\n\u001b[1;32m 1471\u001b[0m \u001b[38;5;129m@_restricted_classmethod\u001b[39m\n\u001b[1;32m 1472\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mload_from_checkpoint\u001b[39m(\n\u001b[1;32m 1473\u001b[0m \u001b[38;5;28mcls\u001b[39m,\n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 1478\u001b[0m \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs: Any,\n\u001b[1;32m 1479\u001b[0m ) \u001b[38;5;241m-\u001b[39m\u001b[38;5;241m>\u001b[39m Self:\n\u001b[1;32m 1480\u001b[0m \u001b[38;5;250m \u001b[39m\u001b[38;5;124mr\u001b[39m\u001b[38;5;124;03m\"\"\"Primary way of loading a model from a checkpoint. When Lightning saves a checkpoint it stores the arguments\u001b[39;00m\n\u001b[1;32m 1481\u001b[0m \u001b[38;5;124;03m passed to ``__init__`` in the checkpoint under ``\"hyper_parameters\"``.\u001b[39;00m\n\u001b[1;32m 1482\u001b[0m \n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 1550\u001b[0m \n\u001b[1;32m 1551\u001b[0m \u001b[38;5;124;03m \"\"\"\u001b[39;00m\n\u001b[0;32m-> 1552\u001b[0m loaded \u001b[38;5;241m=\u001b[39m \u001b[43m_load_from_checkpoint\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 1553\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;28;43mcls\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;66;43;03m# type: ignore[arg-type]\u001b[39;49;00m\n\u001b[1;32m 1554\u001b[0m \u001b[43m \u001b[49m\u001b[43mcheckpoint_path\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 1555\u001b[0m \u001b[43m \u001b[49m\u001b[43mmap_location\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 1556\u001b[0m \u001b[43m \u001b[49m\u001b[43mhparams_file\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 1557\u001b[0m \u001b[43m \u001b[49m\u001b[43mstrict\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 1558\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 1559\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 1560\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m cast(Self, loaded)\n", + "File \u001b[0;32m~/Cheb-AI/chebai_env/lib/python3.9/site-packages/lightning/pytorch/core/saving.py:61\u001b[0m, in \u001b[0;36m_load_from_checkpoint\u001b[0;34m(cls, checkpoint_path, map_location, hparams_file, strict, **kwargs)\u001b[0m\n\u001b[1;32m 59\u001b[0m map_location \u001b[38;5;241m=\u001b[39m map_location \u001b[38;5;129;01mor\u001b[39;00m _default_map_location\n\u001b[1;32m 60\u001b[0m \u001b[38;5;28;01mwith\u001b[39;00m pl_legacy_patch():\n\u001b[0;32m---> 61\u001b[0m checkpoint \u001b[38;5;241m=\u001b[39m \u001b[43mpl_load\u001b[49m\u001b[43m(\u001b[49m\u001b[43mcheckpoint_path\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mmap_location\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mmap_location\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 63\u001b[0m \u001b[38;5;66;03m# convert legacy checkpoints to the new format\u001b[39;00m\n\u001b[1;32m 64\u001b[0m checkpoint \u001b[38;5;241m=\u001b[39m _pl_migrate_checkpoint(\n\u001b[1;32m 65\u001b[0m checkpoint, checkpoint_path\u001b[38;5;241m=\u001b[39m(checkpoint_path \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28misinstance\u001b[39m(checkpoint_path, (\u001b[38;5;28mstr\u001b[39m, Path)) \u001b[38;5;28;01melse\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m)\n\u001b[1;32m 66\u001b[0m )\n", + "File \u001b[0;32m~/Cheb-AI/chebai_env/lib/python3.9/site-packages/lightning/fabric/utilities/cloud_io.py:54\u001b[0m, in \u001b[0;36m_load\u001b[0;34m(path_or_url, map_location)\u001b[0m\n\u001b[1;32m 49\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m torch\u001b[38;5;241m.\u001b[39mhub\u001b[38;5;241m.\u001b[39mload_state_dict_from_url(\n\u001b[1;32m 50\u001b[0m \u001b[38;5;28mstr\u001b[39m(path_or_url),\n\u001b[1;32m 51\u001b[0m map_location\u001b[38;5;241m=\u001b[39mmap_location, \u001b[38;5;66;03m# type: ignore[arg-type]\u001b[39;00m\n\u001b[1;32m 52\u001b[0m )\n\u001b[1;32m 53\u001b[0m fs \u001b[38;5;241m=\u001b[39m get_filesystem(path_or_url)\n\u001b[0;32m---> 54\u001b[0m \u001b[38;5;28;01mwith\u001b[39;00m \u001b[43mfs\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mopen\u001b[49m\u001b[43m(\u001b[49m\u001b[43mpath_or_url\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mrb\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m)\u001b[49m \u001b[38;5;28;01mas\u001b[39;00m f:\n\u001b[1;32m 55\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m torch\u001b[38;5;241m.\u001b[39mload(f, map_location\u001b[38;5;241m=\u001b[39mmap_location)\n", + "File \u001b[0;32m~/Cheb-AI/chebai_env/lib/python3.9/site-packages/fsspec/spec.py:1303\u001b[0m, in \u001b[0;36mAbstractFileSystem.open\u001b[0;34m(self, path, mode, block_size, cache_options, compression, **kwargs)\u001b[0m\n\u001b[1;32m 1301\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[1;32m 1302\u001b[0m ac \u001b[38;5;241m=\u001b[39m kwargs\u001b[38;5;241m.\u001b[39mpop(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mautocommit\u001b[39m\u001b[38;5;124m\"\u001b[39m, \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_intrans)\n\u001b[0;32m-> 1303\u001b[0m f \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_open\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 1304\u001b[0m \u001b[43m \u001b[49m\u001b[43mpath\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 1305\u001b[0m \u001b[43m \u001b[49m\u001b[43mmode\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mmode\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 1306\u001b[0m \u001b[43m \u001b[49m\u001b[43mblock_size\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mblock_size\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 1307\u001b[0m \u001b[43m \u001b[49m\u001b[43mautocommit\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mac\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 1308\u001b[0m \u001b[43m \u001b[49m\u001b[43mcache_options\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mcache_options\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 1309\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 1310\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 1311\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m compression \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m:\n\u001b[1;32m 1312\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01mfsspec\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mcompression\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m compr\n", + "File \u001b[0;32m~/Cheb-AI/chebai_env/lib/python3.9/site-packages/fsspec/implementations/local.py:191\u001b[0m, in \u001b[0;36mLocalFileSystem._open\u001b[0;34m(self, path, mode, block_size, **kwargs)\u001b[0m\n\u001b[1;32m 189\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mauto_mkdir \u001b[38;5;129;01mand\u001b[39;00m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mw\u001b[39m\u001b[38;5;124m\"\u001b[39m \u001b[38;5;129;01min\u001b[39;00m mode:\n\u001b[1;32m 190\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mmakedirs(\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_parent(path), exist_ok\u001b[38;5;241m=\u001b[39m\u001b[38;5;28;01mTrue\u001b[39;00m)\n\u001b[0;32m--> 191\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mLocalFileOpener\u001b[49m\u001b[43m(\u001b[49m\u001b[43mpath\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mmode\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mfs\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n", + "File \u001b[0;32m~/Cheb-AI/chebai_env/lib/python3.9/site-packages/fsspec/implementations/local.py:355\u001b[0m, in \u001b[0;36mLocalFileOpener.__init__\u001b[0;34m(self, path, mode, autocommit, fs, compression, **kwargs)\u001b[0m\n\u001b[1;32m 353\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mcompression \u001b[38;5;241m=\u001b[39m get_compression(path, compression)\n\u001b[1;32m 354\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mblocksize \u001b[38;5;241m=\u001b[39m io\u001b[38;5;241m.\u001b[39mDEFAULT_BUFFER_SIZE\n\u001b[0;32m--> 355\u001b[0m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_open\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n", + "File \u001b[0;32m~/Cheb-AI/chebai_env/lib/python3.9/site-packages/fsspec/implementations/local.py:360\u001b[0m, in \u001b[0;36mLocalFileOpener._open\u001b[0;34m(self)\u001b[0m\n\u001b[1;32m 358\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mf \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m \u001b[38;5;129;01mor\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mf\u001b[38;5;241m.\u001b[39mclosed:\n\u001b[1;32m 359\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mautocommit \u001b[38;5;129;01mor\u001b[39;00m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mw\u001b[39m\u001b[38;5;124m\"\u001b[39m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;129;01min\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mmode:\n\u001b[0;32m--> 360\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mf \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mopen\u001b[39;49m\u001b[43m(\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mpath\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mmode\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mmode\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 361\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mcompression:\n\u001b[1;32m 362\u001b[0m compress \u001b[38;5;241m=\u001b[39m compr[\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mcompression]\n", + "\u001b[0;31mFileNotFoundError\u001b[0m: [Errno 2] No such file or directory: '/Users/ctumes/python-chebai/logs/best_epoch=29_val_loss=0.5266_val_mse=10.9920.ckpt'" + ] + } + ], + "source": [ + "# evaluates model, stores results in buffer_dir\n", + "model = model_class.load_from_checkpoint(checkpoint_path)\n", + "if buffer_dir is None:\n", + " preds, labels = evaluate_model(\n", + " model,\n", + " data_module,\n", + " buffer_dir=buffer_dir,\n", + " # No need to provide this parameter for Chebi dataset, \"kind\" parameter should be provided\n", + " # filename=data_module.processed_file_names_dict[kind],\n", + " batch_size=10,\n", + " kind=kind,\n", + " )\n", + "else:\n", + " evaluate_model(\n", + " model,\n", + " data_module,\n", + " buffer_dir=buffer_dir,\n", + " # No need to provide this parameter for Chebi dataset, \"kind\" parameter should be provided\n", + " # filename=data_module.processed_file_names_dict[kind],\n", + " batch_size=10,\n", + " kind=kind,\n", + " )\n", + " # load data from buffer_dir\n", + " preds, labels = load_results_from_buffer(buffer_dir, device=DEVICE)" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "201f750c475b4677", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "# Load classes from the classes.txt\n", + "with open(os.path.join(data_module.processed_dir_main, \"classes.txt\"), \"r\") as f:\n", + " classes = [line.strip() for line in f.readlines()]" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "e567cd2fb1718baf", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Macro-F1: 0.290936\n", + "Micro-F1: 0.890380\n", + "Balanced Accuracy: 0.507610\n", + "Macro-Precision: 0.021964\n", + "Micro-Precision: 0.908676\n", + "Macro-Recall: 0.020987\n", + "Micro-Recall: 0.872807\n", + "Top 10 classes (F1-score):\n", + "1. 23367 - F1: 1.000000\n", + "2. 33259 - F1: 1.000000\n", + "3. 36914 - F1: 1.000000\n", + "4. 24431 - F1: 1.000000\n", + "5. 33238 - F1: 1.000000\n", + "6. 36357 - F1: 1.000000\n", + "7. 37577 - F1: 1.000000\n", + "8. 24867 - F1: 1.000000\n", + "9. 33579 - F1: 0.974026\n", + "10. 24866 - F1: 0.973684\n", + "Found 63 classes with F1-score == 0 (and non-zero labels): 17792, 22563, 22632, 22712, 24062, 24834, 25108, 25693, 25697, 25698, 25699, 25806, 26151, 26217, 26218, 26421, 26469, 29347, 32988, 33240, 33256, 33296, 33299, 33304, 33597, 33598, 33635, 33655, 33659, 33661, 33670, 33671, 33836, 33976, 35217, 35273, 35479, 35618, 36364, 36562, 36916, 36962, 36963, 37141, 37143, 37622, 37929, 37960, 38101, 38104, 38166, 38835, 39203, 46850, 47704, 47916, 48592, 50047, 50995, 72544, 79389, 83565, 139358\n" + ] + } + ], + "source": [ + "# output relevant metrics\n", + "print_metrics(\n", + " preds,\n", + " labels.to(torch.int),\n", + " DEVICE,\n", + " classes=classes,\n", + " markdown_output=False,\n", + " top_k=10,\n", + ")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.19" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/tutorials/eval_model_basic.ipynb b/tutorials/eval_model_basic.ipynb index bc54464b..cc4ac3a5 100644 --- a/tutorials/eval_model_basic.ipynb +++ b/tutorials/eval_model_basic.ipynb @@ -238,7 +238,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.14" + "version": "3.9.19" } }, "nbformat": 4, From 45228ba74f0de5e59c5c54576d174d02703da838 Mon Sep 17 00:00:00 2001 From: schnamo Date: Fri, 20 Dec 2024 18:06:52 +0100 Subject: [PATCH 15/54] further adjusting evaluation function for regression --- chebai/result/regression.py | 74 ++++++++++++ chebai/result/utils.py | 98 ++++++++++++++++ eval_model_regression.ipynb | 220 ++++++++++++++++++++++++------------ 3 files changed, 319 insertions(+), 73 deletions(-) create mode 100644 chebai/result/regression.py diff --git a/chebai/result/regression.py b/chebai/result/regression.py new file mode 100644 index 00000000..e70c0ddc --- /dev/null +++ b/chebai/result/regression.py @@ -0,0 +1,74 @@ +from typing import List + +import matplotlib.pyplot as plt +import pandas as pd +import seaborn as sns +from torch import Tensor +from torchmetrics.regression import ( + MeanSquaredError, +) + +# from chebai.callbacks.epoch_metrics import BalancedAccuracy, MacroF1 +from chebai.result.utils import * + + +# def visualise_f1(logs_path: str) -> None: +# """ +# Visualize F1 scores from metrics.csv and save the plot as f1_plot.png. + +# Args: +# logs_path: The path to the directory containing metrics.csv. +# """ +# df = pd.read_csv(os.path.join(logs_path, "metrics.csv")) +# df_loss = df.melt( +# id_vars="epoch", +# value_vars=[ +# "val_ep_macro-f1", +# "val_micro-f1", +# "train_micro-f1", +# "train_ep_macro-f1", +# ], +# ) +# lineplt = sns.lineplot(df_loss, x="epoch", y="value", hue="variable") +# plt.savefig(os.path.join(logs_path, "f1_plot.png")) +# plt.show() + + +def print_metrics( + preds: Tensor, + labels: Tensor, + device: torch.device, + markdown_output: bool = False, +) -> None: + """ + Prints relevant metrics, including micro and macro F1, recall and precision, + best k classes, and worst classes. + + Args: + preds: Predicted labels as a tensor. + labels: True labels as a tensor. + device: The device to perform computations on. + classes: Optional list of class names. + top_k: The number of top classes to display based on F1 score. + markdown_output: If True, print metrics in markdown format. + """ + mse = MeanSquaredError() + mse = mse.to(labels.device) + + # my_f1_macro = MacroF1(preds.shape[1]).to(device=device) + # my_bal_acc = BalancedAccuracy(preds.shape[1]).to(device=device) + + print(f"MSE: {mse(preds, labels)}") + # print(f"Micro-F1: {f1_micro(preds, labels):3f}") + # print(f"Balanced Accuracy: {my_bal_acc(preds, labels):3f}") + + # if markdown_output: + # print( + # f"| Model | MSE | RMSE | Macro-Precision | Micro-Precision | Macro-Recall | Micro-Recall | Balanced Accuracy" + # ) + # print(f"| --- | --- | --- | --- | --- | --- | --- | --- |") + # print( + # f"| Elektra | {my_f1_macro(preds, labels):3f} | {f1_micro(preds, labels):3f} | {precision_macro(preds, labels):3f} | " + # f"{precision_micro(preds, labels):3f} | {recall_macro(preds, labels):3f} | " + # f"{recall_micro(preds, labels):3f} | {my_bal_acc(preds, labels):3f} |" + # ) diff --git a/chebai/result/utils.py b/chebai/result/utils.py index 80bf56e2..f8d5cf4b 100644 --- a/chebai/result/utils.py +++ b/chebai/result/utils.py @@ -67,6 +67,7 @@ def _run_batch(batch, model, collate): def _concat_tuple(l): + # print(l[0]) if isinstance(l[0], tuple): print(l[0]) return tuple([torch.cat([t[i] for t in l]) for i in range(len(l[0]))]) @@ -111,6 +112,7 @@ def evaluate_model( else: data_list = data_module.load_processed_data("test", filename) data_list = data_list[: data_module.data_limit] + print(data_list[2:5]) preds_list = [] labels_list = [] if buffer_dir is not None: @@ -165,6 +167,102 @@ def evaluate_model( os.path.join(buffer_dir, f"labels{save_ind:03d}.pt"), ) +def evaluate_model_regression( + model: ChebaiBaseNet, + data_module: XYBaseDataModule, + filename: Optional[str] = None, + buffer_dir: Optional[str] = None, + batch_size: int = 32, + skip_existing_preds: bool = False, + kind: str = "test", +) -> Tuple[torch.Tensor, Optional[torch.Tensor]]: + """ + Runs the model on the test set of the data module or on the dataset found in the specified file. + If buffer_dir is set, results will be saved in buffer_dir. + + Note: + No need to provide "filename" parameter for Chebi dataset, "kind" parameter should be provided. + + Args: + model: The model to evaluate. + data_module: The data module containing the dataset. + filename: Optional file name for the dataset. + buffer_dir: Optional directory to save the results. + batch_size: The batch size for evaluation. + skip_existing_preds: Whether to skip evaluation if predictions already exist. + kind: Kind of split of the data to be used for testing the model. Default is `test`. + + Returns: + Tensors with predictions and labels. + """ + model.eval() + collate = data_module.reader.COLLATOR() + + if isinstance(data_module, _ChEBIDataExtractor): + # As the dynamic split change is implemented only for chebi-dataset as of now + data_df = data_module.dynamic_split_dfs[kind] + data_list = data_df.to_dict(orient="records") + else: + data_list = data_module.load_processed_data("test", filename) + data_list = data_list[: data_module.data_limit] + preds_list = [] + labels_list = [] + preds_list_all = [] + labels_list_all = [] + if buffer_dir is not None: + os.makedirs(buffer_dir, exist_ok=True) + save_ind = 0 + save_batch_size = 128 + n_saved = 1 + + print(f"") + for i in tqdm.tqdm(range(0, len(data_list), batch_size)): + if not ( + skip_existing_preds + and os.path.isfile(os.path.join(buffer_dir, f"preds{save_ind:03d}.pt")) + ): + preds, labels = _run_batch(data_list[i : i + batch_size], model, collate) + preds_list.append(preds) + labels_list.append(labels) + preds_list_all.append(preds) + labels_list_all.append(labels) + if buffer_dir is not None: + if n_saved * batch_size >= save_batch_size: + torch.save( + _concat_tuple(preds_list), + os.path.join(buffer_dir, f"preds{save_ind:03d}.pt"), + ) + if labels_list[0] is not None: + torch.save( + _concat_tuple(labels_list), + os.path.join(buffer_dir, f"labels{save_ind:03d}.pt"), + ) + preds_list = [] + labels_list = [] + if n_saved * batch_size >= save_batch_size: + save_ind += 1 + n_saved = 0 + n_saved += 1 + + if buffer_dir is None: + test_preds = _concat_tuple(preds_list) + if labels_list is not None: + test_labels = _concat_tuple(labels_list) + + return test_preds, test_labels + return test_preds, None + else: + torch.save( + _concat_tuple(preds_list), + os.path.join(buffer_dir, f"preds{save_ind:03d}.pt"), + ) + if labels_list[0] is not None: + torch.save( + _concat_tuple(labels_list), + os.path.join(buffer_dir, f"labels{save_ind:03d}.pt"), + ) + return torch.cat(preds_list_all), torch.cat(labels_list_all) + def load_results_from_buffer( buffer_dir: str, device: torch.device diff --git a/eval_model_regression.ipynb b/eval_model_regression.ipynb index faaca9c3..0e026e32 100644 --- a/eval_model_regression.ipynb +++ b/eval_model_regression.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "code", - "execution_count": 1, + "execution_count": 23, "id": "initial_id", "metadata": { "ExecuteTime": { @@ -11,14 +11,6 @@ } }, "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/Users/ctumes/Cheb-AI/chebai_env/lib/python3.9/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n", - " from .autonotebook import tqdm as notebook_tqdm\n" - ] - }, { "name": "stdout", "output_type": "stream", @@ -29,12 +21,19 @@ ], "source": [ "import pandas as pd\n", + "import seaborn as sns\n", + "import matplotlib.pyplot as plt\n", + "\n", + "from sklearn.metrics import r2_score\n", + "\n", "\n", "from chebai.result.utils import (\n", " evaluate_model,\n", + " evaluate_model_regression,\n", " load_results_from_buffer,\n", ")\n", "from chebai.result.classification import print_metrics\n", + "from chebai.result.regression import print_metrics\n", "from chebai.models.electra import Electra\n", "from chebai.preprocessing.datasets.solCuration import SolCuration, SolCurationChem\n", "from chebai.preprocessing.datasets.tox21 import Tox21MolNet\n", @@ -49,7 +48,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 2, "id": "bdb5fc6919cf72be", "metadata": { "ExecuteTime": { @@ -87,7 +86,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 3, "id": "fa1276b47def696c", "metadata": { "ExecuteTime": { @@ -97,54 +96,61 @@ }, "outputs": [ { - "ename": "FileNotFoundError", - "evalue": "[Errno 2] No such file or directory: '/Users/ctumes/python-chebai/logs/best_epoch=29_val_loss=0.5266_val_mse=10.9920.ckpt'", - "output_type": "error", - "traceback": [ - "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[0;31mFileNotFoundError\u001b[0m Traceback (most recent call last)", - "Cell \u001b[0;32mIn[5], line 2\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[38;5;66;03m# evaluates model, stores results in buffer_dir\u001b[39;00m\n\u001b[0;32m----> 2\u001b[0m model \u001b[38;5;241m=\u001b[39m \u001b[43mmodel_class\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mload_from_checkpoint\u001b[49m\u001b[43m(\u001b[49m\u001b[43mcheckpoint_path\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 3\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m buffer_dir \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m:\n\u001b[1;32m 4\u001b[0m preds, labels \u001b[38;5;241m=\u001b[39m evaluate_model(\n\u001b[1;32m 5\u001b[0m model,\n\u001b[1;32m 6\u001b[0m data_module,\n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 11\u001b[0m kind\u001b[38;5;241m=\u001b[39mkind,\n\u001b[1;32m 12\u001b[0m )\n", - "File \u001b[0;32m~/Cheb-AI/chebai_env/lib/python3.9/site-packages/lightning/pytorch/core/module.py:1552\u001b[0m, in \u001b[0;36mLightningModule.load_from_checkpoint\u001b[0;34m(cls, checkpoint_path, map_location, hparams_file, strict, **kwargs)\u001b[0m\n\u001b[1;32m 1471\u001b[0m \u001b[38;5;129m@_restricted_classmethod\u001b[39m\n\u001b[1;32m 1472\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mload_from_checkpoint\u001b[39m(\n\u001b[1;32m 1473\u001b[0m \u001b[38;5;28mcls\u001b[39m,\n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 1478\u001b[0m \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs: Any,\n\u001b[1;32m 1479\u001b[0m ) \u001b[38;5;241m-\u001b[39m\u001b[38;5;241m>\u001b[39m Self:\n\u001b[1;32m 1480\u001b[0m \u001b[38;5;250m \u001b[39m\u001b[38;5;124mr\u001b[39m\u001b[38;5;124;03m\"\"\"Primary way of loading a model from a checkpoint. When Lightning saves a checkpoint it stores the arguments\u001b[39;00m\n\u001b[1;32m 1481\u001b[0m \u001b[38;5;124;03m passed to ``__init__`` in the checkpoint under ``\"hyper_parameters\"``.\u001b[39;00m\n\u001b[1;32m 1482\u001b[0m \n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 1550\u001b[0m \n\u001b[1;32m 1551\u001b[0m \u001b[38;5;124;03m \"\"\"\u001b[39;00m\n\u001b[0;32m-> 1552\u001b[0m loaded \u001b[38;5;241m=\u001b[39m \u001b[43m_load_from_checkpoint\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 1553\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;28;43mcls\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;66;43;03m# type: ignore[arg-type]\u001b[39;49;00m\n\u001b[1;32m 1554\u001b[0m \u001b[43m \u001b[49m\u001b[43mcheckpoint_path\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 1555\u001b[0m \u001b[43m \u001b[49m\u001b[43mmap_location\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 1556\u001b[0m \u001b[43m \u001b[49m\u001b[43mhparams_file\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 1557\u001b[0m \u001b[43m \u001b[49m\u001b[43mstrict\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 1558\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 1559\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 1560\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m cast(Self, loaded)\n", - "File \u001b[0;32m~/Cheb-AI/chebai_env/lib/python3.9/site-packages/lightning/pytorch/core/saving.py:61\u001b[0m, in \u001b[0;36m_load_from_checkpoint\u001b[0;34m(cls, checkpoint_path, map_location, hparams_file, strict, **kwargs)\u001b[0m\n\u001b[1;32m 59\u001b[0m map_location \u001b[38;5;241m=\u001b[39m map_location \u001b[38;5;129;01mor\u001b[39;00m _default_map_location\n\u001b[1;32m 60\u001b[0m \u001b[38;5;28;01mwith\u001b[39;00m pl_legacy_patch():\n\u001b[0;32m---> 61\u001b[0m checkpoint \u001b[38;5;241m=\u001b[39m \u001b[43mpl_load\u001b[49m\u001b[43m(\u001b[49m\u001b[43mcheckpoint_path\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mmap_location\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mmap_location\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 63\u001b[0m \u001b[38;5;66;03m# convert legacy checkpoints to the new format\u001b[39;00m\n\u001b[1;32m 64\u001b[0m checkpoint \u001b[38;5;241m=\u001b[39m _pl_migrate_checkpoint(\n\u001b[1;32m 65\u001b[0m checkpoint, checkpoint_path\u001b[38;5;241m=\u001b[39m(checkpoint_path \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28misinstance\u001b[39m(checkpoint_path, (\u001b[38;5;28mstr\u001b[39m, Path)) \u001b[38;5;28;01melse\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m)\n\u001b[1;32m 66\u001b[0m )\n", - "File \u001b[0;32m~/Cheb-AI/chebai_env/lib/python3.9/site-packages/lightning/fabric/utilities/cloud_io.py:54\u001b[0m, in \u001b[0;36m_load\u001b[0;34m(path_or_url, map_location)\u001b[0m\n\u001b[1;32m 49\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m torch\u001b[38;5;241m.\u001b[39mhub\u001b[38;5;241m.\u001b[39mload_state_dict_from_url(\n\u001b[1;32m 50\u001b[0m \u001b[38;5;28mstr\u001b[39m(path_or_url),\n\u001b[1;32m 51\u001b[0m map_location\u001b[38;5;241m=\u001b[39mmap_location, \u001b[38;5;66;03m# type: ignore[arg-type]\u001b[39;00m\n\u001b[1;32m 52\u001b[0m )\n\u001b[1;32m 53\u001b[0m fs \u001b[38;5;241m=\u001b[39m get_filesystem(path_or_url)\n\u001b[0;32m---> 54\u001b[0m \u001b[38;5;28;01mwith\u001b[39;00m \u001b[43mfs\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mopen\u001b[49m\u001b[43m(\u001b[49m\u001b[43mpath_or_url\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mrb\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m)\u001b[49m \u001b[38;5;28;01mas\u001b[39;00m f:\n\u001b[1;32m 55\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m torch\u001b[38;5;241m.\u001b[39mload(f, map_location\u001b[38;5;241m=\u001b[39mmap_location)\n", - "File \u001b[0;32m~/Cheb-AI/chebai_env/lib/python3.9/site-packages/fsspec/spec.py:1303\u001b[0m, in \u001b[0;36mAbstractFileSystem.open\u001b[0;34m(self, path, mode, block_size, cache_options, compression, **kwargs)\u001b[0m\n\u001b[1;32m 1301\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[1;32m 1302\u001b[0m ac \u001b[38;5;241m=\u001b[39m kwargs\u001b[38;5;241m.\u001b[39mpop(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mautocommit\u001b[39m\u001b[38;5;124m\"\u001b[39m, \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_intrans)\n\u001b[0;32m-> 1303\u001b[0m f \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_open\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 1304\u001b[0m \u001b[43m \u001b[49m\u001b[43mpath\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 1305\u001b[0m \u001b[43m \u001b[49m\u001b[43mmode\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mmode\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 1306\u001b[0m \u001b[43m \u001b[49m\u001b[43mblock_size\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mblock_size\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 1307\u001b[0m \u001b[43m \u001b[49m\u001b[43mautocommit\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mac\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 1308\u001b[0m \u001b[43m \u001b[49m\u001b[43mcache_options\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mcache_options\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 1309\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 1310\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 1311\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m compression \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m:\n\u001b[1;32m 1312\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01mfsspec\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mcompression\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m compr\n", - "File \u001b[0;32m~/Cheb-AI/chebai_env/lib/python3.9/site-packages/fsspec/implementations/local.py:191\u001b[0m, in \u001b[0;36mLocalFileSystem._open\u001b[0;34m(self, path, mode, block_size, **kwargs)\u001b[0m\n\u001b[1;32m 189\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mauto_mkdir \u001b[38;5;129;01mand\u001b[39;00m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mw\u001b[39m\u001b[38;5;124m\"\u001b[39m \u001b[38;5;129;01min\u001b[39;00m mode:\n\u001b[1;32m 190\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mmakedirs(\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_parent(path), exist_ok\u001b[38;5;241m=\u001b[39m\u001b[38;5;28;01mTrue\u001b[39;00m)\n\u001b[0;32m--> 191\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mLocalFileOpener\u001b[49m\u001b[43m(\u001b[49m\u001b[43mpath\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mmode\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mfs\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n", - "File \u001b[0;32m~/Cheb-AI/chebai_env/lib/python3.9/site-packages/fsspec/implementations/local.py:355\u001b[0m, in \u001b[0;36mLocalFileOpener.__init__\u001b[0;34m(self, path, mode, autocommit, fs, compression, **kwargs)\u001b[0m\n\u001b[1;32m 353\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mcompression \u001b[38;5;241m=\u001b[39m get_compression(path, compression)\n\u001b[1;32m 354\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mblocksize \u001b[38;5;241m=\u001b[39m io\u001b[38;5;241m.\u001b[39mDEFAULT_BUFFER_SIZE\n\u001b[0;32m--> 355\u001b[0m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_open\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n", - "File \u001b[0;32m~/Cheb-AI/chebai_env/lib/python3.9/site-packages/fsspec/implementations/local.py:360\u001b[0m, in \u001b[0;36mLocalFileOpener._open\u001b[0;34m(self)\u001b[0m\n\u001b[1;32m 358\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mf \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m \u001b[38;5;129;01mor\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mf\u001b[38;5;241m.\u001b[39mclosed:\n\u001b[1;32m 359\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mautocommit \u001b[38;5;129;01mor\u001b[39;00m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mw\u001b[39m\u001b[38;5;124m\"\u001b[39m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;129;01min\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mmode:\n\u001b[0;32m--> 360\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mf \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mopen\u001b[39;49m\u001b[43m(\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mpath\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mmode\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mmode\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 361\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mcompression:\n\u001b[1;32m 362\u001b[0m compress \u001b[38;5;241m=\u001b[39m compr[\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mcompression]\n", - "\u001b[0;31mFileNotFoundError\u001b[0m: [Errno 2] No such file or directory: '/Users/ctumes/python-chebai/logs/best_epoch=29_val_loss=0.5266_val_mse=10.9920.ckpt'" + "name": "stdout", + "output_type": "stream", + "text": [ + "\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\n", + "00%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 41/41 [00:04<00:00, 8.99it/s]" ] } ], "source": [ "# evaluates model, stores results in buffer_dir\n", + "# print(buffer_dir)\n", "model = model_class.load_from_checkpoint(checkpoint_path)\n", - "if buffer_dir is None:\n", - " preds, labels = evaluate_model(\n", - " model,\n", - " data_module,\n", - " buffer_dir=buffer_dir,\n", - " # No need to provide this parameter for Chebi dataset, \"kind\" parameter should be provided\n", - " # filename=data_module.processed_file_names_dict[kind],\n", - " batch_size=10,\n", - " kind=kind,\n", - " )\n", - "else:\n", - " evaluate_model(\n", + "model.model_type = 'regression'\n", + "\n", + "preds_list, labels_list = evaluate_model_regression(\n", " model,\n", " data_module,\n", - " buffer_dir=buffer_dir,\n", + " buffer_dir='/Users/ctumes/Cheb-AI/chebai_helper/',\n", " # No need to provide this parameter for Chebi dataset, \"kind\" parameter should be provided\n", - " # filename=data_module.processed_file_names_dict[kind],\n", - " batch_size=10,\n", + " filename='/Users/ctumes/python-chebai/data/SolCuration/processed/smiles_token/test.pt',\n", + " batch_size=32,\n", " kind=kind,\n", - " )\n", - " # load data from buffer_dir\n", - " preds, labels = load_results_from_buffer(buffer_dir, device=DEVICE)" + ")\n", + "# load data from buffer_dir\n", + "# preds, labels = load_results_from_buffer(buffer_dir, device=DEVICE)" ] }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 4, + "id": "e2500283-7b37-4ce9-b044-f4eb64876a5b", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "regression\n" + ] + } + ], + "source": [ + "print(model.model_type)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, "id": "201f750c475b4677", "metadata": { "collapsed": false, @@ -152,16 +158,40 @@ "outputs_hidden": false } }, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "tensor([[-2.1897],\n", + " [-3.9141],\n", + " [-5.6935],\n", + " ...,\n", + " [-0.0093],\n", + " [-5.4903],\n", + " [-5.1491]], device='mps:0', grad_fn=)\n", + "tensor([[-1.9250],\n", + " [-6.4172],\n", + " [-4.8800],\n", + " ...,\n", + " [-0.1700],\n", + " [-4.8041],\n", + " [-4.9500]], device='mps:0')\n", + "1300\n", + "1300\n" + ] + } + ], "source": [ - "# Load classes from the classes.txt\n", - "with open(os.path.join(data_module.processed_dir_main, \"classes.txt\"), \"r\") as f:\n", - " classes = [line.strip() for line in f.readlines()]" + "print(preds_list)\n", + "print(labels_list)\n", + "print(len(preds_list))\n", + "print(len(labels_list))" ] }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 6, "id": "e567cd2fb1718baf", "metadata": { "collapsed": false, @@ -174,39 +204,83 @@ "name": "stdout", "output_type": "stream", "text": [ - "Macro-F1: 0.290936\n", - "Micro-F1: 0.890380\n", - "Balanced Accuracy: 0.507610\n", - "Macro-Precision: 0.021964\n", - "Micro-Precision: 0.908676\n", - "Macro-Recall: 0.020987\n", - "Micro-Recall: 0.872807\n", - "Top 10 classes (F1-score):\n", - "1. 23367 - F1: 1.000000\n", - "2. 33259 - F1: 1.000000\n", - "3. 36914 - F1: 1.000000\n", - "4. 24431 - F1: 1.000000\n", - "5. 33238 - F1: 1.000000\n", - "6. 36357 - F1: 1.000000\n", - "7. 37577 - F1: 1.000000\n", - "8. 24867 - F1: 1.000000\n", - "9. 33579 - F1: 0.974026\n", - "10. 24866 - F1: 0.973684\n", - "Found 63 classes with F1-score == 0 (and non-zero labels): 17792, 22563, 22632, 22712, 24062, 24834, 25108, 25693, 25697, 25698, 25699, 25806, 26151, 26217, 26218, 26421, 26469, 29347, 32988, 33240, 33256, 33296, 33299, 33304, 33597, 33598, 33635, 33655, 33659, 33661, 33670, 33671, 33836, 33976, 35217, 35273, 35479, 35618, 36364, 36562, 36916, 36962, 36963, 37141, 37143, 37622, 37929, 37960, 38101, 38104, 38166, 38835, 39203, 46850, 47704, 47916, 48592, 50047, 50995, 72544, 79389, 83565, 139358\n" + "MSE: 0.5591921806335449\n" ] } ], "source": [ "# output relevant metrics\n", "print_metrics(\n", - " preds,\n", - " labels.to(torch.int),\n", + " preds_list,\n", + " labels_list,\n", " DEVICE,\n", - " classes=classes,\n", " markdown_output=False,\n", - " top_k=10,\n", ")" ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "d1c04107-f374-4496-9053-fce90bb544ff", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[[-2.1897364]\n", + " [-3.9141192]\n", + " [-5.693497 ]\n", + " ...\n", + " [-0.0092898]\n", + " [-5.4903164]\n", + " [-5.1490617]]\n" + ] + } + ], + "source": [ + "print(preds_list.cpu().detach().numpy())" + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "id": "876db3f9-c6ad-46e9-9f3f-3d41d7767410", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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/fffP7vtW2+9xVFHHdXKZO3EE09k9erVNDY25j3nhAkTGDBgAPfddx/pdJpEIsF9993Hvvvuy+jRowFIpVLbtRgHg0E2btzI+vXrC34GEolEkkUIuPdemDIFXnsNvve9nJfEzr6W/sMVV1whRo0aJTZs2NDuNjszovLv9evF3n/7mxh8xx1iv/vuEwf94x9iv/vuE4PvuEPs/be/iX+vX9+t43bGF7/4RXHLLbcIIYSYMWOG+M1vfiN8Pp9oaWkRGzduFECryMNvfvOb7T5R57LPPvuI0047Le9rDQ0NAhC/+93vhBCZ6EIoFMpGUIQQ4vvf/76YNm1a9vu2kREhhPjRj34kJkyY0OoT/B133CHC4XA2spG73yeffCIA8eabb2a337p1qwgGg+Kxxx4TQghxzjnniFNOOaXVec4///xsREUIIR599FFRXl4uksmkEEKIZcuWCUVRslGeQvjZz34mDMMQdXV17W5j27Y45ZRTxOGHH97q5xs3bhRr1qxp92vdunXZbY8//nhx2WWXtdr/gw8+EID48MMP2z33e++9J/baay+hqqpQVVVMmDCh1XHvvvtuEQqFxCuvvCJs2xarV68W++yzjwDEf//737zHlBEViUTSLpGIELNmCZGRK0KcdJIQOX8f24vUdpVdLqLy7W9/m3/961+89tprDB8+vN3t/H4/JSUlrb56A0cIblq8mJZ0mmHhMEHDQFUUgobBsHCYlnSamxYvxukFZXn00UezcOFChBC8/vrrnHHGGey777688cYbLFq0iKFDhzJu3Ljs9vPnz+fUU0/t8JiiC9c5evRoiouLs98PGTKEurq6Dvf56KOPOPTQQ1s5/x5++OFEo1E2btyYd3td15k2bVr2ZwMGDGDChAl89NFHAKxevZqpU6e22q/t9zNmzEDTNJ566ikAHnjgAY455phstKFQRo0aRWVlZbuvX3nllbz//vs88sgjrX4+bNgw9t5773a/Ro0a1aXraEsikeCSSy7h8MMP5+233+bNN9/kC1/4AqeccgqJRAKASy+9lG9/+9t85Stfwefzccghh3DuuecC0i9FIpF0kRUrYPJkmDsXNA1uugmeew5y/j7ubIf3Pv8rJoTg29/+Nk899RSvvvoqY8aM6etLAmB5bS2rGxoYEAhs90tRFIWKQIDVDQ0sr63t8XNPnz6dN954g5UrV2IYBvvssw/Tp09n4cKFLFq0iKOPPjq77ZYtW1i+fDmnnHJKu8cbP358dvFvi/fz8ePHZ39mGEarbRRFwXGcHbmlXsPn83HhhRdy//33k06nmTt3Ll//+te7fJyioqJ2X+tIRHcl9VNVVUVtm/eL931VVVXec8+dO5d169Zx//33M2XKFA455BDmzp3L2rVrmT9/PpD5/fzud78jGo2yfv16ampqsoJu7NixXX4WEolkD0QIuPNOOOQQ+OQTGD4cFi2CH/wA+vgDT59b6F955ZXMnTuX+fPnU1xcTE1NDQClpaUEg8E+u66tiQRpx8Gv539EAV2nMZViq/uptifx6lRuu+22rCiZPn06N910E42NjVx33XXZbZ999lkOO+ywVnUTbTn33HOZNWsWzz777HZ1Kn/4wx8YMGAAxx9/fMHX5/P5tptEve+++/Lkk08ihMgKuzfffJPi4uK8EbJ9990Xy7JYvHgxhx12GADbtm1j9erV7LfffkCmPuOdd95ptV/b7wG+8Y1v8IUvfIG//vWvWJbFGWecUfC9dIQQgquuuoqnnnqKhQsX5hXR9957bzaykY9c0XfooYfy4x//GNM0sz9/+eWXmTBhAuXl5Xn3j8fjqKraSix737cVj5qmMWzYMAAefvhhDj300A6jRBKJRAJAczNceik8/njm+698BR54AAYM6NPL8ujziMqdd95Jc3Mz06dPZ8iQIdmvRx99tE+va2AwiE9VSVlW3teTloVPVRnYC2KqvLyciRMn8tBDD2WLZo866ijeffddPvnkk1YRlWeeeabTtM+5557L6aefzte+9jXuu+8+1q1bx6pVq7j88st55plnuPfeezuMKLRl9OjRLF68mHXr1rF161Ycx+Fb3/oWGzZs4KqrruLjjz9m/vz5/OxnP+Paa6/Nm34YN24cp512Gpdeemk2ejR79myGDRvGaaedBsBVV13F888/z6233sqaNWu4++67eeGFF7aLcO27774ccsgh/OAHP+C8887rMYF75ZVXMmfOHObOnZsV0TU1Na2ESVdSP7NmzcLn83HJJZfwwQcf8Oijj3L77bdz7bXXZrd56qmn2GeffbLfH3/88TQ2NnLllVfy0Ucf8cEHH3DxxRej6zrHHHMMAFu3buWuu+7i448/ZsWKFVx99dU8/vjj/PGPf+yR5yCRSHZjli6FL34xI1J0Hf7w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R8LG0m4CleYoKCigtLSUgoICdF2P1ko477zzSElJYfv27QwYMIDBgwczZsyY6HqlxWJp0WAokRq7B4MGDeKSSy7hvvvu46WXXqKwsJAJEyYwevToNtlltF+/fmRkZDBixAiee+45XC4X//jHP9i3bx9DhgxJ9PBiLi0tjUceeYSJEyeSm5tLt27d+POf/wzAsGHDEjy62OvatWut1ykpKQCce+65CftbZrwdPnyY/v37061bN/7yl79QUlISfa+tzjCMGTOGESNGcM0119CnTx9effVVPB4PDzzwQKKHFnOjR4/m448/Zu7cuaSmpkafc+np6bhcrvgOJu77jM4AI0aMMIETvpYuXWqapmlOnDix3ve7deuW0HG3pMbugWma5v79+82bb77ZdLlcZqdOncyxY8eaoVAocYOOsQ0bNpiDBg0yMzMzzdTUVPMnP/mJuWDBgkQPK26CwaA5duxYMzs720xNTTUHDhxobt++PdHDSoh9+/a1u+3JU6dOrfd3Qlt/jLz++utm165dTbvdbvbp08dct25doocUFyf7dz116tS4j0UJD0gIIYQQotVpuwuMQgghhDjjSaAihBBCiFZLAhUhhBBCtFoSqAghhBCi1ZJARQghhBCtlgQqQgghhGi1JFARQgghRKslgYoQQgghWi0JVIQQQgjRakmgIoQQQohWSwIVIYQQQrRaEqgIIYQQotX6//xmZr2WGo19AAAAAElFTkSuQmCC", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "labels = labels_list.cpu().detach().numpy()\n", + "preds = preds_list.cpu().detach().numpy()\n", + "\n", + "r2_with = r2_score(labels, preds)\n", + "# r2_without = r2_score(y_real, y_noonto)\n", + "\n", + "ax1 = sns.regplot(x=labels, y=preds, label=f'w/ Ontology, r2={r2_with:.2}', color='darkcyan')\n", + "# ax2 = sns.regplot(x=y_real, y=y_noonto, color='black')\n", + "bla = [-12,-11,-10,-9,-8,-7, -6, -5, -4, -3, -2, -1, 0, 1,2]\n", + "ax1.legend(loc=\"best\")\n", + "plt.plot(bla, bla, color='red')\n", + "plt.savefig('../scatter_regression_december2.png')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "21d206ae-4ee3-4a42-a471-57c98e528ea9", + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": { From dbf8532987ca18aaede93c62f44e20eeb2545a24 Mon Sep 17 00:00:00 2001 From: schnamo Date: Fri, 20 Dec 2024 20:54:27 +0100 Subject: [PATCH 16/54] regression adjustments --- chebai/callbacks.py | 1 + chebai/train.py | 2 + chebai/trainer/CustomTrainer.py | 10 +- configs/model/electra.yml | 2 +- configs/training/default_trainer.yml | 2 +- eval_model_regression.ipynb | 141 ++++++++++++++++++++++++--- 6 files changed, 141 insertions(+), 17 deletions(-) diff --git a/chebai/callbacks.py b/chebai/callbacks.py index 764db443..52029dd2 100644 --- a/chebai/callbacks.py +++ b/chebai/callbacks.py @@ -79,6 +79,7 @@ def write_on_epoch_end( labels = labels.tolist() else: labels = [None for _ in idents] + # todo: here adjust for regression !!! output = torch.sigmoid(p["output"]["logits"]).tolist() for i, l, o in zip(idents, labels, output): pred_list.append(dict(ident=i, labels=l, predictions=o)) diff --git a/chebai/train.py b/chebai/train.py index afc039cb..d69d6094 100644 --- a/chebai/train.py +++ b/chebai/train.py @@ -46,6 +46,7 @@ def eval_model( for batch in dataset: for molecule, label in batch: model_outputs = model(molecule) + # todo: this is also just for classification, adjust to regression prediction = [1.0 if i > 0.5 else 0.0 for i in model_outputs] predictions.append(prediction) raw_values.append(model_outputs) @@ -146,6 +147,7 @@ def _execute( prediction = model(molecules) loss = loss_fn(prediction, labels) data_size += 1 + # todo: this is also just for classification, adjust to regression f1 += f1_score(prediction > 0.5, labels > 0.5, average="micro") train_running_loss += loss.item() diff --git a/chebai/trainer/CustomTrainer.py b/chebai/trainer/CustomTrainer.py index 2690be58..1638c450 100644 --- a/chebai/trainer/CustomTrainer.py +++ b/chebai/trainer/CustomTrainer.py @@ -123,9 +123,13 @@ def _predict_smiles( ) features = torch.cat((cls_tokens, x), dim=1) model_output = model({"features": features}) - # todo: adjust this later with flag - preds = model_output["logits"] - # preds = torch.sigmoid(model_output["logits"]) + print(model.model_type) + # todo: check again + if model.model_type == 'regression': + # todo: do we actually have logits here? + preds = model_output["logits"] + else: + preds = torch.sigmoid(model_output["logits"]) print(preds.shape) return preds diff --git a/configs/model/electra.yml b/configs/model/electra.yml index 56de72eb..4beaed65 100644 --- a/configs/model/electra.yml +++ b/configs/model/electra.yml @@ -1,7 +1,7 @@ class_path: chebai.models.Electra init_args: optimizer_kwargs: - lr: 1e-1 + lr: 1e-3 config: model_type: regression vocab_size: 1400 diff --git a/configs/training/default_trainer.yml b/configs/training/default_trainer.yml index 0ce68a49..91aa4244 100644 --- a/configs/training/default_trainer.yml +++ b/configs/training/default_trainer.yml @@ -1,4 +1,4 @@ -min_epochs: 20 +min_epochs: 100 max_epochs: 100 default_root_dir: &default_root_dir logs logger: csv_logger.yml diff --git a/eval_model_regression.ipynb b/eval_model_regression.ipynb index 0e026e32..287f6df7 100644 --- a/eval_model_regression.ipynb +++ b/eval_model_regression.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "code", - "execution_count": 23, + "execution_count": 1, "id": "initial_id", "metadata": { "ExecuteTime": { @@ -11,6 +11,14 @@ } }, "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/ctumes/Cheb-AI/chebai_env/lib/python3.9/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n", + " from .autonotebook import tqdm as notebook_tqdm\n" + ] + }, { "name": "stdout", "output_type": "stream", @@ -46,9 +54,17 @@ "print(DEVICE)" ] }, + { + "cell_type": "markdown", + "id": "a867fc46-b9c7-42d1-8c4b-030ebd3470d0", + "metadata": {}, + "source": [ + "## With Ontology finetuning" + ] + }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 18, "id": "bdb5fc6919cf72be", "metadata": { "ExecuteTime": { @@ -71,7 +87,7 @@ ], "source": [ "# specify the checkpoint name\n", - "checkpoint_name = \"solFinetuningAGAIN_MSE/version_1/checkpoints/best_epoch=29_val_loss=0.5266_val_mse=10.9920\"\n", + "checkpoint_name = \"solFinetuningAGAIN_MSE2/version_0/checkpoints/per_epoch=99_val_loss=0.5071_val_mse=11.0217\"\n", "checkpoint_path = os.path.join(\"logs\", f\"{checkpoint_name}.ckpt\")\n", "kind = \"test\" # replace with \"train\" / \"validation\" to run on train / validation sets\n", "buffer_dir = os.path.join(\"results_buffer\", checkpoint_name, kind)\n", @@ -86,7 +102,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 19, "id": "fa1276b47def696c", "metadata": { "ExecuteTime": { @@ -107,7 +123,7 @@ "output_type": "stream", "text": [ "\n", - "00%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 41/41 [00:04<00:00, 8.99it/s]" + "00%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 41/41 [00:45<00:00, 1.10s/it]" ] } ], @@ -191,7 +207,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 20, "id": "e567cd2fb1718baf", "metadata": { "collapsed": false, @@ -204,7 +220,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "MSE: 0.5591921806335449\n" + "MSE: 0.5399385690689087\n" ] } ], @@ -220,7 +236,7 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 7, "id": "d1c04107-f374-4496-9053-fce90bb544ff", "metadata": {}, "outputs": [ @@ -242,15 +258,113 @@ "print(preds_list.cpu().detach().numpy())" ] }, + { + "cell_type": "markdown", + "id": "97fc34b6-22aa-4c36-a0ad-3e0d4e0a0649", + "metadata": {}, + "source": [ + "## Without Ontology Finetuning" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "604f800f-752b-424f-afcc-0d25fa706fe9", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[False, False, False]\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Some weights of ElectraModel were not initialized from the model checkpoint at None and are newly initialized: ['embeddings.LayerNorm.bias', 'embeddings.LayerNorm.weight', 'embeddings.position_embeddings.weight', 'embeddings.token_type_embeddings.weight', 'embeddings.word_embeddings.weight', 'embeddings_project.bias', 'embeddings_project.weight', 'encoder.layer.0.attention.output.LayerNorm.bias', 'encoder.layer.0.attention.output.LayerNorm.weight', 'encoder.layer.0.attention.output.dense.bias', 'encoder.layer.0.attention.output.dense.weight', 'encoder.layer.0.attention.self.key.bias', 'encoder.layer.0.attention.self.key.weight', 'encoder.layer.0.attention.self.query.bias', 'encoder.layer.0.attention.self.query.weight', 'encoder.layer.0.attention.self.value.bias', 'encoder.layer.0.attention.self.value.weight', 'encoder.layer.0.intermediate.dense.bias', 'encoder.layer.0.intermediate.dense.weight', 'encoder.layer.0.output.LayerNorm.bias', 'encoder.layer.0.output.LayerNorm.weight', 'encoder.layer.0.output.dense.bias', 'encoder.layer.0.output.dense.weight', 'encoder.layer.1.attention.output.LayerNorm.bias', 'encoder.layer.1.attention.output.LayerNorm.weight', 'encoder.layer.1.attention.output.dense.bias', 'encoder.layer.1.attention.output.dense.weight', 'encoder.layer.1.attention.self.key.bias', 'encoder.layer.1.attention.self.key.weight', 'encoder.layer.1.attention.self.query.bias', 'encoder.layer.1.attention.self.query.weight', 'encoder.layer.1.attention.self.value.bias', 'encoder.layer.1.attention.self.value.weight', 'encoder.layer.1.intermediate.dense.bias', 'encoder.layer.1.intermediate.dense.weight', 'encoder.layer.1.output.LayerNorm.bias', 'encoder.layer.1.output.LayerNorm.weight', 'encoder.layer.1.output.dense.bias', 'encoder.layer.1.output.dense.weight', 'encoder.layer.2.attention.output.LayerNorm.bias', 'encoder.layer.2.attention.output.LayerNorm.weight', 'encoder.layer.2.attention.output.dense.bias', 'encoder.layer.2.attention.output.dense.weight', 'encoder.layer.2.attention.self.key.bias', 'encoder.layer.2.attention.self.key.weight', 'encoder.layer.2.attention.self.query.bias', 'encoder.layer.2.attention.self.query.weight', 'encoder.layer.2.attention.self.value.bias', 'encoder.layer.2.attention.self.value.weight', 'encoder.layer.2.intermediate.dense.bias', 'encoder.layer.2.intermediate.dense.weight', 'encoder.layer.2.output.LayerNorm.bias', 'encoder.layer.2.output.LayerNorm.weight', 'encoder.layer.2.output.dense.bias', 'encoder.layer.2.output.dense.weight', 'encoder.layer.3.attention.output.LayerNorm.bias', 'encoder.layer.3.attention.output.LayerNorm.weight', 'encoder.layer.3.attention.output.dense.bias', 'encoder.layer.3.attention.output.dense.weight', 'encoder.layer.3.attention.self.key.bias', 'encoder.layer.3.attention.self.key.weight', 'encoder.layer.3.attention.self.query.bias', 'encoder.layer.3.attention.self.query.weight', 'encoder.layer.3.attention.self.value.bias', 'encoder.layer.3.attention.self.value.weight', 'encoder.layer.3.intermediate.dense.bias', 'encoder.layer.3.intermediate.dense.weight', 'encoder.layer.3.output.LayerNorm.bias', 'encoder.layer.3.output.LayerNorm.weight', 'encoder.layer.3.output.dense.bias', 'encoder.layer.3.output.dense.weight', 'encoder.layer.4.attention.output.LayerNorm.bias', 'encoder.layer.4.attention.output.LayerNorm.weight', 'encoder.layer.4.attention.output.dense.bias', 'encoder.layer.4.attention.output.dense.weight', 'encoder.layer.4.attention.self.key.bias', 'encoder.layer.4.attention.self.key.weight', 'encoder.layer.4.attention.self.query.bias', 'encoder.layer.4.attention.self.query.weight', 'encoder.layer.4.attention.self.value.bias', 'encoder.layer.4.attention.self.value.weight', 'encoder.layer.4.intermediate.dense.bias', 'encoder.layer.4.intermediate.dense.weight', 'encoder.layer.4.output.LayerNorm.bias', 'encoder.layer.4.output.LayerNorm.weight', 'encoder.layer.4.output.dense.bias', 'encoder.layer.4.output.dense.weight', 'encoder.layer.5.attention.output.LayerNorm.bias', 'encoder.layer.5.attention.output.LayerNorm.weight', 'encoder.layer.5.attention.output.dense.bias', 'encoder.layer.5.attention.output.dense.weight', 'encoder.layer.5.attention.self.key.bias', 'encoder.layer.5.attention.self.key.weight', 'encoder.layer.5.attention.self.query.bias', 'encoder.layer.5.attention.self.query.weight', 'encoder.layer.5.attention.self.value.bias', 'encoder.layer.5.attention.self.value.weight', 'encoder.layer.5.intermediate.dense.bias', 'encoder.layer.5.intermediate.dense.weight', 'encoder.layer.5.output.LayerNorm.bias', 'encoder.layer.5.output.LayerNorm.weight', 'encoder.layer.5.output.dense.bias', 'encoder.layer.5.output.dense.weight']\n", + "You should probably TRAIN this model on a down-stream task to be able to use it for predictions and inference.\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\n", + "00%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 41/41 [00:51<00:00, 1.26s/it]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "MSE: 0.5454557538032532\n" + ] + } + ], + "source": [ + "# specify the checkpoint name\n", + "checkpoint_name = \"solFinetuningAGAIN_MSE_NO_ONTO/version_0/checkpoints/per_epoch=74_val_loss=0.6037_val_mse=11.0069\"\n", + "checkpoint_path = os.path.join(\"logs\", f\"{checkpoint_name}.ckpt\")\n", + "kind = \"test\" # replace with \"train\" / \"validation\" to run on train / validation sets\n", + "buffer_dir = os.path.join(\"results_buffer\", checkpoint_name, kind)\n", + "# make sure to use the same data module and model class that were used during training\n", + "data_module2 = SolCurationChem()\n", + "# load chebi data if missing and perform dynamic splits\n", + "data_module2.prepare_data()\n", + "data_module2.setup()\n", + "\n", + "model_class2 = Electra\n", + "\n", + "# evaluates model, stores results in buffer_dir\n", + "# print(buffer_dir)\n", + "model2 = model_class2.load_from_checkpoint(checkpoint_path)\n", + "model2.model_type = 'regression'\n", + "\n", + "preds_list2, labels_list2 = evaluate_model_regression(\n", + " model2,\n", + " data_module2,\n", + " buffer_dir='/Users/ctumes/Cheb-AI/chebai_helper/',\n", + " # No need to provide this parameter for Chebi dataset, \"kind\" parameter should be provided\n", + " filename='/Users/ctumes/python-chebai/data/SolCuration/processed/smiles_token/test.pt',\n", + " batch_size=32,\n", + " kind=kind,\n", + ")\n", + "\n", + "# output relevant metrics\n", + "print_metrics(\n", + " preds_list2,\n", + " labels_list2,\n", + " DEVICE,\n", + " markdown_output=False,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "c1aa283a-161a-447a-9b28-8452ff7a413a", + "metadata": {}, + "source": [ + "## Plot everything" + ] + }, { "cell_type": "code", - "execution_count": 29, + "execution_count": 22, "id": "876db3f9-c6ad-46e9-9f3f-3d41d7767410", "metadata": {}, "outputs": [ { "data": { - "image/png": "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/fffP7vtW2+9xVFHHdXKZO3EE09k9erVNDY25j3nhAkTGDBgAPfddx/pdJpEIsF9993Hvvvuy+jRowFIpVLbtRgHg0E2btzI+vXrC34GEolEkkUIuPdemDIFXnsNvve9nJfEzr6W/sMVV1whRo0aJTZs2NDuNjszovLv9evF3n/7mxh8xx1iv/vuEwf94x9iv/vuE4PvuEPs/be/iX+vX9+t43bGF7/4RXHLLbcIIYSYMWOG+M1vfiN8Pp9oaWkRGzduFECryMNvfvOb7T5R57LPPvuI0047Le9rDQ0NAhC/+93vhBCZ6EIoFMpGUIQQ4vvf/76YNm1a9vu2kREhhPjRj34kJkyY0OoT/B133CHC4XA2spG73yeffCIA8eabb2a337p1qwgGg+Kxxx4TQghxzjnniFNOOaXVec4///xsREUIIR599FFRXl4uksmkEEKIZcuWCUVRslGeQvjZz34mDMMQdXV17W5j27Y45ZRTxOGHH97q5xs3bhRr1qxp92vdunXZbY8//nhx2WWXtdr/gw8+EID48MMP2z33e++9J/baay+hqqpQVVVMmDCh1XHvvvtuEQqFxCuvvCJs2xarV68W++yzjwDEf//737zHlBEViUTSLpGIELNmCZGRK0KcdJIQOX8f24vUdpVdLqLy7W9/m3/961+89tprDB8+vN3t/H4/JSUlrb56A0cIblq8mJZ0mmHhMEHDQFUUgobBsHCYlnSamxYvxukFZXn00UezcOFChBC8/vrrnHHGGey777688cYbLFq0iKFDhzJu3Ljs9vPnz+fUU0/t8JiiC9c5evRoiouLs98PGTKEurq6Dvf56KOPOPTQQ1s5/x5++OFEo1E2btyYd3td15k2bVr2ZwMGDGDChAl89NFHAKxevZqpU6e22q/t9zNmzEDTNJ566ikAHnjgAY455phstKFQRo0aRWVlZbuvX3nllbz//vs88sgjrX4+bNgw9t5773a/Ro0a1aXraEsikeCSSy7h8MMP5+233+bNN9/kC1/4AqeccgqJRAKASy+9lG9/+9t85Stfwefzccghh3DuuecC0i9FIpF0kRUrYPJkmDsXNA1uugmeew5y/j7ubIf3Pv8rJoTg29/+Nk899RSvvvoqY8aM6etLAmB5bS2rGxoYEAhs90tRFIWKQIDVDQ0sr63t8XNPnz6dN954g5UrV2IYBvvssw/Tp09n4cKFLFq0iKOPPjq77ZYtW1i+fDmnnHJKu8cbP358dvFvi/fz8ePHZ39mGEarbRRFwXGcHbmlXsPn83HhhRdy//33k06nmTt3Ll//+te7fJyioqJ2X+tIRHcl9VNVVUVtm/eL931VVVXec8+dO5d169Zx//33M2XKFA455BDmzp3L2rVrmT9/PpD5/fzud78jGo2yfv16ampqsoJu7NixXX4WEolkD0QIuPNOOOQQ+OQTGD4cFi2CH/wA+vgDT59b6F955ZXMnTuX+fPnU1xcTE1NDQClpaUEg8E+u66tiQRpx8Gv539EAV2nMZViq/uptifx6lRuu+22rCiZPn06N910E42NjVx33XXZbZ999lkOO+ywVnUTbTn33HOZNWsWzz777HZ1Kn/4wx8YMGAAxx9/fMHX5/P5tptEve+++/Lkk08ihMgKuzfffJPi4uK8EbJ9990Xy7JYvHgxhx12GADbtm1j9erV7LfffkCmPuOdd95ptV/b7wG+8Y1v8IUvfIG//vWvWJbFGWecUfC9dIQQgquuuoqnnnqKhQsX5hXR9957bzaykY9c0XfooYfy4x//GNM0sz9/+eWXmTBhAuXl5Xn3j8fjqKraSix737cVj5qmMWzYMAAefvhhDj300A6jRBKJRAJAczNceik8/njm+698BR54AAYM6NPL8ujziMqdd95Jc3Mz06dPZ8iQIdmvRx99tE+va2AwiE9VSVlW3teTloVPVRnYC2KqvLyciRMn8tBDD2WLZo866ijeffddPvnkk1YRlWeeeabTtM+5557L6aefzte+9jXuu+8+1q1bx6pVq7j88st55plnuPfeezuMKLRl9OjRLF68mHXr1rF161Ycx+Fb3/oWGzZs4KqrruLjjz9m/vz5/OxnP+Paa6/Nm34YN24cp512Gpdeemk2ejR79myGDRvGaaedBsBVV13F888/z6233sqaNWu4++67eeGFF7aLcO27774ccsgh/OAHP+C8887rMYF75ZVXMmfOHObOnZsV0TU1Na2ESVdSP7NmzcLn83HJJZfwwQcf8Oijj3L77bdz7bXXZrd56qmn2GeffbLfH3/88TQ2NnLllVfy0Ucf8cEHH3DxxRej6zrHHHMMAFu3buWuu+7i448/ZsWKFVx99dU8/vjj/PGPf+yR5yCRSHZjli6FL34xI1J0Hf7w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R8LG0m4CleYoKCigtLSUgoICdF2P1ko477zzSElJYfv27QwYMIDBgwczZsyY6HqlxWJp0WAokRq7B4MGDeKSSy7hvvvu46WXXqKwsJAJEyYwevToNtlltF+/fmRkZDBixAiee+45XC4X//jHP9i3bx9DhgxJ9PBiLi0tjUceeYSJEyeSm5tLt27d+POf/wzAsGHDEjy62OvatWut1ykpKQCce+65CftbZrwdPnyY/v37061bN/7yl79QUlISfa+tzjCMGTOGESNGcM0119CnTx9effVVPB4PDzzwQKKHFnOjR4/m448/Zu7cuaSmpkafc+np6bhcrvgOJu77jM4AI0aMMIETvpYuXWqapmlOnDix3ve7deuW0HG3pMbugWma5v79+82bb77ZdLlcZqdOncyxY8eaoVAocYOOsQ0bNpiDBg0yMzMzzdTUVPMnP/mJuWDBgkQPK26CwaA5duxYMzs720xNTTUHDhxobt++PdHDSoh9+/a1u+3JU6dOrfd3Qlt/jLz++utm165dTbvdbvbp08dct25doocUFyf7dz116tS4j0UJD0gIIYQQotVpuwuMQgghhDjjSaAihBBCiFZLAhUhhBBCtFoSqAghhBCi1ZJARQghhBCtlgQqQgghhGi1JFARQgghRKslgYoQQgghWi0JVIQQQgjRakmgIoQQQohWSwIVIYQQQrRaEqgIIYQQotX6//xmZr2WGo19AAAAAElFTkSuQmCC", + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAioAAAGdCAYAAAA8F1jjAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy80BEi2AAAACXBIWXMAAA9hAAAPYQGoP6dpAAEAAElEQVR4nOydZ3hc1dW2733KNI26ZLlJlrtNs40BEwhgCJgWXiAhEFogISR8CSSUFEhPeHkxIZQEElIglNADJPQWjB2qAeMCtnG35aIuS5o+p+zvx5kZq1uyZVzY93XJ8sxp+8xI2s+svdazhJRSolAoFAqFQrEHou3uASgUCoVCoVD0hhIqCoVCoVAo9liUUFEoFAqFQrHHooSKQqFQKBSKPRYlVBQKhUKhUOyxKKGiUCgUCoVij0UJFYVCoVAoFHssSqgoFAqFQqHYYzF29wB2Ftd12bJlC/n5+QghdvdwFAqFQqFQ9AMpJZFIhOHDh6NpvcdN9nqhsmXLFiorK3f3MBQKhUKhUOwAGzduZOTIkb1u3+uFSn5+PuDdaEFBwW4ejUKhUCgUiv7Q3t5OZWVlbh7vjb1eqGSXewoKCpRQUSgUCoViL2N7aRsqmVahUCgUCsUeixIqCoVCoVAo9liUUFEoFAqFQrHHstfnqPQHKSW2beM4zu4eikKxV6DrOoZhqJJ/hUKx29nnhUo6naa2tpZ4PL67h6JQ7FWEQiGGDRuGz+fb3UNRKBSfYfZpoeK6LuvWrUPXdYYPH47P51OfEBWK7SClJJ1O09jYyLp16xg/fnyfZkwKhUKxK9mnhUo6ncZ1XSorKwmFQrt7OArFXkMwGMQ0TTZs2EA6nSYQCOzuISkUis8on4mPSerToEIxcNTvjUKh2BNQf4kUCoVCoVDssex2oXLjjTdy6KGHkp+fz5AhQzjjjDNYsWLF7h6WYhcwc+ZMrrzyyt09DIVCoVDsRex2oTJv3jy++93v8u677/Lqq69iWRazZs0iFovt7qHtVcybN6/P5oyO43Dbbbdx4IEHEggEKC4u5uSTT+att94a8LU+K4JjV4joJUuWcNRRRxEIBKisrOS3v/3tdo957bXXOOKII8jPz2fo0KH8+Mc/xrbtnRqHQqFQ7C3sdqHy0ksvcfHFF7P//vszZcoU7rvvPmpqaliwYMHuHtpexdNPP81pp53W4zYpJV/96lf5zW9+w/e//32WL1/O3LlzqaysZObMmfz73//+dAe7h5NOp4HBF9Ht7e3MmjWLUaNGsWDBAm6++WZ+9atf8de//rXXYxYvXswpp5zCSSedxMKFC3nsscd45plnuPbaa3doDAqFQrHXIfcwVq1aJQH50Ucf9bg9mUzKtra23NfGjRslINva2rrtm0gk5LJly2QikdipMTmuKz+orZUvrV0rP6itlY7r7tT5+uLZZ5+VhYWF0rZtKaWUCxculID88Y9/nNvnkksukeeff36n48aOHStffPHFHs/56KOPSkA+88wz3bZ96UtfkqWlpTIajUoppfzlL38pp0yZIh944AE5atQoWVBQIM855xzZ3t4upZTyoosukkCnr3Xr1kkppZw7d6489NBDpc/nk0OHDpU//vGPpWVZuWsdc8wx8vvf/37ucUtLi7zwwgtlUVGRDAaD8qSTTpIrV67sNL6//vWvcuTIkTIYDMozzjhD3nLLLbKwsFBKKeW6deukEEK+//77nY657bbbZFVVlXQcp7eXuRPZe/7b3/4mq6urpRCix/0aGhokIOfNm9ev83blT3/6kywuLpapVCr33I9//GM5ceLEXo+57rrr5CGHHNLpuWeeeUYGAoHce7KrGKzfH4VCsRezZImUM2dKWVs76Kdua2vrdf7uyG6PqHTEdV2uvPJKjjzySA444IAe97nxxhspLCzMffW13DEYzKmp4aQnnuBLTz/NxS+9xJeefpqTnniCOTU1u+R6Rx11FJFIhIULFwLep/qysjLmzp2b22fevHnMnDkz93jp0qU0NDRw3HHH9XjOhx9+mAkTJvQYcbnmmmtobm7m1VdfzT23Zs0a/v3vf/Pcc8/x3HPPMW/ePGbPng3A73//ez73uc9x6aWXUltbS21tLZWVlWzevJlTTjmFQw89lMWLF3PXXXdxzz338L//+7+93uvFF1/MBx98wDPPPMM777yDlJJTTjkFy7IAeOutt7jsssv4/ve/z6JFizjhhBO44YYbcsdXV1dz/PHHc++993Y677333svFF188oKqV1atX8+STT/LUU0+xaNGiHvdpa2sDoKSkJPfcySefTDgc7vVr//33z+37zjvvcPTRR3cyUDvxxBNZsWIFW7du7fGaqVSqW2lwMBgkmUyqqKNCodh1SAl/+QscdhjMnQs/+MHuHMuew2WXXSZHjRolN27c2Os+n2ZE5bUNG+S4v/1NVvzxj3K/e+6R0+6/X+53zz2y4o9/lOP+9jf52oYNO3Te7XHwwQfLm2++WUop5RlnnCFvuOEG6fP5ZCQSkZs2bZJAp8jDDTfcIM8666xezzdp0iR5+umn97itpaVFAvKmm26SUnrRhVAo1OnT+g9/+EM5Y8aM3OOukREppfzJT34iJ06cKN0O0aY//vGPMhwO5yIbHY9buXKlBORbb72V27+pqUkGg0H5+OOPSymlPOecc+Spp57a6Trnn39+LqIipZSPPfaYLC4ulslkUkop5YIFC6QQIhfl6Q+//OUvpWmasqGhodd9HMeRp556qjzyyCM7Pb9p0ya5atWqXr/Wr1+f2/eEE06Q3/rWtzodv3TpUgnIZcuW9Xjdl19+WWqaJh9++GFp27bctGmTPOqooyQgH3744X7f446gIioKxWeU1lYpv/IVKT25IuXJJ0vZx9/HHWWvi6hcfvnlPPfcc7z++uuMHDmy1/38fj8FBQWdvnYFrpTMnj+fSDrNiHCYoGmiCUHQNBkRDhNJp5k9fz6ulIN+7WOOOYa5c+cipeSNN97gS1/6EpMnT+bNN99k3rx5DB8+nPHjx+f2f/rpp/mf//mfPs8pBzDO6upq8vPzc4+HDRtGQ0NDn8csX76cz33uc52cf4888kii0SibNm3qcX/DMJgxY0buudLSUiZOnMjy5csBWLFiBYcddlin47o+PuOMM9B1nX/9618A3HfffRx77LFUV1f372YzjBo1ivLy8l63f/e73+Xjjz/m0Ucf7fT8iBEjGDduXK9fo0aNGtA4ujJr1ixuvvlmLrvsMvx+PxMmTOCUU04BlM+JQqHYBbz/PkybBv/8JxgG3HwzPPcc9PH3cVez2//SSSm5/PLL+de//sWcOXMYPXr07h4SAAvr61nR0kJpINDNdl8IQUkgwIqWFhbW1w/6tWfOnMmbb77J4sWLMU2TSZMmMXPmTObOncu8efM45phjcvvW1taycOFCTj311F7PN2HChNzk35Xs8xMmTMg9Z5pmp32EELiuuzO3tMvw+Xx87Wtf49577yWdTvPwww/zjW98Y8DnycvL63VbXyJ6IEs/Q4cOpb7Lz0v28dChQ3u9/tVXX01rays1NTU0NTVx+umnAzBmzJgB36dCoVD0iOvCLbfAEUfAunVQXQ1vvukt+ezmD0W73UL/u9/9Lg8//DBPP/00+fn51NXVAVBYWEgwGNxt42pKJEi7Ln6j55coYBhsTaVoSiQG/drZPJXbbrstJ0pmzpzJ7Nmz2bp1K9dcc01u32effZYjjjiiU95EV7761a9y3nnn8eyzz3bLU7nlllsoLS3lhBNO6Pf4fD5ft07UkydP5sknn0RKmRN2b731Fvn5+T1GyCZPnoxt28yfP58jjjgCgObmZlasWMF+++0HwMSJE3n//fc7Hdf1McA3v/lNDjjgAP70pz9h2zZf+tKX+n0vfSGl5IorruBf//oXc+fO7VFE33333ST6+BnoKPo+97nP8dOf/hTLsnLPv/rqq0ycOJHi4uI+xyKEYPjw4QA88sgjVFZWcvDBB+/IbSkUCkVnmprgoovghRe8x2edBX/7GxQV7dZhZdntEZW77rqLtrY2Zs6cybBhw3Jfjz322G4dV1kwiE/TSPXiV5G0bXyaRtkuEFPFxcUcdNBBPPTQQ7mk2aOPPpoPP/yQlStXdoqoPPPMM9td9vnqV7/KmWeeyUUXXcQ999zD+vXrWbJkCd/+9rd55plnuPvuu/uMKHSlurqa+fPns379epqamnBdl+985zts3LiRK664gk8++YSnn36aX/7yl1x99dU9LlGMHz+e008/nUsvvTQXPbrgggsYMWJELmJwxRVX8MILL3DrrbeyatUq/vKXv/Diiy92i3BNnjyZww8/nB//+Mece+65gyZwv/vd7/Lggw/y8MMP50R0XV1dJ2EykKWf8847D5/PxyWXXMLSpUt57LHH+P3vf8/VV1+d2+df//oXkyZN6jSOm2++mY8++oilS5dy/fXXM3v2bP7whz+g6/qg3KdCofgMM28eTJniiRS/H+66Cx5/fI8RKbAHCBUpZY9fF1988W4d17SKCiaWlNCcTHbL75BS0pJMMrGkhGkVFbvk+scccwyO4+SESklJCfvttx9Dhw5l4sSJAMRiMV577bXtChUhBI8//jg/+clPuO2225g4cSJHHXUUGzZsYO7cuZxxxhkDGtsPfvADdF1nv/32o7y8nJqaGkaMGMELL7zAe++9x5QpU7jsssu45JJL+NnPftbree69916mT5/OF7/4RT73uc8hpeSFF17IRRuOPPJI/vznP3PrrbcyZcoUXnrpJa666qoeG+RdcsklpNPpHpd9qqur+dWvfjWge4TBF9GFhYW88sorrFu3junTp3PNNdfwi1/8gm9961u5fdra2rqZyr344oscddRRHHLIITz//PM8/fTTA37PFAqFohOOA7/5DRx3HGzZAhMnwvz5cNll0OXD4O5GyIFkWe6BtLe3U1hYSFtbW7fE2mQyybp16xg9evQOdX+dU1PDt195hUg6TUkgQMAwSNo2LckkBT4ff541i+OqqgbrVgbMU089xc9+9jOWLVu228bwaXPppZfyySef8MYbb3R6/vrrr+ef//wnS5Ys6fR8PB6ntLSUF198sVNJt2L77Ozvj0Kh2EPZsgUuuABef917fPHFcOedMIDI+mDQ1/zdkd0eUdmTOa6qir/MmsVB5eXELIvaWIyYZXFQefluFykA4XCYm266abeOYVfzu9/9jsWLF7N69WruuOMO7r//fi666KLc9mg0yscff8ydd97JFVdc0e34119/neOOO06JFIVCoQB46SWYOtUTKXl58MADcO+9n7pIGQgqotIPXClZWF9PUyJBWTDItIoKtD0sNLavcvbZZzN37lwikQhjxozhiiuu4LLLLsttv/jii3nkkUc444wzePjhh1XexiCiIioKxT6EZcFPf+qVG4OXl/LYY96Sz26ivxEVJVQUCkWPqN8fhWIfYf16+OpXvRwUgMsv9wTLbv697q9Q2e3lyQqFQqFQKHYRTz4Jl1wCbW1eJc8998AgWTh8WqgcFYVCoVAo9jWSSfjOdzxPlLY2OPxwWLhwrxMpoCIqCoVCoVDs9XTMpRyxaRP7X345IlsF+eMfw/XXQxfX8b0FJVQUCoVCodgB9pRCizk1Ncx+9102rG7ijHff5hcv/xORTpMoLsb/8MNoJ530qY9pMFFCRaFQKBSKATKnpobZ8+ezoqWFtOvi0zQmlpRw7YwZO2xd0VX4TBkyhMUNDTQlEpQEAiAETYkEzfE4pcEgQ0IhtqZSXP3ACwxZ2Mod7z7BrNoPAZhfPo5LzjqXQEMDv62p2e12GjuDEioKhUKhUAyAjmagpYEAfsMgZdssaWzkWy+/zA8OPZTRhYUDirJ0FT624xBNpSjbJPHHIBGEuhEgNZCAqWkU+nzk1Tsc//p6frnwISrjjTgIHhw1i/vHH0f5Co0a2cC3Ui/z1xNP3GvFihIqij2GuXPncuyxx7J161aK9qA+EwqFQpHFlZLZ8+cTSacZEQ7neo8FTZMC12VDeztXvf46RX4/Pl3vV5Slq/DZFIlQvNRiyirQHRB44mTyEthUBSsOAMt1aYon+MGz73DVR8/ic20a/IX8duJ5LC0cgz8FLcUwZjl8Uhxh9vz5zKys3Cs9wFTVzz7CvHnzqKys3KlzLF26lLPPPpvy8nL8fj8TJkzgF7/4BfF4fEDnmTt3LkIIWltbd2o8ezqLFy/m3HPPpbKykmAwyOTJk/n973+/U+eUUvKLX/yCYcOGEQwGOf7441m1alWfxziOw89//nNGjx5NMBhk7NixXH/99Z16VNXX13PxxRczfPhwQqEQJ5100nbPq1AourOwvp4VLS2UBgIIwGi0MDemSNbGqWlvx5ES23Up9PsJmyZLGhv59iuvMKempsfzdRI+eXm0bYow6TWLSZ+AkREp4H03HKheBzNfhuqaBE///QF+vPhf+Fybd0smc/m0q1haOAaASD74EqC7ULnU5ZOmJhbW138qr9FgoyIq/cB1JWtW1dPelqCgMMjY8RVo2p6lSp9++mlOO+20HT7+3Xff5fjjj+f444/n+eefp6Kigvfee49rrrmG1157jddffx2fzzeII967sSyLBQsWMGTIEB588EEqKyt5++23+da3voWu61x++eU7dN7f/va3/OEPf+D+++9n9OjR/PznP+fEE09k2bJlvZqu3XTTTdx1113cf//97L///nzwwQd8/etfp7CwkO9973tIKTnjjDMwTZOnn36agoICbr31Vo4//niWLVs2oM7ZCsVnnaZEgrTrEm5wCS9uRW+1wYUALqEwbJyo0VTmCZCwz8cIw2BzNNprRCMrfEqawJzTyMRmMJ2+xzClaQOP/vUhKlJbsYTO30efyr+Hfx6EQAJbS6CwZVskIhQBGi2aOnR+35tQEZXtsGhBDT/7wRNc/9OnueX/XuL6nz7Nz37wBIsW9KyOd5bnnnuOoqIiHMf7SV20aBFCCK699trcPt/85je54IILOh33zDPP5Loop1Ipvve97zFkyBACgQCf//znef/993u9ppSSSy65hMmTJ/PUU09x2GGHMWrUKL7yla/w7LPP8s4773Dbbbfl9hdCcPfdd3PmmWcSCoUYP348zzzzDADr16/n2GOPBaC4uBghRK4T9kDHBfDkk0+y//774/f7qa6u5pZbbum0vba2llNPPZVgMMjo0aN5+OGHqa6u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", "text/plain": [ "
" ] @@ -263,11 +377,14 @@ "labels = labels_list.cpu().detach().numpy()\n", "preds = preds_list.cpu().detach().numpy()\n", "\n", + "labels2 = labels_list2.cpu().detach().numpy()\n", + "preds2 = preds_list2.cpu().detach().numpy()\n", + "\n", "r2_with = r2_score(labels, preds)\n", - "# r2_without = r2_score(y_real, y_noonto)\n", + "r2_without = r2_score(labels2, preds2)\n", "\n", "ax1 = sns.regplot(x=labels, y=preds, label=f'w/ Ontology, r2={r2_with:.2}', color='darkcyan')\n", - "# ax2 = sns.regplot(x=y_real, y=y_noonto, color='black')\n", + "ax2 = sns.regplot(x=labels2, y=preds2, label=f'w/o Ontology, r2={r2_without:.2}', color='darkslateblue')\n", "bla = [-12,-11,-10,-9,-8,-7, -6, -5, -4, -3, -2, -1, 0, 1,2]\n", "ax1.legend(loc=\"best\")\n", "plt.plot(bla, bla, color='red')\n", From fa97f453f5817a25b9bb31649629dfc0a7ba6a14 Mon Sep 17 00:00:00 2001 From: schnamo Date: Wed, 8 Jan 2025 16:48:14 +0100 Subject: [PATCH 17/54] fix union expression --- chebai/loss/semantic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chebai/loss/semantic.py b/chebai/loss/semantic.py index 271c3124..a4f6f7d4 100644 --- a/chebai/loss/semantic.py +++ b/chebai/loss/semantic.py @@ -60,7 +60,7 @@ def __init__( pos_epsilon: float = 0.01, multiply_by_softmax: bool = False, use_sigmoidal_implication: bool = False, - weight_epoch_dependent: Union[bool | tuple[int, int]] = False, + weight_epoch_dependent: Union[bool, tuple[int, int]] = False, start_at_epoch: int = 0, violations_per_cls_aggregator: Literal[ "sum", "max", "mean", "log-sum", "log-max", "log-mean" From 8b91dce4a6db48ff936cb93e3a0530af9bee0e88 Mon Sep 17 00:00:00 2001 From: schnamo Date: Wed, 8 Jan 2025 17:04:27 +0100 Subject: [PATCH 18/54] fix tuple issue to make it backwards compatible --- chebai/loss/semantic.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/chebai/loss/semantic.py b/chebai/loss/semantic.py index a4f6f7d4..6237158c 100644 --- a/chebai/loss/semantic.py +++ b/chebai/loss/semantic.py @@ -2,7 +2,7 @@ import math import os import pickle -from typing import List, Literal, Union +from typing import List, Literal, Union, Tuple import torch @@ -60,7 +60,7 @@ def __init__( pos_epsilon: float = 0.01, multiply_by_softmax: bool = False, use_sigmoidal_implication: bool = False, - weight_epoch_dependent: Union[bool, tuple[int, int]] = False, + weight_epoch_dependent: Union[bool, Tuple[int, int]] = False, start_at_epoch: int = 0, violations_per_cls_aggregator: Literal[ "sum", "max", "mean", "log-sum", "log-max", "log-mean" From 677d6ecdb068a85c1720fd0a07b7f2a8f6a020e7 Mon Sep 17 00:00:00 2001 From: schnamo Date: Mon, 13 Jan 2025 15:20:04 +0100 Subject: [PATCH 19/54] wandb --- chebai/models/base.py | 3 +- configs/model/electra.yml | 1 + configs/training/wandb_logger.yml | 4 +- eval_model_regression.ipynb | 424 ------------------------------ 4 files changed, 5 insertions(+), 427 deletions(-) delete mode 100644 eval_model_regression.ipynb diff --git a/chebai/models/base.py b/chebai/models/base.py index b14867ca..cda7f16a 100644 --- a/chebai/models/base.py +++ b/chebai/models/base.py @@ -44,7 +44,8 @@ def __init__( exclude_hyperparameter_logging: Optional[Iterable[str]] = None, **kwargs, ): - super().__init__() + super().__init__(**kwargs) + # super().__init__() if exclude_hyperparameter_logging is None: exclude_hyperparameter_logging = tuple() self.criterion = criterion diff --git a/configs/model/electra.yml b/configs/model/electra.yml index 4beaed65..5241cfce 100644 --- a/configs/model/electra.yml +++ b/configs/model/electra.yml @@ -10,3 +10,4 @@ init_args: num_hidden_layers: 6 type_vocab_size: 1 hidden_size: 256 + out_dim: 1 diff --git a/configs/training/wandb_logger.yml b/configs/training/wandb_logger.yml index b0dd8870..6a3c80bb 100644 --- a/configs/training/wandb_logger.yml +++ b/configs/training/wandb_logger.yml @@ -1,6 +1,6 @@ class_path: chebai.loggers.custom.CustomLogger # Extension of Wandb logger init_args: save_dir: logs - project: 'chebai' - entity: 'chebai' + project: 'cheb-ai-sol' + entity: 'ch-tumescheit-university-of-zurich' log_model: 'all' diff --git a/eval_model_regression.ipynb b/eval_model_regression.ipynb deleted file mode 100644 index 287f6df7..00000000 --- a/eval_model_regression.ipynb +++ /dev/null @@ -1,424 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "id": "initial_id", - "metadata": { - "ExecuteTime": { - "end_time": "2024-04-02T13:47:31.150545Z", - "start_time": "2024-04-02T13:47:27.181585Z" - } - }, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/Users/ctumes/Cheb-AI/chebai_env/lib/python3.9/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n", - " from .autonotebook import tqdm as notebook_tqdm\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "cpu\n" - ] - } - ], - "source": [ - "import pandas as pd\n", - "import seaborn as sns\n", - "import matplotlib.pyplot as plt\n", - "\n", - "from sklearn.metrics import r2_score\n", - "\n", - "\n", - "from chebai.result.utils import (\n", - " evaluate_model,\n", - " evaluate_model_regression,\n", - " load_results_from_buffer,\n", - ")\n", - "from chebai.result.classification import print_metrics\n", - "from chebai.result.regression import print_metrics\n", - "from chebai.models.electra import Electra\n", - "from chebai.preprocessing.datasets.solCuration import SolCuration, SolCurationChem\n", - "from chebai.preprocessing.datasets.tox21 import Tox21MolNet\n", - "import os\n", - "import tqdm\n", - "import torch\n", - "import pickle\n", - "\n", - "DEVICE = torch.device(\"cuda:0\" if torch.cuda.is_available() else \"cpu\")\n", - "print(DEVICE)" - ] - }, - { - "cell_type": "markdown", - "id": "a867fc46-b9c7-42d1-8c4b-030ebd3470d0", - "metadata": {}, - "source": [ - "## With Ontology finetuning" - ] - }, - { - "cell_type": "code", - "execution_count": 18, - "id": "bdb5fc6919cf72be", - "metadata": { - "ExecuteTime": { - "end_time": "2024-04-02T13:47:35.484307Z", - "start_time": "2024-04-02T13:47:35.477111Z" - }, - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[False, False, False]\n" - ] - } - ], - "source": [ - "# specify the checkpoint name\n", - "checkpoint_name = \"solFinetuningAGAIN_MSE2/version_0/checkpoints/per_epoch=99_val_loss=0.5071_val_mse=11.0217\"\n", - "checkpoint_path = os.path.join(\"logs\", f\"{checkpoint_name}.ckpt\")\n", - "kind = \"test\" # replace with \"train\" / \"validation\" to run on train / validation sets\n", - "buffer_dir = os.path.join(\"results_buffer\", checkpoint_name, kind)\n", - "# make sure to use the same data module and model class that were used during training\n", - "data_module = SolCurationChem()\n", - "# load chebi data if missing and perform dynamic splits\n", - "data_module.prepare_data()\n", - "data_module.setup()\n", - "\n", - "model_class = Electra" - ] - }, - { - "cell_type": "code", - "execution_count": 19, - "id": "fa1276b47def696c", - "metadata": { - "ExecuteTime": { - "end_time": "2024-04-02T13:47:38.418564Z", - "start_time": "2024-04-02T13:47:37.861168Z" - } - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "\n", - "00%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 41/41 [00:45<00:00, 1.10s/it]" - ] - } - ], - "source": [ - "# evaluates model, stores results in buffer_dir\n", - "# print(buffer_dir)\n", - "model = model_class.load_from_checkpoint(checkpoint_path)\n", - "model.model_type = 'regression'\n", - "\n", - "preds_list, labels_list = evaluate_model_regression(\n", - " model,\n", - " data_module,\n", - " buffer_dir='/Users/ctumes/Cheb-AI/chebai_helper/',\n", - " # No need to provide this parameter for Chebi dataset, \"kind\" parameter should be provided\n", - " filename='/Users/ctumes/python-chebai/data/SolCuration/processed/smiles_token/test.pt',\n", - " batch_size=32,\n", - " kind=kind,\n", - ")\n", - "# load data from buffer_dir\n", - "# preds, labels = load_results_from_buffer(buffer_dir, device=DEVICE)" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "e2500283-7b37-4ce9-b044-f4eb64876a5b", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "regression\n" - ] - } - ], - "source": [ - "print(model.model_type)" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "201f750c475b4677", - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "tensor([[-2.1897],\n", - " [-3.9141],\n", - " [-5.6935],\n", - " ...,\n", - " [-0.0093],\n", - " [-5.4903],\n", - " [-5.1491]], device='mps:0', grad_fn=)\n", - "tensor([[-1.9250],\n", - " [-6.4172],\n", - " [-4.8800],\n", - " ...,\n", - " [-0.1700],\n", - " [-4.8041],\n", - " [-4.9500]], device='mps:0')\n", - "1300\n", - "1300\n" - ] - } - ], - "source": [ - "print(preds_list)\n", - "print(labels_list)\n", - "print(len(preds_list))\n", - "print(len(labels_list))" - ] - }, - { - "cell_type": "code", - "execution_count": 20, - "id": "e567cd2fb1718baf", - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "MSE: 0.5399385690689087\n" - ] - } - ], - "source": [ - "# output relevant metrics\n", - "print_metrics(\n", - " preds_list,\n", - " labels_list,\n", - " DEVICE,\n", - " markdown_output=False,\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "d1c04107-f374-4496-9053-fce90bb544ff", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[[-2.1897364]\n", - " [-3.9141192]\n", - " [-5.693497 ]\n", - " ...\n", - " [-0.0092898]\n", - " [-5.4903164]\n", - " [-5.1490617]]\n" - ] - } - ], - "source": [ - "print(preds_list.cpu().detach().numpy())" - ] - }, - { - "cell_type": "markdown", - "id": "97fc34b6-22aa-4c36-a0ad-3e0d4e0a0649", - "metadata": {}, - "source": [ - "## Without Ontology Finetuning" - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "id": "604f800f-752b-424f-afcc-0d25fa706fe9", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[False, False, False]\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "Some weights of ElectraModel were not initialized from the model checkpoint at None and are newly initialized: ['embeddings.LayerNorm.bias', 'embeddings.LayerNorm.weight', 'embeddings.position_embeddings.weight', 'embeddings.token_type_embeddings.weight', 'embeddings.word_embeddings.weight', 'embeddings_project.bias', 'embeddings_project.weight', 'encoder.layer.0.attention.output.LayerNorm.bias', 'encoder.layer.0.attention.output.LayerNorm.weight', 'encoder.layer.0.attention.output.dense.bias', 'encoder.layer.0.attention.output.dense.weight', 'encoder.layer.0.attention.self.key.bias', 'encoder.layer.0.attention.self.key.weight', 'encoder.layer.0.attention.self.query.bias', 'encoder.layer.0.attention.self.query.weight', 'encoder.layer.0.attention.self.value.bias', 'encoder.layer.0.attention.self.value.weight', 'encoder.layer.0.intermediate.dense.bias', 'encoder.layer.0.intermediate.dense.weight', 'encoder.layer.0.output.LayerNorm.bias', 'encoder.layer.0.output.LayerNorm.weight', 'encoder.layer.0.output.dense.bias', 'encoder.layer.0.output.dense.weight', 'encoder.layer.1.attention.output.LayerNorm.bias', 'encoder.layer.1.attention.output.LayerNorm.weight', 'encoder.layer.1.attention.output.dense.bias', 'encoder.layer.1.attention.output.dense.weight', 'encoder.layer.1.attention.self.key.bias', 'encoder.layer.1.attention.self.key.weight', 'encoder.layer.1.attention.self.query.bias', 'encoder.layer.1.attention.self.query.weight', 'encoder.layer.1.attention.self.value.bias', 'encoder.layer.1.attention.self.value.weight', 'encoder.layer.1.intermediate.dense.bias', 'encoder.layer.1.intermediate.dense.weight', 'encoder.layer.1.output.LayerNorm.bias', 'encoder.layer.1.output.LayerNorm.weight', 'encoder.layer.1.output.dense.bias', 'encoder.layer.1.output.dense.weight', 'encoder.layer.2.attention.output.LayerNorm.bias', 'encoder.layer.2.attention.output.LayerNorm.weight', 'encoder.layer.2.attention.output.dense.bias', 'encoder.layer.2.attention.output.dense.weight', 'encoder.layer.2.attention.self.key.bias', 'encoder.layer.2.attention.self.key.weight', 'encoder.layer.2.attention.self.query.bias', 'encoder.layer.2.attention.self.query.weight', 'encoder.layer.2.attention.self.value.bias', 'encoder.layer.2.attention.self.value.weight', 'encoder.layer.2.intermediate.dense.bias', 'encoder.layer.2.intermediate.dense.weight', 'encoder.layer.2.output.LayerNorm.bias', 'encoder.layer.2.output.LayerNorm.weight', 'encoder.layer.2.output.dense.bias', 'encoder.layer.2.output.dense.weight', 'encoder.layer.3.attention.output.LayerNorm.bias', 'encoder.layer.3.attention.output.LayerNorm.weight', 'encoder.layer.3.attention.output.dense.bias', 'encoder.layer.3.attention.output.dense.weight', 'encoder.layer.3.attention.self.key.bias', 'encoder.layer.3.attention.self.key.weight', 'encoder.layer.3.attention.self.query.bias', 'encoder.layer.3.attention.self.query.weight', 'encoder.layer.3.attention.self.value.bias', 'encoder.layer.3.attention.self.value.weight', 'encoder.layer.3.intermediate.dense.bias', 'encoder.layer.3.intermediate.dense.weight', 'encoder.layer.3.output.LayerNorm.bias', 'encoder.layer.3.output.LayerNorm.weight', 'encoder.layer.3.output.dense.bias', 'encoder.layer.3.output.dense.weight', 'encoder.layer.4.attention.output.LayerNorm.bias', 'encoder.layer.4.attention.output.LayerNorm.weight', 'encoder.layer.4.attention.output.dense.bias', 'encoder.layer.4.attention.output.dense.weight', 'encoder.layer.4.attention.self.key.bias', 'encoder.layer.4.attention.self.key.weight', 'encoder.layer.4.attention.self.query.bias', 'encoder.layer.4.attention.self.query.weight', 'encoder.layer.4.attention.self.value.bias', 'encoder.layer.4.attention.self.value.weight', 'encoder.layer.4.intermediate.dense.bias', 'encoder.layer.4.intermediate.dense.weight', 'encoder.layer.4.output.LayerNorm.bias', 'encoder.layer.4.output.LayerNorm.weight', 'encoder.layer.4.output.dense.bias', 'encoder.layer.4.output.dense.weight', 'encoder.layer.5.attention.output.LayerNorm.bias', 'encoder.layer.5.attention.output.LayerNorm.weight', 'encoder.layer.5.attention.output.dense.bias', 'encoder.layer.5.attention.output.dense.weight', 'encoder.layer.5.attention.self.key.bias', 'encoder.layer.5.attention.self.key.weight', 'encoder.layer.5.attention.self.query.bias', 'encoder.layer.5.attention.self.query.weight', 'encoder.layer.5.attention.self.value.bias', 'encoder.layer.5.attention.self.value.weight', 'encoder.layer.5.intermediate.dense.bias', 'encoder.layer.5.intermediate.dense.weight', 'encoder.layer.5.output.LayerNorm.bias', 'encoder.layer.5.output.LayerNorm.weight', 'encoder.layer.5.output.dense.bias', 'encoder.layer.5.output.dense.weight']\n", - "You should probably TRAIN this model on a down-stream task to be able to use it for predictions and inference.\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "\n", - "00%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 41/41 [00:51<00:00, 1.26s/it]" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "MSE: 0.5454557538032532\n" - ] - } - ], - "source": [ - "# specify the checkpoint name\n", - "checkpoint_name = \"solFinetuningAGAIN_MSE_NO_ONTO/version_0/checkpoints/per_epoch=74_val_loss=0.6037_val_mse=11.0069\"\n", - "checkpoint_path = os.path.join(\"logs\", f\"{checkpoint_name}.ckpt\")\n", - "kind = \"test\" # replace with \"train\" / \"validation\" to run on train / validation sets\n", - "buffer_dir = os.path.join(\"results_buffer\", checkpoint_name, kind)\n", - "# make sure to use the same data module and model class that were used during training\n", - "data_module2 = SolCurationChem()\n", - "# load chebi data if missing and perform dynamic splits\n", - "data_module2.prepare_data()\n", - "data_module2.setup()\n", - "\n", - "model_class2 = Electra\n", - "\n", - "# evaluates model, stores results in buffer_dir\n", - "# print(buffer_dir)\n", - "model2 = model_class2.load_from_checkpoint(checkpoint_path)\n", - "model2.model_type = 'regression'\n", - "\n", - "preds_list2, labels_list2 = evaluate_model_regression(\n", - " model2,\n", - " data_module2,\n", - " buffer_dir='/Users/ctumes/Cheb-AI/chebai_helper/',\n", - " # No need to provide this parameter for Chebi dataset, \"kind\" parameter should be provided\n", - " filename='/Users/ctumes/python-chebai/data/SolCuration/processed/smiles_token/test.pt',\n", - " batch_size=32,\n", - " kind=kind,\n", - ")\n", - "\n", - "# output relevant metrics\n", - "print_metrics(\n", - " preds_list2,\n", - " labels_list2,\n", - " DEVICE,\n", - " markdown_output=False,\n", - ")" - ] - }, - { - "cell_type": "markdown", - "id": "c1aa283a-161a-447a-9b28-8452ff7a413a", - "metadata": {}, - "source": [ - "## Plot everything" - ] - }, - { - "cell_type": "code", - "execution_count": 22, - "id": "876db3f9-c6ad-46e9-9f3f-3d41d7767410", - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAioAAAGdCAYAAAA8F1jjAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy80BEi2AAAACXBIWXMAAA9hAAAPYQGoP6dpAAEAAElEQVR4nOydZ3hc1dW2733KNI26ZLlJlrtNs40BEwhgCJgWXiAhEFogISR8CSSUFEhPeHkxIZQEElIglNADJPQWjB2qAeMCtnG35aIuS5o+p+zvx5kZq1uyZVzY93XJ8sxp+8xI2s+svdazhJRSolAoFAqFQrEHou3uASgUCoVCoVD0hhIqCoVCoVAo9liUUFEoFAqFQrHHooSKQqFQKBSKPRYlVBQKhUKhUOyxKKGiUCgUCoVij0UJFYVCoVAoFHssSqgoFAqFQqHYYzF29wB2Ftd12bJlC/n5+QghdvdwFAqFQqFQ9AMpJZFIhOHDh6NpvcdN9nqhsmXLFiorK3f3MBQKhUKhUOwAGzduZOTIkb1u3+uFSn5+PuDdaEFBwW4ejUKhUCgUiv7Q3t5OZWVlbh7vjb1eqGSXewoKCpRQUSgUCoViL2N7aRsqmVahUCgUCsUeixIqCoVCoVAo9liUUFEoFAqFQrHHstfnqPQHKSW2beM4zu4eikKxV6DrOoZhqJJ/hUKx29nnhUo6naa2tpZ4PL67h6JQ7FWEQiGGDRuGz+fb3UNRKBSfYfZpoeK6LuvWrUPXdYYPH47P51OfEBWK7SClJJ1O09jYyLp16xg/fnyfZkwKhUKxK9mnhUo6ncZ1XSorKwmFQrt7OArFXkMwGMQ0TTZs2EA6nSYQCOzuISkUis8on4mPSerToEIxcNTvjUKh2BNQf4kUCoVCoVDssex2oXLjjTdy6KGHkp+fz5AhQzjjjDNYsWLF7h6WYhcwc+ZMrrzyyt09DIVCoVDsRex2oTJv3jy++93v8u677/Lqq69iWRazZs0iFovt7qHtVcybN6/P5oyO43Dbbbdx4IEHEggEKC4u5uSTT+att94a8LU+K4JjV4joJUuWcNRRRxEIBKisrOS3v/3tdo957bXXOOKII8jPz2fo0KH8+Mc/xrbtnRqHQqFQ7C3sdqHy0ksvcfHFF7P//vszZcoU7rvvPmpqaliwYMHuHtpexdNPP81pp53W4zYpJV/96lf5zW9+w/e//32WL1/O3LlzqaysZObMmfz73//+dAe7h5NOp4HBF9Ht7e3MmjWLUaNGsWDBAm6++WZ+9atf8de//rXXYxYvXswpp5zCSSedxMKFC3nsscd45plnuPbaa3doDAqFQrHXIfcwVq1aJQH50Ucf9bg9mUzKtra23NfGjRslINva2rrtm0gk5LJly2QikdipMTmuKz+orZUvrV0rP6itlY7r7tT5+uLZZ5+VhYWF0rZtKaWUCxculID88Y9/nNvnkksukeeff36n48aOHStffPHFHs/56KOPSkA+88wz3bZ96UtfkqWlpTIajUoppfzlL38pp0yZIh944AE5atQoWVBQIM855xzZ3t4upZTyoosukkCnr3Xr1kkppZw7d6489NBDpc/nk0OHDpU//vGPpWVZuWsdc8wx8vvf/37ucUtLi7zwwgtlUVGRDAaD8qSTTpIrV67sNL6//vWvcuTIkTIYDMozzjhD3nLLLbKwsFBKKeW6deukEEK+//77nY657bbbZFVVlXQcp7eXuRPZe/7b3/4mq6urpRCix/0aGhokIOfNm9ev83blT3/6kywuLpapVCr33I9//GM5ceLEXo+57rrr5CGHHNLpuWeeeUYGAoHce7KrGKzfH4VCsRezZImUM2dKWVs76Kdua2vrdf7uyG6PqHTEdV2uvPJKjjzySA444IAe97nxxhspLCzMffW13DEYzKmp4aQnnuBLTz/NxS+9xJeefpqTnniCOTU1u+R6Rx11FJFIhIULFwLep/qysjLmzp2b22fevHnMnDkz93jp0qU0NDRw3HHH9XjOhx9+mAkTJvQYcbnmmmtobm7m1VdfzT23Zs0a/v3vf/Pcc8/x3HPPMW/ePGbPng3A73//ez73uc9x6aWXUltbS21tLZWVlWzevJlTTjmFQw89lMWLF3PXXXdxzz338L//+7+93uvFF1/MBx98wDPPPMM777yDlJJTTjkFy7IAeOutt7jsssv4/ve/z6JFizjhhBO44YYbcsdXV1dz/PHHc++993Y677333svFF188oKqV1atX8+STT/LUU0+xaNGiHvdpa2sDoKSkJPfcySefTDgc7vVr//33z+37zjvvcPTRR3cyUDvxxBNZsWIFW7du7fGaqVSqW2lwMBgkmUyqqKNCodh1SAl/+QscdhjMnQs/+MHuHMuew2WXXSZHjRolN27c2Os+n2ZE5bUNG+S4v/1NVvzxj3K/e+6R0+6/X+53zz2y4o9/lOP+9jf52oYNO3Te7XHwwQfLm2++WUop5RlnnCFvuOEG6fP5ZCQSkZs2bZJAp8jDDTfcIM8666xezzdp0iR5+umn97itpaVFAvKmm26SUnrRhVAo1OnT+g9/+EM5Y8aM3OOukREppfzJT34iJ06cKN0O0aY//vGPMhwO5yIbHY9buXKlBORbb72V27+pqUkGg0H5+OOPSymlPOecc+Spp57a6Trnn39+LqIipZSPPfaYLC4ulslkUkop5YIFC6QQIhfl6Q+//OUvpWmasqGhodd9HMeRp556qjzyyCM7Pb9p0ya5atWqXr/Wr1+f2/eEE06Q3/rWtzodv3TpUgnIZcuW9Xjdl19+WWqaJh9++GFp27bctGmTPOqooyQgH3744X7f446gIioKxWeU1lYpv/IVKT25IuXJJ0vZx9/HHWWvi6hcfvnlPPfcc7z++uuMHDmy1/38fj8FBQWdvnYFrpTMnj+fSDrNiHCYoGmiCUHQNBkRDhNJp5k9fz6ulIN+7WOOOYa5c+cipeSNN97gS1/6EpMnT+bNN99k3rx5DB8+nPHjx+f2f/rpp/mf//mfPs8pBzDO6upq8vPzc4+HDRtGQ0NDn8csX76cz33uc52cf4888kii0SibNm3qcX/DMJgxY0buudLSUiZOnMjy5csBWLFiBYcddlin47o+PuOMM9B1nX/9618A3HfffRx77LFUV1f372YzjBo1ivLy8l63f/e73+Xjjz/m0Ucf7fT8iBEjGDduXK9fo0aNGtA4ujJr1ixuvvlmLrvsMvx+PxMmTOCUU04BlM+JQqHYBbz/PkybBv/8JxgG3HwzPPcc9PH3cVez2//SSSm5/PLL+de//sWcOXMYPXr07h4SAAvr61nR0kJpINDNdl8IQUkgwIqWFhbW1w/6tWfOnMmbb77J4sWLMU2TSZMmMXPmTObOncu8efM45phjcvvW1taycOFCTj311F7PN2HChNzk35Xs8xMmTMg9Z5pmp32EELiuuzO3tMvw+Xx87Wtf49577yWdTvPwww/zjW98Y8DnycvL63VbXyJ6IEs/Q4cOpb7Lz0v28dChQ3u9/tVXX01rays1NTU0NTVx+umnAzBmzJgB36dCoVD0iOvCLbfAEUfAunVQXQ1vvukt+ezmD0W73UL/u9/9Lg8//DBPP/00+fn51NXVAVBYWEgwGNxt42pKJEi7Ln6j55coYBhsTaVoSiQG/drZPJXbbrstJ0pmzpzJ7Nmz2bp1K9dcc01u32effZYjjjiiU95EV7761a9y3nnn8eyzz3bLU7nlllsoLS3lhBNO6Pf4fD5ft07UkydP5sknn0RKmRN2b731Fvn5+T1GyCZPnoxt28yfP58jjjgCgObmZlasWMF+++0HwMSJE3n//fc7Hdf1McA3v/lNDjjgAP70pz9h2zZf+tKX+n0vfSGl5IorruBf//oXc+fO7VFE33333ST6+BnoKPo+97nP8dOf/hTLsnLPv/rqq0ycOJHi4uI+xyKEYPjw4QA88sgjVFZWcvDBB+/IbSkUCkVnmprgoovghRe8x2edBX/7GxQV7dZhZdntEZW77rqLtrY2Zs6cybBhw3Jfjz322G4dV1kwiE/TSPXiV5G0bXyaRtkuEFPFxcUcdNBBPPTQQ7mk2aOPPpoPP/yQlStXdoqoPPPMM9td9vnqV7/KmWeeyUUXXcQ999zD+vXrWbJkCd/+9rd55plnuPvuu/uMKHSlurqa+fPns379epqamnBdl+985zts3LiRK664gk8++YSnn36aX/7yl1x99dU9LlGMHz+e008/nUsvvTQXPbrgggsYMWJELmJwxRVX8MILL3DrrbeyatUq/vKXv/Diiy92i3BNnjyZww8/nB//+Mece+65gyZwv/vd7/Lggw/y8MMP50R0XV1dJ2EykKWf8847D5/PxyWXXMLSpUt57LHH+P3vf8/VV1+d2+df//oXkyZN6jSOm2++mY8++oilS5dy/fXXM3v2bP7whz+g6/qg3KdCofgMM28eTJniiRS/H+66Cx5/fI8RKbAHCBUpZY9fF1988W4d17SKCiaWlNCcTHbL75BS0pJMMrGkhGkVFbvk+scccwyO4+SESklJCfvttx9Dhw5l4sSJAMRiMV577bXtChUhBI8//jg/+clPuO2225g4cSJHHXUUGzZsYO7cuZxxxhkDGtsPfvADdF1nv/32o7y8nJqaGkaMGMELL7zAe++9x5QpU7jsssu45JJL+NnPftbree69916mT5/OF7/4RT73uc8hpeSFF17IRRuOPPJI/vznP3PrrbcyZcoUXnrpJa666qoeG+RdcsklpNPpHpd9qqur+dWvfjWge4TBF9GFhYW88sorrFu3junTp3PNNdfwi1/8gm9961u5fdra2rqZyr344oscddRRHHLIITz//PM8/fTTA37PFAqFohOOA7/5DRx3HGzZAhMnwvz5cNll0OXD4O5GyIFkWe6BtLe3U1hYSFtbW7fE2mQyybp16xg9evQOdX+dU1PDt195hUg6TUkgQMAwSNo2LckkBT4ff541i+OqqgbrVgbMU089xc9+9jOWLVu228bwaXPppZfyySef8MYbb3R6/vrrr+ef//wnS5Ys6fR8PB6ntLSUF198sVNJt2L77Ozvj0Kh2EPZsgUuuABef917fPHFcOedMIDI+mDQ1/zdkd0eUdmTOa6qir/MmsVB5eXELIvaWIyYZXFQefluFykA4XCYm266abeOYVfzu9/9jsWLF7N69WruuOMO7r//fi666KLc9mg0yscff8ydd97JFVdc0e34119/neOOO06JFIVCoQB46SWYOtUTKXl58MADcO+9n7pIGQgqotIPXClZWF9PUyJBWTDItIoKtD0sNLavcvbZZzN37lwikQhjxozhiiuu4LLLLsttv/jii3nkkUc444wzePjhh1XexiCiIioKxT6EZcFPf+qVG4OXl/LYY96Sz26ivxEVJVQUCkWPqN8fhWIfYf16+OpXvRwUgMsv9wTLbv697q9Q2e3lyQqFQqFQKHYRTz4Jl1wCbW1eJc8998AgWTh8WqgcFYVCoVAo9jWSSfjOdzxPlLY2OPxwWLhwrxMpoCIqCoVCoVDs9XTMpRyxaRP7X345IlsF+eMfw/XXQxfX8b0FJVQUCoVCodgB9pRCizk1Ncx+9102rG7ijHff5hcv/xORTpMoLsb/8MNoJ530qY9pMFFCRaFQKBSKATKnpobZ8+ezoqWFtOvi0zQmlpRw7YwZO2xd0VX4TBkyhMUNDTQlEpQEAiAETYkEzfE4pcEgQ0IhtqZSXP3ACwxZ2Mod7z7BrNoPAZhfPo5LzjqXQEMDv62p2e12GjuDEioKhUKhUAyAjmagpYEAfsMgZdssaWzkWy+/zA8OPZTRhYUDirJ0FT624xBNpSjbJPHHIBGEuhEgNZCAqWkU+nzk1Tsc//p6frnwISrjjTgIHhw1i/vHH0f5Co0a2cC3Ui/z1xNP3GvFihIqij2GuXPncuyxx7J161aK9qA+EwqFQpHFlZLZ8+cTSacZEQ7neo8FTZMC12VDeztXvf46RX4/Pl3vV5Slq/DZFIlQvNRiyirQHRB44mTyEthUBSsOAMt1aYon+MGz73DVR8/ic20a/IX8duJ5LC0cgz8FLcUwZjl8Uhxh9vz5zKys3Cs9wFTVzz7CvHnzqKys3KlzLF26lLPPPpvy8nL8fj8TJkzgF7/4BfF4fEDnmTt3LkIIWltbd2o8ezqLFy/m3HPPpbKykmAwyOTJk/n973+/U+eUUvKLX/yCYcOGEQwGOf7441m1alWfxziOw89//nNGjx5NMBhk7NixXH/99Z16VNXX13PxxRczfPhwQqEQJ5100nbPq1AourOwvp4VLS2UBgIIwGi0MDemSNbGqWlvx5ES23Up9PsJmyZLGhv59iuvMKempsfzdRI+eXm0bYow6TWLSZ+AkREp4H03HKheBzNfhuqaBE///QF+vPhf+Fybd0smc/m0q1haOAaASD74EqC7ULnU5ZOmJhbW138qr9FgoyIq/cB1JWtW1dPelqCgMMjY8RVo2p6lSp9++mlOO+20HT7+3Xff5fjjj+f444/n+eefp6Kigvfee49rrrmG1157jddffx2fzzeII967sSyLBQsWMGTIEB588EEqKyt5++23+da3voWu61x++eU7dN7f/va3/OEPf+D+++9n9OjR/PznP+fEE09k2bJlvZqu3XTTTdx1113cf//97L///nzwwQd8/etfp7CwkO9973tIKTnjjDMwTZOnn36agoICbr31Vo4//niWLVs2oM7ZCsVnnaZEgrTrEm5wCS9uRW+1wYUALqEwbJyo0VTmCZCwz8cIw2BzNNprRCMrfEqawJzTyMRmMJ2+xzClaQOP/vUhKlJbsYTO30efyr+Hfx6EQAJbS6CwZVskIhQBGi2aOnR+35tQEZXtsGhBDT/7wRNc/9OnueX/XuL6nz7Nz37wBIsW9KyOd5bnnnuOoqIiHMf7SV20aBFCCK699trcPt/85je54IILOh33zDPP5Loop1Ipvve97zFkyBACgQCf//znef/993u9ppSSSy65hMmTJ/PUU09x2GGHMWrUKL7yla/w7LPP8s4773Dbbbfl9hdCcPfdd3PmmWcSCoUYP348zzzzDADr16/n2GOPBaC4uBghRK4T9kDHBfDkk0+y//774/f7qa6u5pZbbum0vba2llNPPZVgMMjo0aN5+OGHqa6u5vbbbwfgG9/4Bl/84hc7HWNZFkOGDOGee+7p89odEUJw11138T//8z/k5eVxww038I1vfIPf//73HHPMMYwZM4YLLriAr3/96zz11FP9Pm9HpJTcfvvt/OxnP+P000/noIMO4oEHHmDLli38+9//7vW4t99+m9NPP51TTz2V6upqzjrrLGbNmsV7770HwKpVq3j33Xe56667OPTQQ5k4cSJ33XUXiUSCRx55ZIfGqlB8VikLBilqcMl/ox29xcI1BXYQbAPC7TDhQ5eR61zCtTZGo4UASgIBVrS09BjRaEgkcDcmKHsnTrDVi5r0hpAuZ218nd8t+RMVqa1sCZZwzZTv8u8RR4EQpHwQzYPils6Tu25DYYNLWTA42C/Hp4ISKn2waEENd9zyCuvWNBIMmZSU5hEMmaxf28gdt7yyS8TKUUcdRSQSYeHChYC3pFNWVsbcuXNz+8ybN69Tk72lS5fS0NDAcccdB8CPfvQjnnzySe6//34+/PBDxo0bx4knnkhLS0vP97loEcuWLePqq69G0zr/SEyZMoXjjz++24T261//mrPPPpslS5ZwyimncP7559PS0kJlZSVPPvkkACtWrKC2tja3HDLQcS1YsICzzz6br371q3z00Uf86le/4uc//zn33Xdfbp+vfe1rbNmyhblz5/Lkk0/y17/+lYaGhtz2b37zm7z00kvU1tbmnnvuueeIx+Occ845PV63N371q19x5pln8tFHH/GNb3yjx33a2tooKSnJPX7jjTcIh8N9fj300EMArFu3jrq6Oo4//vjc8YWFhcyYMYN33nmn13EdccQRvPbaa6xcuRLwlqTefPNNTj75ZMATiECniIymafj9ft58880BvQYKxb6MKyUL6up4ed06FtTV4fbQYWZK+RBGrZSQdnFDGhgC4YBwwRXgT8D4j6DszTgFr7ZS+GIr+Q0uadftFtGYU1PDL994g6HLbXQbXG3bUk9XCtNRrl96D99c/wKGdJlXNoUrpl7Jqnxvyb+9ADQbwrGej6+o9ca+N6KWfnrBdSWPPzSfeCxNWfm2hCm/38RXZtDcFOXxh+Zz0LTKQV0GKiwsZOrUqcydO5dDDjmEuXPnctVVV/HrX/+aaDRKW1sbq1ev5phjjskd8/TTT3PiiSfi8/mIxWLcdddd3HfffbmJ6m9/+xuvvvoq99xzDz/84Q+7XTM7wU2ePLnHMU2ePLnbhHbxxRdz7rnnAvB///d//OEPf+C9997jpJNOyk3UQ4YMySXF7si4br31Vr7whS/w85//HIAJEyawbNkybr75Zi6++GI++eQT/vOf//D+++9zyCGHAHD33Xczfvz43DmOOOIIJk6cyD/+8Q9+9KMfAXDvvffyla98hXA43Ndb0Y3zzjuPr3/9671uf/vtt3nsscd4/vnnc88dcsghLFq0qM/zVlRUAFBXV9fpccft2W09ce2119Le3s6kSZPQdR3Hcbjhhhs4//zzAZg0aRJVVVVcd911/OUvfyEvL4/bbruNTZs2dRJwCsVnmf6WG69b3UBBXKMpoCHTLr6kBAcMt4PIkOD4QAqB3mKR/0Y7RYf5aYzHeXndOkoCAd6vr+fnb7yBU59iahQsE0yr57FNaV3Nj1Y8Qmm6nZRmcNfYM3i54jAQAkeDWB7kt/d8rARcHYodg3WrGxg/cehgvmyfCkqo9MKaVfVsqmmhoDCQEylZhBDk5wfYVNPCmlX1g/7GH3PMMcydO5drrrmGN954gxtvvJHHH3+cN998k5aWFoYPH95pMn766adzORFr1qzBsiyOPPLI3HbTNDnssMNYvnx5n9cdSH/Kgw46KPf/vLw8CgoKOkUyurIj41q+fDmnn356p+eOPPJIbr/9dhzHYcWKFRiGwcEHH5zbPm7cOIqLizsd881vfpO//vWv/OhHP6K+vp4XX3yROXPm9Ptes2TFUE98/PHHnH766fzyl79k1qxZueeDwSDjxo0b8LUGwuOPP85DDz3Eww8/zP7778+iRYu48sorGT58OBdddBGmafLUU09xySWXUFJSgq7rHH/88Zx88skDes8Vin2VvsqNv/3KK/xl1qycWGlvS2BIQUUgyNamGLieGOj6cVVYIEMCVxMQcSlfkOQ3xus0FUjitk3acZBAWToTjTG9iEpHNOlw/ob/cO7G19CQrA9VMHvSBWzI8+acRNCLouRHer4viVfOHC4KYEqd9jaVo7JP0d6WwLZdTLNnLWf6DGzb3SVv/MyZM3nzzTdZvHgxpmkyadIkZs6cydy5c5k3b16naEptbS0LFy7k1FNP3eHrTZgwAaBPwZDdJ4vZxYpZCIHrujs8hl3J1772NdauXcs777zDgw8+yOjRoznqqKMGfJ7ekk6XLVvGF77wBb71rW/xs5/9rNO2gSz9DB3q/fGp77KOXV9fn9vWEz/84Q+59tpr+epXv8qBBx7IhRdeyFVXXcWNN96Y22f69OksWrSI1tZWamtreemll2hubmbMmDEDfh0Uin2JruXGQdNEE4KgaTIiHCaSTjN7/vzcMlBBYRDD0Ei0pTDQ8Jk6pq51FyppF5l2Ee0uug2hVknVvBTj56bIr/NECkDa54kJ0wIzve34slQrNy35C+dv/A8akpcqDuPKqd/LiZRIvrfM5OslCiMFYELZkDBDwiEMQ6OgUOWo7FNkfxgty+5xu5W2d9kbn81Tue2223KiJCtU5s6d2yk/5dlnn+WII47ILbeMHTsWn8/HW2+9tW2slsX777/Pfvvt1+P1pk6dyqRJk7jtttu6iY3Fixfzn//8J7fM0x+y1UHZhOAdHdfkyZM77Q/w1ltvMWHCBHRdZ+LEidi2ncvnAVi9ejVbt27tdExpaSlnnHEG9957L/fdd1+fyzcDZenSpRx77LFcdNFF3HDDDd22Z5d++vrKJkGPHj2aoUOH8tprr+WOb29vZ/78+Xzuc5/rdQzxeLxbbpGu6z0Kx8LCQsrLy1m1ahUffPBBt4iVQvFZo1O5cQ/R866JsGPHV1BSlkc6ZaNpoAlBdvW/09E2GBEXzcmIBgBbkr8V9lsMI9dBWT3ktYMLBBPbEmkPa17GHz+8jQPb1xHX/cyeeB6/n/AVUroP24BEAMKR7lEcCQgBeUV+hgwvYPzoIZQWhIhEkoysKmHs+Ar2RtTSTy+MHV/ByKoS1q9txFdmdPoBllISiSSpHlO+S9744uJiDjroIB566CHuvPNOAI4++mjOPvtsLMvqFFHpWO0D3qf+//f//h8//OEPKSkpoaqqit/+9rfE43EuueSSHq8nhOCee+7hhBNO4Mtf/jLXXXcdQ4cOZf78+VxzzTV87nOf48orr+z3+EeNGoUQgueee45TTjmFYDBIOBwe8LiuueYaDj30UK6//nrOOecc3nnnHe68807+9Kc/AV7uxfHHH8+3vvUt7rrrLkzT5JprriEYDHb7g/PNb36TL37xiziOw0UXXdTve+mLjz/+mOOOO44TTzyRq6++OpdHous65eXlwMCWfoQQXHnllfzv//4v48ePz5UnDx8+nDPOOCO33xe+8AXOPPPM3HLfaaedxg033EBVVRX7778/Cxcu5NZbb+2U8PvPf/6T8vJyqqqq+Oijj/j+97/PGWec0WmZSqH4LJItN/YbPU+HAcNgayqVS4TVNMERR41n1Sf1uK4EJB1XUAWg6QLX8Z50JWiZ7Ya97ft+H21bMsr+tTJcm2+sf4EvbX4DgFXhEdw46QJqg2UAxIPgT0Kgh8/PEtA0EAgKAn7yQgGslE0kkiQY8nH2+TP2OFuN/qIiKr2gaYKzz59BMOSjuSlKKmnhupJU0qK5KbrL3/hjjjkGx3Fy0ZOSkhL2228/hg4dysSJEwEvQfW1117rJFQAZs+ezZe//GUuvPBCDj74YFavXs3LL7/cLXejI0cccQTvvvsuuq5z8sknM27cOK677jouuugiXn31Vfx+f7/HPmLECH79619z7bXXUlFRkZtQBzqugw8+mMcff5xHH32UAw44gF/84hf85je/yZU7AzzwwANUVFRw9NFHc+aZZ3LppZeSn5/fzXPk+OOPZ9iwYZx44okMHz6807b77ruvm7DpD0888QSNjY08+OCDDBs2LPd16KGHDvhcWX70ox9xxRVX8K1vfYtDDz2UaDTKSy+91Ol+1qxZQ1NTU+7xHXfcwVlnncV3vvMdJk+ezA9+8AO+/e1vc/311+f2qa2t5cILL2TSpEl873vf48ILL1SlyQoFXrmxT9NI2TZImTNwMxotkJKkbePTtE6lvdMOGUV+QQDD1HFcF8txvXwQvMofp4Ny6e0vi8CbgLPbhyWauHXxH3Mi5d/DP881Uy6nNliGBGIhL+qi9ZJWJjXwB0zCeX5sx6WlOUYiYVE9ppwrrpnF1Ol7p30+gJB7eTZde3s7hYWFtLW1UVBQ0GlbMplk3bp1jB49ulezrO2xaEENjz80n001Ldi2i2FojKwq4ezzZ+z2N/6pp57iZz/7GcuWLdut49iT2LRpE5WVlfznP//hC1/4Qu75aDTKiBEjuPfee/nSl77U6Zhf/vKXzJs3r1MJuGJwfn8Uik+Dneli7ErJSU88wdqP66ha6WK0Ot5ajAZ2kU7NBI0xBwzlpbPOyp3TdSU/+8ETrFhZy1YspAu6BCMhQYKU2yIl/Zlgj25cxPdWPUnYSdJuBLl1wjnML90fgLQJuNs3gUPA8OGFXHHNLML5/j3aoDRLX/N3R9TSz3aYOr2Kg6ZV7pHOtOFwmJtuuml3D2O3MmfOHKLRKAceeCC1tbX86Ec/orq6mqOPPhoA13VpamrilltuoaioqFv0CeDFF1/MLbEpFIq9izk1Ncx+9102rG5CJh1EQGfUuDKuPfzwfjXh04Tg3HA1D36wEZF2cQIawhBIW6I1WYxq1zj38OpOwkfTBF85/zCu+9W/0JISgjro4CDREy4io062J1L8Tppvr32GU+rmA7C0oJrZk86nyV8EeEs9gUTvUZmO6Lrgy+ccyrRDRvVj770LJVT6gaaJPbL2XOUXeAm5P/nJT1i7di35+fkcccQRPPTQQ7mqpJqaGkaPHs3IkSO57777MHpYh846uCoUir2LOTU1XHn/8+R/lKAq4pX5Ss0itmgTV654ntsvOnW7YsV1JateXU+RZhIrcEk6DlJKhA6BApNwWmPVq+txTzyk0wdUd4SfDYeYVCwHX5sLKUADa6hJqtqP/8MoZrL361bF6vnJJ/+gOl6Pi+CxyuN4cNQJuELH1cDSvaWe7ZEVQ76AydDhhds/YC9ECRXFXs2JJ57IiSee2Ov26upq5RWi2CfYmeWNfRFXSmY/MZfSd+IYNjh+ATporiCvzcX3TpzZeXOZedWFfb5OWc+ssuI8hvtN4paF7broQqC7gpRIs3Z1A/c89hYtkQRh0+S4GRNp9CdoHaIRrC7G1+wgki4yoGGXGdTH4xSvh+FberiglMyqf5/vrPk3AdeixQxz88RzWVTsWUCkfGCkwddftwcNcEHXxF5bfrw9lFBRKBSKPZz+uqbuzQy0+euC2jpi7zbhtyTxACAkuN5SjhkUGHFJ4t1mFtTWcejwYb2ep6NnlgDyTJN4LM3WlhjptI3juLgSnrzr3dwxz97/AYXD8yma6JKqdNDKt/lKtafTJNZFGdvU/VohO8nlq5/iuEbPUuHDovHcPPFcWn35ACR94E93P643JF4+jAaMHFG815Yfbw8lVBQKhWIPZiCuqXsrO1K08PS7yzDbXBzNK/eVmmcV70pJSkr8fg1fu8vyT2r7FCodPbP8fk+kNNS347ouUnrlxR2RAC60boowsgG2HGTTPMFPWkp8uk5iXYQp73nJrx0da8dFNnHtiocYmWjCFhoPjDqRJ0bORAoNW/dOPBCRQubcQoLp07no0qP2iNzJXcFnQqio0L9CMXDU783up6traraMPmiajDAMNkejzJ4/n5mVlbt0GciVkgX19czf4q1lzBg2jOlDhw7KNbPNX+OxNAWFAUzTwLLsXPPXnkprXSl54+1V+JPeRJ1VA64O6YDXyTiNS8AV+LYz+Xf0zDJLdba2xHBdF00TpK3u6y8d71hPw4gFFlvXW6wdD0lg6nvbKnQEgJScvuUtvrnuOUzpUO8v4qZJ57O8oBrwqnp66/HTXy78xpH7ZBJtln1aqGQTKuPxOMG9tL21QrG7iMfjQPd2CYpPj4G4pk7vo83CzjCnpoYfzZvH0qYm0hm3Y1PTOKC0lN/OnLlT0Zwdbf76xGuLMT6OoknPtwQNkKA54I8DQXCEZ7w2rXp4j9fOkvXMuuOWV2ioj5BK2Wiahiv7lyQiJBRuhakfeMswHcuIw1acq1Y9zpHNSwF4u3R/bht/NlEzhARsfedFimNAoLrn9h77Cvu0UNF1naKiolyzvFAotEPGXgrFZwkpJfF4nIaGBoqKitB1fXcP6TPLQF1TB5s5NTV87YUXqIvFADCFACGwXZdFjY1c+Pzz/OPU7VfW9MaONH91XclrTy5B2OAaIByQLiA8q3rhgi8BjgllVQWMn7D9vI2p06u44ppZ3P2nuaxd04h0ZK63z/YQZKzvu/ic7N+2jh+ufJihyVbSQufuMafx7LAjvNdP90SVsT1vlF5wM8JMSG/Jq7apl9bJ+wj7tFCBbY3e+ursq1AoulNUVNRnM0TFrqeja2qwh8hWT66pg4UrJTfOn09jJrLm7yBYNU3Dcl2aEglufPfdHV566k/z10gk1an565pV9bTVR3ECAonw+ulkbWEz6K4X3Tjj3EO2m7eRTeK1LJvPfX4s9bVtpCwbJ7VjKkJIl7M3vs6FNa9gSJdNwTJmT7qANeERAFjGNiv9HcHt0FfI1UEagmFlvZul7Qvs80JFCMGwYcMYMmQIlrWTMTaF4jOCaZoqkrIHMK2igoklJSxpbGSE0b3nWEsyyUHl5UyrGPxqj4X19Xzc2IgrJWaXppdCCAxNw5aSj5ua+rX01FNVT9dE1q701Py1vS2B5oJf07CSTq9maIamMb6PtiGwLYl37eoGYtEUjuMl0O5odlZxup1rVj3KIS2rAJhTPo07x32JhBHICYydESlAJ1GmuaDnGZwyY/LOnXQPZ58XKll0XVd/eBUKxV6FJgTXzpjBt195hc3RKCWBAAHDIGnbtCSTFPh8XDtjxi5JpG1KJEg5TqYjb/fzZ59JOc52l556q+r5ynmHbUtkLTNI2Da262JoGkHD6LH5a92WNqLRFMJyMDsoCtlRS7kQMgz++dB7TJlW1dmoLeNHs3DBBl6/fxGpaJpU0vJM3jSQO7gcM23rSn6w6hFKU1GSmsmfxp7JqxWH5JZ69B1d5hHd+/t0bGYYtnU+XrRpt7d02ZV8ZoSKQqFQ7I0cV1XFX2bNyvmobE2l8GkaB5WX71IflbJgEL+ue/1qpOwmVrJzp1/X+1x66quq585bX+WU/5nCuo3NrN7URMqQuJn8Db8tKM4PdWr+umhBDU8+9j6ya80wXm6Kq3mTuj9gUF4aZlNNC6tW1KNpXiRmRaKN++pXsqKlhZFz4oS2uuiuQLgSKbxcl4FKPk06XFDzCl+teR0NybrQUG6cfAEbQ564crQdEylZMbJpLBQ1QH6kyw6GoLgkhLBkjwnH+xJKqCgUCsUeznFVVcysrNxlzrQ9LctMq6jggPJy/rtxI5brdspRkVJiuy6aEBxQVtbr0lN/qnpe/e8nLD3QJf9jCEUEwvKiI5EC2HKgpKW887kScYtwaZCWhlg3UaFlxIob0rCEJJGwuON3r9DeliCesmi1UiQKBBXVfoIRiWOAFpe5ZRkYmFApT27lB2seYkrLBgCeH3o4fx3zP6R1E0fzxJPWX4fZLgi8aEqkACo2QzIPSoIB/JqOaegU5gXQhCCVtLolHO9rKKGiUCgUewGaELukBLkvs7XrZsxgeXMzdbEYKcfB6FD1A1AeDHLd4Yf3Kpi2V9UTzg+wfn0TkeEm5qmlOB2s6K1SnYZYLOcTkz1XfmGAmniUdAD8Paw4pX0QddLE6tP4kmDXOpSX5dMgk9gSQq0SZ1EC6YBtgsm26JAYQHLK4c0fc+WaxylKJYjpAW4ffxZvlk8BvCjK9gRKRzO43nA1b4zC9b4bfoMheZ1LkXtKON7X0La/i0KhUCj2RbLLMuvWNBIMmZSU5hEMmTmztZJGeOCUU5g6ZAimEAS2uhTUORS1CaaWlfHAdkqTt1fVYwmJbbuUSAOhadjlJlalH7vcRGhaJ5+Y7LksJEnbxvJlynS7YKZBt8BIemJgq2HR7lgkHQcMQdwvEY63HCM6iAnh9G9CNF2bS2r+za+W3U9RKsGKcCWXT7uSN8unIAGXnkWKxCuf9uUZuFPCWNsp1JJ4VT2mLZCZ6ExrMtkt0benhON9jT1CqPzxj3+kurqaQCDAjBkzVDdbhUKh2MV0XZbx+000TeD3m5SWhUnE0zz+0Hxmjqzkb/sdzSVrh3DsEj9HfOzjCx8FOPw9nbUf1bKgrq5Xz5GOVT09kU7bSA30vJ5NBQOGQTpTBp09V1siieW4CLvnCIjmQijubUv5IS1hcySKIyVOJrfFCnj76va26Ed/JsNhiUZuWn4nX9nwFgBPjjiaH0z5DnXBUpwOZcMdyQ7RNoGRfn79v2fy0h++zYwTJ3ROAO6wv21AIuQJG9sHsTD4LLAch3iH6lUpJZFIkpFVJftsnx/YA4TKY489xtVXX80vf/lLPvzwQ6ZMmcKJJ56ofE8UCoViF9Jfs7UXn1nMnbe8SvPGdoYU5lFYEqJNplmxqo6H/vgmF/zlSU564gnm1NR0u0bWnj7SnuzWkkFKSTpmkS7QiBX2PMaOPjFjx1cQGhKivS2BdMGfBLw+hL1imZ5wyIoUV0qE8CIxjg4yk7jbn7yUo1o+5I7Fv2e/ls20GSF+sf83uHvMadia0WNlTu4+heedsnoSnHHN55k2fRSaEFx8ymGIkE4qD5wCnXSeIJ4H8TDE87wxSg20lKR1iEDTvUaLyaSF60pSSYvmpijBkK9TwvG+yG4XKrfeeiuXXnopX//619lvv/3485//TCgU4u9///vuHppCoVDss/THbM22XV58dkku6pISkppohIR0cUIapgMVyy2WNDTw7Vde6SZWsvb0wZCP5qYoqS6TbEF+gODhpTSnUj0KmZZkkoklJUwZMoQFDfUsrEphGxBMbquk6W0SE0Aw5kVNvBNmlncyS0OODqvHbn8S9DtpLtv4OD9d+ghhK8WSwjF89+Creb9kMllLk75yW7ICxio3GFNclHt+/IQKqqvLMBxBWnNx/QLH8MaFBH/KG/v4ZTBqrRf1ERJS0TQtzTESCYvqMeU99kLa19itybTpdJoFCxZw3XXX5Z7TNI3jjz+ed955p8djUqkUqVQq97i9fd+2DlYoFIpdQX/M1kB6gqIwAAg2RyLYroupaWhC4PolvjaXUakA60nyk//+l18eeSRDOlQlZe3pswm7kUgKw9CoHlPO2efPoKWcPn1iTqyu5uQnnmBhQwMtRpLSg2Dyot575Ai8KIsADBe0OKT9YFieY212LcY2oXA700dlopYfrnmQCVsbcBE8XHU8j1Qdjys0r1JI9i8aozsQcgQlHcq4NU1w2SXHcNPsF9gaiZMyvJNpjifENMeL+Ai/hjQEluUSsARBv8lpXzqYaYdUMXZ8xT4dScmyW4VKU1MTjuNQ0aW0raKigk8++aTHY2688UZ+/etffxrDUygUin2Wjl2DfWXdXW8jkSQlpWGam6IkpcvalmbimVyTlHTQhMDUBJoLyahFVLN4v76ec559ljzD4IDycq7L+LxMnV7FAVNH8sL85dQ2tTOsrIBTZkzG0L14Rm8+MSdWV3PrBx/QlEjgSIkEmkq9yp5QPLPs08Oyi8BbcpHS2xZIdr9/w4Lhm3t5caTk2LZ3+d7yZwjaNs2+Am6aeB4fFY31NjOwCiFNwqi3LX5a9DLXfuXYXALy1OlV/PjaUzx33PWNxGJp0ik7l+QrHCDuYvkFwq9RUZKP1Z5m0YINnHXuoZ8JkQJ7YXnyddddx9VXX5173N7eTmVl5W4ckUKhUOx9dOwa3NwUJT8/gOkzsNI2kUiSYMjHyacdxP33v83G1nasjBrI6hlXSizbM4LbZMeI2972WDpNwrL478aNLG9u5oFTTgHICZG06+Kr1bhz87KcYV1PPjFThgxhxoMPUheP51xYAQraPOHhZkuAexAMnkld39EOQc9iI2gnuLT2CU5ZvwSA94snccuEc2jzhXORmoHi4kWA0q80cmX8eW6/6NROYuWgaZW8+Mxi7r/7TZyUvS3vRoJmgd+RlISDFPr9pPK1fd43pSu7VaiUlZWh6zr19fWdnq+vr++1GZrf78fv938aw1MoFIp9mu0tyxwwdSR3/PNNtAYXM08j1WFmF9IrBW4vkLTkbzunmXGztVyXuliMy159Fdd1iVoWpYEAfsMgZdssaWzk26+8wl9mzeK4qqqcT0zWfO5/58xhzYoGwmQqXnzQVgi+dCY/ZTs+JTsiKEalN3LdJw9S3daCLTTuqz6Zp0YcjRRav3xPesLVMsdJr1ty/kcJZvfQyPGl5z4iFk1tW07KbsrUPMe3JpEFoc+Eb0pXdqtQ8fl8TJ8+nddee40zzjgDANd1ee2117j88st359AUCoXiM0H2E31XZ1pNEyyoq2PDBMGIVg0j4Tm52gJ0CUYaHAPWjiM3qWpCoGfdZ3WdlG2zZutWigIBqgsKcstLQdNkhGGwORrNGbppQuTM51ava2Rre5zDMgmzju59Jf2QCHrJsNlclEGpCJEux0ff4PtLXsB0Xer8xcyedAErCqr61aAwO46O2gK8qh2hbXvC9WmEIrBhdedGjqtW1LNhfRMAhqljpTs0W8wsYaXTNsmkjSbY531TurLbl36uvvpqLrroIg455BAOO+wwbr/9dmKxGF//+td399AUCoXiM4GmiU7LCK6ULKir46V162gslRR8Pp/wkgRmq4VuS1wNkiGoH5bxB5HeklDXLsuaEFiZ7ss9lUB3NHTTN6dzPYHa7RSal8sLgCG9CphAEoraOpxjEO49KGN8d9OjHL/ey4t8o+wgfj/+LGJGsF8iRQKiUPdccNMOnYqXsgm3LkgD8IFIgEx2buS4YtkWzxVYF2hCoGkC15V0fMmkhGQijeO43Ro17uvsdqFyzjnn0NjYyC9+8Qvq6uqYOnUqL730UrcEW4VCoVDseubU1OTySWKWRWsqxdKgzfAvhClp13Br4pjrkvgSMGoNVK7zDMk2TtSIDOkiHYQXDugqYLIEDIOtqRSNsTj/fWgB8ViaYJGf5o1xNDwzNpFJiN0VaaOj5Bp+ufhhhkfaSQuDv4z9H14YejgI0S+RAl7EZOyQEiJtSZqbop3KrHPOtwLcoAauQGoSEejSyFF0/o+ua0jpiZ6OYiUWTVFcmrfP+6Z0ZbcLFYDLL79cLfUoFArFbmZOTQ3ffuUVIuk0pYEApcEgiZYW4rZNTSRCMB5kyGoLYQkcvyCFCw7kt8PEhS6rp0HrEE+USClxMj2BjF56AWUN3dL1Ca+PT4GftkgKYW+rrNk107HLrPhrfP/DV9GlZGOwnBsnXcC68PB+C5QshhDUbm7FSvfcIlkankiRpkCLu8QKBaPGdW7kOHHyMAxdw3FchNDQNIFh6DiOi9uhU3RldQlf/9Yx+7xvSlf2CKGiUCgUit2LKyWz588nkk4zIryt0/HI/HzWt7WRth0Ci6KItMAOCRwJptDBgJTm4E/ByBUuW8tAZhoXCiHINwwSjoOUsnMJtOuS2BJnYqAAtzZFIpEmGk2STNo5cbIrRIrfaOOatY9w9IY1ALxScQh/GnsmKd03YJEiAMeRpJI2uq5hGN6SjW27SDxnXJkn0KRAxFwsA6IHBrm+SyPH8ROHMmp0KWtXN+I4LpqmIYQXWQEXKSUjKou55Y/nYxi73af1U0cJFYVCodgFuFJ2KrfNGqDtqSysr2dFSwulgc6W+vk+H9WFhbTWtBOMOKR8EoEgaBoMzXTyXdfWhuW6hNqhYq1LW4kgVgjloRBXHXIIf168uJOhGxsTBBfHGNsuKTDbeXDOW8SiqexK0S5BAqPMT7jh3Ucpj8dIaD7uHPcl5lRMz20fCB3fSSG8PB9XZmz6M5U+pgN2XCJ1l1SRRvDwUm47a2a3Ro6aJrjk/83kdzc8T1trolMURdMEhUUhvnvVCZ9JkQJKqCgUCsWg0zHPI+26+DSNiSUlOd+QPZGmRIK06+I3uk8L+T4fxf4wumyjOC9EYTBAyDRzk/UIX4iWSAwsydjlgC4xwiYnnDaRq6YfwrSKitzrYW2IU/lBCp8tKC3JpyjoZ/OmrcCuEym26XB65EWueGMeAGvyhnHjpAvYHBoyYIEC28qHs34tILBsB0dKXAGO33OhM6XGwaeM54ADRjB50jCmDxvaq1idOr2KH/z0VB578F3Wr23CthwMU6d6TBnnXHD4Z265pyNKqCgUCsUg0jXPozffkD2NsmAQn6aRsm2CZndL/aQhydMEId0gr8P2eCxNtDmB7nhusAE0pCWRzRZzHljI+rc2ccn/m8lLZ53Fgto67r7+VZq1doZWFiCASHsS23JyZbiDjRFu4dcfPcSMTV4fomeGHcndY07F0swdEingRVDC+QGikSSBoEmoOED91iha2kv81ZLemaVweX3Nek46YwqHDh+23fP2VSr+WeazGUdSKBSKXUDXPI+gaaIJ4fmGhMNE0mlmz5+Pu6tCBzvBtIoKJpaU0JzsudNxXZ6FWerHitu57VJKtrbEcF132zEuGLqGaeogYf3aJv7wu5dZ8uFG8tugaUs7rl/Q0BqlZmMLDQ3tOI4cdJFi61CVt4QHX7+NGZtqiBhBrp/8Ne4adwbpHRQpEtAMQVlZGPCWZQoKgjTHE+gpieZmrPs177uQkP9JitlPzO33e54tFZ9+2GjGTxz6mRcpoISKQqFQDBq95XlAd9+QPYlsPs0XRo3C1DQ2RSLELQtXSuKWxeZolAK/n3MumEGoQyfkZMIildomXIQQGIbnmSKEyCSDQjSS4ua7XuWUfzxKazRJS1uC1sY4yYSN7Q48P2R7JPItzo4+xV9f+gdFySTL86u4fNqVvF12YL+vld3PFV75dTLoGdy5QpJI2oweW8bosUOIJdO4ERshPYGSXQ8T0qv40V1IvNvMgtq6Qb7Lzw5q6UehUCgGib7yPGCbb0hHs6/dTdd8GldKbClpTiTQNC3XIDCbXzNtSEXOcj8RT+NKiWnqOLZE17uKM3BdSAuXpvVbGb7FawbYcWFpMOMFEqC8gTvefpCD6moBeHzksTww6kQcTe+XSEmanomdz/KEx9oJUGr4KFtqoWX7GdlJ1q1u4qhjJ1DX1O51Os5+7M8YvGW9U6Qm8LW7LP+ktl/LP4ruKKGiUCg6ke21otbIB8528zwyviGdzL52I6+t38D3H3uRVNSiON+PVhEi5Tg0J5P4dJ3vTp3K8aNGdapY6phHsXzpFh594F0MQ6elOdYtipQ1LGtLpfFZ3gTuZiz4B5tEEPYzP+B3zz5FnmXRauZx88Rz+bB4Yp8CJVtGbFre/3UJmg3xPFg1GUojgiGL0pCJmEhA1wTRaJKXn/+YMQcOpb3VE55ZgZLzTvFpuK6L5no9ihQ7hhIqCoUiR7bXyqaaFs/S29AYWVXC2efP+ExXHfSXbJ7HksZGRhhGZ98QKWlJJjmovLyT2dfuYuGCDdx867NUNKfQpQDNwilKEp+SR2B4mM3RKK/V1PDDww7rVqmSzaMYO76Cd99cw+qV9YDs5JUipcR1XYShoacz5m2ul2w6mEigtSLFFauf4oJFHwKwqHAcv510Llt9Bf2KovisbUs9uuP17gkkYcxKKG6TOZGSrfTRNQ2hgWU5bF7ZhAgIUkJi6F4EBZ2cpaxrSXyGxrTq4YN7458hVI6KQqEAPJFyxy2vsG5NI8GQSUlpHsGQyfq1jdxxyyssWlCzu4e4x6MJwbUzZpDv87E5Gu2e5+Hzce2MGbvdT2XRghp+d9NLOI1JMAVuSOCaAr3FIvxmO74tFiWBAJ80N/PwsmW8vG4dC+rquiWEaprg7PNnkJ/v5eTYtpsRKHKbcVlAw8hYyeuDnI9imeBUbuYfc3/PBYs+xEFw/6gT+emBl9LST5EiuvxfSLylHAmFrV50JbtR4uXhaB1ycJJJi/KSfAxHkNYkbkakuFJi2Q6mBdXVZYyfsE2cuq5k1Yo6Fry3jlUr6jr5pii6oyIqCoUC15U8/tB84rE0ZeXbXEn9fhNfmUFzU5THH5rPQdMq1TLQdjiuqoq/zJqVy/vYmkp1y/PYnWTf62Q8TTooMDMJrxjg6hpa3CW0OEbTcUHq43GunjsXPZOr0tELJrtE6DgOXzrnEF567iPWrWnAshwvt8VvkB8O0NweB7xJXnpGq4NCc4nkSPttZj/2HAHHpslXyOxJ57G0cEyvAsUBUkFv+SmQ8JZ5JNs6HzsZozbhQiDVRcQ43o7Z18uV0qvwkZJpB1Xx/oL1bI3ESRkulu6JnaAtKC4Mcdklx+R+b1TUcuAooaJQKFizqp5NNS0UFPZcrZKfH2BTTQtrVtV36rKr6JnjqqqYWVk56M60g+F2m32v8wr8NMe9iE/uHELg+jVEq0XLRgu7QBI2TYoCgU5eMD8ZMYVVr67vNNmOqCzmtDOn8dHiTTQ3RQFIpR1ERjVkBcDOxg5cDepHx/nF+//ky0s/BmB+yWRumXAOETOvz/MLQU6V6JlzCciJJy3b7Tizv6SzWNFckK4kmbG1x/W2v2TVcu43prPq1fWsXd+IZTmYPp0xE8o7CZBs1DIeS1NQGMA0DSzLzkUtr7hmlhIrPaCEikKhoL0tgW27mGbPfxJMn0EkkqK9bc+pVtnT0YRg+tDBE3WD5Xabfa+LC/MIpJMkLButY7WODjIh0ZIQKjUpyST+Bk2TEYZB25o2Hnv6LYo1X6fJdsO6Jurr2jjjK4dQ097GppqtrP1wC24X6bAzUi2aD4Eh63nmXw8zqm0rltD5++hT+ffwzyP7IdiEBF8K0r5tY+l0lEsnYaXrAtfpPH7bcXEzHilCgmvAkqIo6zYv5s/fO4Gvp/w9JqKrqOWOo4SKQqGgoDCIYWhYlo3f371axUrbGIZGQeGeUa3yWaO/brf9ibhk32vbshmal8eG9nYsx0XXvNwLx3KRGrgBwfBwuNOxQkoqP7Jx4g5WsYmtgU8T+P0mVsilrraNP97xGo4JehqQYBudy5F3BAlsrHY5rWkeN9z/EoZ0qQ2UcOOkC1iVXzkgbxTdyYiRbUGkbmSf0nUN5LYOxjLzT8eEYKEJ9nvbpWZCgpvef4+XzjqrxyiXilruOEqoKBQKxo6vYGRVCevXNuIr616tEokkqR5Tztjxu79a5bNGJ7fbvDzMZgeRTOMLaARK89gci+Xcbn/73nvbjbh0fK9Ly8KMKiigLhYjads4rouZkrQXQEllAfk+X+44c3Ma/3vt+JodkBBpTNDekkALG4QMg3hLEplZQrF8XuIseGW/O0PKD7UTovz+1Uc4cdVKAOaVTeEP479M3AgOaClJw0uSlQKkDsL2XHQdxxusrmu4tvd/LSPcXCEwTYHtuLn7A0+0pP3g+iW+ZpvqDwQbtQYW1tf3GElTUcsdR1X9KBSKXPVGsIPrqOtKUkmL5qYowZCPs8+foULSu4Gs2+3wrTpFL7VR8Gor+fPaKXi1laKX2hi+VWdxYyNff/FFljQ2EjZNhuXlETbNXMRlTs22iq2u77XfFYwtLKIqGKbM9VEYCmCN8JG3Veaa75ib04TeaENvcwBvokdkqmPabCItSaT0klQRmYiDyOy3E9QPBbNyNXPvv5UTV60kqZncPv4sZk86n9gARUoWgTc+J+ANzrFdNF2g6QLpbFMimuZVMWmaYEhFIcGKkHd/eAIlVgRWEGwN4n6JsCQlS1M0xuI9Xrdj1LInVNSyd5RQUSgUgGfkdcU1s6geU04iYdHSHCORsKgeU66S/HYhrpQsqKvrtQS4KZHAt8Wi9O0YelMaCbg+LzdCb7EofSeO2Jggaln97i/U8b1uiyWpa2gjHkmhuwJTCipW2pTOiVDw4lbMzSmCiyKIpIuje8dLPBHiat4DPWPklg3EZSf0HdUptg7LD3Q4s+VlXrr3rwyNRdgQquDKqd/j5aEz+pWP0hUptiXyGhKEJRF+jfxwgPz8IKGQH93QcvcgpcTnNxhSUUAwz0d7PImQ4OiQ7qAlsp2UU6Yk0C5J1/ccEclGsiLtPfdSikSSjKwqUVHLHlBLPwqFIofq3vrp0p8E2RJ/gBGL0mgx15sU016pidQ991PSDlUroanat93+QtOHDs3lsfzHrmXOtCQtZRDc4lK22sJ0BaUF+VQU+tnY2o7WZJH/WivCAlzQrUySKeA63n+yzfdEpgIm25BPsi0xdSC0FkHzuFYefOphjtqwDoCXKg7jz2NPJ6n7+j64LzJLPq4JdfuZ6MU+fjzzc5x95EGsW9NAe1uCcH6QlZ/U8ug/3sVKOxQVh/D5DNqiCYh7N5jy93x6W0BQCobpoR63ZyNZd9zyCs1NUfLzA5g+AyttE4kkVdSyD5RQUSgUnci6jip2Lf1NkK17r55Aq1fXK3W2JYHaoEddUn7Ii4KM6sge5siO/YWywmhxQ0OuS7I/oDGtwSu9TQRgSzLGqIICqkoKqWtph+xyD50jJBoZsdLhMXiCJdTz6kefuALWjYdJzjKe//NjlCbixHU/fxj3ZeYNmTYoRnGOD9AFw8eXcu1pR+fEYMef94mTh1JZVZrzOolGUtjCJVkgCMRA6jLXGiBHJqoU9JsUFfW+dJONZGXPHYmkMAyN6jHlykelD5RQUSgUnyn2hF5GnRJkw9tKVbMlwJujUWbPn8/RI0by8rNLEHjLLDmxkM3/kF6yqquDG7cRdP+4n+0vtK6tjVs++ID2ZBLRaFGalKRMkNLFbPOa8UnAdV3qolFGBsP4k4K+WtQMVu5APASfTLG57u0XuOrNNwBYFR7BjZMuoDZYtlMiRYCXfyIlehqGjS7mL5edh6H3PvqukcXNdpwrFv+X0fNSBFockgHZybdOAH4LRo4p3u7SjYpaDhwlVBQKxWeGPcUVNJsgWxrouVQ1u1zzwvzltDRH0TWBrgls6fXTyX6iFwIMKXA0QYuwKenQawe29Rc6sLycJ1euRG5MMPkTB1odz3xN8zxFdBvSJggkWhqciEVtc+sut3aXwJZKSA1r4sUHH2L6lk0A/Hv45/n76FNJazs/RWmGQBdec0AJhAyjXyZ5HSOL06Tkzs3LWDuxjtELJMGUi+MTuBkHWpF0MQIGl3z9qH4JDhW1HBgqmVahUHwm2NFeRttLdt0RmhIJ0q6L3+h5Ig4YBmnXpbapHfBKV3EhoOv4u3xJV1IxpACjwt9rf6Evjx/PpuWNVH9gYW51sHUv18LWIRgHwwYz7eWgBBLe5NuRXSFX0iYsPgT2E4tY8Ifbmb5lExEjyK/2u5i/jD19h0VKp7EKENKLpvj9JmVlYdpaE6xZVT+gc2Z7OInKIOumG6SLdXQbjIQES2KVGJzznSOZNn3UDo1Z0TcqoqJQKPZ5dtQVdLDcYLtSFgzi0zRStk3Q7G6Hll2uGVZWgGHoFBQYbG2JYdsuuu75e0gpc+WzZ3/5EC46uKTX/kJpy6ZkaQrNlqQCkA2UuDokghCOgD9FznHVFdv68uwKkdJcBqv3T/Pbl5/h0vfnA7C0oJrZk86nyV+0w9eUACJzH0BRSYhwwIeue72HYmmL9q1xFqzbwtgJA2s/0KmH07Bm9GabQFowckghPzn1KL5QrUTKrkIJFYVCsc+zI66guWTXVIqhMZOAbZA0JEvshk7JrjvCtIoKJpaUsKSxkRG6njFxc5EBDatUpyWZ5KDyck6ZMZl3q5axfm0j5UPy2bo1jpW2M+Wt0vP4qCpCP7CAQp+PF778ZRY3NHRzpn3m7aUE2iWOT+B07QooIBXo0KQv8/I4tuyxvLhrUu1AcDVYORlC4Xre+vM/2K+pHhfBY5XH8eCoE3CEvoNn7j5IXQjyAj7y8vy0p9PUtLaSSlpoNvzyw3f5e2TNgAXnrurhpOgbJVQUCsU+z0BdQbPJrnJjgv1WuhitKXChQIOSIp2aCS6z589nZmXlDk1S2aWEK+9/nsDcZkIRcjkj8XyoODDItV+cgaFruZLWRNyLBrmuJJ2yicRTJHSXt0cneOGVlztFe04cPbrT9YbpIUwEceHSceUqayfval50RWQea32ENHZ0So7kw5JpkrNXvM+dd/+boG3RYoa5eeK5LCqeMCiRG0EmmuKCg2RLLEoBNo2JBK7t4EuCm68TNHSWNGxfcPbWkmAwezgpto8SKgqFYp+j6wQTLhhYL6OF9fVsXNZA9QcWmi1x/ZpnHuKA0bJ9u/T+UNII+y8RbG33zMJc05tg89pg2CKX4lMkVHUvabVtF1tImvNdtkzS8VcGGdZLaXOWoqIgBUE/6aQnxDQXDMsTKsA2jxG9w3ODhARqRkPtmCR3P/kk53y0CIAPi8Zz88Rz2erLH9Tr5bxbBCQtm2gkipmGYMobjBZ1GfJ6bLuCc05NDbPffZcNq5uQSQcR0Bk1roxrDz98p5b9FANHCRWFQrFP0WNeSXExY4aEaN8S6Vcvo8ZYPJfT4Ya0baYZBri6hhZ3+7RLh77LoLM5M6Rdxo0so749SmsihY1Lyi9piyS47tZn+eENp/GF6lGdSlpbWxP89IO3WEkbI/Lzey1t7jj5RtpTkJb442DKbVERN2PMlq2q0J2d627claQfPp4Go9Kb+PD2hxjT2oSDxv3VJ/LEyJm4YnDrObImc64Au0hgpkCPSwwbEOCENGRAbFdwzqmp4cr7nyf/owRVuWiXRWzRJq5c8Ty3X3SqEiufIkqoKBT7OP3pqLuv0KuJWlMTdcME+zdq/XIFTdcncjkdXXNaEALHJ7bZpY/tPo7tlUFnc2Z8PoONG1tIpm00CX4BUhc4PrCaU3z/sRf5w7mncFxVVa6kdUFdHSsWxSg1g/1yol20oIY/3vYqrpTouoa0O/SzkRk7/EyZciA5eO9F/VBYepDkO/Pf5LcvPI/Pdaj3F3HTpPNZXlC9S5J0RYfv6cl5tBZKCt+KEYiDzNfQtIww6kNwulIy+4m5lL4Tx3TA9Wue0Z4DeW0uvnfizM6by8yrLtxnf4/2NJRQUSj2YXZV1cqeyHZN1Igy9HN5HLIpuF1X0GxOR0JIui8UgS0kQXq2S8+WQcdjaQoKA5imgWXZuTLoK66ZheM4JBIWyUQay8l4z2cN3WwvAiB9glTU6hYd6U9pc9aJ1nZc7r73v7S2JygsD6FHUrQ1dY4CCSDl85Z9Oqya7DC2Dp8cAPHyOE89+BhfXLEMgLdL9+e28WcTMXu2mB8s3ExTxNDCOOmDAphpr4mg0U/BuaC2jsS7zQRscPO6R9OMmEvi3WYW1NZx6PBhu/ReFB5KqCgU+yj9tWjvyt4agemXiZqe4IYfnEBRRPTpCurldARI20ksx0XXvJJgV0ocV6JLQUEw0M0uvb9l0F/75udJpSwc1/XKgCGnDqSWSWq1JXn5vk7REeh/afO6tjZO+fPDOKvrcQxobrMwbW+Zp+u7GUhmGgzuJK1FsGQ6HFy/jsdufpgR0VYsofO3Mafx7LAjdqiZ4EDJOvcKR1KwKo3reJ4t/RWcyz+pxdfuVWDRg7iRAQ1fu8vyT2qVUPmUUEJFodgH6a9Fe9ckwr05AtPvSEMygVYYoskHBOkxfDB2fAXjRpfjrqojargkHQdHeuW6QUMnnNYYN7q8m116f8ugN21szoUvpASR6ULsPZH57oJP10m7Fk2JbR15p5QPYaKdx7pNWwkXh3DKzNyEmnWiHRkOc8sHH0BdgmoJwtRwATvt9GCy76G5vWzoB66AteNh/TiX6+a8zi//8wqGdNkcKOPGyRewJjxilyz19IQApCFwghpa3EEKr1uy7boYmrZdwelLezkpribRevjhcDWJ5nr7KT4dlFBRKPZB+mvR3vGT+o5GYPYU+hNpcF2XX7/9NrWxWJ9CrGOnWzOexgwGQRfgSKyETSi/5063/S2DbmqI4A+YuAmJ5rhIp+dOw2JTEt8kk7KgN5Fmc1/yV21ldCKNq6Wwiw0S08JEhmi0JJPkmyYSaEkmGZZngmaBA1rKwRjEHJQs8RB8dDD4jXZe/cujHLt+FQBzyqdx57gvETcCg3/RLmTaHuWQpkC6Lq6UuHkaxZZOyqBfgnNa9XAMQyNpu2g9NGt2LYnP0JhWPXxX35Yig7LQVyj2Qfpr0Z79pN41AhM0TTQhvAhMOEwknWb2/PmDYh+/q8iaqDUnk0jXxWi0MDemMBotpOtSF4sRsSzWt7cTNk2G5eURNs2cEJtT09lCP1sWPHpMOVgSO2qDJRk9ppwrrpnVY2+ggsJtZdA9kS2DLq8oIBj0bPzRehApmZk3vDzFxJi3/LZoQQ2/u+EFlizcSDKSQk9L9CT462zCr7ciNyY8k7jRY9iwugljY4otsShtQRfR7mAkehrRjiOBzZXw9kw4pHElH//uNo5dv4qkZnLr+LO5eeK5u1ykCDon0GYHJhIuRkRiWHDEoWMoK8yj2DGpCoYZFc6nKhim2DEpzA92E5zjJ1RQXV2GmQbLdnI/866UWLaDaUF1dRnjJ/TdfFAxeKiIikKxD9LfPIbsJ/UdicDsafRpohaG4HhgmDmgpbCBdrodO76CkVUlrF/b2GcZ9DHHTea1l5axbk0DPlMn7TpeRCDz0VG4XnKr5sKY1QLbdvndb1+kuSnqnStzToG3dGQmYOIywWmHjOLev77JAW0uWubeHQ30nVjW6Ym0CcumQHOFw/89/wo/eON1NCTrQkO5cfIF1IR2zySefbV1LdMtWWjUftzIqadPZcF7670qrISDYWiM7iGJGrxo2mWXHMNNs19gayROynCxMs0Hg7aguDDEqbMO5PFXFpL2weRJw5g+bOhekce1t6KEikKxD9LJot3oPmFmLdqnVXgTykAqSfZkupmo+bwJJtwumLBI0pbnQxQNTIgNpNNtxyWjvsqgDcNznL3lxhew27z+PQ4gXZkzX9PyDUrzgrTURvjij+5Gq430eM3s3STq4jz8h//idz0hYfvASEMoNtBXsW+ayuHjqTAkuZW3/vAQh23ZAMDzQw/nr2P+h5TeU9rqrkPXBY7TJSQlwdB1yivyScTTLHhvPb/57ZdZt6ahX4Jz6vQqfnztKTz+0HzWrm/EshxMn0750Hza02n++NfXsW0vETpdoBE8vJRrz5q5Ry+N7s0ooaJQ7INkowvffuUVNkejlAQCBAyDpG3TkkxS4PNx7YwZuU+BA43A7Il0MlGrLCNh27kESstx2FLfRtHHSdorg92qOQZTiHV1ku2tDHrq9CpOO/Ng/nHPm0gpMRCgCXRDI784SHFBiPZkioZYFHep3O46vZCgJyERylTwyMFN+HQ0WDUZNoyFM5d8zL2PPU5hOkFMD3D7+LN4s3zKp5Ywm0WaguLiPJoaop2eN306ZWX5hPJ8GLrGppoW1q1p6LfghO7RtNrNrdx/31u0RZNYPhBBgSY1Aq0u1n8auTKmjOB2FbtNqKxfv57rr7+eOXPmUFdXx/Dhw7ngggv46U9/is/XQwaTQqEYEJ26vfbQUbfjH9SBRmD2RDpW3GhCkNdBcMUAxycwWh2MJhu7vLMY60mI2Y7LC/OXU9vUzrCyAk6ZMRlD719aX3+XjKYdUsVz/8rDxkUKMA2dwrwgIuMYWx+JIpHoPae8dEOTXgTF0T2xog2SHX4k3ys7Tods/vTP5/h/770FwIpwJbMnnU9tsHRwLjRALFN696kJNE3guhLTpzOysiT3Wnft4zQQstG0hR9s4IG73yISTSIAvwMyLXGDEpmnYcZd8j9KMPvdd3e4/5Oid3abUPnkk09wXZe//OUvjBs3jo8//phLL72UWCzG7373u901LIVin6K/3V4HGoHZE+mr4iZkmvj9BnbEQiQdOrpq9CTE7n3xfR57cD5WcyqX5/Kn0tc554IZfP3kQ7c7lr7s8ztS40/R4LdwGpOkgwJhCQJWkqF5eWiAE7dxQzp6e/8VhwAMBxgEkSKBDWO8SMqY5kae+vNDHNi4GYAnRxzNfdUnY2m7LzDv2lCfiOMX3muu6xplZfmdXuuufZwGyqIFNdw2+yWi0aQXMRLe0pywQY+6OGEN168Rikg2rG7ao/O49lZ220/YSSedxEknnZR7PGbMGFasWMFdd92lhIpCMYj0t9vrQCIweyIdK266Nh4UQKkvQL1mU+emCFp6r0Ls3hff58E730CkXQhoYAiwJXZDkgfvfAOgT7GyPfv8LHNqarjsP68ix0mqWzX8SYnjg7ibZkPSIuRo2IbAHmbiax3kToH9INunp3kIXPjeh9z1r6fIs1O0GSFumfhV3i+ZvEuWelwA0XsHZ1d4y1wCMC1wcLGl5zxbPsRb7snSUx+nAY0la+AXT28TKB2N+VzQEi5OvuYJ2qSzx+dx7Y3sUTkqbW1tlJSU7O5hKBSfWfobgdkT2V7FjUw4jBszBG2CnxVbt/YoxGzH5bEH53siJU9DuAJhSdCE9zjmbT//+IPZsLaxW8SkP/b5U6dXdS4HH1tILGgRWhxDtHrusY4maQw7NA2ByvXJQbG2Hwh1w7yqHpM0//jHv7lgyfsAfFQwhpsmnUeTv3CXXVvDEyO9qqDMi+ACrgnJg/NotdJUrpbEE2kMQ+uzj9NAyC4nhvP9JJMWwnW3jUFscxEmnfl/QN+j87j2VvYYobJ69WruuOOO7UZTUqkUqVQq97i9vX1XD02h+EzR3wjMnkZ/Km4uu+QYDjq4dyH2wvzl3nKPIdAjrjcJSbxJSffyXOyGJN+97AFSbelOEZOvnHcY/3z4ve3a5x80rZKFDZ3Lwa0RPjaWSVpq2tGTEr8FKR3GrgLdhkQAgr03ah40bB2WHwhbquCALbU8de+DjG9twEXwcNXxPFJ1PM4gdzzuCc31hEhPV+rooGtYEFyVInFIkA1FNuMaC2ivi/XZx2kgZJcTiwvyaG9LYifTuHQRjBK0tEusRGPUuLI9Oo9rb2XQhcq1117LTTfd1Oc+y5cvZ9KkSbnHmzdv5qSTTuIrX/kKl156aZ/H3njjjfz6178elLEqFIp9i/5W3PQmxGqb2tEsiZ4RKLKDo5iwwbC98uHGze1UDMnvFDG5bfZLpNM2hUU9dzXO2uevWVVPk69zObiUkrp4nEDapXoNhKKg2WSiK2AbmWWHXVhWs7XYc5hNhCTf/e+7/O75Zwi4Ns2+Am6aeB5LinpoE70L6Sv+kXtfJGhRm9J34qyfbvLFq45grB3qVwlyf8guJ9q2TUlJHuk6G9txO10f6Qm86IFBrj/88L0i+ri3MehC5ZprruHiiy/uc58xY8bk/r9lyxaOPfZYjjjiCP76179u9/zXXXcdV199de5xe3s7lZWVOzxehUKxbzFQk7aODC3J9ypl3EyJb4dDpNyWN5FfFMjlwWQjJnVb2kgmLUrLwj2eu2P1SVlV53LwuG0TrLWYtNiLoNg+cIQnVHRn8L1QOuIKWDsB1kyAgmSCR+5+gtNXLgHg/eJJ3DLhHFp9Pd/TriTbXBDpiTWBJ9Rk5j3RJEjD63As4i5Dl1uUBoOMHz540cDR44ZQMDSP2g1bKSwJMXRoIY1NEVJp22t7ADg+MGaVc9tXjt3j87j2VgZdqJSXl1NeXt6vfTdv3syxxx7L9OnTuffee9G07YcU/X4/fn9vbbUUCsWeTH+rYXaWgZi0dWRiSWmuNLgvAkaXZF0hyMv3E4+nicdS5Bd0z1PoWH0ytks5uO04jFrplSGnA95k/Gmsy8fy4KNp0FYCh63fyBP3P0hltAVbaNxXfTJPjTgadxCXetIa+PrhkisFJA4MkjAk4eVJNNtb5smKFCE9geUENYSmYZkOeVEoaBcwSC14sg06N5a3MXRtmvbaFHrIoGJIGGG5tLUnMfw6Z33nCL5ywlQVSdmF7LYclc2bNzNz5kxGjRrF7373OxobG3Pbhu6F6+MKhaJv+lsN0x92leCJRJJIQyBd6X16h9yn+o7LLq7bfbbNywvQrEWJRlKE8wO92uePHV/RrRy8qE0QioLl2zYZ66ZGpgZm0JHA5ir45ABwNZdrX3mD6//zAoZ0qfMXM3vSBXxSMPjRAaFDNADh7eTbCAFGk0XdOEhNgwlLwWjLRFmk114g5ffGrjkS3RSEHZNo++BU3HRq0FkZJO73E1wUxW112JJoozgUZPLkYTuV/6LoP7tNqLz66qusXr2a1atXM3LkyE7b5B7c+EyhUAyc/lbD9PdcgyV4ulLrxLF1IAhaOmOYlvlz5OpezxwJWLK7gLAtm7xwANOn9WmfnxVUHcvBN9bVo7mQFqAh8OkawpZIOfiVPmkfLD0IGoZDaTTGY/c9xhc2LAfgjbKD+P34s4gau6ZyxbCgZhQYNeDv4Jrb8R6lACco0FscxiyQrJymsWoa7P+OiyvANTxDO29n8Bs6Q/xBdIsd9krpSNcGnUIInBEQHe5Db7Jo2RpHDC3gN5d9ud8GgIqdY7e9yhdffLFXMtjDl0Kh2HfIeVFkqmH8fhNNE/j9JqVlYRLxNI8/NB/X3f7vflbwrFvTSDDkdR8Ohsyc4Fm0oGa75+gLX0WQZIHAsEXuj2N2Es39sRR43iodyEZMxowr56ofn0z1mHISCYuW5hiJhEV1Lx2Xj6uq4qWzzuKmk44lP+Aj4Hh5KcKWEHcHXaQ0lXvdjhuGw8wVa1n629v4woblpIXBnWPP5P8mXbDLRAp4L11hG6SDmeRgOosUR4N4CBy/wA4JdBuqVrhEiyBaALoEn1/Hb+j4dB1dEwzLy0MmHEZWleyQV0pXem3QKQROuQ9zdB4rjBiLGxt2+lqK/rHHlCcrFIp9k47W9turhukrr6Sr4Omr/HdHl4HK80LEhxmE69O5PIisfNIc7/+uCYnWFMEiHdNnkE7bbG2NYwR0pp4ynqmHVHHQwZXd7PddKfn9C2+zsaGVyiFF/L9ZM/AZOpoQVPrC+F0NKw4u3ge2wRQpjgYrJ0PNWNBcl/97+jV+9Oar6Eg2Bsu5cdIFrA0PUnLHdiht9KqY4nmgWxDIuE1IIBkAxwCkRBcC2+dVQPlbJTUTNQ5YDFrcBb+Gq3nVWYnWFAX5gR32SunKvtKgc19CCRWFQrFL6cvaHvrfi2WwBA/0nuMypXwIJQ0S2/D8OjS3e0mwJqFiSAFNW6MkWiwSrkMiX1A3Gd5eM5/fblkCQtAYj5N2XXy1Gj+f8wbBRTEC7RLDhoXAk396myO/egDnTN6PO299lbTreEZn7uAu90QKYMnBXkRieGsbT/z9ET5XuwaAV4dM50/jziShD36BQm+ebdnqHVfPVFGlt3mm+FOQMMCv69iui5txf82zNELjCogVQGhxDK3VQjgSQ9cYN3kI55x/+KDliuwLDTr3NZRQUSgUu5S+rO1hWzXMZjtO07p1vbrhDpbg6SvHJZjnQ291SPi9CTKQOZXsMBTNgtVbmqiZrNNsWqR8YJfoDMsPIByHRY2NSCkpCwYpDgSwauIUvpfCl/ByXLKnMtth4V8/Zln+CgxNIyJsdDF46/HZPj0rJ3tmdact/oT7Hn+UknSMhObjznFf4rWK6YN0tZ6v39NzUnivg+50qOIRnljRXTClYFRBAQBbownarCRGnokuBE1lLlsOczCbPVO8YNiEiXBC/wpN+8W+0KBzX0MJFYVCsUvZnrV9c2uMaIHgiiVvkF4s8WkaE0tKuvUX6q/g6SuhcntJvQccXY3rSPQAmNFMVKCrn4oDbtolv8Zl5eFgZj79b4xEQEqcTJ5dYyJBSyLB1I/BlwCjlwKeVMQiZXjbRcYFd2eb6CQDXtlxSzkYjsPtj77IdxfMA2BN3jBunHQBm0JDdu4iA8TWwScFDjJXRWVnEmN1h9xrHNB0QqYJUpKwBUPGDEGf4GdxUxPNiYRnFFxmUB4O49M0ljQ18e1XXuEvs2YNio9Jp4qsSIShMZOALUgYLpuCaQKGwZfGj9/p6yj6j0pZVigUu5SstX0w5KO5KUoqaeG6klTSora+ja2uxepxkrDPx7C8PMKmyZLGRr79yivMqdmWHJsVPJH2ZLek+2wya18Jlf1J6l3y7gakkBi2hp7xShfZpM+sgBCeyVcoAsXtGpoQmJqG5TikMmXLWV2T3wZ5ES9S0Cc2uS7NOytS6obBWzM9kTK6sYUPfvennEh5ZtgRXDX1ik9dpLgapAJe/o2heS+opnmvmxv0HovMa1QSDJBOWjQ3RXNtD1446yxGFxSQ7/MxoaiIiSUlFPr9BE2TEeEwkXSa2fPn4w5SMcZxVVX8ZMQUpr4tKZ0TIW9uGyWvRRg3N4W5Jc2N8+dz0hNPdPr5VOw6lFBRKBS7nKy1fddqmGiBYMMhJoVjCwmaJpoQvU4+fQme7KSWTah0pWRBXR0vr1vHgro6XCn7leOSaEthhzS0lNtJMAiJlzuSya2wTW9iNdPgSElms4eEglYobYCCFjDsAeSc7ESTZNuAj6bC4kM9Z9vz3l3C4ltvY0pTDVE9wPWTv8afxp1JWusejRpM3Gx3YTx3XaeD9b8jwLUlpq4RMHSEAMcUWCEQGpiGhky53SqlFjc0UBuLMTwvjzyfr9P7J4SgJBBgRUsLC+vrB+UeFi2o4c1/LKY4qlGSH8QOClwTCiKC8QtdSpvoUUwrdg1q6Ueh2MdxpdwjuiF3tbbfbMe5YskbhLtMPNB98sn25umpl4+uC4ZUFHDE0RPIC/t4bf0Gbnr/PVa0tHjJrJmlpHOCo7ab46JJgX9iPvaiNnxW50Ta7AgtA1zbixLEDJdUB3FR0ghjVkNe1EvElXiW+P1lR9+VXJ+ePPCnLf728LNcuPQdAJbnVzF70vnUB3a+M71leOXTvdFRpJDJt8m+hGY602kYcCwXvV0S1gW+PBNNFxQOC/Clcw5h6PDCbiZ+n2YlTsfIW2l5mNWtrYDA9OlIU6LFXYo/TiFOKmRzLMbs+fOZWVmpnGl3IUqoKBT7MFkb8K6Tdtf8j0+Ljtb2TevWkV4sBzz5dBQ8Cz/YwNtvrKKlKca//7mAJ574gDpfmi2TdMKj8vAbBinbZkljI1sizewn5HZzXI47ciKPxD9k2OLOM3J28vWlvJyK9iJoL9y2vaQR9l/iCRPL9MqYdRfM1OCbtmVxhdejZ+0Eb2wHbG7gyXsfZEJbLQCPjzyWB0adiK3pfZ+oH9gaJPMAV8OM9LyWle2FJPGcYw3bez0EYGYs8LWgjmO52LZEOBLLSiHKTE792hROPXlqj+f9NCtxOkbeErZN0rbRs2XPQuD6NfRWG7PZoaSou5hWDD5q6Ueh2EfJ2oAvaWwkbJp95n/sDjpOPj3R1+SjaYJYNM3Lz39EfW07wZBJcWke7TKNr8Vm9AIbf61FNJkkuDrF+FVgN6VoDzm095Lj0twao8FvcdfmpeTVuVjmtqoUV3j5I1J4k7HmwLpxbFMg0ouk6HbG2l33clocHdK7aKUllgfvfR7WTvTG8Z3XP+C9O37PhLZaWs08frb/Jfx99CmDIlLAE10BVyB9glRYdKqE6glfGq8iyvQSaW0DYvkQC0gSeWAXaDj5Gq4JEc3m/zYt6vVnMluJ05zs+b1rSSaZWFIyKJU4HavLbNdFQudoiY63DJh0CRgGaddVniq7GBVRUSj2QXqyAQe8/A/DYHM0uttD1jtTBtqT+VvMskjiQkigxx3Cb0YoToHheEs4ecITEOmgv5vFfXNrjK2uxYZxJsURnXAUnJBGynExE52t9B3NMyWzswJEwrBNXuKsndEEQnjdlpHg24m8k57o2KfHMSCcSPGP+5/ijDUfArCocCw3TzyXZn9h3yca4DUFYEYlVtBBD+pYjoMv0Xk7bDPJ01wIJjzLfk1kvmsCKcE09Nxx6IK8qMSuT/X6M9m1N1JJIEDAMEjaNi3JJAU+H9fOmDEoP8sdq8sMTUPg/T7lzu0AGsiApjxVPiVUREWh2Afp1QacXZN8OBBcV7JqRR0L31/PxUPGk2+abI5GiVsWrpTELYvN0SgFPh8/PvQw1qysZ8F761i1oi5ns79qZT1r1zeiBXXito0EzyBMSizp9cgJxbx8ComXT+ICmg2JWIpQyN97Uq/t7SwMsW1yylT+0GESLm2AkevgkHdg0lIvghBMetfVLW9/X9rbd7BI+2DRobB0qidSZqzZzMc3/Z4z1nyIg+CBUbP4yYHfGlSRAp7QCBb60DRBIK2Rb+lo0osypXxebkwy6EVNBJ3bDdQP875rhvezp3d1j81EKIql0efPZLY30kHl5cQsi9pYjJhlcVB5OX8epNJk6FxdFjQMAoaBk23vICVaysUpMrBK9UGN5Ch6R0VUFIp9kD3VBnzhgg3cc+8bNGxuQzqSoM/kC0NCrB3nZ4WeYGsqhU/TOKi8nHPD1bz2h/e6GbONP6GaR5Yux40msIIgkoKAYVDk93sVQtJL3IRMcmeHpj1uxr+jrTXO9b87i1g02S2pVwY8e3Y77uJPdkmolWBkSnzGrNqWMGuZ5EIKesYoLhnw8lkGi8Zy+HgapAOAlPz4xXf4zbxn8bk2Tb5Cbpp0Hh8Vjun3+TpGQfqzb5u0cUNgWJLIAX6aEy4VS23SQe9EtvSElJ6JYEm8HJV4GDRdUGT4aHOs7lGPTITCyDNJu+k+fyaPq6piZmXlLk0Oz1aX3XHLK7Q0RSkN+tli29gpx8uz8WlsPcDPllhsUCM5it5RQkWh2Afpb/JhSSDAgrq6T6Ui6N4X3+exP72Fk7KxfAJ0CNg24Y1pKrcG+eYlMygcW0hZMIi2OcWdt7zazZht5ao65q/YSN1EneG6wJQCV4OE5SU9SrwOvbk76HorAnRNEI+n2byphS/M2r9bUm9LgYsblBQ3d7fP74iWWdIRAnx2xgMl670iPbGi9XF8f3E0WLkf1GQ0SGkkziP3/pMTNn4MwPySydw64RzazLx+n9MFT8D1067f1QBTYCAQtktzIkHc9rJkNQekgRdtkts6G2uO95qky3USTRK2pnAC3utlaJ56FJaLlpA4hTrRAonP2f4yiibELk9c7VpdVur4iEqLWBHUTTZJl8FBJeW7LSn9s4YSKgrFPkh/8j9GhsP85M03WfkpVAS9tn4D9973Fr6kjZunYWgarpQkXJeU7kIUFr2wiv/93VkA/Oy3T3RrPujzm0R9LqLdpWKLhluko7fYENLQdI2U43UN1LaXEyIEUkoa69uBzqLOcl0aN7Sih6GkaTunIROxySTYupnlIR2BI+WgrKu3Z/r0xDxHeY5buoEHH32IYcmtWELn76NP4V/Dj8q40fUfKbxokK15EaDtjVXmZfZIuGgpScUyGym9iImR9pZ9nEwuSi43x/L6DPmGBrCDJrzRji/pYpkuOhIjKRE23usXcSh+pZ2Rh5ftMcsoXcvpwwVB2gskLankbi3z/yyihIpCsYPsKf4kPbG95ENDCOricTZFo5QGAp3KeAfTjhy81+mm59/AbHMgqOe8MTQh0HSB5bhEdSfXUBBgU00LZp5BezqNoWmETJO4ZZF0HPSAhtHmEp+aRzASR4u7uH4NQ4DteMmzkIkadHk7BICUCAHlFd7snxV1K5dsYcgyi+FRb/LtLzLTp0aTUFASJBlN46Z2LoNWAuvHwqrJXlRCOC43/HseP5z/EoZ0qQ2UcOOkC1iZX7lD59dkxm2XDpGg3saiQVKTaAkXM+7db9rw+ge5aQgkIRiHpPQSjA0JetrLV6mZICjx+3FGmMSOKsS3MILZbGNY0lseMsAOChwBoVZJ6TtxlhyzcdAaDO4sHcvpFbsPJVQUih1gT/Mn6Yls8mF2nNn8jwPLytiaSrEpEvlUKoIW1tezqaGNEdKb3Lqia4Kk6xBLpFnw3jo2peI0RKIkAyCFQODl1OT7fLhSIjSB5kqcfJ3o5wsILY6ht9porudxHy0R5LdINNvLSckmwIrMJ31HSvLzAxxz3GTAE0xfCVXx0PwN6I5namYb2wQP9J7PkX1OE2CgoRs6Ub/0cmTktnLmgZAIeLkoLZlGe8Obo/zz3kc4on4lAPPKpvD78V8mbux4pYlgWxNARCYaJLvfo8TbUUtJAsnM+ILgZmYOy+8tCwUTXudj3fXyUdoLJOvGQ2qYSSizpGaN8JEeWkTo6WZEu8TxgzQFQng5RhUlIaz2NI8/NJ+DplXmBK1CoYSKQjFAsv4kkXR6l0cjdpaekg9d4Kynn+5XRdBg5AI0JRIkfRI0gbQlbrYyJHNt3QIjJknINI898j5R20JLZ3rCBL0lophlEUmnkRKk4838660oBSPzsU4uwmiyScfStAgbp1Qnf2mSyo8cNDcT7aCT5Qm2Hz5evImp06u494X3eeL2N/GnvJ0Me5vD6nbJiJ9s9GZrOol0XEQmVyMZ8KqA+nu6uuGw9CDPAh/g9A9Wc8+/HqY0HSGlGfx5zBm8OPSwAS/1bPcesj4xmf9rLpg+nbTlIGSmekp4HjFOl5Qn1wcJzTN0Wz8RGOpngz+JaehU5eXlfsba02kimyJMjEtSfhCmwK/rlAWDlIdCCCCVr+UiayqSociihIpCMQD2Bn+SrnRNPnx53bpPtSKoLBgkVgiRPJdQW6ZqRXjj8jkCPeJN7IZfIxFwsdKSQMoTL650cE2Bi9dPB+lNiLECaAo7bG1vZ1RBAeFSg61xmwmBAi6cdCBX2m+SsB3GrugeGUmbkI4kuWn2C0w5bgz/efojjJTM5ZtIukdBenons/tnm+nhF6RsGyPjwJr2eSXEqQC5aERv2AYsPxC2ZFZyDMvh9sf+w/9b/Boakg2hCm6cdAHr8wZ38s7eVzbaBJ7okhoEi/20R+LoNtRXQUUNWL7u5zA1DcdwwYJoHtiFEtPWGRoKUeDzDmhPp6lpb6cw7olHaXq5PGnHoT4eJ2AYFPh8mD6DSCRFe5syUFNsQwkVhWIADMSfZE+11P407cgBtqZSRGyb1WM9i3l/0osYOEKixbxcBTQoKAmxKRVDR0NqLsIBLS4xhUTXvElSc73Jf914r3LEcl2ia9ooWw1j2yUFZjvPv/0Ok3SLT8ZAWxEUtnnRDUfzBEG2AU1LW5S5T32MyHpkZPJFs8siWg9LIR0R2QTarA+YkGiOt+yhW9596rZXrtvn61MCH03zvEgAxm1p5Z/3P8zUlnUAvFRxGHeNPZ2U3oNK2Al6S6DNJgY7uifyooWweRiUb/Zef7fL8p0QAj866C6hsI/fHHcc/1i6lI+amnIusnWxGI7r4vgFriYRLtiZ2m5HOmyKRJhcWpprY1BQqAzUFNtQQkWhGAB7qj/JQNgZR9iB4krJb997j4CuE6lwWT5VMnqlJBQF08kIDx3Ky8Pofh0RlZgxuS0pJPNJX3dAS3pVJM0HB0iWuji2TXGjZOxiSdDVKCvJpzgUYGssgb/Z5cCF3vmTge6TKwJSGgQSLjIgwNl2zWxPH1fzJm0H0HvJM9Fcb7+No0BU+0kYLhsCKUatgfGfeHkbvb42AtZMhLXjySmiC/+7jDtefIxCO05c9/OHcV9m7pBpO/UebA8pei7DdiIW0ido2t+kPT9NLAzhdu9166jgpOuipSBWKJgwsYIL9tuPkfn5uUTuYCaJG6A5XxILZ1x82Sb2Eq5NQyyGHnepHlPO2PF7RuWPYs9AOdMqFANgZ/rT7ClkK4Lyfb5eHWEHy8QqG4EalpdHdWEhyWEmi44QLJ4h2DjWi3CkwhAM+dCFwEx4gkFqmWoUAakgJELevrYP9Kog44uKGFtYyOR1OqYNQ4bkE/AbRKw0KeGS9HvRDMPqPd8ku5QkDJC66DXpVRpeFCd7HtHhK3ue/CisCCdoLnApaYKqDRnvkV6I5cH8z29rJhhI2tx/9zM88Oy9FNpxVoVHcPm0K3epSMnebtacrePzmoRRI0sxvlDOpiKvKd/GCQLX8MRXtqWAlol6WTpEDwxy7eGHownRzUXWcV3sTLXV1gqBYUMwCoG4l8MTisHWhijBkI+zz5+hEmkVnVARFYViAHya0YhdSW8VQQeVD66JVccIVFAI8n0+4paFXegSCrvoNVEcKbFdF5/UvAhFdjklE+Fwda/KxNUhLwpam8AuFxS2CfztkrTfW1qwMg3kpPTWbGzTW7ow7G3JqR0RmX+kBDeooUcdL9+kQ5WQkF4Oiub2XL0jAMOFgq1elKC90OKQTHNC2/CSUDsigU2jYMX+nvgBmLKuiUcffIhJ7ZsA+Pfwz3PP6FOxtMH/85y5vVxZcvY+pQbxjLY2JRQKHxd8/UgWBNp4/bXXPGfaIRqrpklGrnAJRUFY3nGRAvAdVsRt557Q6ecmm8j98LJlfG/OHNrTacqbBSPWubmmjVnPG93xxnbgsWP2mNJkxZ6DEioKxQD4NJujDRauK3OmVQWFQcaOr0DTxKdiR95TPkwomxczVJIujGO22OhC4NouesaMTcqMPb0hcA3pWd9reM6oSS97VSQdXMfFMSDtuLkeMo6nVnAyE7HP6pCbkkV6S0/4NLBcZBicsI6WcBB251Jdox+WKKYNRc1Q1OItj1g9iJS0D5ZOgYZh28bw3VcWcdPrT5DnpIgYQW6ZcA7vlu4/8Be6H7hsy6fRhEAKmV1ZQ2Qcan0Bgwp/EN2CouIgo31Q4PeTdhzSjkNjGTSWQF4r+C1ImdBeCFOG9DyVaEJw3n77cduCBSyqb6BqhUS3IRnCS6i2vQEI4SVJL/9wE64rVURF0QklVBSKAfJpRSMGg0ULanI24B375Zx9/gymTq/a5XbkfUaggE0TdcZ9CIm2FAGfgSYEhgDH8SZR25AYzjYHVTSBExDELYu4k2S08PJHNE1gZZoS6haEkt7+QoKwIRz1SmuzCbmmBbYpKJ0xhIb36tGiDrZfQFigRWROZGQLevqzRj7+E++77ngl106Hv66NQ+DjqZmKJ6CwPc29/3iGM9fPB2BpQTWzJ55HY6B4x1/s7SAADEFJSYh0xCKVshCa8Kp8HEllOExxQZCWpigjM3ki7Q31hE2TcCiExKveaYjHiZVASggkoEtJTSTSa2m+JgSXHnQQP3nyPwSjmehWNgFZz7xeQiANaKuLqtJkRTeUUFEodoBPIxqxsyxaUMMdt7zSrV/O+rWN3HHLK1xxzaxdHmbfbgSqMsg5h09h1avr2bihGQDX9nIZNCEw0iDSAs3USEuHWLFgYyCFz9IZVl1E+qNGQq0uceGFPXTbc0ntmhyqSa9E2HA8AREtgHXjJG/lNzB0ms7Q5TahqESk6CRSpN4PS/4M2eqe7HKQlu7epwfgc8vqeejxfzA6Vo+L4PHKY3lg1Cxc0YMb3g7QNd/E1b3XwzKhsqKA4mCAuJmmob4d13Uz0RUwhaClqXOeSCehmZfH1vokxXEJAY1IocSWkqBpMio/ny2xWK+l+d+aMoX7X3gfzW3Dyqg+3crku7h4bsFALJVk4Qc1SqgoOiFktn5sL6W9vZ3CwkLa2tooKCjY3cNRKPYIXFfysx88wbo1jZ365YCXw9HcFKV6TDn/+7uzPpUw+/acfF1X8ugbi7nvkXcQy2Ne3oTwlns0BDKzHHDCedOYfPxoyoJBGhIJ/t/fn6HqAwvd9pZbgolM5CX7OmRLjjNLG/EQLJ4O7UXkPtV7tvpQ1A6VtRpDV3q5Ltk8mR19ddoLYMl0iOVnruNIrnvmfX7+7r8JuBYtZpibJ57LwuIJO3iF7kg8QdJpzMITBUKCz69TUV5AKM9HPJampSVKMuklyxaX5HWKtmWZU1PDlfc/T3hJAl+76y3JaRALQ81EjYKxhRRkco9ilsVTp5/eY5Tu4XmL+PuNr2EZEl0KzExpuswkzRhCIF0oLcvjmutOUbkqnwH6O3+riIpCsQ+yZlU9m2paKCjs2e8lPz/AppoWnnt3Gf5hoR4jQrbr8ujy5WzImKp9dfLkXNfb3uit/9HMkZWMiPtYuH4LaR9MnjSM6cOG5q43d9NGfrj0bUY0xikwtiWwShccpCc4dMlLc5dx4AljmT50KH9etIhNRTaRg2DMai83JCtSJJlk0ezt6IDTwXitY3lt5rFuQdEmd1tFzw5+hJPA+nGwalKmcgmoaE7ywANPMWvLQgA+LBrPbyeeS6svf8cu0hsC1kzwXoeq9ZnGgz5PxAUSYKcd6uvaKK8owDQ0/H6DvLCf0848mGmHVOXylzpS0gj7LxE0t0viOsjM8ll+O+y/GOIFYI3Yfmn+V4+awn8f+4j1axtxU26uuksIgSEEuBJ/wMC2HGWjr+iEEioKxT5Ie1sC23YxzZ5/xZO4NERjXPvyXLYO1bpFOG55/33+b/582lKpnKXJ919/nZ/MmME1hx7a4zl7i5qcG65m1avrO+XJfFJVgpn55O5KyY/mziVeGycU9SZVMo3ysmJDGuCXGm5ziu8/9iK3f/Vknly5EoHXE6elDCYsg9FrtjmrdgorZEpwNReKtkJ7l1SQkkaYvBjMPnxP+kMi6Jm3bS3b9tysBZv4+9MPMSLRhIPGA9Un8vjImUgxuO4QWV1lmTAkY86WDmSe18HSBEFbw0o5NNa3U1QcYvTYId0iKB1xXcnjD82HtMvIoUWsaW3zluUMgfCBFncJLY7RNtzcbmm+pgkuu+QYbrnxRZoTEYQu0IRAILwlKE2jpCSMrgtlo6/ohBIqCsU+SEFhEMPQsCwbv7+z+2x7Os3G1nYEEl+eybC8QKdeRV+oquLvH3+M7bqYmoYmBK6UbE0mue6NNwC6iZXe+h+t/biOBz/YSJFmUlac12OejDXc5OPmZkY0eG6uHTsAOjqk/Rk/Nh2MtCAVtfjZG2+wJRajLBikLh4HAclgJpLSweQkW17cUbNUrodo/ramf0iYuMyLOPRkfNZfaofDsileWTSAbkn+7/G3uGrxc5jSocFfxE0Tz2Np4egdvka2ars3pACf7ZVxW75MU0QECPCHTKqKiolEEiQTFhd/+yiOPX6/PqMWHSNzPp9J0DRIWHbuGNevobfa6E0WLYHUdkvzp06v4rQzp/KPe97yxusCQuL3mxSX5BHK8+G6UtnoKzqhDN8Uin2QseMrGFlVQqQ9Scc0NAnURaNoSRdZbGAMDaAJ4fUqCoeJpNPc8/HHWK5LQNcxMkLF0DQCuo7tuvzf/PnY7rZEkK79j4Km6Z3TMKha6SLSLjGfi89vomkCv9+ktCxMIu51yn138xby6x0q15HrOOwK77vueALCsEHaEjTIy/eztq2NuGUxLBwmoHtJqK3FXiRFy9TcikyJc9dp2Jf0rPxLGqCgFSZ95HmgCOnltLgMDMuAJQfDkkO2iZRRW+LMve1+frToaUzp8Hbp/nxn2lW7VKSA1zAwzzQwpMAwNExNQwiv3cDQvDyEgHA4gKZpFBWFtru00jEyJ4CheXlehZXjVVihg3QlLVvj/S7Nn3bIKIqKQ5SVh6kYVsCwEUUMH1lEKM8zu1E2+oquKKGiUOyDaJrg7PNnEAz5aG6KkkpauK6kLZrAiVjg00hMCXfqwpvNFbBdF0OIHnNbTE2jLZXi0eXLc88vrK/nk5YWQoZBJJ0mblkAGE02RquDDGgkHSf3fPZc2TyZJcs2U73Ki3w4egfH14xgEdITF3pa4hQZaBV+XCnRhNcEcEgoBHgJstFMyoeWyXHpSjbZ1EzD1AUw7T3PRTZ7zawRWn9pKYG3Z0LtyG0X+Mq8dbz3p9v4fONSLKFz15jT+c3ki4iaof6fuBc6Rou6bdMhnScoqsr3lswyXaZDpsmogoJcg8CBCIGOkTmAAp+PUQUFBE0DV0psy8UVMHpoMX/uZ9fwseMrqBxVSiplEwr5CATM3M+alJJIJMnIqhJlo6/IoZZ+FIp9lKnTq7jimlk5H5VIJIUtXOJFGvbBBTgjutu1OpnoS1eRkkUTAltKNrS35577T00N9bGYZ9QmBBrgNwzGRH0UuCAMz8StYxQGwPQZNLfGWbxwPcWZpQpbepU7mtwW2ZBkyor9gtiUPJKOQ8gwGB4Os769nXzT9AzjkKzcDw5YmFlC6oVQ0nOj1fDKh7NhCoEnllzRafWpR1wBqyd6zRGzOwbiLrc98jqXfvIKOi6bA2XcOPl8VodH9nGmgWEHOuTRdC3BdiAQkYxeZWAPLaKtOU5hYYg8n2+b+MsIgf7208lG5tavbcRX5vngFPh85Pt8xNJp2lriDBtXzO2XnYeh90/hZUX0Hbe8QnNTlPz8AKbPwErbRCJJZaOv6IYSKgrFPszU6VUcNK2SNavqaW1N8HZzLTes/ZCwz6Wkh/31Dp9se8LN+F2MypQSzqmp4c4PP8SWEiOzRCSlJGHbbLJtyjSJtEHodKsYSqdtoo6F7Was801AeAmp/mSXMmMByf1DpIebtESjHFRezv9n783j5CrrtP3rec5Sa1fv6c6+bwQSQoAoKiIquDsqKghuozPqbM5vVASdd2Z0fAUHXF51HB3HHQQVHVfAIMgqBgghYcm+J6T37trr1DnneX5/nKrq6k530kk6JIFzfT4NdNdyTlVIn7u+y31fff75fOSuuxgolRBCYEtJtkPz9ErF2WtHPh5AGgJP6Voqcm0FuWKjX6uqHGElOZ8I1o4zTcM/W7Yjww9+ciurBrcF70v7Sr624K0UzehhnmliaEAaYBoGnvbxbTDGEWLSg57dQ0RtE0NDrr+I2QR2xDomIXA4UVHKOjQ1xPjg+y+csEipMpaINk3JnHnthx3uDXlhEgqVkJDnOVIK9sXKXL/xMTb395Mul+kvlegtFpmWTNJg26A1Rp9LbNCjWQgGGzSW1If4r7hK0RyNcvnSpbXZlLLvkzBNip6HUWnJ2EKQbvTJJSCZVkRT1rB1fuW5BocK5JNgTY8itwaP9UVgyFZIDLdvhAYLyHYYdNWFJlYdgq/785954MABXN/HEAIzboLtQykom1QFiSEE/ugEPkVtI6i+/TMW1ZyezcuC7CEA4cMHf7+VLzx4C81ujpK0+Mb8t7Cm49wRbbVjRRFsVhtIXv2aZdy+fgv6wPirSUKDW/IolzykEEgp6Cq6RKIWsZh1TELgRImKehE9Ot4hJKSeUKiEhDzPGWsjZ08mQ9512ZVOsyQfo+VpB2PIJ6VgsW3SE3HZucAn2yERBC2hatXkmvPPx5SSdV1dbBkYoC0Ww9WavZkMrlKYUta8SLbP15z1pCBZEpSKZdDgOG5gMhaVdC21aOiIopscov0upagebvkYgXV+zIFis2QwpVjeOjKmoOoQ/N8bNvDvDz+M4/tMLZgIkUebgF8xjvPBRY0QIYIgB+gwYx81HBuePht667ZlGzI+37hpDe/a9Uckml3xTj6/9Cr2xY9vtqL+XAwgGjMxTYOuZ9Nc9Zcv5QfX3Y0sj33OgmCl2DCDMlFLa5JCoYxlG7zzqtW89k0rjkkInChRIaUIV5BDjkgoVEJCnseM3sgRQhAD5jQ20pXLETno0rwhH6QMx006GpJEhcToz5J40uMpFH2VNV4BJG2bu/bsYVVnJ67vj0hGnpVK0ZXPU/I8fK0DB9x22Ha2YOFGl9L+waCtIoKNlIamBEqXGHIcIisSJB/MEHMUvi1QBuBpjLKmJRXnkveuYuWq2WPGFEgh+PDZZ7OguZl/fuABugYHaRIaYYNVDOZOxqMayne4xkXvFHhqZbAmXX3QizYO8j+/vpllmT0A/K7zRXxr3psoG9b4TzQBRogUI5BQbe0NSBF4i5w/qLCVxB9jN2mECBNBC84wJR2dKfr7cjx0/zZe+6YVx3xuoagIOVmEQiUk5HnM+u5utgwM0Bod6VCbsm0amppIPDSA5flMmdJAzLLwlMKTMHNqMwe70yza5VKcKmmKRGhPJCjX+a187NxzRyQjp2ybBsuit1ikK5/H15q2viCsL5LVwy0WqSkbmp6DGWb1wlNOmd5pFvNeFKPt6XLgy1EGX4DZHuXaf3o9K1fNHvG6RjvgDjoO//HIIzyby1FoAqdBkkxrtNSIw2T1CB3Mv4y19+sbsOUM2Fe3UWy68I+/fIp/efynNHhF8kaUryy8jAfaj10A1DM6p6daIYnELLJZh/7eLCiNaQh8vzr4HMzYjHgeHeQlGYYcsWEVmqiFnI6cEkLFcRxWr17Nhg0bWL9+PWefffbJPqWQkOcFfcVireoxGqvfJ5rVOBHoKRbx8vnAGwOIGAZaaKIZzTLVgJ8MNoRilsV00+RALsfPt21jUXMzT/b1DScja43bVaS5oIjmYe42iJTUiCuw8oGiCmYvXFj+GGxY5bKp02f2K5I07vZx02Vko8kNH3w9K+eNFCmjHXCV1mTLZaKGwdREgrZ4nOLZRSL3Z7GqrR8ZmIuNNnQTjG3ylmkMvFHydQ73HV0e/3Xrb3nLgcCsbEtyJtctuZKuWOvR/rFMiKDVFoiN6kpxW3sDUgZOrlWBMtbcs1YaO2rVzP4s2wxN1EJOW04JoXL11Vczbdo0NmzYcLJPJSTkeUVbLDai6lGPKCmUr/EMUL6P1hrpARpKwsOXEFcgS5r6ooQQgpZolK0DA1y7ejW7MxkO5HJMGzRo2Fhk8ZCHVEH1YSwvk5EnAZYLZz0Be+YrmnsyNOQFFoJUzOKerz9K25UysNqvBBd+4d6HyRg+yakxIpbFloEBSp4XXLx7y8QHygAUZlmktriBEKl7AUpWNnvq1pKraGDXAthel9ODhkv/1MvX77qZBfkDAPx8+oV8b85r8eQJ/BWqg6qOZRsM9ueZM6+dJcumk0hGyOcclFJjihQI2jQtLYnaPG9oohZyOnPShcodd9zBmjVr+PnPf84dd9xxsk8nJOR5xcqODha3tLCxt3e46lHBj4AvdLBd42tsJ/DiqN0uQRmQNTyiREY8bzWAbm5jI9+65BKuv+1evD/1IsoaxwLDCEzVjkilv2F5QYvIN6G1OU5bQwKvzmr/dW9awWNrd7H+mf1M8RQdhsBv8Rk8K4oXU0zpF8x7SpHM5gJn2jGovvLR4kkFDvM40UNzeiJF+PRtj/Oxp39B3HdIm3G+uPhyHmlZOoEXd5wIcC1NT0+WVEOUd1y5moWLO5i3YArbtnTh+wqn5KHUyBcsBHR0NtacXo/WOyUk5FTjpAqV7u5u/uqv/opf/vKXxOMTc210HAfHGV7Py9QZT4WEhIxECsE1q1fzoTVrOJDL0RKNEjVNSp5Ht11kfjKwkZdexVukugKjKiZrGgaH8kydlhjxvPUBdCundPCH/TG2yghGu0lXvoDlgTgKM3qhg80cPw7RqIUhBUbEwm4z6TqY5nvfegClgwqCAeBpZJdLy6DHjHmaWTsC91qoJPIehQ++1PDsdNi8fNgCH2DBjjJf/99fcmn3owA8mZrHF5ZcQV+kaeJPfqxI0GYgrVpmpPjIBy+qrQG/7Yrz+I/P30656BNPRYhHLXxXkc87lF2feNzGMARK6dBELeR5wUkTKlpr3ve+9/HhD3+Yc889l927d0/ocddddx2f+cxnTuzJhYQ8j6j6jVTnOgYdB1tKZjc2cnBJP00P+YETbF2rQ1AZMgXmr9cUyMKSBFQM3QZKpVoA3Y6tQXBda3McO2KRLpcp+e6Ef7loMSwsBGKEMVyx4JLLDn8wEfX/ocEsauZuquT7VPKBjsa+xDXhmeXQVWceK3247K6D3PCnm5hV7EEh+PGsV/HjWa9CTXLi8Vj4zQbZlzVQLnrkDcXn3/MqEnnJukd2ce/Tu7jrD0/j58pIV+MUPbKZEvFkhKVnTmfV+XNY98ju0EQt5HnFpAuVa665hi984QuHvc+mTZtYs2YN2WyWa6+99qie/9prr+Wf/umfat9nMhlmzpx5TOcaEvJCoeo3Ur8po4C37/kZnulj+CNbIlXxIDXECxB/qIB6rEj6rCh7FgQ26p8873x2bO1m3SO7KJVcGlLRWnDdHi+Dkv6IVtJ4aBuEU0n+NY2aMVwhX6br4NA4D6KmWszKrK6WTCy5r8JACzx5DpTqirmpAc2//eLP/M22XxNRHv12ii8svoKNTQsm9qTHiTZAlDVaCp5tVSwvJfnVlx4KhEe+RLHgogXomMBPGuAohKPJeGUWvmoOb3vdebzlHeeGJmohzyuEHs8r+xjp7e2lv7//sPeZN28e73jHO/jNb34zsmfu+xiGwZVXXskPfvCDCR0vk8nQ2NhIOp0mVbH1Dgl5LlBKn9YXBKU1F17/Xex7BnEigeW80MEmjj1qvqSqC7QAfUETV1y4gm13BZ/cSyWXfM7Bsg3a2hqIJ2wy5TIH+jMYmcMrFW2AFwcrG7i9TpvRTCpiozUc2D9IqViuDYwKASPGMWoBNsH5KRncR4yx3TPidYtgWHbXgpHPsfKJIl9e83Ne3hcM9T/avJgbF11O2k5O6P2cCGP931F/qn6DBE+z+3ybuG1yxkYBZUUyFWHfs0PgVu4twU8aaLuy+pPzMadE+e0P/+ao7exDQk4WE71+T3pFpb29nfb29iPe76tf/Sqf+9znat8/++yzXHrppfzkJz9h9erVk31aISGTyhPr9tYsxT1PYZqSGbNaxi2xj/b9GMu4bLI50jGlEHzkxav4n3v/EITxmYCGaOHQ57JNiRbge4rY43ke2L2BYqFMqjFKQypGuTxA2fHo6c4wpSNFQ8LGiBiUo35tduSQ8xPgRcAoAwbEIxYNdlBNcRwXt+yN8Aip5vHULuxjWN/ruv8ei1wyqKLU5/RYDrz3d/v47PqbmFoawBOS7895LT+ffiH6OWj1VM/biwk8oTEEzO1oZt4zmnQ5S1t7kqF8KUhDrlSNhAJZVPi2AUKgoxK33+H2tZt40wXLTvg5h4Q8l5y0GZVZo+LAk8ngU8v8+fOZMWPy0kZDQiabJ9bt5WtfXEMhH1yoLcvErdtQ+fuPXTJCrIz2/bClZHFLywgr+Mlmose84sIV3H7TOg7uGaQkg+2bQ7xGRLDuKoQAqSkWXcRQgWnTm2oV0ba2Brq70nieT19fliYjieO4mAhUXKAtEE5gviaUDvw/DGiMRJg+s5mLXrqYO369oRZ857o+SmmUPnwnp35mRai6leJRaGDfnMDATdX91pu6T/Fvv32Av9x1O6ZWdEWauW7JVWxJTf6fy3jiSUpB57QmtEUtjfhvL34l1937a1KNgVGf6/nDFrqiMjDsa4Sn0aZAmAIczcG+cLkg5PnHSV9PDgk5nVBK89Ob11LIl2lrT9Yu1JHKhkp/X46f3ryW5StnIqUYM2fHqXN3/dYll0y6WDmaY0op+MRHXs0Xrr+dgUwB3x+5LiMq9/FV1Ww+wLKM2mvXOkgmbmyKk8kUccs+6f48UoHfalJckUDbArPbBcCbYqKAoXSRf77oJVx10UqkFCxY2FGrUjklF03gTnukNONSNDi1iDO2b4sTgadXjMzpQcPFD+S57sFbOX9wMwAPtp7Flxe9nbw5+V4jhzt/rTWGIcilS7U04kKuhOcpLCv4FW2ZRl3k8/BrqC1WOQqhwSxplNKnVQsyJORInDJCZc6cOeNGy4eEnCrs2BZsuFQ/6dYz2qp8/qKOQ3J2YKS76/Vr13LRzJmT1gYaK9vnSMc8e9UsPnnN6/jpzWvZvPkgxXJlQEUE18LAqn3476ZgeLOmkC8zMJDHLXuVFo1GSMEZ587ktmgXCSVp3lDAGPLQSqMFlFOSnjMssp0GS5dOrV1U64PvBgcK/MuXfovuKx82h0cTZPBsOQMWPwPJ7Eix0tMRhAmW62xgGtLwgd/t4NNP/5i2coayMPnWvDfyu6kvnpTE4/HOc9xUZg1DgwUWLOqotQ63benCNCWu6xGJWDQmovQaWbSnR/S3hK+RQz7CAyTc+5ON7PjzgXDLJ+R5RTh1FRJyFGTSxRGfdEdj2Saep8iki+Pm7MCwu+uWgQHWd3dP2vkd6zHPXjWLz914GZ+/8e3EYsGcyKgiSg0NZDIlBgfy9HRnKDseUgZBg0IE/h17n+5hdtam8U95ZL+LYygKlqYkNcagz5S1JdS+Ip968EHu2bu39tzV4DtjbpzuaYd3tlUiqKbY5cD/5OELYe2LA6M63wjWjtevHilSFj2t+PoP7+LG9d+irZxhX6ydj5799/xu2gUnTKSMhWEIbNvANINfwS+/eDGfu/GymriYv7CDGbNayGZKgWOwELS0JmoeN6IaDVBQCC9oBTW3JYgn7FoL8ol1ew9zBiEhpw+hUAkJOQpSjbHaJ92xqLcqP1zODgTurmWl6CtOXv7K8RxTSsGSM6byrvdfEFwExzmGlkGVpb8vh1K6JlB0JTE5GjVxPZ/EEwWMsqZga9yKx4kyglaM4cGsrZqH9+/n3b/7Hd984olazhBAb75A5IA77O0y1nlUnlN6YDuAgEQBcg2BaNk3Z/i+ZhnedEean/7vf/OevWsw0Nw1ZRV/v/Kj7EpOO/IbO0kIAZYlMc3h1pmUgiVnThvRrpFS8I4rVxOL2/T35XBKLi0NcRpbYsGAcVWwaMAStE5J0t6cJBKxaG1LUiyU+enNaw9xrQ0JOR0JhUpIyFEw+pNuPVWr8hmzWpi/sGNEzs5Y1Lu7ThaTccy5F82kf9EYQkeAiku8ZLABpDUgAnGilMb3FVJKWlqSRKMW5aILtjxU8QhwLUjkwOj3eTaX59O/uJvX3PhDfnzfEyilKR4sEM1qynZF4Ihg/bj6pQnWqWPFICto/hZo7g0Gate+bGSY4JQD8IlbN/PD+7/MivQOitLmhkWX88XFl1MyIjyXGIZEVgzttA7es0QiwssvPtSS/+xVs/j7j13CnHntFIsuA/15otJk5cpZvPzNy0gkbZqnJFgwt522xmHn4NEtyFMNpTTbtnSx7pFdbNvSFYqpkCNyysyohIScDlQ/6X7ti2tqGyqWbY5pVX64nJ3R7q6TxYhjGgZWv48oqWB9tdWY0DH7ikXSUw2a9/rBRxktQIKKBhdYAXgRhV0KxIrva4QA0zJIJiNIQyD8oMKi0NiGpKxUbZ0YKp4nLrT2wqJNkMhpfNXLd+++mztvehw5K4ZQQdvGcsHwg7lRWRcmWD0XTdAiUgY8Wx+0rODcR3yuXnsHb99/HwA7ElO5bslV7I9PmbT3/GhQSlXSj4eF3TvfvbrWAhpN/dxOvV/P+sd289T9u2lpTIw533SqpiUf7Vp/SAiEQiUk5KipftKt/sIdz6r8cDk7A6USKdvmmtWrJ9VPpXrMf/zB74je2088O7y2W2iAjrNiXPOGwx+zLRZDRA20FGhLgDnyvqoauCOgqTmOQJDLlfBcn6HBIumhIkIObwRVSyr1n5tlpW0xa1fwb9cC1wapNPt3D6D3BD+XFbESLVYeM+pcNfDsDNh81sicnmQa3njPAP/y5M0syQazGr+eegHfnvcGXGnxXKAgGHilUrrWwfvhugohIJmM8s53r+Ztl5932Oepzu3UU9+CjEQOfT0nOi35WMwOj3atPySkSihUQkKOgfE+6Y7+ZT1ezs7y9vYT5qPS0gvLNgoGM+BYGmUH+TUNGcGsjYKWXuAwh13Z0cHsBW3kn9hPIq1QhhwxaOr7ipgSNCQjlB2PUtENwgKN6jCtwnN9NMFsiGePmojVQbKyqFRHnAg1BeJLUBGIVao1lhvcXopBLD/yacbK6QGYvwne+/BG/r+tPyPpl8iaMb6y8O081HbWhN/Do3DiP+xzAFi2QWtjHA1c+sazMKSgvSPFyy9eOm4l5UhUW5C7d/Zitx1arTuRacnHUhU52rX+kJB6QqESEnKMjPVJdyzGytk5Uc601QsCZcWCmW0UPQ9PKUwpiZkmAxO4IEghuOZFL+Ift/wO++ECZj5oHSmpUa7GdqG5McHb3rqKm777EL6vME2jdvxgwNbAMCVF10MXNIYNngiqIpYbVHi0Ciopo8skWoAXEUgnWGeOOBwyVDvQCk+uHJnTYznwkgdc/mnjb3jjwYcB2NQwi+uXXEl3tGXC7+Hx/qloAWaDRTxi0RSLEokYDPTnmTOvnSve/WIQwXbW3fv2HPH/hfHchSfaggTYtqVr0mIejrUqcjRr/RP5OxXywiIUKiEhzwFSCFZ1nvhfwPUXBCkECWtkW2CiF4SLZ83iK+99Pdcn7sV5oA8r52NosG2DOQva+PAHXk4iaXPbLRZUrPWVCgovkYhFc0sCwxD0DuYZsD3MvI9RaUHlUtDfCnN3DLdzfGPk8V2piUrYOxda+6BxcDjRedsS2F2f0wN0HIDXP9TDp5+5iXn5gwD8ZMYr+OHsS/HlqCc/DIc1ZjvC7bXnsAXTmxuwK8JhoD9fEw737t83YZfiI7kLH6kFCfDPH79t0uZBjqcqMpG1/lNxpibk1CAUKiEhzyMm84LQ0gvn7o+xgwhly0NKyczpzbzn/S/j7FWzWPfILqSUTJ/RjOv6+L7CMCSRiFnzU4maJle9/3x+sncbT+7tpmhpTBcWbgLTA9MlEDpGMIviVU5b+oEo6W+HXfPhgnsDkbBxFWQbh89RKFjxGFy54TH+bvsviCqXISvBDYuvYF3z4uN6L0e3f+ovvfU/VxVLe9+G3NIIq8pNFHoK5EYJh4F2JuwYPFF34fFakBvX75v0eZDjqYqc7JmakNObUKiEhDyPmKwLQn2Jv6kxhtUSXOh6ujP855fu4q3vPBdfKUDjuj7R6PjHevmi2bzv9efxfx58kFvWPM7sjR6GG7RzZCXCxvCDgdlSDHwzaA9lU5BphEVPwmAbbFkWbPZUaRiClz7o8E+bfsGreh4PzrtxAf+x5AoG7BOTpK4IMoqEDuZpqt4w+QaInN/MF654NRfNOFQ4IOA1t902Icdg4KjchUe3IE/UPMjxiOCTOVMTcvoTCpWQkOcRk3FBONyFzosperozfOtrfyQWtyjky6SHirS0JoINoFr+jyaTKdLR2ch9W/bwt3/4A1tklmlbFIYHTjQQKbFicNFXBG2gaDGorrg27FwAC5+BgQ7oG3W6C56BVz9xgE9tvpkZxV58BDfPfjW3znwlapISj0cP02qCmRjPDOZmnDikV0TRMYOepMePXv+KWkVkdEVhXVfXUTkGT/S+Y7UTT9Q8yPGI4KNZ6w8JGU0oVEJCnkcc7QVhrDXT8S50hXyZ3p5sxaBL45S82vBsX2+O9FCRtikNWKZkYCBPueyz78AgT3/9QZSE+XGJmdP4UYmUGl9oyjGwi4F5GwStHDQ4NiQzsGfBSAt8uwTn/knzrq1/4q92/hZbe/TZjVy35F083TjvuN47MfqbOqWiBRTiQbUHgiFguwRuVGJ2REi4kinxOOMxEcfgQcepOQYfzX1Hc6LmQY5XBE90rf904FjWs0OOnVCohIQ8z5joBWG8NdOzV8065EKnNQwM5GvJvJ6ncF2/4rSqa993H0wTi1l4nsK2DbKmT1mChcTMKISr0bYgIg0KysOHmhFcdVjVM2DPfNg/Z+Tr6twHL36swD9t+Rkv7X8KgLUtS/nioneSsRIcD4eY51YiAaooMSxSYNiwLp8p4TVqVkyZclgTvXrH4Jh1aDVitGPw0dx3NEeqfJTLHkpqns4NQtfEN9Amoyoy0bX+U5nQtO65JxQqISGnOWOtsB7pgvDEur189cbfk804xOImiYSNkLB7Zy/79w6glBpxoXMcF7fsISV4XlD+kIaoPJ9ASIHvKSJREw1EYxbx5ii96TRGxYdFxQRGWSPyimIycKqNlCrFi4oxWiYVDMwWksOvT3qwYh28fNturtn8YzqcQVxh8N25r+N/p71sUsMEhRREIibRpM1Abz7I1CFwxDX84e0kWdlgypuKlGEc0bjvaF2Kj8fR+HCVj7Tj0N2bIZ8SfHLzn7G3GeNuHY3FZFRFJrrWfyoSmtadHEKhEhJyGnOkFdaxLghKab7zX/fS250FoFgoIwTYtklTS5xCoYzyNZl0kbb24ELn+4pqgUHrQBtIKfF8n/rIo0jEJJMukUpF8bUO7O2rcyuWRBkK4QMeSFFp+ehApOxcCDsWBwKgSmoQzn1YccWu+3j/7jsxUByMtvD5JVexrWHmpL+fUgpaW5NE4xYDQ/kgmbjSBpKaWgXIcoM162Kz4NpzzhnzIj+6PfDJ887nw3+4a0IuxcfjaCyl4LIrzufLX7iDrmfTJJMR4skIQ8USPf05PBPyKxJMTcbG3To6HM+HqsixEJrWnTxCoRIScpoy0RXW0dzx6w3s3NELuhqSF4gPx3Hp7c7S3BzHUWBaslbiD34pa3y/okoEOM6hwYeZTAmlNEIITCkD7xOtkULga005EgzQRspBO0XoYPbjyZUw1Fr3RBoWPg0rN+f4xNZbOHdwKwD3tq3gqwvfRsE8MWusLa0J4gkbgIamGJn+YjA3Q8XITQVuu74JQ2dG6EjavGqM93i89sCnXr2CW3K7j+hSfDyOxk+s28tttzyCW1aUSi6FQhnZl8M1odAk8M9JIacHgz/jbRIdidO5KjKeid6RCE3rTh6hUAkJOQ1RWh/VCmvtcUpzx282opXGsoza44QAISS+r8jmSkQiFm98yzk8sW5P7WIrDQmoygDt2Ofl+xoBlIouScsg6ktK2kfaBq4fhBy6ViBOGtJBTs8zZ4FfN0oRLcCqh+ElB7bzyc0/psXN4kiT/5r3F9zZef6EWj0+QRzRRKg+m2EIUqlhATS1JUXacSAbuNKZFUfdQqOguCJOtlWzvKXlkBbM4doD3T9K84WPvRr1ssgRL5TH4mhcf+zGpiitbUny+RLpTImidikuT9RESu31H2aT6Pk2NHqkCuThCE3rTh6hUAkJOQ1Z3919TCusO7Z1M9CfG/NiI4RASknZ8YlELFaeO4vLrjivdqHqejbNz368lq6u7GHPTQODgwXS6QKGEES0RkkPaQdzHtlG2LIEoiXonTrysdP3wJkbfN6z+y4u33cPEs3e2BT+79J3sycxsU+pQbuJCYX11ESKKWlsjDHQP3JItFFYDCbKHFhkIBstzLiBRFDIlWnLWnzydecfIgSP1B742c2P8LkbL5vYBV9DKg2kIdUITOHQyd8jHDuViqFsQba7TNPTDplZ8UPE3libRM+3odFjrUBWCU3rTh6hUAkJOQ0Zb9214Lp4SiGFoKzUISus1U97wYXYxzRHixxQStHalqx9eq4vY+/JZPjV/zxyRCt5DSgNlhGE+vieJlbxT9myNDBIyzYP3990YfljsHT/EJ/c/GPOyuwC4M6O8/mv+W/GMewJvzcaKJsQdSf8EF71mmVc9MqlhwyJLlrYycJXz+GW3G72PdNDw7oC0YymE0EqZnB39yO0XilqF+7JbA8crVA43LEtw8C3BeaQj9nn4bWPvNCO3iR6vg2NHmsFsp7QtO7kEQqVkJBTjIn00Eevu2bLZQ7m8ziehwLQGlNKdqXTIx4XfCo0SKVMBgfyeJ7CMERtHdfzFFIKXvvG5WN+4jeaLJQFhscRKxZaBL/AhRYYBpTRbF1ascCve+rmXlj5GLyk6xk+tvUnNHoFCkaEry54G/dOWXnU758kqNxUZ2DGE1XVnycSNq9/8woWLu4cd0j07HVTuPFnd1IqKBItERpiUbwxLtyT1R44FqFwuGPHLYtIxMTLuoiSDwwLldGbRM/HodFjrUDWE5rWnTxCoRIScgox0R56/bprSin2ZrP4WmMKgQRcrVFac+Ojj7KopaX22PpPhe1TGhgcLOCWvYpnSOCRMmdeO69904oxz29qWwptgPaCeQ2hg6+x0BriTVGSUZt0TPPAzDTZurVjoWDR07Bgh8cHdt3OW559AIBtyel8fslVHIy1HfP76NmQiUPz4PjnB8FcysIlnbVPwVIK5i/qYH13NzuLRTI93axon8LPbn4E5fhM62ysXeiMMS7ck9EeGC0UECKolKGINkYopJ0xhcLhji2AVjtKt/ToUg4x1xh3k2jb1q5aZQYhyFeqdKaUxC3rtBwaPVrDvfF4PpnWnU6EQiUk5BThaHroUojaCuueTAZfa2wp0VC7qMxqaCBTLh+SC1P9VFgsBBdCpTRlx6NUcmloiPKBj7x83E+Fr1u9lG+03YM+4CDUEdKENRRyDs/Okzw8JYdft3acyMLKR2BBXx/Xbr6ZRbn9APzvtJfy3bmvx5XH96vJ8qB7OiQKw14tY5we0VSEd171otrrHVMoegmadmVonkA7ZzLaA/UtnKzr0pXPU/K8miFeFIm/q/cQoXCkY+uiz4J5U5ALbfbs6CdX8hFRg+UL2rjmRS+q/b9VrcyUtGLP4ODIY5smHbE4nqdOq6HRERVI08Ts8xAlhY5KvDbziCZ69bxQ17NPJpMTihESEnJcjOihJxI0DEFkf5mGIZieSJCtCA5VZ1py8axZfOzccxEiaN24WuNrTcyymJVKkYpEDsmQgeFPhXPmtVMquhTyZQAWLOrg7z9++NkD05C8890vwose/vVoCU4M1p7t82DnSJEycye85F547e4n+Pr6/8ei3H6yZox/O+N9fGv+m2siZQKzsOMiATcp2D1vpC/L6HPMnBFl+TmBH0tVKG7s7SVpWUxNJEhaFru6BhnMFynpsVedLNusXbirQjAWt+nvy+GUXJTSOCWX/r7chNoDI4RCJkPR9ZBCYEmJFIKi8hksFLl/216U1qzr6uL3u3axvqebt195/mGP/eoLl3DuWsnKx+Cs9UHL7dw/C1p6h4+faozhCc2+oTGO7XrsG8rgCX1aDY1WK5DOnjypOwZJ3TVEw30ZUncNkbpjkPKePIvH2OAaj+rs1qrz57JwcWcoUk4wYUUlJOQUoNpDnzZo0PRAGmPIqyT1gd9kos+MsMU4tIc+t7GRpkiEpkgkaP1ISbzOzXS8kvbxfCp8/2vPQ2v44dfuI5of+z49HfD0cnDrc3qcYGB2Wk+ZD+/8Na/tWgvA06k5XLfkSvoiTUf1nh0Oz4CiqZnRG6xD+2ZgLidUMLuiTbA9QWlHlnUHu1g1tXPcYctkcxwlHLr7MnixGJZpkGyI1t6r0e2c420PVFs43dkcSutgILmCFAKJRAnFt7c+ya3FPWwd1Sa84t0r2HbX7kOOver8Odz+6w3B6vJh5l7mLphCJq6QPQqZNGobQkFFDij4ZKYo5i6YMml/XicaKQRXJOdw02P7EGWFH5UIU6A9jexzmZ2RXPGiORPyUwl57gmFSkjIKUBfsYj9rEvrOhfhKlREBkYgPhgDLq0P+2RXWYcIjrZYjIhhYAhB0j50M+ZwJe3jMe36y9edhx01+cH/vRvhDQ+tegZsWXZoTk97F5y1DuZnuvnU5h8xp9CNQvCTmRfzo9mvRolDXU+O55IhfGgYhEQuECraCARKFQ2YhsDOKDZtPoiUYtxhS/vZMlYJ0IqBfKDMenuytLQkaGqJj9nOOR4hOH9hB6nOBAe3ZjHio8pBWiMdRbFRsk4Nkuou0hmPj2gT7kqn+eY/vJpZTqR27Lnzp/AvV/98QgOyG3p72LNIMH1IIgsj/1+UjkLZkj2LBBt6e8YdPD3VUEqz7a7dNEmLfEpR8v1g0NuAaMoiWZZsu2s36tJzw+rIKUgoVEJCTgFaIlE6N7lQVqiEHPa5MEEZEpFXdG5yaYmM7LkcbYbMZKG0xvLBRuJrhRYw1AxPnn1oTs/SJ2HGXs2l3Y/ykR2/JKpcBqwkNyy+gvXNi8Z8/uqrOJb2j6o8wYJtQSZPuW6utP4S5EuNqcAujz9sGduQJ/Fo/pATUUrT15cjl3doaU2M3c4RkGmEPhuIDR/8SCZqUgrOfu1Cnt7ZjVXUqIgeIRS0Jdm9MHidLZFILbhwxKrto4/w+Ze+lIHKsXds75nw2nSfXWRoiiT1shTJDYWguucQVPdaLHIr4gw1ukccPD2VqM79tDUnmBaxamv81QHhcsk97QaEX0iEQiUk5BQglREkclCywRxdfhYC1wqqA6mMgGnDN9UP1R7I5WiORNAEfip5z6MlEjliYN7Rcs/evVz/8MM4t+wn4mu0gN0LYceiUTk9Q3DBUybxnhx/t/3nvKL3CQDWNS3khsVXMGQ3HPY4hiHw/KOTKhooxoNWT7QYrClLBcoYziiq3dfTmKbByjnTyMXGSCxWiviGQKRUX5fUjMg2css+f/OPrzqknTPe9tYVyTm1tszhvFFWrprNF1c/RscmFzutRgiFgTMjdMXySC2wjJGVKCEEUcPgwf37edP//i8IERw7GyPluOPOldSvTbfNCgZPc1Mk/mubDhk8LXgetjuxwdNThfrVbQEkRqVSh66ypzahUAkJOQXIZYokDQvHKOP6CkMKpBAorfGVRpgQ8STrdz/LwsUjfVWquTBX33svT/X341b87W0pmdvYeMznNNYn/z/u3cvV//kbGjeWiBUrOT3nwFBL3QM1zN8KC7bCgsxurt18E9NK/fhIfjjnUn464yK0GH+OX0Atf2iCBrPB+VYeKzSYXtCGMn2IOFCsVDTqRYbtCuYsaGPhog4QhyYWR3Y4iLJGy8ATRgqBbRjBQLMOjPG01gwNjRzUGW97a+dTXdz02D6apEVbc+Kw3igrOzqYecYUNrb1MNuJIku6JhRyjoNKK+KWRXzUBbfqp+NVxFFrPI7jeWwrZZjvOkQKJVqThwqM+jmb+fVVumRyhDnciazSnUhCV9nTm1CohIScAqQaY8QjFhHLpt8tUfK8IH1YA2jwNDnf418f/zPfze4YM5sk7Ti0Zw0afBszYeI3mezPZmurzRfNmPjMxFiuqI1NcbZ199Ge9tHAgZmw6cxDc3rOWgctA5o3P/sQH9z1Wyzt0xNp4vrF7+KZxrlHfC80IAyJrlRTjkasCILQw/rnwgjEimuBksFQbcwXtDYm+PAHhlexD0ksznpBNUUEz2vJQFxJEfxACInrKnq7M7XjjeuAapp0bFWIsiKfUkyLWAjGN1EbsX4uHFqa6hKUHQcpBK1jVDQO5vP4WmNISdy2kUIQsyyiMxsopsr0D+ZoTkRHCN3Ra9Ojq3RHm958KhK6yp7ehEIlJOQUoP4X6YK2JoqeR6ZcprdQQCmN7Qr8VhOzI3KIr4rSmutvu5cpf86SyALKA+ngN5k0rIizO1Xm+tvu5Q/7YxOyY693RbUSJmbUpFxw2b6tG3Qw8/HMCuieNvI1TN0HZ2yAJqfAP237KRf0Pw3An1qX8aWF7yBnxcd9/VWfjiqer2hrSTDQnx9RBTkc1RpN/fMIwCOw7rfLYHqCVMxmyZLOQ1776MRibXkkRfC8VmVguZ4gJRraO1K1nz164CAHH+tmbglkYwlnfgSkxOzzMId8/Kik5PsUXLfWfhjPWn+8BOVzpkxh0HHYn80GA6GV8yq4LiU3yA2ImibxunkbISXFFQkif8rR3Z2hpSl+WFfV40lvPhUJXWVPb4TWE/01cGqSyWRobGwknU6TSqWO/ICQkFOUqkAoFsokG6LsLWQplTxsD7Qlyb00hTvdRmvNgVyO5e3t3HnZZdx29wa++eW7MVzQUXnI4GVmkU1iS4kWGaG1OV5rOWQzwS/o+paDUpp//vhtbNnWRd6ubkdAJKuQHvS3wZMrA4+UKmY5EChTD8KZ6V18cvPNtJfTuMLgv+e+gZ/Pegm2Lwi8bw81b9IEWznSH/lzGcQEHZefCgSVlEwjHFhu8otL30hrS+Kw1aRqhEFPNs+3P/o7SvkylmXgo2tzLgYC1/VJJqP8+Fd/g2lKfn7ro/zoB3+iVHBrykvbgsKKBF6LScN9GVQ88LuZk0rRGBne3VZKM9Cf52Ofeg2rzp875vnURyrcu29frb1UrXj0F4vsz2axDCPw0Rm1Baa0Jrsjw8U9KYq9xQnlB00kzuF04vkWtHi6M9Hrd1hRCQk5DibzF3m9/8bO3b14OQ/LEPgtJoUVCdzpwYWnPptk3cEu7v75RmRZo+s8L6rbQrKgaNxYxAeMKSb2EVoOO7Z1s31XL4O4+B4YUmAoQMOWM2D3gpHn3NwXeKPEHcU79/2Rq/aswUBxINrGdUuvZHtyBrY/XDEZN3dHVVosdapEq2pe0PjOstXnHE/MaIJh2kQO/mr5cl704vlH/HOQQgRrt51QeM+L+c4378cp+4H/SuV4Ugef0t/57tU1kfK9bz2Ar1RwLhU1JhxN4tEcxTNiIIMBXmGAKUfKtcPNSNTOp46xKh5ojWUYdMbjh4gUCFbVy9MsPvihV9OUFRNqAY517NOZ0FX29CQUKiEhx8hEc3mOhuov0pvuXc/n7n2IpqY4qs0aua7CsJHbps0HSXfn8CMiuIDW3ccHPKGJuOBGoStfIF0u05lIkLLtMVsOQ0NFMsUSvq1rGyV5y+fJl0Kubi5XKFi4CebsgJZyhk9suZVzhrYF70v7Sr624K0UzeFV6sNdBgSMqzQ0gTlbyQ6+sRzwLPAiEM8GwuZwFRclA6HSIE3eNOPI8zGj2T0ftizVzNkaDOjKysyKa8HuRZrd88HzFD/50VqUUtiWgaMUSgdtIU3wXkW3FXHbLGS/SzQ1cgj2WGckLp41i4tmzqwJ5ZZYjE/dfz9P9vWNaAlVj1Edgl01tRM57YV7YT4e/6CQk0MoVEJCjoGjyeU5WqQULDtjGs42m7wliI1Roakaudnl4EIciZgUPR9pBPf1tabs+xhUzNhEUB0puh57MhlmV1oDo9cyD/oFXDSmFmg0+zoVO2ZrVN0WbCIbVFFSWThncCuf2HILzW6OkrT4z/lv4a6Ocw8RVkeNCCpHbkxTtoYrGdoMkpv3LxTMe0qjFdju+EJIEqx7JyzrqDc6PKX4/Nq1DM6HgwsEUw4IogVNKS7oma4pac3n165leX+MfN7BMCSiYjdfrrTMhKiEN7ow0KhpzUqSZUm55E7OjISGVBpIQ6oRrjl/NR/+w13PmyHYkBAIhUpIyFEz7mZHveFWXRDgsTBhI7c507jdNGi1TJ5VeVzfx1QC7SlkpZWiBVhSgBBIQ+D6iq58ngbbPqTlYHfEKKUERknx9FIYaB55XrN2BonHtu/znj2/5537/wjArngnn196Ffvik7M1ITRYEYO8VXEQrZqlSTBV4JEC4Npgexy296PRdE5rPOqNjls3bSLtOFhSIqSkt053CsBSirTj8IendqArrSAAo7LG7FbWl3Xl9UxpTHDFX5w9pr39scxIjDdv8alXr+CW3O7nxRBsSAiEQiUk5KhZ193Nk729RAyDoueNKOPXz4+MzuU5Gia6IrpwxvC2UKcVZXCggPYVZnVytfKJXjoaZSgwBYYUlDyPfLlMKeuMaDm0J+LsPsug11C4dWMOdgnOXA/tvdBeGuSaLTezLLMHgN91vohvzXsTZeNQf4rjIZWKkVdFXNerVUykCl5PKRaIFgBfBn4pYyE0GLbkPR982VFXK/ZkMsGMyzhiUwqBpzWlWFA5UUpjGMNixah4rlTnVj7+ygt41aXLUJeee9wzEvWbWalRuT3dP0rzhY+9GvWyyPNmCDbkhU2YnhwSchRUWz49xSIHcjl2DA2xdXCQbLlcu0/UNCkrddwW49WByeXt7eRdl4P5PHnXZXl7O9+stJaqa5dSSjK9BQw/uEhWvT+EDrZppAtmRmGkfQw38GVJDxRGtBwyjsMNjz7KszF3hEhpPwgv+WMgUi7oe4pvrP8yyzJ7yBtR/u+Sq/jawrdNukgBQOsge4eKUZsONowKSTgwTVNIBtWUcuQwCckGvPKdZ3POebOP+vCzUykEjEisrkdpjQBWvXQeiUQE3w8qKPUIQPuaZCLCRa9cChx/8q5Smp/evLaW2xOJWEgpiEQsWtuSFAtlfnbzI6yc0sGlc+eyqrMzFCkhpzUnvaLyu9/9js9+9rNs3LiRaDTKy1/+cn75y1+e7NMKCTmEqkgZKJWQQmCIYIC16A3PfTTY9mGDAI+W0QOTY306Xr5yJs0tcdJDBaByAR19bZWVAosHRlahbZi6uJkPvv9Czl41i/v27eM9d9zB3kxmxMNmZUyWPOphK5e/2vk73nTwIQC2JGdy3ZIrORhrPa7wwPHQQH9fDjtiErclJa2wyuCZsHuRIBaxKK2wiD2cx/RAxSSyrBDVNpCAUlJgv7Kdf/jgK47pHC5fupSP/vGPDJZKwZ/1qPabqxTN0SjvOmsZiXeX+N63HsB1fQxDIqVAKY3vK6SUte2gyaCaWzOR3J5waDTk+cBJFSo///nP+au/+is+//nPc/HFF+N5Hk899dTJPKWQkDGpn0uZ3dDA9nSaoutiSYktJWWlOJjPk7SsSbcYr66IVleh79q9e4Rg2bGtm/RQkanTGwFBPufQ3z/S1h01MujPjJt86ctXICR88r77+OJjj+HXVQPmNzYyxY4Su6ubmYVertl8MwvyBwC4bfqFfH/Oa/FkkJtS3ayZTJQEyzIoOx5WWYAF+SbJ4DKb5pkxWoAe08E5P0r7JpdIRoEBwhB4cUH3PAN3aZxvXnrRMVcTTCn51OrVXPvAA5R8H0vKWqyBWwm0+9Tq1ZhS8rbLzwPgJz9aSz7v4PvBXE0yGeWd715du30yqM+tGYswtybk+cZJEyqe5/HRj36UG264gQ984AO1n59xxhkn65RCQsZlfXc3WwYGaI1GkVLSmUiwN5OpXbAMoOi67MlkaIlGj2q74khpunD4VejGtI/nKWzbolR0yaSL43qLaBmsy8q0z3d/8TDfdnbyeE9P7XYpBP/fqlV8ZMYZfO7Tv2Lpnsf5u+2/IO47pM04Ny6+nEdalo4QPZ4B9iQKFS3ATQimtjZgK8HQQIGGjgT7X5PgwNAQ5XJ5eED0DavRSvOF3z3A/p40JVvjt5osbm2dlOHRj50XCIzPr11L2nHwKu2e5miUT61eXbsd4G2Xn8ebL1vFffdsorc7Q3tHipdfvHTSKilVwtyakBcaJ02oPP744xw4cAApJStXrqSrq4uzzz6bG264gTPPPHPcxzmOg+M4te8zo0rVISEngr5ikbJSREwTtKYlLYjlYvToEv1JHy0EimCu4YaLLjrsBbLeJC69I80Tt287rFPmH/bs4f133EHOdWmOROiMxyn7fm0V+t/mr0JrTTpdIJsp4fuBahhLrAgNhiXYN1XzD7v/jFu3djwjmeR7r3kNpSeGuPaz3+ODz/ycS7ofBWBj4zz+9ewr6I03IVUwXGtUxIkyQLvUjjnu666c0OEu20qClxBoS2BKSSxiIVsFxazL11a8nFwTY7bAXvE3s06Yg+rHzjuPj65axa2bNtVafJcvXXqIaRuAaUpeecmySTnueIS5NSEvNE6aUNm5cycA//Zv/8aXvvQl5syZwxe/+EUuuugitm7dSktLy5iPu+666/jMZz7zXJ5qSAhtsRi2lLCvSONTDsaQR0pBh4Ryo0H3EpOhKZJvvvrVnDd16rjPU18ZsZ91mfmYg+0JWluStDQmDknT7WvTvPM3v2GoEkSXd136SiU6EwmmJ5Okd6T50R8fxM57eK5/SC7O6Et12YQNZ2u6Rp3i2xct4ksvvpDvfvleDv7+AW7cdBOzij0oBDfNeSXfXvIqfDNQNcoIqh6xQsUAjWDzxqjY3WsCQzTLHRYlunIygkCwlCPBgKzlQTYVeIGUYyDtYFMmZpq1bapqKyOXKbJqydimbSfaQdWUkquWnVgBMlHC3JqQFxqTvvVzzTXXICqDZ+N9bd68GVWJov/0pz/N2972NlatWsX3vvc9hBD87Gc/G/f5r732WtLpdO1r3759k/0SQkIOYWVHB4vzMRofymEMuChLoOICZQnsQZ/ORxzOKiUPe7GsDuNu7O0laZrM3OpjuFCMaJ4t5cl57ojNjW9+5z7+8o47GCqVMIXAqgzwFl2XvZkM3t4Ccx5z8Xsd4qkI4jAXJiGgrx0euogRIiVpmHz/Na/h/8xcybUfuoWmn/2IrzzxVWYVe+i3U3x09V/z/QWXAgamB0ZlDdg3wDcDQWIoMPSwnb0Qwc/cUV0JoYPHlWKgowJDQ65FMHheFB0VmIZEaY2Ugs5EoiaynFIZ1/V5+MHt3L3maTxvkgdijoDSmnVdXfx+1y7WdXWNuwX0XFKNW5gzr51i0WWgP0+x6DJnXvuI7KaQkOcDk15R+djHPsb73ve+w95n3rx5HDx4EBg5kxKJRJg3bx579+4d97GRSIRIXZhXSMhzgoZ52wU7vCCJ1zCCT/FKKjxDYzsw/UkP5WukeahgGG0SZ1XSdFVUYpkjTdiEECQbouze3YfqlIiEwJCyJgQsKXF9n9gTeaQLTkwQSdrYhkFfT/aQqoovYetS2DMq5qa1D75w9nmIPw/x2e//kr/f8jMu7NsIwMbpZ/Ku976TwkCUBZvAdobbSL4RzLq4NuydBXN2VYSLEbjGGn7ga2L4wbFlZZBX60Cs2AhEETxLYJ7XhNPoUmwoEx9SxBosOpPJWlbNQH+O/r5gMPj2X23g9l9v5Gtf/gMXvfUs/uGDrzjha7cnIiZhsghza0JeKEy6UGlvb6e9vf2I91u1ahWRSIQtW7bw0pe+FADXddm9ezezZx+950FIyIlkx7ZuCj0FprQ20O+WKHkeuqywihqpQCLo2TXIx//ux7zvry485BNt/TCuEAJR8hGeRlRqmoYRmLAVXJeEZeEKjecpmlSEXuGPyG4RQtCUkUSzCtcOqpSmlMQbI6QzRVzHDwzIfE22ATasglxdMKnwYdFmmLsD/vfRtcwf2MNXN9/E1NIAnpCsf+uHUF/5FPlv/ZyZu118ORzuB4EAUToQKe19gfgoxKn1mQwfTBciTnBbKRq0gQxVqcgUNPkmQfnFjXz1Pa9DAhtm7uWPP3gCVfaJqGC1t783y9BQsLkiZNAy0kpTyrvccdPj3L5zJ9d85NijCo7EiYxJmCzC3JqQFwInzfAtlUrx4Q9/mH/9139lzZo1bNmyhY985CMAvP3tbz9ZpxUSMibVldDmeJSFzc3MiCRIFAWWDuzSrcpmx4G9g3zti2t4Yt3IqmD9MK51oExiXR5R1hhZhZHxsTIK4Wq8Sku0XPbQEuKpCBHTxBtVJqlm/HhSEzVNfK3ZNjRI1vTx0bi+Ztc8ePjCkSIlmYEXPxCIFLTizbv+yBc3fJ2ppQEORpv50Ic/yf2ffD///NBDtD/jYpUrpnF1UcWqIhqm74OGdFA1qcc3AkFSn25cikI+Hgga1wTH0GxPFHnHr3/Npx98kHlnTeUTn3wtcyutjL5RIsUXoCDwg5GVaIBHh/jQnXdyz2EqsMfK6ApYzLKQQgQxCckk2XKZ69euPSXaQCEhz3dOqo/KDTfcgGmavPvd76ZYLLJ69Wruuecempubj/zgkJDnkPqVUNs2KaYd0BqzEkSnK7MVTS1x8jmHn968luUrZ9bK8PXDuMmHC4iyjzaC6kZgxKax81CMlWmwbcp5l3JKkm+EqSTYk8lQVgqzMufl2LqSeyNI2TZ7Mhl8pZCWIN8MmxZpBkYVNmfvgEXPBPMkKTfPx7bcyvmDmwG4Z9pZXPGBt9OXiiHuv5/OvMnidNDKqQ7AVpGBZQlWLvjeEuCXg5aYbwYixfCHU5EjJYiIyhCtHQzSpgqC+W6cfEKMqFB87sbLuOPXG/jpzWtrx1OV1pGoiBQqqcSGC8b20nHnKo3F6ApYPZMVkxASEjIxTqpQsSyLG2+8kRtvvPFknkZIyBGpXwlNJCOUyx5SytpFzPc1dsQkGrWRQrB/7wB//MMzNDXFSTXGWLFgCoubm8nfvT+onMQlwgMzF6zK+AQCYKi/QCZXorkhTuxFKfqdHNOTSWanUhzM53E8D18pnAZNOSWZWrIYKBYp+4Hi6WnT7FgEXt0gq12Csyo5PQBnDe3g6i0/pq2coSxM/mvBG7nhLReQbTZQvg9ao3MuhlsnUirXaqFHbhJVt3wMBbEiFGPBfMpoZ1yhA4EUK1Wez9AYRUWsPToiyLG5V/OLnz5GOl069A+hOutSt0rU5MgTIhhGrKOPQdQ0GXSc445JCAkJOTIn3UI/JOR0oH4ldGiggFIa0wwqKb4fVFNaWhIIAa6rGBzI8+2v3Ys0ZM0b5dIz27g9s4+iqVG+RgswYkHFwVDBtoxQUIjCs8s1733ZUnZt2FALJVzQ1MRQqcSQ45C0bT74/nO4/TuPoDI+fhR2LYKeUWvHUw7CsifAdkFoxRV77+Zde+/CQLMv1s7nl1zF1pZpuFFjhOeKcIZ9WESlgoEOzq+eantHEbSHIqUxRMro91IDHiT/nCNnSNzpdlCh6O/nO/c9QCFfprEpSk+Xe+gfxIiTBNFoTUqu0miqFTDH84hZh5qqTWZMQkhIyOEJQwlDQiZIdSV0+sygNel5CqWCSsqUjhTxhE0hX6a3J4Pva2Jxi5bWBLG4xa4dPTx421MYZT1chiBolRQSwZebEChb4J6XpKdV8/vdu/mvV796OJQwl8Pq8zjXaeT6Jat5z6Wr2HdehIPTYMN5o0SKhuZeOPvRQKS0Ommue/K/effeNRho7pqyir9f+VF2JafhWJBp1CM0QNmuiJC6Hwo1huhguFqidSC4Jmqnb2R9kg9msA6UiZomRr9Hz4E0qcYoDQ3RsbdXKmJJKNC2ID3LOCGCYWVHB4tbWugvlQ4JGtRaM1AqsbilZdJiEkJCQsYnFCohIUfB2atmceN/vouFSzqIx206pzUyfUYz8YSN1tDfn8P3FdGYSbJysfU9jeN4FHNlpAeJPMQLYHpBtUJKUCZ4QoMp0DGzNgPRHIlw52WXccP81bzpySQrH4PG+zP88isP8YGPfJ+tXobNZ0IpXneSlevqmY8HwuLcgc184/EvsyK9g6K0uWHR5Xxx8eWUjGDNX/rg+ArX92tP4UXBtwNjN6EqCcxHeG+qogWCgdvDUnWpdRTxDXlKrku0LNC+xrJMpJS0tCTGfqwKHp9fEWegXD4hgkEKwTWrV9Ng2xzI5Si4LkprCq7LgVyOlG0fVUxCSEjIsRMKlZCQo8Q0Je/7qwtJNcXI5xzKjotSmmy2iFNyMQxJS0sSIQSFfJme7gyO49UGUrUG6UG0CNIdrlwYZSg3Srw2k6hp1loaG9bt5bffXEv3rkGGyg69wmG3WeSXUwYYio5R5tBw5npoKPp8YNdv+dzT36HRy7MjMZW/X/lR7u5YNeLutgszd0FyiJrIKbcYeC0mygxmSiZyOdZUcnpMcCoiZ7ydmOqciTIFxpBH8WCRGVMaidkWrusB0NyaoK0tyWinem3B4KoY2+fpEyoYLp41i29dcslwRSufJ++6LG9v55unwGpySMgLBaFH1zVPMzKZDI2NjaTTaVKp1JEfEBIySTyxbi8/vXltLadHKUU+59DekSKZjKC15tn9QziOixLgKT3cSqm2foxgddf2wDMhfUECa06SguuSLZd5R2wWW2/ZglcM7PGVhAOzYcsZwWNrVIZFYjlY8Tgs6hrg2s03syQbrO7+euoFfHveG3DlofMWELjI+ibkk7BzAajpNnOzEZr/lEcUFaZ35PdDEwiQXENgr2+6wTlJPfI+VO4nADcpEGVN70sSfOm9r+Purz7C7p29tLYla4PKSil6BnNk0kVKccGWSyPYlvWcGa/VZzNNdo5QSMgLmYlev8Nh2pCQY2S0M+jQUIHvf+sBLCsoATiOV9sO8vRwW6UcDS7isuLiWs272btI0Do9gqk1+7JZYl0e69Y9Q6QUVCecCGxefujAbMeBYMYlmYUznoSLujbyj1t/RtIvkTVjfGXh23mo7axxX4cmqIAgoCEDZz0JXkuCfW0e2XNtZjxSmpBQgUA8dU+D6XvBGmv7B4K1bFVpD7kaU0quvvDFvHLObFqvFGNk2PiYSjC9o4lL//JcGuc3PqeC4UTnCIWEhByeUKiEhBwH9c6gSmnuvvOZWqqt76vAzl7omn28MsCNBF/SBascVDCGWiDhgddVZFOsTNHzWbK14uRayel5+uxA5FQxyzBvK0zbB7lGmNrj8tc7f8MbDj4MwDOpWVy/+Ep6omMHfFZRBkgLTMNARkDmfRbttvjy/3kdvYUCX0j/Hh4vHPG90ARVGT0zysFOzYV7EnTtHkBVxYoAw5AopdHowKm2BJYpeOrXW3ki1VIbWK5WqrJZB9OUzJnXPiJROiQk5IVD2PoJCZlEnli3l699cQ3FQpmIbdLXl0Oh8fzAgKQUD1o8gmBA1XQCt9aoE2zLKBm0X7o6YcF28AzYOwf2zht5nJbewBvFLgXPNavQw7Wbb2JePsjQ+sbKV/CZl1/K8vUGxhG2cAoJMKNGkF2kNdpVdNgx3v2eC7jjD0+xeUsXpjOx159uhs2vtDlv6lRuf+vb+P1vNvLznzxG18E0Sh1aXjENSWtrAtf1icXtWqCeUjrMsAkJeZ4z0et3KFRCQiaZ6uzKvj39DA0W8H2FJ0ElJMoSuEqhfB0M0/qBOZtrgWGKYPXWCURNIQ5PnQ2F5PBzCx8WbYLZO4cHXF/d/Rh/s/1/iakyg3aCv3zrFfxy1WIAZm+HhZsZV6yUosGGT7RibOb6iphp0FAysCMGpbLHoOtgT8CmRAPbloK3Msn3X/e62uzIXbt28/Gv/4bUNod4prJaXHGZ9eOSma2NNFgW/X055sxr53M3XhaKkpCQFwDhjEpIyAlmvE/99bMr6x/bw2/+9wl6c3nKWmMisJSAkkb6QQXFiQACLEOipMZB09cBOxYF8xxVkhlY/ngwRwIQ8x3+dvsveFXP4wD8uWMBb3v/FTzbmqqZte1ZANlGWLgJ4vlgsFWLwBrfqbSgzEolxVcV4zorQm6oiBDQ2BpnoL+MLulD5k1G41kw0A6fPuecmkj5w549XP673zI01SEVg5Vrg9ckLYkwBZ7SQWp0czMNDVH27x1gx7buMGgvJCSkRihUQkKOgdEbP1X32eocRXV2ZeHiThYtmco3v3Mf23f2oPM+SoATDzZjypUhVgGUfR8vAluWBS2UembtCCop1crI/NwBrt18MzOKvfgIfjD3Ej5++cWkWw2q2kZrjSIQD2vbIJUOwgzLVvBcySxQ8Z9TWhOzTDricYoDgX29iEieHcjiK40vA2v8eqpFmqooyjaCaLe4a88ePnbeedy7bx/vv+MOhhwnSHf2g0eVLUAoIhgYcjg1OmabZLMOmXRoSx8SEjJMKFRCQo6S6hxKIV8m1RjFskxc12P3zl6+9sU1tTmLKmevmsU3Vl7FLfdv4P/84T4GcImUYdn6oKIiCIzfelth56icnkgp8ERpq+T0oDVvOPgwf73zN9jaozfSyL+ueBebUvMwvGBDZVoiQV+xSMGrW9URkGka/nbnQli2IcjnaWmIEomYxCyTfMYBNL5SDPUFA7QxUfFEEePb43tRcM5JMrXBZsvAAI8e7OL639yP8axDowGlJvBsgZaB2PINcJUiYhj4OkiNdn0P05SkGkNb+pCQkGFCoRISchQopfnpzWsp5Mu0tQ97fUQiFnabSX9f7pDkZAi2gxYt7qDwjIX0JX6vi5IKqYKqyu4F0Duq2zHlICzdAJFy8H3SK/KPW3/KS/ufAuDPLUv5/JnvZCCRwPSD57GlpC0eZ6A0RqjfKDwrCAnMD5XIEwy2tnc0kM85+NXB14rZmlSVyosctsgXldvdZklhdQpregRDa9w9Bb7+r7fj7xtioQJXaIpJzf7FkkIDJNKgoqAqgkgAhhBkMyXmzGtn/sLQlj4kJGSYUKiEhBwFO7Z1s3/vAKnGaE2kVBFCjDtncc/evXzi3nvpLRRQGlIVYVG2g4FZp66IIH1Y/DTM3D0c+rc0s4drNt9MhzOIKwy+M/f1/HLaSynZAsuFXAoyjTAjGmWwWKRQdmnOBOvPrg2DKWrlj5ZeWLYx8G8pJwUzEkmEglLRpb83B4YIVpb9ikGbqBi0qYrzrAXlCJTOiGJMjeG1W0FJCGBfkZmPOWTw8U0QlkS7PokMLFivODhXEM1r7FLwPJ6pSEiTYtohHrd5x5Wrw0HakJCQEYRCJSTkKMiki3iewrLG/qtjjTFncc/evXxozRp6CgWaemHedojlYd8c2D2fEf70DWk4YwNEC8GPDa142/77eO+eOzG14tloK9ctuZJtDTOBoLrh2YEXCwIKnkdTj2LV09CQD7JztIRCA+xZKBhog/nbNaYHKi7AB08pGqIRIrbJgf2DGJaBG4VIYXhDBwKRIv1A+Gw+C5oWRGiM2rVz10oR25DH9gStnUnS6XSQqmwZOMInUoLmbs22swUztmriOTA9QSphM3du6JMSEhIyNqFQCQk5ClKNMUxT4roekcihdvRueeSchdKaq++7j72ZDKluxdKNgQvs+vMCk7YqQsGcHTBzTzDsKgQ0lXN8fOstnDu4FYA/tp/N1xa8jYIZuL4pEQyw7lwImXZosCwW5KLMerrMYKaMioBjBTMhiTQsfkJzYKEgkQtaOHZWIxRk8gVyoohhBIpE+QqkwE0KzKJC+AyXdiSkF9sMTXHRjoNlGERNk5LnUXy2wPyMprWlgYRtEzVNiq6HZUhs08C3fWI5KFmw7sUwrWDzrytX8/JFs0OflJCQkHEJhUpIyDiMlfEyf2EHM2a11Nxn69s/Wmuy2ZFzFv+9YQMbenpQSjN7O3R3wPalgRtslUgRlj0RDM72tsPMfbByYDuf2HILreUMJWnxX/PfzJ0d5wcKRgSmcVuXwP45EDEkc1IpLCGwH8qgyxZmyqLgeUgt8KVGRQNzuM5dGulRs8QXEkzTAK1xXb/moBsRBkWpECkDfBBKoxVIpRmcIlnR3k5zLMaW/n56CgWkECwyYjRa0ByPIoDORII9mQyurzCkwLINcBUp3ySZjPDdt7+WV82e/Zz9eYaEhJyehEIlJGQM7tm7l+vXrmXLwABlpbClrIXgvePK1Yfk0ZTLHoNDBcyowdmvWwgiEDr/vWEDntYkM0Gbp3/KyOO0H4S5O8AqQbwEzRmfK/f8gcv33Y1EszPZwWfOuooD0c5g80ZCtiGoogy0B0UOBXTn88zvNokM+hgNEToSUfZms3gqmHzVOmjZWKXAQwUNGGAZRtB5EgLLMoKUZ6VpjkVx3CKu0hiGQBoCkVfkmwRWR4T/uOgilNb88wMPsDOdRmlNj3ZI+i6DhRKtyRgp22Z2KkVXPk/J81CexpCwaFor17zuwjB9OCQkZEKEzrQhIaOozpRky2Vao1EipslQqcSQ45C0bf7n0ksZ2j7E3T/fSLo7h+cqcr4bWN8vtShPC5J9z2ht5T/Xr8fTGuGPNG8zXViwGZr7g/+OlWBKaYirN/+YszK7ALi983y+Oe/NlA07yNExYP8s2HImgf28EFhS0tirmblF0ZAOhmcNQxCNWERSNgOqTMF18bUGBdFiUE0RgGlKTClr56TrqirJhgh2yqa/XMJxPIyyRlkC85XtXHPZRQDBe+Q4dOYtop6gZCoSj+aIp2FaZyONkUjwvEC+XCY9UGDq7Ga+8v/ehWkMHzfkxBFGEYScyoTOtCGnFWO1WZ6LZNyxzuP6tWvJlstMTybJuS77hoZwPA9fa4Ych9f/4he0RCLIswSR6Qo3X0ZEJXZnDCklhtY8evAgf9izh+qngHqR0tobeKhEK6nIUsPq/mf4p60/odErkDci/L8Fl3H/lLNRgpqjmunD7EDDcHA6tGUgnlO0HQxmTXwR/IUWgOO4uP0+0ztSkBR4SlEqubjaxXcDC3+tFAqNEEHFRSmFYRqYpqSjs5FMukiLb6Nsi8aZSV75tuVc9soVALzmttvQ+4qcsVVhDjmgICVBxQwUHt09GSLtTdi2iVv2KGUdmhpifPD9F4Yi5TniSKaEISGnC6FQCTnpHK7N8ly3B9Z3d7NlYIDWaJSc67Ink8HXGrMimqrmZAOlErMbGzmYzFGMKkAhc7lgZkVrfK0ZXaqUfmBlP3MXEMT5YPse7999O2898AAATzfP4AsLr6Qn0oYvKiMpethoTQBzdgVfGl2/MBQYsgG+H9xZa8XQQJ5pM5oAcDNlps5sob83h5SCXNahXPZQKjhOJGKRTEbQwN9//BKkZMxP4uu6utj3TA9zHnORnkZFJBiAHyQvaykoRSCTLyGzIkw/PgkcrSlhSMipTChUQk4qY7VZHM9jY28vH1qzhm9dcslzKlb6ikXKShExTfYNDeFrjS0luiJQqmigK5ej7PvDVROtMbSmOgJSTywPZz0OjYMVJ1pgWrGPazbfzKLcfgBum/Ey/mfe60AHfy0NPcYT1TG63iT0SPdYpTQlx2VgIE+55BGL27z7L1/Kz299lN07e5k6vZFy2cf3FYYhsW2Dgf48c+a1s3Dx+C2C3nyBlqedQKTE5bCHignKkMiCwo3Aa/96NWemWsKWw3PMsZoShoScqoRCJeSkMbrNUv2FGrMsppsmB3I5rl+7lotmznzO2kBtsRi2lAyVSjieV6uk1GuGqpOq4/vB7Ef19QAuh2oLuwTn/hmUDrZ9LB8u7H2Cj267jbjvkDHjfGnRO7h32jIMBZZ77OcvNEhDoJUO2jm+ZrA/j2FIDFPy81sfZdX5c+juSjPQn6ehIUosZuOWPQb688QmYLpW7i4SzWh8WxxieocQ+LYgmtE0x6OsOn/usb+YkGPiWE0JQ0JOVUKhEnLSqG+zjPULtSUaZcvAAOu7u1nV+dz8Ql3Z0cHilhYe6+rC1xqjMmyq61o5QggMKfHrqilVxiqAdOYNLMfH8CHulfnQzl/z2q61ADyZmssXlryLvkhTYJU/CaPtyh/5JKnGKK1tDXiez+6dvXR3pXndm1aw7pHd7N87QDbrHFV7ZqoRx0JQFJpDnWTAE5oYgqlG/PhfzBicKvNMpyrHYkoYEnIqEwqVkJNGfZtlLKKmyaDj0Fec+C/U472ISSG4ZvVq3nfHHaQdB18pDCFG6Aer0gpiAgtz0oPOg4Fd/ex8N9du/hFzCt0oBLfOvJibZr8aJYJJ26pd/ejgv6NBVP9R9xyGYQQVFUPWSv/rHtnNZ//jbeza0XPUGyFNTTFSsShlr1TzSJFCoLTGVxpDC1KxKE1Nkx8ueCrNM52qHK0pYUjIqU4oVEJOGtU2i+N5xKxDf6GWPC8I2YtN7BfqZF3ELp41i+++5jW88ze/YchxUABaY1QEjwTKvl8bnB2NUblox3JBQnFLn8cl3Y/ykR2/JKpcBqwkNyy+gvXNi0Y8TlTdX4+R2kNHnVI+79DSmgwGc+tK/7t29BxT6X/+wg4WzG1HbesiZypKlRaYAGwpiLmSKbNTzF0w5YjPdTScavNMpypHa0oYEnKqE+4Jhpw0qm2W/lKJ0XY+WmsGSiUWt7SwsuPIv1CrF7GNvb0kLYupiQRJy6pdxO7Zu/eozu1Vs2fzkze+kWmV55nR0MCcVAoBFH0fH/DGECmCwMW1cz+86EGY2V3i6i238P9t+xlR5bKuaRF/c84/HSJSao89ymqKqPsaD8/1cZzhwRfLNvE8dcylfykF77hyNY0NMZp9i1mxJO3RGJYS6LxPFo+729O87hc/P+r3fTxGzDMlEjQMQWR/mYYhmJ5IkC2XuX7tWtTpbQs1KVT/fGJxm/6+HE7JRSmNU3Lp78tNaA4pJORUIhQqISeNapulwbY5kMtRcF2U1hRclwO5HCnb5prVq4/Yuhk9lBuzLKQQwVBuMnnMF7FXzZ7ND173Os7r7ERpTc51iRjG+A/Q0NwLK9bC8sfhjP79fHX9/+MVvevxkXx3zmv55zM/wJDdcFTnMR4TvcxoDb4/vLE0GaX/s1fN4u8/dglz57VTKnmkBwuossJrMSlc2IicGTtmkTgW1XmmaYMGTXemSd01RMN9GVJ3DdF0Z5ppg0Ztnilk+M9nzrx2ikWXgf48xaLLnHnt4WpyyGlH2PoJOalcPGsW37rkklrLZtBxsKVkeXv7hFs2J3Io9+JZs7ho5kzu3bOH7z/zDDc988yY92vpgYWboTEd5OG8+dkH+cCu32Fpn+5IE9cvuZJNqTlHdezJQild+/Q8maX/s1fN4syzZ/C6b/6Y3V0+Lc1x/LYgUTEGk7q51VcsYj/r0rrORbhqhHeLMeDS+rBPdpV1VPNMz3fOXjWL5Stnhs60Iac9oVAJOelUxcCxDsGeiKHcKq7v80x/P59/5BHuHqcy0NodVFBsFxrcAv+09Se8eCAQNA+1LuPLC99Bzjq6DZiJjquMNSZjmkGh1PdV7TatwCm5ZLOlSS39b+jtYYuZJzk3gT9qzmgyN7daIlE6N7lQVqjEod4tIq/o3OTSEokez8t53iGlCFeQQ057QqESckoghTjmC9lkD+VWyTgOt23dyjX330/vOCInmofl6yHiwrL0Lq7e8mOmOEOUhcF/z3sjv5l6waFeI0dASlCmQLsaHQHhglBBIKFQHN4EToA0JGiNUgKtA4v89FCBWNyedIfYEykS60llBIkclGxq3jY1hMC1IJEL7se04zpUSEjIKUYoVEJOe6pDuRt7e5luHrrlMFAqcVZbGwr4/a5dh63YKKXZtqWLrV39fPfgFn757K4Rt0d9gSM16CCz58wnIOYo3rHvj1y1Zw0Giv2xNj639Cp2pKaDqjjMHobRZ2FIycwFbWzp6sPMK3RcYOWDPJ9qpaX6GFlZla4OI0sp8T0VWOJHA0t8z/O5/D0vYumyaZNe+j9RInE0uUyRpGHhGOUxV6KlJUj6FrlM2PoJCXm+EQqVkNOe6lDuh9as4UA2W5fmq+lKuJhSMug4XParXx12bfmJdXu55aaH2djXy0MLHLKJYYUhFZy5w6Bjn2bI1vgGtPVBq5Ph41tu5ZyhbQCsmXoO35jzFgr2xFoQY0kGyzZoiUV5zaVncu/tz+AXPVyzknqs60SKIbAsA88LEo+ndDZimrLOEt9koD/HnHntvOEvVp6Q2YSJiMTl7e0T2tw6HKnGGPGIRcSy6XdLlCohkQKIWSatVgTDJfQGCQl5HhIKlZDnBRfPmsWnpq/gJzetxe3PIhQkJLQ2m+xfbLBfZw/rvbF+3R6+fMPv2dBS4KmzPFTdPlwyDUuegYTro31oGQjEwsrBrXxiyy00uzmK0uJLy97C72adS7wgMPxhAzetxxYkY/5MQLIhyp5dfcg9A0xvSNKVS49wmxUCbNvEtCRCCExLUi77ZDNFWloSdZb4J34VdYRIzOVoiUaJmiYlz2OgVJrw5taRqPcGWdDWRNHz8JTClJKYaTLQl2NG6A0SEvK8JBQqIc8Lnli3lwd/tIHmvMRqaQApQGl6BnJMe8Sj+WUpKIEolbGjkmhrggP5PNevXcu5Uzr4+k33c+eCHL1NasTzzt4Oi7YE4sA3wHDAVD5X7V3DO/b9EYlmV7yTzy+9it3JDkwfyhGIFoMqDASPE6NaQONdtqUM5koMQ9LfF6QxCxHMrYBAVTJ8HMfD9yXtU1JYtmSgP49b9kmni0gpn9PE4snY3DoSVW+Qr31xDQN9uSCjyK4IsqHQGyQk5PmM0KOdtk4zMpkMjY2NpNNpUqnUyT6dkJOAUpp//vht7NrROyItNu+67BgcxM5phBRoUwTJgRL8JpOBZTa9rZrXtM7khzs349bJ9ogDZz4OrQOAGm65tJcG+eSWm1mW2QPAbztfxDfnvQnPCOYzFFCKB/ePlILHeGbwb7MubPBwl1PDkCg1vLEjpaht8riuX/u5YUjsiMn0Gc2Apq8vR0dniivfdwFNzfHnfBX1ucjgeWLdXn5681r27x3A8xSmKZkxq+U5EWQhISGTy0Sv3ye1orJ161Y+8YlP8NBDD1Eul1m+fDn//u//zite8YqTeVohpxnjpcV6SiE8kD7gapQt0FFR895ofNRl22rBd/KbR/xNmDVkMmujR2IItBgWKS/ue4r/b9tPafCK5I0oX1l4GQ90rggs9v3APVECsSI4ERhsha6pUE5CIgvzNgVVFnmYjwbSEAgJ2h/+mSaoogRDs8M/FzIwb3Mcl2jUItUQZaAvT1Nz/KSspB7P5tZECb1BQkJeeJxUofKGN7yBhQsXcs899xCLxfjKV77CG97wBnbs2EHnc5SWG3L6M15arCEEVlFTF3sMQqBN6G+CZxZqSvbwld/04SUHk8zolxzwM8FDNNjK4wO7fsubn30IgC3JmVy35Eq6Y62YUgbtGOrSlXVQPRlsB9Fh09mjad7rYfiB4qner94rZXiLRyBG1Vu0Alf5HErQJqq6zr5QUnFDb5CQkBcWJ02o9PX1sW3bNr7zne+wfPlyAK6//nq+8Y1v8NRTT4VCJWRCKKUZGiqgfEU+VyLZMFxVMZRAVld6JSg0ntbsmaXZM2PkhOtUx2LlBsnsZJSc72JV2jbTC71cs/lmFuQPAHDb9Av5/pzX4sngr47rqVo+T728sHxYsAn8rWWkXxdorGvdpxFCpfrfvqcQddWBsYov9bcKEbSA4PDW+M9FWyYkJCTkRHDShEprayuLFy/mhz/8Ieeccw6RSIRvfetbTJkyhVWrVo37OMdxcByn9n0mk3kuTjfkFKR+XiGfd8hkS6TTRVpaksQTNspXSAS+1ngGDMQV25ZCvi5qR2i4YtoC/nLOEm5+8gF6DmYolTwMH17R8zh/t/0XxH2HtBXnPxZfzmPNS4fFhRgZIjhaeAAYdYWQ6m3GqO/rH6814B9+bKx6HK3AjphEItZhrfEnK1U6JCQk5GRw0oSKEII//OEP/MVf/AUNDQ1IKZkyZQp33nknzc3N4z7uuuuu4zOf+cxzeKYhpyJPrNvL1764hkK+TKoximmm6O3JUCq6dHelae9IkS+Xg7aMgD3zYOdCUHWZgomi4CXbI1zzunOZNaOVR5Zs4/57thDxy/zDjl9ySfejAKxvnsfnz3wXA1YjpkdNhYyVdPxcTabryj+bmuKUnfGt8aup0tly+bDr2SEhISGnKpOennzNNddUVirH/9q8eTNaa/72b/+WKVOm8MADD/DII4/wF3/xF7zxjW/k4MGD4z7/tddeSzqdrn3t27dvsl9CyCmOUpqf3ryWQr5MW3uSSMQi2RCho7MRK2Lg+Yqe7gxD+SKFJKxfDduXjBQpnQdh9cOwsrmdiGHy2Wv/l/vv2cKc/EG+uv7/cUn3oygEn7/o1Vz81x/ikTMb2b4Y9sxmhMfKZOCLoDqjZGCTXy21qMM+CqyISankjpuKe6JSpcej6uq77pFdbNvShVKn9UJhSEjIKcKkryf39vbS399/2PvMmzePBx54gEsuuYTBwcERa0kLFy7kAx/4ANdcc82EjheuJ7/w2Lali3//9K+IxS0ikWAtOFMu05XPU/I8hKMwPNiwCgZbwK8TFpYLS7cIpjwLvqm5+BVL2XjfLrLpIq/t+jMf2vlrIsqjO5Hiw69/F/u9+SRyw1k7RqWi4ptgOUe2x58ITiQ4L6EJPjpU1qE1wTHLNphmZchWBi0f7Sne+J5zedmi2eNuvqzr6uKtv/oVScsa096+4LrkXZdfvPnNx72tE64Nh4SEHC0nbT25vb2d9vb2I96vUCgAQTZJPVIGHhIhIeMxessnUy6zJ5NBKY0hBX5MsG2Gpq9t5ONa++CMp8B2NeUGiWcIHvrNJuJekWu33caFfRsBuHfRGXz4VW9nys4kSQ9cC5QViAnLBSUCU7dyEhqyE0s5Phy+EQifiANGXeigBpwYeBYIQ2JUopJFXuE0Gbzk4sWsmjZ13Od9rgIDR7fhLMvEdT127+zla19cc0ilJyQkJORoOGkzKi9+8Ytpbm7mve99L//yL/9CLBbj29/+Nrt27eL1r3/9yTqtkBOMUnpMD4zxfj4WqcYYpilxXQ87YtGVy+MphSEEmbhm0yJNITHyMc2RCEbUY9tSH7sIs3YqUjlYlN3HNZtvYmppAGUY/M9l7+RfXnw+sx9wMbyg2lFVIlpWBlkrZm6FBDhRiJaO/f2otnyUAUbUAE8ji5piRCO8oIKjJAipA68WR+GaEHtRK6umHr4K8lwEBo5uw1U3riIRC7vNpL8vx09vXsvylTNDr5OQkJBj4qQJlba2Nu68804+/elPc/HFF+O6LsuWLeNXv/oVK1asOFmnFXICGa89sOr8Oax7ZPchPz/7dQtJzkuxrqsLgLmNjVy+dOmI3Bc/XibrltEa9s7U7JtTmfOoQ2igx8HIBTk90/aC6SnecuAB3r/7dkytcKbOYOt/foP/2/MsnWlBMufhWMEKs+FXTN8UNR8UQwXmbW4le7DqQjt662c8aps7lW8kleOUNSoqSb8oRnc+z8ytmmQuCCTUUpNvFOTOivHlyy464nrxiMBAw8Dq9xElhY5K3FZjUgIDxzPbg2BgvqEhyv69A+zY1h16n4SEhBwTJ9Xw7dxzz+X3v//9yTyFkOeI8doD27Z0sXH9PqIxi9a2BJZlMlgo8fgz+/jT5j08dRb0VzqJEvjoH//Ip1av5h1XruYL199Of2+OchK2LoNMU90BNURMg2SXz9xtgTOsXQ5mSlJuno9tuZXzBzcDcP855/PiO2/n2VyG8p370UXQvkYSVEuMUV5r1W0f0wOlgqpIMQoNvmRKWwNXvv8CHt25n/t+9iSM5dNGUEHxLTCSJm2eQd4po4Si3GxQPDuJO0VSzrs8PcVjSs7EdkFEDWYvaOPfX/SiCW3qVAMD//EHvyN6bz/x7PCsTaEBOs6Kcc0bji8wcDyzvSovFBO6kJCQE0cYShhywhmvPWDbFp6nUCr4sm2TrOvybCmPaymsEszdDv1tQCWmZ6BU4toHHuDfLriAg+dHObA9x645wUW/ivQDIZDo8jljA1jlwHVWajhraAdXb/kxbeUMjmHyH5e9k/+56KX8wndpi8VQSrHfL9Gqg2BBQSBEqhWQqp0+DFdRqmihSDZEaWtv4NrXXMr8piZ+8O0HRyQfQ/BEEUPyqrefzd/95UXs2t7D/dv28pM9W9lu5ChrF9uVnNvZydXnn09zJHLMRm0tvbBso2AwA46lUXbw/jRkBLM2Clp6geMYH6lvw1UHm+s5nAldSEhIyEQIhUrICWe89oDjuHiuj2FI3LJPyfHoKuZRSqMIhlgTOWhMQ6YRGtJBVaRsK/7vww+jTCgtHHUwXVlDVjB/SyAmDAWGVly+927etfcuDDT74u189qXvY91rl1EuFOgrFnnl7Nl4WjOY1AgdCBu/rp+jCZ53tO19FSkFXQeH+NoX1/CaN6zgtpsfRSsqsxkawzRoSMWIxy2y2RK71z3L+rN209Qc5/2vO4/3i/Mm1T22KhApKxbMbKPoeXhKYUpJzDQZmIT5kfo2nN1mjvjzPZwJXUhISMhECYVKyAlnvPaA7wcJwYYh8H1NwSlT8rxhHxEJ0oXWXli4idqa8GALbIr5lCNjHEyA8GHqAWgaDFo9LU6aq7fcwor0DgB+P+NcvnbmWykTQXU72I3BQOmGnh5MKWnOCbQIjOIkULUDEWps4yEtwTIkqCB3Jz1U4EffeRDPC0SYlAKtQfmKbLqIBEpFl22bu7nhc7cTjVq1Vd5Vk7gdUy8QpRAkRg3UTsb8iJSCd1y5mq99cQ39fTkaGqJYtolb9sY1oQsJCQk5Gibd8C0kZDT17YF6DEMiRPDJX4jhFkt1ElVWVnVn7oKGDJRN2LoENpzLCJESMQysyid5swzT98EZTwYiZdXAZr7x+JdZkd5BwbD57IrL+cK576IUjYKCXKZEZyLBgwcOcP3atRQ9jylEEAKK8aA6I3Ul9Xic11e9TUpJ2fFwyz6e59e9RoGUAsOQ+L6ivz9H2QkEWTIZIRa3aqu8T6zbO2nv+0TmRzxPHff8yNmrZvH3H7uEOfPaKRZdBvrz45rQhYSEhBwtYUUl5IQzXnsgErEwLQOn5BKNWUSjFsIJBAs6mC2ptmAGmzk0p0fBtP1wYKaP0BArwIKtMH0/GMrnvXvu4O377wNga8M0/s85V7Ev2U7U1eBpfKUZEmX2dHfzSGWzCOBZBZ0SLAQqGrQwjBK1wdhabaC2tkNtW0lr8CppxkElRdderxCi5tYapCSDYcoTtsr7XM6PnL1qFstXzpzwinlISEjIRAmFSsgJ53DtAdOUuFJiGBJTCaKGQankES1X/EUU7J0DuxeMtMCPFmDpM5AahHwi2MxZ+iQk89BRGuCazTezJBtUJ34+6wK+dsYb8LFI5EGqIPFYSZj7NOxcqBmo8yg0yyA9sNy6WGR9aAihqPxD6+EvCO4rpcC0DDxXYZrBxdr3h40Mq1Wk/r5cLURxsld5n+v5ESlFuIIcEhIy6YStn5DnhPHaAwsXd/L+D72MhYs7KRVdkmUD04d8I+xaABvPgZ2LR4qUjmfhnPUQzQaCYsYeWLU2ECkv6dvI1x//Mkuye8maMf7PWe/h64vegnADkVL1RIGgGJJKw7KN0NYb/GVo6YVlTwa3q/p2FCM3fiomsSNQSmHZBlqDaRm0tTUgpcDzFL6v8LyRjsuGEbSKerozFPLlSWvFVKkKxFjcpr8vh1NyUUrjlFz6+3Lh/EhISMhpQVhRCXnOOFx74C3vOJcd27rp6c/xWLqXbx54mv39QyMEilWGeVuhbSCotBhesHY8Yz9YyuWvd/6GNxx8GIBnGmZx3ZKr6I43o4FoeeS5KBG0lERlbGbOdhhohXnbg+ctRgO7fLtcmZWhsp4sBNIQaKXRWo8QK0KIWvvHsgxicYspHSkGBvKUiiNPwDCCgE5EIHAGB/K0tiUmfZW3KhCrRnvZrINpSubMaw9zeEJCQk4LQqES8pwyXntACGiamaLQLPn9HevYPDQEdSKlrRuWbYCILyhHNL4H8YqPyYxCD9duvol5+SB1+9aZr+D7sy/FlwaGCjJ0IBAntVXjSkVEVhxnGzKwYBMkM8H94oVK7g4gxXAlRUpoaU2QyzqUHW9EOyeRjDB/YQerzp/D7b/eUGtztbQkOPisCwwLG9/X+P7w0IvjuAwNFVmwqGPSV3nD+ZGQkJDTmVCohJx0HM/jYD7P493d/MOauzlQytdukz4sfhpm7q6KBR24tBJ8var7Mf5m+/8SU2WGrARfWHwFj7Utrj1e+cP9TU0giKDOyE0HVRl8mLVrWJxUk4sBzEpQplKBwEDD1GlN5PMlclkHyzL4i7efwznnzakJgPkLO2pVjGKxjAYsy8Tz/NpAbfVctK48L5ywVkw4PxISEnK6EgqVkJPKQLFIT6HAtzdu5KuPP45X10tJpWHZE4cmFEsg6jv87fZf8KqexwFY37iALyy5goFoathGlooYGDkTOzxnUqmo1J63boREENxZGKLmheK6QQXE83wGB/KYpmTRks4xWyj1VYxNTz3LLT/8M4WCgxACywrWlKuCpUpjU4zlK2ce9XsYcnqgtJ5UQ7+QkBcKoVAJOSm4vk9XPs/2oSGuvu8+HuvuHr5Rw/ydsHBrYASHGN64EcC83AGu3XwzM4q9+ELwo1mX8NOZF6OERKpKNUSMFCEQfK8UICsVmZGzrSNEDATHMxleLa7Op/zlh19Oc0v8iC2UahVj/sIO7rnrGbZu6sI0A9EjpYHSGq00Smksy6BUdMPwvucp9+zdy/Vr17JlYICyUthSsrilhWtWr55QblNIyAuZUKiEPOekHYfeQoFfbt/OZ/70J3KuW7stVhK8ZFeUyM4SSIHSGlG1rdea1x98mL/e+Rts7dEVb+RzZ76LbdF5h6wNCz36qAE1sTJKW6hxYo99XwWmdJXWjGWZNDbFWHX+3Al/QpZScMHLFrJtc3eliqJrlR6tNYYhaWlLUiq6YXjf85B79u7lQ2vWkC2XaY1GiZgmjuexsbeXD61Zw7cuuSQUKyEhhyEUKiHPGb5SdOXzHMzl+MzDD/PbnTtH3P6K9um0/HaQhDDppwRa16ooSbfAR7f9jJf2PwXAn9uW8tlz34njJVAmRPJjHHAcZKAVamgOrb7UbtPguYFYEULglFxu/v6feGqwn1tyuyf8CXnlubP5xU8eo1Ry8b1g3kWIwPSuuSWBYQi8MLzveYfSmuvXriVbLjM9ORzIGbMsppsmB3I5rl+7lotmzgzbQCEh4xAKlZDnhFy5THehwJ8OHOCT99/PwfywskhaFv96wQVctnAR31x/Jzu394AQ+J5CAEsye7hm8810OIO4wuBbC17PTYtfStwVTG1LEY2YZAaL5IaKQZViAufjy5Frx4dDCDBNA98PfFL2HRjkya8/wJ5zLZKzExP6hDx/YQfzFkxh144ekg1RlAoqKZFI8Fewvy8Xhvc9D1nf3c2WgQFaoyMDOSEQvi3RKFsGBljf3c2qzrDlFxIyFqHhW8gJRWlNVz7P7nSa69eu5b133DFCpJzb0cGv3/IW3rdsGe1WlJdetJiy4wciRSsu2/dHbtj4DTqcQZ6NtvAP5/wtP53/MnqmCfChrH0MH1KJCO3tDZjm+P9La1mx5wfOu2QBzbMajihSgKAFVREWbW0N5G2FKCtmbVXETBMpRPAJOZkkWy5z/dq1qFFucFXztXgiQj7nYBoS2zYpO15ovvY8pq9YpKwUEXPsz4RR06SsFH3FsOUXEjIeYUUl5IRRdF268nk2DwzwsXvvZdPAQO02Uwg+es45fGjFCqYmkpSyZW794wZu/+FjOI5HYznHx7beynmDWwC4t20FXzrjbXS3xdi7WOKZGnZohrrzlEQRrStusYyI4BmBUJUhWwEvWjyLqDD4094tR3wdArAjJi0tCbQtKBV8jKjEHPIx+zy89iBH50ifkEPztRcebbEYtpQ4nkfMOjRvqeR52DJI7w4JCRmbUKiETDpaa/qKRQaKRX68aRNfePRRnKq5GTC3sZEbX/5yXjRtGs2Gze+e2Mq3f/gg4skcAlgxtJ2rN/+YFjdLyTD5twv/gpvOW40bNcg1AULQ1O1jekH7xjc0thls0Xjl4DjjiRWpwbAkQkq2PLp/Qq+ntTVJY1McIYJBYA0IU0BZI0ojV4eipsmg44z7CTk0X3thsbKjg8UtLWzs7WW6eWje0kCpxPL2dlZ2hC2/kJDxCIVKyKTieB4Hcjnu3buXrzz+OE/29Y24/fIlS7h29WpmNTRglDU/eXAjP/jmA0QHfKT2uXLPH7h8391INFumdPKOd13J1lnTkVrTvl/T3K0pxaBjj8Y3CLxOdLDiq0dJE1H7R4XKzR2djTz8wDbcso+UAr8ytHsIAqQQRGNWzZzNlDIQQZ4GCTo6stU0kU/IofnaCwcpBNesXs2H1qzhQC5HSzRK1DQpeR4DpRIp2+aa1avDQdqQkMMQCpWQSWOgWOS3O3dy3dq1bB0YoL7W0GDb3HDhhbxm7lymxOLkhkrce/9Wbv3mfUTLmjZniKs3/5izMrsA+OUFL+Mjf/FGuoRg/laf2VvB9IK142p7p2xDMQ4pT+J76pCQQOCQsooQgnPOm8OfH9xOU3Mcz1M4josvQFcN2CqbRoYUSCGw7eG/JnHLImoYeHkXr83Caxu+LfyEHDIWF8+axbcuuaTmozLoONhSsry9PfRRCQmZAKFQCTluXN/nYD7Pnbt28bF77x3hiwJgSUnKsmiPx2k3o3TtT3PrDx/m/nu2IIDz+5/hn7b+hEavQD4S4fPvfj+/v/ACmsplYo8OMn9TIFCUCAZiq0Ztdjn4WcFWiMrPLScI/KMSDOh5Cq11EBhoSaJRmxkzm/E8hW0Hcyc93RmE0ghDgBBopVC+JpGIYNsmA/1BZo9lm7hlj2RZMmhL9i6S2J4XfkIOOSIXz5rFRTNnhs60ISHHQChUQo6LqnnbEz09fPSeeyjVzaIIoDORoNm26SsW+ebDj9MxT/CNr97NQHcOU3m8f/ftvPXAAwBsmjmba//mb9k/PWiLxAyD6dsCkeILEJUuS71YiTqgy8PZPRDk5kgpmNKRAhGYtkkpyOUc5s5rZ/EZ0zBNiet6xBN2LeHYLXu1qoppGbz3gy9l+syWQ4ZfFy3sZOGr59R8VMJPyCETQQoRriCHhBwDoVAJOSY8pejO58k4Dt9+8km++vjj+HW9l6hhMKOhgZhpYinBFM+m5+Eu/v17v0IomFrs45rNN7MoFwy03nLxq/nqu96Ja9u155Bbi0G7xxgeNdEwpvGJHrWVrHTQv7ErVZBstkS8sgK8cHEHM2a1sHtnL3abSTxhE4vbOI6L5/nksg4LFnXw2jetQEox7vDre/W54SfkkJCQkBNMKFRCjppsuUxPocCedJqr77+fdfU5PQQrme2xGLZhYJXByHgk1xaId3kAXNj7BB/ddhtx32EwGuODb38nT174ElJ1IkVrjZcuBy0cwyAiBJ4O5lC01qi64ZOxNnyUAelcEUPLMVeA33Hlar72xTX09w23dQRQKro0pKIjPE3GG34NPyGHhISEnHhCoRIyYZTWtSrKr3fs4LMPPzxiHsUQgo54nJZYDAuBWdBYz5ZJrs0iS5qIX+ZDO3/Na7vWAvDEvIX8wwc/wJ9sEyOTYV5jIzHLqs17TG00kbLSjjEEpgjSBD1fjRAqUGkFCdCWQNlBRtCL37aMly2aPeYK8OE8Tc5+3UK6W3zWdXWFVZKQkJCQk0woVEImRKFi3tZfLPJvf/oTt+/aNeL2N86bR3ehwK50GtsDo+gTf6pIdFMRAczKd3Pt5h8xp9CNEoLvveb1/Pfb34pvmswoFOjO53EOFvHLBUTUYPmCNq5+zfn894bfkM06AIjKFs4IBPgpoxLYA9oUKKWQBehsS7Hq/LnjvqbRniZbimm+372Vm3espbxNYQvBYj/JO2cv4sKFs0K/k5CQkJCTQChUQg6L1preQoEhx+HPBw/yyfvvp6vOAr/Btvm3F7+YNy9cyKb+fj5++92k92aYtdEnMuCD1lzS/Sgf2fFLosqlvyHFv/zVh1h79lm15+gYNJj+OExxNFKBZcG8PkFa9dLQGCebdfC84WCeeq2gYhJtjRQPytXYpmTlnGlHfH3Vts49e/fyr2seqyXcJnsUsSdy+EN5blXd/D4RY8Hc0EE2JCQk5LkmFCoh41LyPLryeXLlMl95/HG+++STIxou53d28oULL2RBczOtVoRkXPCX9jzueWgDwtXEvRJ/t/0XvKJ3PQB/WryUf/ubjzDQ3FR7DutAmcQDGYSraGiLkIpFcV2PbVu62Lh+H9GYRVNznEy6iFKBiYrSICV4EhxbY2iNFEG7x/cVtgtzFrSxcNHEvExGJ9zaz7okH8wiXIWKSMooMrrMrp29fO2La/j7j10SipWQkJCQ54hQqIQcQtW4bKBUYtvAAB+/774ROT2WlHz0nHP4wFln0ZFIYLpwcN8Qt/zozzx471YEsCC7n2s238z0Uh/aMPjuW97OP194AdMaG4eXdrQmtiEHZYWRsmhMxIJcHdvC8xRKBV9t7Y20tiXJZUu4rkep5DF1WhP9mQKD2QKOqXANkD7EPEFzY5wPf+DlE27TjEi4BeIb8oFIiUsQAkNLSloRa4xQTDv89Oa1LF85M2wDhYSEhDwHhEIlZARl36crn6fouty8aRP/MSqnZ14lp2dlRwdTojHyaYdNW7r49tfvpetgGrTmTc8+xAd3/RZL+3jTZ+D/8CbmLphLcpSNuOoqIQY9VFTSmUzWBIzjuHiuj2FI3LKP43hEoxapxsCW3im5ZNJFrrrqRTx0/zZ27u7FdX0s22DeoqNvz9Qn3Jp9HsaQh4oEIgWC7R5fa3ytaWiIsn/vADu2dYc2+CEhISHPAaFQCakxWCrRVyzSk89z7QMP8MCBAyNuf9eSJVx9/vl0JhI0YDLYm+euO57i57c8iucpkm6B/2/bT7mg/2kAnNe+AfP73yMypY2L4RAb8ea8wkLQ0ZTCEIK042BKCX6whmwYAt8P2jn1WLZJNuvQOa2Rz9142XEH/NUn3EZKChRgDN+utEYQ5PxYRnDsTHrs0MGQkJCQkMklFCoheErRlc9TcF3+sGcP//zQQwyWSrXbW6NRrnvZy7h49mymxGJ4eY89B/v43rce5Mkn9gGwLL2Lq7f8mCnOENq2Kf37dUQ//o8IOezENtpG3DlY4KaN99KdyeL4FYEgISIlBqCURggwjJFubm7ZwzQlqcbYpAT81SfcJqMRkIBP7W+HrzQxyyRuWZRLbu3YISEhISEnnlCovMDJOA49hQK5cpnr1q7lp1u3jrj9FTNn8n9f+lJmNDTQZkfJDBTZ8MRevvtf95MeKiK04h37/si7967B0Apvzjz8W24h9qLzxzxevUna3WIPPdohMqSwRSVwUICSPkqDoSAWs4hERgb/ZbMl5sxrZ/7CyQn+q0+43aNLLGmU2IM+nhT4OtgM6kwk4AQcOyQkJCTk8IRC5QWKrxQ9hQLZcpmNvb184r772J3J1G6PGgbXrl7N5UuW0BaLYXmCnoNZfnnbY/z+t0+iNTSXM3x8y62cM7QNgNLb3oH17f8m0tx4xOMrrfnCL+7DKCqEBvSwDb70K98LkIak7Hi1QMBstkSsYoU/mcOs9Qm3+5b20LnWxywo7LhJRzJJRAn6+3In5NghISEhIeMTCpUXIPlyme5CgZLn8d8bN/L19etH5PQsa23lxosuYnFzM1NicQpph917+/mf/7yXXTv6AFg5uJVPbLmFZjeHisVxbvwSsY/8dW0A9UisO9hF8c/9RLXAbxDIkkb4etgLX0I5IeiY00S2uzDCOfZEeZnUt6bWn7uHJ+7YRqYrj5t10Sf42CEhISEhYxMKlRcQqmLelnYc9mWzXH3ffTze01O7XQAfWr6cv125krZ4nAZM0n0FHn5gGzd990+USi5S+7x7zxrese+PSDTukqXoW28ltmL5YY87Orxv0+aD2BmFjkowBX4EhKdrcyoaEGXF+W89gwtmTD+uYdmjodqaWvX6TtRrzz/uQd2QkJCQkOMjFCovEIoVC/yy7/Or7dv57J//TL4up2d6Msl/XHgh50+dypR4/P9v787jo6rv/Y+/zpmZzExIJhCyABICAUQQBRokQmlZpKEW+4Mu3Ku1CNqfCIJdQK4glEjbiNelraKiImUREEq1+ihSS9CwFEKAsNgE2QmDgSwQyGSZzHbO/SMwEtkSyCyBz/PxmMfDM+fknM/3BGfeOed8v1981V5OnTnHisU55Gw+DEB87VmeObCcOx3H6/b5yGNEvPEahqgW9Y6lafolw9IfOHsWt6YRoap0i41loDcWRQNN1VHPd0zWjcpF+9BQNTB7bvxh2evVFA/qCiGEuDEBCyqZmZl88skn7Nmzh4iICM6dO3fJNna7nYkTJ5KdnU1UVBRjx45l7ty5GI2Sn5qKruuccTopr63lnMtFxpYtfFpYWG+b/9e5M7P79yc+MpLWJjNV5bUcPlTCO69voLS47rmV/qfz+c2h1UR7a9Cio3G9Ph/rIw9fcrw9eXb/RH81Lg/nPC6cNoXWvVpAUgtcXi9flJVR5DjDbQYFr1dHjbhkN40aBl8IIcTNK2CJwO12M3r0aPr378/ChQsvWe/z+RgxYgRt2rRh69atnDp1ikceeQSTycTzzz8fqLJuKa7zQ+C7fD5yTp7kmU2bKKmp8a+PjohgzoABjEhJobXVismtU15aTdan+Xy4cic+n4ZJ8/KLY2sYeXILAO7e34IV72Ptfvslx9uTZ2feK+uoqXYTHWOhVK/Fq0OLCp3InBqqDEbU2yK4zWikSK+kTUsDpjNe3Grd4G43Mgy+EEKIm1PAgsqcOXMAWLx48WXXr1u3jn379rF+/XoSExPp3bs3v//973nmmWd47rnniIi4zJ/ZosEuDN7m8nr5U14ef8nPr7f+wjw9HWw2EixWnA43X5U6WPT2ZvL3fgVAO2cZM/Yvp0tV3cBvziefwvzKS6gW8yXH0zSdvy7PpabaTVx8FDVeL7U+H6pJRTOBoUqjRW4lVQOj8cabiLVaOXmHkzu/MFJV7brhYfCFEELcnEJ2jyUnJ4e77rqLxMSv/2IePnw4EydOpKCggD59+lz251wuFy6Xy7/suKhLrQDPhSHwvV4OnT3L1A0bOHD2rH/9hXl6HuvZk5YWCzbFRMUZJ//Zc4K/vLXJP+LqkNJdPHX4Q6w+F75WsXjeWYj1p6OueNwjh0r4yl6OLcaCoih4NQ0dMHhAdWooXh1juRfbv87ha23C0CsSe4LK9x/7Fkc/s9/wMPhCCCFuTiELKsXFxfVCCuBfLi4uvuLPzZ0713+1RtRX4XJRVlODV9NYtm8fL+3cifuieXo6t2zJy4MG0TMujtYWKzh9lFU4+Oivu/jXJ/8BwOxz8+SRj0gv2QGA+94BsGIFlk7JVz22o8KJ16thMtX9kzKqKgaPjlqt+QdyU3TQDQqGcg/Rmx207Gemz4+SeUx61wghhLgC9dqbfG369OkoinLV1/79+wNVKwAzZsygoqLC/zpx4kRAj9cc+DSNk1VVlFRXU1xdzePr1pGZm1svpHzP1o4nvR1xFVQQZ7TgPufi6JFSXpjziT+kdKw+xWt7XiO9ZAe6ouCcNgPjpo1EXCOkANhirBiNKh6PF4BIo4mIWr4eyE2pe+kmpW5WYrdG8kGdXvEJ/t41qf060bVbGwkpQggh/Bp1RWXq1KmMGzfuqtukpKQ0aF9t2rRh+/bt9d4rKSnxr7sSs9mM2XzpMxK3qqrzg7f5NI31x48z89//SulVNwAAGTNJREFU5txFt8ZsipGee8B44hTr9ZMArH57K3fefRt7d5/AVesFXef+4m1MOPYPInwefAltcC9egvX+9AbX0blrIu07xFJ4tIyIOCMulxeDpuBVdXQdVB10Y113ZJ8GBouKrUbl2OFS6QIshBDiihoVVOLj44mPj2+SA/fv35/MzExKS0tJSEgAICsrC5vNRo8ePZrkGDczTdcpranB4XJR7fEwNzeX1d+Yp+duU0vi157DVFs3JohqUPH5NKqr3WzPOQZApNfJLw99wKDTewFwDf0e6ntLsbZrXHhQVYX/ejiNea+s48zpKoxGA+g6JlWte15FAbdFQQesJiOJ1kg8lR6ZhVgIIcRVBewZFbvdTnl5OXa7HZ/Px549ewDo0qULUVFRpKen06NHD8aMGcOLL75IcXExs2bNYtKkSXLF5BouDN7m0TT2lpby9MaN2Csr/eutRiPP9O3H3rk7cNaCyaiiKAo+n46mfb2f2ytPMOPActo4z6AbjdT+dg6Wmc+gGAzXVVfv1A48NTWdvy7PpfBoWd2bOkRaI4iMMWMwGzCqqn8WYl1mIRZCCHENAQsqs2fPZsmSJf7lC714srOzGTx4MAaDgTVr1jBx4kT69+9PixYtGDt2LL/73e8CVVKzp+s6p51OztbW4tU03tq7lzf37Kk3T0/PuDheHjSI07tPk1vlwaAq6Dr4fBqaVredomv8qGgzjxauxahreJM64H1vOdZBA2+4xt6pHbi7TxKHDpQw7+V1lJY4SEiMRlW/fhwqEDMgCyGEuDkpun7Rt1wz5HA4iImJoaKiApvNFupyAsbl9XLq/BD4JxwOpm3axO6L5ulRFcU/T0+UycSG1V+wcmkuRmPdlZQLv2Wbp5opB1eRVv4lAEf7DKLDug8wxrVu8povDADnrHETHW25ZAbkp6amSxdkIYS4RTX0+1vGqm8Gyp1OztTWomkafz98mN/n5FDj9frX3xYVxUuDBpGamEhLsxmLV6FFVN3tM6/3oqstFUd5Zv8K4twVuBUj73T+IXc8P4eUAIQUqH8r6Ct7eVBmQBZCCHFzkaASxjw+H6eqq6n1ejlbW0vG1q386xvz9Izq0oXf3nsvMRYL8WYLLoebM9Uujh0+7d9G1TUetH/Gz+xZGND5KjKe57v9nNNtU5gw7M6AtuHCrSAZJ0UIIcT1kKASps6dHwJf03W2FhXxzObNlF40T4/t/Dw9P0hJIdJkwqaYqDrj5GTROd55PZsTx8sBiHVVMO3A+/SuOALA+sS+vNF5FJ4IK4+OScNobNRQOtdFZiEWQghxvSSohBmvplFSXU21x4PL6+WPeXksLiiot829bdvyv9/9Lm2joog1W1CcGhXV1WzddIgVS7bhdtXdFkot38+0Q6uIcVfhVCN4vcuP2dC2Ly1amPn5mDR+8uA9oWiiEEII0WASVMJIpdtN6fnB2w6Ul/P0xo0c/MY8PVNSUxnXsycWo5HWJjPV52pxVNay7C9b2HF+bBSD5mPs8X8y+quNALh79GTnLzNJioxnSqKNQUO7B+VKihBCCHGjJKiEAZ+mUVpTQ6XbjabrLN23j1e+MU9P1/Pz9NzRujUxZjNWr4LjjJPDh0p4940NnC6rAiChtpyZB1dwe8VxAJyPPU7Ea3/mOy0iQ9I2IYQQ4kZIUAmxmvODt3k1jZKaGmZs2sSWkyfrbTOmRw+e7tuXFhERxJktuB1uztW6+fQf/+Hjv+3yj4/y7dNfMOXIB0S6a9BsMbjefAvrww+GollCCCFEk5CgEiK6rlNWU+Ofl+dfhYXM3rKl3jw98VYrc7/zHb7Tvn29B2bLy6tZOH8j+wtOAWDSPIw/+g8eOJUDgPtb98CKFVi7dQl+w4QQQogmJEElBC4evK3K4yFz2zY+PHSo3jbDOnTg9wMH0tpqpVWEGbVWx1FTw95ddha/vZmqqrpA076mlFmHVpDsKALA+dRvML/0Aqo5wr8vr6ax8ssvOe5wkGyz8WD37hhVeUZFCCFE+JOgEgI1Xi9un4/dpaVM27iRE9+Yp2dmWho/vf12zEYjsSYzNedqcTrd/O39nXy+bp9/22ElO5l87CPMHhe+1nG4F/wF649+WO9Yr+zYwfO5uVS4XOiAAvwqO5tn09KYeo/0+hFCCBHeJKiEgFfTmLdrF/P37q03T8/dcXG8NGgQHWNiiDk/wmzlGScni87yzuvZfGWv6wFk8bn45dGPGFK8EwDXwO+iLFuGNTmp3nFe2bGDGZs349U0TKqKqihous7Z2lpmbN4MIGFFCCFEWJOgEmSHz57lwTVryCsp8b+nKgoTevXiyd69MRsMxFmseBxuHC4Pm7MPsuq9bbjddT2AUqqK+O3h92lTWYKuqjifmYllzmxUU/1fpVfTeD43F6+mYTEYUBTFfyyDolDr8/F8bi6/Sk2V20BCCCHClgSVIFpz5AgPrllDtcfjf6/9+Xl6vpWYiNVoJEaNoOqMk8qqWt57dwt52wvrNtR1RpzKYcLxNRi9Hnxt2uJe8h6R6fdd9lgrv/ySCpcLk6r6Q8oFiqJgUlUqXC5WfvklP78zsMPoCyGEENdLgkoQdW/dmosjw4+7dmVmWhrRZjMtI8wYzj8we/hgCQte30D5mWoAojw1TD32IfeW7AXA9b3hqEuXYm2TcMVjHXc40Km7gnI5qqLg1XWOOxxN1TwhhBCiyUlQCaLOLVvy2tChTNmwgTkDBvD9Tp0wqSqtIyx1D8x6vKz9+Av+8eFu/9godziO89sjK4mtOo1uMuF87g9Ypz+Nco3bNck2Gwqg6fplw4qm6yjntxNCCCHClQSVIBvXsyffad8eTdexmc1YPHUPzJ45U8XC+Rs5+GUxAIqu8dOiTYw7/imq5sPboSPe5SuIHNi/Qcd5sHt3fpWdzdnaWgyKUu/2j67reDSNVhYLD3bvHpB2CiGEEE1BgkqQKYpCfGQkPq8PX5WXKreX3TuPs2TBv6k+PzZKjLuKZ4+t5u7Suq7ItSN/jHHhAiytYxt8HKOq8mxaGjM2b6bW56vX68ejaf718iCtEEKIcCZBJQQsqJw5W0Oty8Pq5dvZsH6/f12vc4eZeXQV0dXn0CxWav/3ZayTJ1zzVs/lXOh6fGEcFe/52z2tLBYZR0UIIUSzoOj6RQN5NEMOh4OYmBgqKiqwNZPnLSora9mXX8SCeRso+qpubBRV9/FI0ef8V2EWiq7juf0OtOUrMPftc8PHk5FphRBChJuGfn/LFZUg03WddWvzWfLOZjyeurFR4lznmF24mq6lBwFw/mwMEfPfxGSLapJjGlVVuiALIYRoliSoBFGlw8mrL65jy6av5/Xpd2Yf0ws/wFrjQIuKovZP84j8/+NCV6QQQggRRiSoBNH2nKP+kGLUvDxxch0PHMsGwHNXL7T33yfyTumFI4QQQlwgDyoE0dD0Hnx3SDfaOk/zxsEF/pBS8/hE1G05mCWkCCGEEPXIFZUgUhSF33Q6h6FgHiZnDVrLVtTOf5vIB0eHujQhhBAiLElQCZaaGvj1r7EsWACAu9+96MuXE9klJcSFCSGEEOFLbv0Ew7590K8fLFgAioJv+rMYNm3ELCFFCCGEuCq5ohJIug6LFsHkyeB0QmIiLFuGYdiwUFcmhBBCNAsSVALF4YCJE2HFirrl9HRYurQurAghhBCiQeTWTyDk5UFqal1IMRhg7lz45z8lpAghhBCNJFdUmpKuw7x58PTT4PFAhw7w/vswYECoKxNCCCGaJQkqTaW8HB57DD7+uG551ChYuBBiGz7jsRBCCCHqk1s/TeHf/4bevetCSkRE3VWVDz+UkCKEEELcILmiciN8PnjhBcjIqPvvrl1h1Sroc+UZjzVdZ3dJCaedTuKsVvokJqIqShCLFkIIIZoPCSrXq7gYxoyB9evrln/+c3jzTYiOvuKPfG6380JuLgfKy3FrGhGqSrfYWKanpTG0Q4cgFS6EEEI0H3Lr53pkZUGvXnUhJTKybqyUpUuvGVKeWLeOL8rKiDKZaNuiBVEmE1+UlfHEunV8brcHsQFCCCFE8yBBpTE8Hnj2WRg+HEpL4a67YOdOGDcOrnL7RtN1XsjNpdLt5raoKKwmE6qiYDWZuC0qikq3mxdyc9F0PXhtEUIIIZqBgAWVzMxMBgwYQGRkJC1btrxk/d69e3nooYdISkrCarXSvXt3Xn311UCVc+Psdhg8uG5MFF2HCRMgNxe6X3vG490lJRwoL6e1xYLyjUCjKAqxFgsHysvZXVISoOKFEEKI5ilgz6i43W5Gjx5N//79Wbhw4SXr8/LySEhIYNmyZSQlJbF161bGjx+PwWBg8uTJgSrr+nz0UV3X47NnwWaDd9+F0Q2f8fi004lb0zAbL3+6LUYjZ10uTjudTVSwEEIIcXMIWFCZM2cOAIsXL77s+scee6zeckpKCjk5OXz44YfhE1RcLpg2ra67MdRNLLhyJXTq1KjdxFmtRKgqLq8Xq9GI8bQXpVZDt6h444zUer1EqCpxVmsAGiGEEEI0X2HV66eiooLYa4w94nK5cLlc/mWHwxGYYg4dgv/+b9i9u2756achM7NunJRG6pOYSLfYWI7mF5N4UMN4zgcaoIK3pYFzt6t069mGPjLEvhBCCFFP2DxMu3XrVlatWsX48eOvut3cuXOJiYnxv5KSkgJT0LRpdSElLg4++QReeum6QgqAqig8FNWR5J0e1NMefEbQIhV8RlBPe0je6eGhqI4ynooQQgjxDY0KKtOnT0dRlKu+9u/f3+gi8vPzGTlyJBkZGaSnp1912xkzZlBRUeF/nThxotHHa5D58+GnP4U9e+AHP7ihXWmazqGsQlqqJow2Ez4DeHQdnwGMNhOtVBOHsgrRNOn1I4QQQlysUbd+pk6dyrhx4666TUpKSqMK2LdvH/fddx/jx49n1qxZ19zebDZjNpsbdYzr0rYtrF7dJLs6cqiEr+zlxLVqQTuziRqPB6+mYVRVIk0m3LUevrKXc+RQCV27tWmSYwohhBA3g0YFlfj4eOLj45vs4AUFBQwdOpSxY8eSmZnZZPsNN44KJ16vhslkRAFamEz11psijFRWunBUSK8fIYQQ4mIBe5jWbrdTXl6O3W7H5/OxZ88eALp06UJUVBT5+fkMHTqU4cOHM2XKFIqLiwEwGAxNGobCgS3GitGo4vF4MZtNl6z3uL0YjSq2GOn1I4QQQlwsYEFl9uzZLFmyxL/c5/xEfdnZ2QwePJi//e1vlJWVsWzZMpYtW+bfLjk5mcLCwkCVFRKduybSvkMshUfLiIgz1hv0Tdd1Kitr6ZgST+eu0utHCCGEuJii68173HaHw0FMTAwVFRXYbLZQl3NFe/LszHtlHc4aN9HRFkwRRjxuL5WVtVgjI3hqajq9U2ViQiGEELeGhn5/h0335Jtd79QOPDU1nY4p8TidHsrPVON0euiYEi8hRQghhLiCsBrw7WbXO7UDd/dJ4sihEhwVTmwxVjp3TURVZfwUIYQQ4nIkqASZqirSBVkIIYRoILn1I4QQQoiwJUFFCCGEEGFLgooQQgghwpYEFSGEEEKELQkqQgghhAhbElSEEEIIEbYkqAghhBAibElQEUIIIUTYkqAihBBCiLDV7EemvTCnosPhCHElQgghhGioC9/b15obudkHlcrKSgCSkpJCXIkQQgghGquyspKYmJgrrlf0a0WZMKdpGidPniQ6OhpFadrJ/RwOB0lJSZw4ceKqU1DfrKT9t3b7Qc7Brd5+kHMg7Q9c+3Vdp7Kyknbt2qGqV34SpdlfUVFVlfbt2wf0GDab7Zb8B3qBtP/Wbj/IObjV2w9yDqT9gWn/1a6kXCAP0wohhBAibElQEUIIIUTYkqByFWazmYyMDMxmc6hLCQlp/63dfpBzcKu3H+QcSPtD3/5m/zCtEEIIIW5eckVFCCGEEGFLgooQQgghwpYEFSGEEEKELQkqQgghhAhbElQuIzMzkwEDBhAZGUnLli0vWb93714eeughkpKSsFqtdO/enVdffTX4hQbQtc4BgN1uZ8SIEURGRpKQkMC0adPwer3BLTSIDh48yMiRI4mLi8NmszFw4ECys7NDXVZQffLJJ6SlpWG1WmnVqhWjRo0KdUlB53K56N27N4qisGfPnlCXEzSFhYX84he/oFOnTlitVjp37kxGRgZutzvUpQXUG2+8QceOHbFYLKSlpbF9+/ZQlxQUc+fO5Z577iE6OpqEhARGjRrFgQMHQlKLBJXLcLvdjB49mokTJ152fV5eHgkJCSxbtoyCggJmzpzJjBkzeP3114NcaeBc6xz4fD5GjBiB2+1m69atLFmyhMWLFzN79uwgVxo8DzzwAF6vl88//5y8vDx69erFAw88QHFxcahLC4oPPviAMWPG8Oijj7J37162bNnCz372s1CXFXT/8z//Q7t27UJdRtDt378fTdN4++23KSgo4E9/+hNvvfUWzz77bKhLC5hVq1YxZcoUMjIy2LVrF7169WL48OGUlpaGurSA27hxI5MmTWLbtm1kZWXh8XhIT0+nuro6+MXo4ooWLVqkx8TENGjbJ598Uh8yZEhgCwqBK52DtWvX6qqq6sXFxf735s+fr9tsNt3lcgWxwuAoKyvTAX3Tpk3+9xwOhw7oWVlZIawsODwej37bbbfp7777bqhLCam1a9fqd9xxh15QUKAD+u7du0NdUki9+OKLeqdOnUJdRsD069dPnzRpkn/Z5/Pp7dq10+fOnRvCqkKjtLRUB/SNGzcG/dhyRaWJVFRUEBsbG+oygiYnJ4e77rqLxMRE/3vDhw/H4XBQUFAQwsoCo3Xr1nTr1o2lS5dSXV2N1+vl7bffJiEhgdTU1FCXF3C7du2iqKgIVVXp06cPbdu25f777yc/Pz/UpQVNSUkJjz/+OO+99x6RkZGhLics3Myfe263m7y8PIYNG+Z/T1VVhg0bRk5OTggrC42KigqAkPy+Jag0ga1bt7Jq1SrGjx8f6lKCpri4uF5IAfzLN+OtEEVRWL9+Pbt37yY6OhqLxcIf//hHPv30U1q1ahXq8gLu6NGjADz33HPMmjWLNWvW0KpVKwYPHkx5eXmIqws8XdcZN24cEyZMoG/fvqEuJywcPnyYefPm8cQTT4S6lIA4ffo0Pp/vsp9zN+Nn3NVomsavf/1rvv3tb9OzZ8+gH/+WCSrTp09HUZSrvvbv39/o/ebn5zNy5EgyMjJIT08PQOVNJ1DnoDlr6DnRdZ1JkyaRkJDA5s2b2b59O6NGjeKHP/whp06dCnUzrltD269pGgAzZ87kJz/5CampqSxatAhFUVi9enWIW3H9Gtr+efPmUVlZyYwZM0JdcpO7ns+FoqIivv/97zN69Ggef/zxEFUugmXSpEnk5+ezcuXKkBzfGJKjhsDUqVMZN27cVbdJSUlp1D737dvHfffdx/jx45k1a9YNVBccTXkO2rRpc8nT7yUlJf51zUVDz8nnn3/OmjVrOHv2rH+q8zfffJOsrCyWLFnC9OnTg1Bt02to+y+EsR49evjfN5vNpKSkYLfbA1liQDXm95+Tk3PJfCd9+/bl4YcfZsmSJQGsMrAa+7lw8uRJhgwZwoABA3jnnXcCXF3oxMXFYTAY/J9rF5SUlDSrz7gbNXnyZNasWcOmTZto3759SGq4ZYJKfHw88fHxTba/goIChg4dytixY8nMzGyy/QZSU56D/v37k5mZSWlpKQkJCQBkZWVhs9nqfZmFu4aek5qaGqDuHvXFVFX1X21ojhra/tTUVMxmMwcOHGDgwIEAeDweCgsLSU5ODnSZAdPQ9r/22mv84Q9/8C+fPHmS4cOHs2rVKtLS0gJZYsA15nOhqKiIIUOG+K+offP/h5tJREQEqampfPbZZ/5u+Jqm8dlnnzF58uTQFhcEuq7z1FNP8fe//50NGzbQqVOnkNVyywSVxrDb7ZSXl2O32/H5fP6xErp06UJUVBT5+fkMHTqU4cOHM2XKFP/9SoPB0KRhKJSudQ7S09Pp0aMHY8aM4cUXX6S4uJhZs2YxadKkm3KW0f79+9OqVSvGjh3L7NmzsVqtLFiwgGPHjjFixIhQlxdwNpuNCRMmkJGRQVJSEsnJybz00ksAjB49OsTVBV6HDh3qLUdFRQHQuXPnkP2VGWxFRUUMHjyY5ORkXn75ZcrKyvzrbtYrDFOmTGHs2LH07duXfv368ec//5nq6moeffTRUJcWcJMmTWLFihV8/PHHREdH+7/nYmJisFqtwS0m6P2MmoGxY8fqwCWv7OxsXdd1PSMj47Lrk5OTQ1p3U7rWOdB1XS8sLNTvv/9+3Wq16nFxcfrUqVN1j8cTuqIDbMeOHXp6eroeGxurR0dH6/fee6++du3aUJcVNG63W586daqekJCgR0dH68OGDdPz8/NDXVZIHDt27Jbrnrxo0aLLfibc7F8j8+bN0zt06KBHRETo/fr107dt2xbqkoLiSr/rRYsWBb0W5XxBQgghhBBh5+a9wSiEEEKIZk+CihBCCCHClgQVIYQQQoQtCSpCCCGECFsSVIQQQggRtiSoCCGEECJsSVARQgghRNiSoCKEEEKIsCVBRQghhBBhS4KKEEIIIcKWBBUhhBBChC0JKkIIIYQIW/8HkRl57At7Q7cAAAAASUVORK5CYII=", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "labels = labels_list.cpu().detach().numpy()\n", - "preds = preds_list.cpu().detach().numpy()\n", - "\n", - "labels2 = labels_list2.cpu().detach().numpy()\n", - "preds2 = preds_list2.cpu().detach().numpy()\n", - "\n", - "r2_with = r2_score(labels, preds)\n", - "r2_without = r2_score(labels2, preds2)\n", - "\n", - "ax1 = sns.regplot(x=labels, y=preds, label=f'w/ Ontology, r2={r2_with:.2}', color='darkcyan')\n", - "ax2 = sns.regplot(x=labels2, y=preds2, label=f'w/o Ontology, r2={r2_without:.2}', color='darkslateblue')\n", - "bla = [-12,-11,-10,-9,-8,-7, -6, -5, -4, -3, -2, -1, 0, 1,2]\n", - "ax1.legend(loc=\"best\")\n", - "plt.plot(bla, bla, color='red')\n", - "plt.savefig('../scatter_regression_december2.png')" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "21d206ae-4ee3-4a42-a471-57c98e528ea9", - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.9.19" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} From 2c159e8339b87d12db9cf94c418ba817c7ce3254 Mon Sep 17 00:00:00 2001 From: schnamo Date: Thu, 16 Jan 2025 16:48:18 +0100 Subject: [PATCH 20/54] fix issue with solubility dataset read in --- chebai/models/base.py | 1 + chebai/models/electra.py | 2 +- chebai/preprocessing/collate.py | 2 +- chebai/preprocessing/datasets/base.py | 1 + chebai/preprocessing/datasets/solCuration.py | 150 ++++++++++++++----- chebai/preprocessing/datasets/tox21.py | 2 +- configs/metrics/mse-rmse-r2.yml | 11 ++ configs/model/electra.yml | 4 +- configs/training/solCur_callbacks.yml | 4 +- configs/training/wandb_logger.yml | 3 +- 10 files changed, 134 insertions(+), 46 deletions(-) create mode 100644 configs/metrics/mse-rmse-r2.yml diff --git a/chebai/models/base.py b/chebai/models/base.py index cda7f16a..2339c843 100644 --- a/chebai/models/base.py +++ b/chebai/models/base.py @@ -246,6 +246,7 @@ def _execute( loss_kwargs = dict() if self.pass_loss_kwargs: loss_kwargs = loss_kwargs_candidates + # todo: check here too loss = self.criterion(loss_data, loss_labels, **loss_kwargs) if isinstance(loss, tuple): loss_additional = loss[1:] diff --git a/chebai/models/electra.py b/chebai/models/electra.py index 05139324..fb9c271d 100644 --- a/chebai/models/electra.py +++ b/chebai/models/electra.py @@ -315,7 +315,7 @@ def _get_prediction_and_labels( if self.model_type == 'classification': return torch.sigmoid(d), labels.int() if labels is not None else None elif self.model_type == 'regression': - return d, labels if labels is not None else None + return d, labels else: raise ValueError('Please specify a valid model type in your model config.') diff --git a/chebai/preprocessing/collate.py b/chebai/preprocessing/collate.py index ecbcb876..89571399 100644 --- a/chebai/preprocessing/collate.py +++ b/chebai/preprocessing/collate.py @@ -106,7 +106,7 @@ def __call__(self, data: List[Union[Dict, Tuple]]) -> XYData: lens = torch.tensor(list(map(len, x))) model_kwargs["mask"] = torch.arange(max(lens))[None, :] < lens[:, None] model_kwargs["lens"] = lens - + return XYData( pad_sequence([torch.tensor(a) for a in x], batch_first=True), y, diff --git a/chebai/preprocessing/datasets/base.py b/chebai/preprocessing/datasets/base.py index dfa0f999..3d565b3b 100644 --- a/chebai/preprocessing/datasets/base.py +++ b/chebai/preprocessing/datasets/base.py @@ -292,6 +292,7 @@ def _load_data_from_file(self, path: str) -> List[Dict[str, Any]]: Returns: List: A list of dictionaries containing the features and labels. """ + print("what???") lines = self._get_data_size(path) print(f"Processing {lines} lines...") data = [ diff --git a/chebai/preprocessing/datasets/solCuration.py b/chebai/preprocessing/datasets/solCuration.py index a1192401..a9129e08 100644 --- a/chebai/preprocessing/datasets/solCuration.py +++ b/chebai/preprocessing/datasets/solCuration.py @@ -55,38 +55,11 @@ def download(self): def setup_processed(self): print("Create splits") - data = self._load_data_from_file(os.path.join(self.raw_dir, f"solCuration.csv")) - # todo: figure out where the groups are supposed to come from - # groups = np.array([d["group"] for d in data]) - # if not all(g is None for g in groups): - # split_size = int(len(set(groups)) * self.train_split) - # os.makedirs(self.processed_dir, exist_ok=True) - # splitter = GroupShuffleSplit(train_size=split_size, n_splits=1) - - # train_split_index, temp_split_index = next( - # splitter.split(data, groups=groups) - # ) - - # split_groups = groups[temp_split_index] - - # splitter = GroupShuffleSplit( - # train_size=int(len(set(split_groups)) * self.train_split), n_splits=1 - # ) - # test_split_index, validation_split_index = next( - # splitter.split(temp_split_index, groups=split_groups) - # ) - # train_split = [data[i] for i in train_split_index] - # test_split = [ - # d - # for d in (data[temp_split_index[i]] for i in test_split_index) - # if d["original"] - # ] - # validation_split = [ - # d - # for d in (data[temp_split_index[i]] for i in validation_split_index) - # if d["original"] - # ] - # else: + print(self.train_split) + print(os.path.join(self.raw_dir, f"solCuration.csv")) + data = list(self._load_data_from_file(os.path.join(self.raw_dir, f"solCuration.csv"))) + print(len(data)) + # data = self._load_data_from_file(os.path.join(self.raw_dir, f"solCuration.csv")) if 0 == 0: train_split, test_split = train_test_split( data, train_size=self.train_split, shuffle=True @@ -130,13 +103,16 @@ def _load_data_from_file(self, input_file_path: str) -> List[Dict]: Returns: List[Dict]: List of data dictionaries. """ + print("!!!!!!!!!!!!!!!!") smiles_l = [] labels_l = [] with open(input_file_path, "r") as input_file: reader = csv.DictReader(input_file) for row in reader: - smiles_l.append(row["smiles"]) - labels_l.append(float(row["logS"])) + if not row["smiles"] in smiles_l: + smiles_l.append(row["smiles"]) + labels_l.append(float(row["logS"])) + # print(len(smiles_l), len(labels_l)) # labels_l.append(np.floor(float(row["logS"]))) # onehotencoding # label_binarizer = LabelBinarizer() @@ -144,14 +120,112 @@ def _load_data_from_file(self, input_file_path: str) -> List[Dict]: # onehot_label_l = label_binarizer.transform(labels_l) # normalise data to be between 0 and 1 - labels_norm = [(float(label)-min(labels_l))/(max(labels_l)-min(labels_l)) for label in labels_l] + # labels_norm = [(float(label)-min(labels_l))/(max(labels_l)-min(labels_l)) for label in labels_l] for i in range(0,len(smiles_l)): - yield dict(features=smiles_l[i], labels=[labels_l[i]], ident=i) + yield self.reader.to_data(dict(features=smiles_l[i], labels=[labels_l[i]], ident=i)) + +class SolESOL(XYBaseDataModule): + HEADERS = [ + "logS", + ] + + @property + def _name(self): + return "SolCuration" + + @property + def label_number(self): + return 1 + + @property + def raw_file_names(self): + return ["solCuration.csv"] + + @property + def processed_file_names(self): + return ["test.pt", "train.pt", "validation.pt"] + + def download(self): + # download and combine all the available curated datasets from xxx + db_sol = ['aqsol','aqua','esol','ochem','phys'] + with open(os.path.join(self.raw_dir, "solCuration.csv"), "ab") as dst: + for i, db in enumerate(db_sol): + with request.urlopen(f"https://raw.githubusercontent.com/Mengjintao/SolCuration/master/cure/{db}_cure.csv",) as src: + if i > 0: + src.readline() + shutil.copyfileobj(src, dst) + + + def setup_processed(self): + print("Create splits") + print(self.train_split) + print(os.path.join(self.raw_dir, f"solCuration.csv")) + data = list(self._load_data_from_file(os.path.join(self.raw_dir, f"solCuration.csv"))) + print(len(data)) + # data = self._load_data_from_file(os.path.join(self.raw_dir, f"solCuration.csv")) + if 0 == 0: + train_split, test_split = train_test_split( + data, train_size=self.train_split, shuffle=True + ) + test_split, validation_split = train_test_split( + test_split, train_size=0.5, shuffle=True + ) + for k, split in [ + ("test", test_split), + ("train", train_split), + ("validation", validation_split), + ]: + print("transform", k) + torch.save( + split, + os.path.join(self.processed_dir, f"{k}.pt"), + ) + + def setup(self, **kwargs): + if any( + not os.path.isfile(os.path.join(self.raw_dir, f)) + for f in self.raw_file_names + ): + self.download() + print([ + not os.path.isfile(os.path.join(self.processed_dir, f)) + for f in self.processed_file_names + ]) + if any( + not os.path.isfile(os.path.join(self.processed_dir, f)) + for f in self.processed_file_names + ): + self.setup_processed() + + def _load_data_from_file(self, input_file_path: str) -> List[Dict]: + """Loads data from a CSV file. + + Args: + input_file_path (str): Path to the CSV file. + + Returns: + List[Dict]: List of data dictionaries. + """ + smiles_l = [] + labels_l = [] + with open(input_file_path, "r") as input_file: + reader = csv.DictReader(input_file) + for row in reader: + if not row["smiles"] in smiles_l: + smiles_l.append(row["smiles"]) + labels_l.append(float(row["logS"])) + + for i in range(0,len(smiles_l)): + yield self.reader.to_data(dict(features=smiles_l[i], labels=[labels_l[i]], ident=i)) -class SolubilityCuratedData(SolCuration): - READER = dr.ChemDataReader class SolCurationChem(SolCuration): """Chemical data reader for the solubility dataset.""" + READER = dr.ChemDataReader + + +class SolESOLChem(SolESOL): + """Chemical data reader for the solubility dataset.""" + READER = dr.ChemDataReader \ No newline at end of file diff --git a/chebai/preprocessing/datasets/tox21.py b/chebai/preprocessing/datasets/tox21.py index 4bdfbdee..d9a789a7 100644 --- a/chebai/preprocessing/datasets/tox21.py +++ b/chebai/preprocessing/datasets/tox21.py @@ -145,7 +145,7 @@ def _load_data_from_file(self, input_file_path: str) -> List[Dict]: labels = [ bool(int(l)) if l else None for l in (row[k] for k in self.HEADERS) ] - yield dict(features=smiles, labels=labels, ident=row["mol_id"]) + yield self.reader.to_data(dict(features=smiles, labels=labels, ident=row["mol_id"])) class Tox21Challenge(XYBaseDataModule): diff --git a/configs/metrics/mse-rmse-r2.yml b/configs/metrics/mse-rmse-r2.yml new file mode 100644 index 00000000..ad7bb53f --- /dev/null +++ b/configs/metrics/mse-rmse-r2.yml @@ -0,0 +1,11 @@ +class_path: torchmetrics.MetricCollection +init_args: + metrics: + mse: + class_path: torchmetrics.regression.MeanSquaredError + rmse: + class_path: torchmetrics.regression.MeanSquaredError + init_args: + squared: True + r2: + class_path: torchmetrics.regression.R2Score \ No newline at end of file diff --git a/configs/model/electra.yml b/configs/model/electra.yml index 5241cfce..b66b1a53 100644 --- a/configs/model/electra.yml +++ b/configs/model/electra.yml @@ -1,9 +1,9 @@ class_path: chebai.models.Electra init_args: + model_type: regression optimizer_kwargs: - lr: 1e-3 + lr: 1e-4 config: - model_type: regression vocab_size: 1400 max_position_embeddings: 1800 num_attention_heads: 8 diff --git a/configs/training/solCur_callbacks.yml b/configs/training/solCur_callbacks.yml index eb221331..97cb4b2d 100644 --- a/configs/training/solCur_callbacks.yml +++ b/configs/training/solCur_callbacks.yml @@ -2,11 +2,11 @@ init_args: monitor: val_mse mode: 'min' - filename: 'best_{epoch:02d}_{val_loss:.4f}_{val_mse:.4f}' + filename: 'best_{epoch:02d}_{val_loss:.4f}_{val_mse:.4f}_{val_rmse:.4f}_{val_r2:.4f}' every_n_epochs: 1 save_top_k: 3 - class_path: chebai.callbacks.model_checkpoint.CustomModelCheckpoint init_args: - filename: 'per_{epoch:02d}_{val_loss:.4f}_{val_mse:.4f}' + filename: 'per_{epoch:02d}_{val_loss:.4f}_{val_mse:.4f}_{val_rmse:.4f}_{val_r2:.4f}' every_n_epochs: 25 save_top_k: -1 diff --git a/configs/training/wandb_logger.yml b/configs/training/wandb_logger.yml index 6a3c80bb..f883f387 100644 --- a/configs/training/wandb_logger.yml +++ b/configs/training/wandb_logger.yml @@ -1,6 +1,7 @@ class_path: chebai.loggers.custom.CustomLogger # Extension of Wandb logger init_args: - save_dir: logs + save_dir: /Users/ctumes/Cheb-AI/runs_no_onto/ + # version: no-onto project: 'cheb-ai-sol' entity: 'ch-tumescheit-university-of-zurich' log_model: 'all' From b537b7fd776e6afc535e05a111a0bc6a493ec8e9 Mon Sep 17 00:00:00 2001 From: MGlauer Date: Fri, 17 Jan 2025 13:57:14 +0100 Subject: [PATCH 21/54] Fix missing label handling --- chebai/models/electra.py | 11 ++++++++++- chebai/preprocessing/collate.py | 7 ++++++- chebai/preprocessing/datasets/tox21.py | 6 +++--- chebai/preprocessing/reader.py | 11 ++++++++--- 4 files changed, 27 insertions(+), 8 deletions(-) diff --git a/chebai/models/electra.py b/chebai/models/electra.py index 7009406d..91bb64e4 100644 --- a/chebai/models/electra.py +++ b/chebai/models/electra.py @@ -287,9 +287,13 @@ def _process_for_loss( tuple: A tuple containing the processed model output, labels, and loss arguments. """ kwargs_copy = dict(loss_kwargs) + output = model_output["logits"] if labels is not None: labels = labels.float() - return model_output["logits"], labels, kwargs_copy + if "missing_labels" in kwargs_copy: + missing_labels = kwargs_copy.pop("missing_labels") + output = output * (~missing_labels).int() + return output, labels, kwargs_copy def _get_prediction_and_labels( self, data: Dict[str, Any], labels: Tensor, model_output: Dict[str, Tensor] @@ -310,6 +314,11 @@ def _get_prediction_and_labels( if "non_null_labels" in loss_kwargs: n = loss_kwargs["non_null_labels"] d = d[n] + + if "missing_labels" in loss_kwargs: + missing_labels = loss_kwargs["missing_labels"] + labels = labels * (~missing_labels).int() + return torch.sigmoid(d), labels.int() if labels is not None else None def forward(self, data: Dict[str, Tensor], **kwargs: Any) -> Dict[str, Any]: diff --git a/chebai/preprocessing/collate.py b/chebai/preprocessing/collate.py index ecbcb876..921243f8 100644 --- a/chebai/preprocessing/collate.py +++ b/chebai/preprocessing/collate.py @@ -64,7 +64,7 @@ def __call__(self, data: List[Union[Dict, Tuple]]) -> XYData: Handles both fully and partially labeled data, where some samples may have `None` as their label. The indices of non-null labels are stored in the `non_null_labels` field, which is used to filter out predictions for unlabeled data during evaluation (e.g., F1, MSE). For models supporting partially labeled data, this method - ensures alignment between features and labels. + ensures alignment between features and labels. Missing labels are passed as a loss keyword. Args: data (List[Union[Dict, Tuple]]): List of ragged data samples. Each sample can be a dictionary or tuple @@ -81,10 +81,13 @@ def __call__(self, data: List[Union[Dict, Tuple]]) -> XYData: if isinstance(data[0], tuple): # For legacy data x, y, idents = zip(*data) + missing_labels = None else: x, y, idents = zip( *((d["features"], d["labels"], d.get("ident")) for d in data) ) + missing_labels = [d.get("missing_labels", [False for _ in y[0]]) for d in data] + if any(x is not None for x in y): # If any label is not None: (None, None, `1`, None) if any(x is None for x in y): @@ -97,11 +100,13 @@ def __call__(self, data: List[Union[Dict, Tuple]]) -> XYData: else: # If all labels are not None: (`0`, `2`, `1`, `3`) y = self.process_label_rows(y) + else: # If all labels are None : (`None`, `None`, `None`, `None`) y = None loss_kwargs["non_null_labels"] = [] + loss_kwargs["missing_labels"] = torch.tensor(missing_labels) # Calculate the lengths of each sequence, create a binary mask for valid (non-padded) positions lens = torch.tensor(list(map(len, x))) model_kwargs["mask"] = torch.arange(max(lens))[None, :] < lens[:, None] diff --git a/chebai/preprocessing/datasets/tox21.py b/chebai/preprocessing/datasets/tox21.py index 4bdfbdee..1b054f76 100644 --- a/chebai/preprocessing/datasets/tox21.py +++ b/chebai/preprocessing/datasets/tox21.py @@ -68,8 +68,8 @@ def download(self) -> None: def setup_processed(self) -> None: """Processes and splits the dataset.""" print("Create splits") - data = self._load_data_from_file(os.path.join(self.raw_dir, f"tox21.csv")) - groups = np.array([d["group"] for d in data]) + data = list(self._load_data_from_file(os.path.join(self.raw_dir, f"tox21.csv"))) + groups = np.array([d.get("group") for d in data]) if not all(g is None for g in groups): split_size = int(len(set(groups)) * self.train_split) os.makedirs(self.processed_dir, exist_ok=True) @@ -129,7 +129,7 @@ def setup(self, **kwargs) -> None: ): self.setup_processed() - def _load_data_from_file(self, input_file_path: str) -> List[Dict]: + def _load_dict(self, input_file_path: str) -> List[Dict]: """Loads data from a CSV file. Args: diff --git a/chebai/preprocessing/reader.py b/chebai/preprocessing/reader.py index e220e1e4..cfbbb9a2 100644 --- a/chebai/preprocessing/reader.py +++ b/chebai/preprocessing/reader.py @@ -92,13 +92,18 @@ def _read_group(self, raw: Any) -> Any: return raw def _read_components(self, row: Dict[str, Any]) -> Dict[str, Any]: - """Read and return components from the row.""" + """Read and return components from the row. If the data contains any missing labels (`None`), they are tracked + under the additional `missing_labels` keyword.""" + labels = self._get_raw_label(row) + additional_kwargs = self._get_additional_kwargs(row) + if any(l is None for l in labels): + additional_kwargs["missing_labels"] = [l is None for l in labels] return dict( features=self._get_raw_data(row), - labels=self._get_raw_label(row), + labels=labels, ident=self._get_raw_id(row), group=self._get_raw_group(row), - additional_kwargs=self._get_additional_kwargs(row), + additional_kwargs=additional_kwargs, ) def to_data(self, row: Dict[str, Any]) -> Dict[str, Any]: From a99e438090b92d585cef69dec900e1f3bbfde904 Mon Sep 17 00:00:00 2001 From: schnamo Date: Fri, 17 Jan 2025 14:08:04 +0100 Subject: [PATCH 22/54] add more datasets --- chebai/preprocessing/datasets/solCuration.py | 26 ++++++++------------ chebai/preprocessing/datasets/tox21.py | 2 +- configs/training/default_callbacks.yml | 4 +-- 3 files changed, 13 insertions(+), 19 deletions(-) diff --git a/chebai/preprocessing/datasets/solCuration.py b/chebai/preprocessing/datasets/solCuration.py index a9129e08..2ddf57df 100644 --- a/chebai/preprocessing/datasets/solCuration.py +++ b/chebai/preprocessing/datasets/solCuration.py @@ -131,7 +131,7 @@ class SolESOL(XYBaseDataModule): @property def _name(self): - return "SolCuration" + return "SolESOL" @property def label_number(self): @@ -139,28 +139,22 @@ def label_number(self): @property def raw_file_names(self): - return ["solCuration.csv"] + return ["solESOL.csv"] @property def processed_file_names(self): return ["test.pt", "train.pt", "validation.pt"] def download(self): - # download and combine all the available curated datasets from xxx - db_sol = ['aqsol','aqua','esol','ochem','phys'] - with open(os.path.join(self.raw_dir, "solCuration.csv"), "ab") as dst: - for i, db in enumerate(db_sol): - with request.urlopen(f"https://raw.githubusercontent.com/Mengjintao/SolCuration/master/cure/{db}_cure.csv",) as src: - if i > 0: - src.readline() - shutil.copyfileobj(src, dst) + # download + with open(os.path.join(self.raw_dir, "solESOL.csv"), "ab") as dst: + with request.urlopen(f"https://deepchemdata.s3-us-west-1.amazonaws.com/datasets/delaney-processed.csv",) as src: + shutil.copyfileobj(src, dst) def setup_processed(self): print("Create splits") - print(self.train_split) - print(os.path.join(self.raw_dir, f"solCuration.csv")) - data = list(self._load_data_from_file(os.path.join(self.raw_dir, f"solCuration.csv"))) + data = list(self._load_data_from_file(os.path.join(self.raw_dir, f"solESOL.csv"))) print(len(data)) # data = self._load_data_from_file(os.path.join(self.raw_dir, f"solCuration.csv")) if 0 == 0: @@ -210,10 +204,10 @@ def _load_data_from_file(self, input_file_path: str) -> List[Dict]: labels_l = [] with open(input_file_path, "r") as input_file: reader = csv.DictReader(input_file) + print(reader.fieldnames) for row in reader: - if not row["smiles"] in smiles_l: - smiles_l.append(row["smiles"]) - labels_l.append(float(row["logS"])) + smiles_l.append(row["smiles"]) + labels_l.append(float(row["measured log solubility in mols per litre"])) for i in range(0,len(smiles_l)): yield self.reader.to_data(dict(features=smiles_l[i], labels=[labels_l[i]], ident=i)) diff --git a/chebai/preprocessing/datasets/tox21.py b/chebai/preprocessing/datasets/tox21.py index d9a789a7..48d696f6 100644 --- a/chebai/preprocessing/datasets/tox21.py +++ b/chebai/preprocessing/datasets/tox21.py @@ -68,7 +68,7 @@ def download(self) -> None: def setup_processed(self) -> None: """Processes and splits the dataset.""" print("Create splits") - data = self._load_data_from_file(os.path.join(self.raw_dir, f"tox21.csv")) + data = list(self._load_data_from_file(os.path.join(self.raw_dir, f"tox21.csv"))) groups = np.array([d["group"] for d in data]) if not all(g is None for g in groups): split_size = int(len(set(groups)) * self.train_split) diff --git a/configs/training/default_callbacks.yml b/configs/training/default_callbacks.yml index ade7d149..29f23d53 100644 --- a/configs/training/default_callbacks.yml +++ b/configs/training/default_callbacks.yml @@ -2,11 +2,11 @@ init_args: monitor: val_micro-f1 mode: 'max' - filename: 'best_{epoch:02d}_{val_loss:.4f}_{val_macro-f1:.4f}_{val_micro-f1:.4f}' + filename: 'best_{epoch:02d}_{val_loss:.4f}_{val_macro-f1:.4f}_{val_micro-f1:.4f}_{roc-auc:.4f}' every_n_epochs: 1 save_top_k: 3 - class_path: chebai.callbacks.model_checkpoint.CustomModelCheckpoint init_args: - filename: 'per_{epoch:02d}_{val_loss:.4f}_{val_macro-f1:.4f}_{val_micro-f1:.4f}' + filename: 'per_{epoch:02d}_{val_loss:.4f}_{val_macro-f1:.4f}_{val_micro-f1:.4f}_{roc-auc:.4f}' every_n_epochs: 25 save_top_k: -1 From 9b084cb0ba212c3e560a20ed4eee97243d51e4a0 Mon Sep 17 00:00:00 2001 From: schnamo Date: Fri, 17 Jan 2025 15:24:23 +0100 Subject: [PATCH 23/54] merge branches part 2 --- chebai/preprocessing/datasets/tox21.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/chebai/preprocessing/datasets/tox21.py b/chebai/preprocessing/datasets/tox21.py index 22d8f9fb..b1f9606f 100644 --- a/chebai/preprocessing/datasets/tox21.py +++ b/chebai/preprocessing/datasets/tox21.py @@ -69,11 +69,8 @@ def setup_processed(self) -> None: """Processes and splits the dataset.""" print("Create splits") data = list(self._load_data_from_file(os.path.join(self.raw_dir, f"tox21.csv"))) -<<<<<<< HEAD - groups = np.array([d["group"] for d in data]) -======= groups = np.array([d.get("group") for d in data]) ->>>>>>> b537b7fd776e6afc535e05a111a0bc6a493ec8e9 + if not all(g is None for g in groups): split_size = int(len(set(groups)) * self.train_split) os.makedirs(self.processed_dir, exist_ok=True) From 326e9a26790f102e0b9bf8753c311aa71c9b5921 Mon Sep 17 00:00:00 2001 From: schnamo Date: Sat, 18 Jan 2025 10:25:27 +0100 Subject: [PATCH 24/54] add more datasets --- .../datasets/molecule_classification.py | 151 +++++++++++++ .../datasets/molecule_regression.py | 207 ++++++++++++++++++ chebai/preprocessing/datasets/tox21.py | 16 +- chebai/result/regression.py | 7 +- configs/data/clintox_moleculenet.yml | 3 + configs/data/freesolv_moleculenet.yml | 3 + configs/data/lipo_moleculenet.yml | 3 + configs/data/solubilityESOL.yml | 3 + configs/model/electra_tox.yml | 13 ++ .../training/wandb_logger_no_onto_clintox.yml | 6 + .../training/wandb_logger_no_onto_esol.yml | 6 + .../wandb_logger_no_onto_freesolv.yml | 6 + .../training/wandb_logger_no_onto_lipo.yml | 6 + configs/training/wandb_logger_no_onto_tox.yml | 6 + configs/training/wandb_logger_onto.yml | 6 + .../training/wandb_logger_onto_clintox.yml | 6 + configs/training/wandb_logger_onto_esol.yml | 6 + .../training/wandb_logger_onto_freesolv.yml | 6 + configs/training/wandb_logger_onto_lipo.yml | 6 + configs/training/wandb_logger_onto_tox.yml | 6 + 20 files changed, 463 insertions(+), 9 deletions(-) create mode 100644 chebai/preprocessing/datasets/molecule_classification.py create mode 100644 chebai/preprocessing/datasets/molecule_regression.py create mode 100644 configs/data/clintox_moleculenet.yml create mode 100644 configs/data/freesolv_moleculenet.yml create mode 100644 configs/data/lipo_moleculenet.yml create mode 100644 configs/data/solubilityESOL.yml create mode 100644 configs/model/electra_tox.yml create mode 100644 configs/training/wandb_logger_no_onto_clintox.yml create mode 100644 configs/training/wandb_logger_no_onto_esol.yml create mode 100644 configs/training/wandb_logger_no_onto_freesolv.yml create mode 100644 configs/training/wandb_logger_no_onto_lipo.yml create mode 100644 configs/training/wandb_logger_no_onto_tox.yml create mode 100644 configs/training/wandb_logger_onto.yml create mode 100644 configs/training/wandb_logger_onto_clintox.yml create mode 100644 configs/training/wandb_logger_onto_esol.yml create mode 100644 configs/training/wandb_logger_onto_freesolv.yml create mode 100644 configs/training/wandb_logger_onto_lipo.yml create mode 100644 configs/training/wandb_logger_onto_tox.yml diff --git a/chebai/preprocessing/datasets/molecule_classification.py b/chebai/preprocessing/datasets/molecule_classification.py new file mode 100644 index 00000000..1e329887 --- /dev/null +++ b/chebai/preprocessing/datasets/molecule_classification.py @@ -0,0 +1,151 @@ +from tempfile import NamedTemporaryFile, TemporaryDirectory +from urllib import request +import csv +import gzip +import os +import random +import shutil +import zipfile +from typing import Dict, Generator, List, Optional + +from rdkit import Chem +from sklearn.model_selection import GroupShuffleSplit, train_test_split, StratifiedShuffleSplit +import numpy as np +import pysmiles +import torch +from sklearn.preprocessing import LabelBinarizer + +from chebai.preprocessing import reader as dr +from chebai.preprocessing.datasets.base import MergedDataset, XYBaseDataModule +from chebai.preprocessing.datasets.chebi import JCIExtendedTokenData +from chebai.preprocessing.datasets.pubchem import Hazardous + +class ClinTox(XYBaseDataModule): + """Data module for ClinTox MoleculeNet dataset.""" + + HEADERS = [ + "FDA_APPROVED", + "CT_TOX", + ] + + @property + def _name(self) -> str: + """Returns the name of the dataset.""" + return "ClinTox" + + @property + def label_number(self) -> int: + """Returns the number of labels.""" + return 2 + + @property + def raw_file_names(self) -> List[str]: + """Returns a list of raw file names.""" + return ["clintox.csv"] + + @property + def processed_file_names(self) -> List[str]: + """Returns a list of processed file names.""" + return ["test.pt", "train.pt", "validation.pt"] + + def download(self) -> None: + """Downloads and extracts the dataset.""" + with NamedTemporaryFile("rb") as gout: + request.urlretrieve( + "https://deepchemdata.s3-us-west-1.amazonaws.com/datasets/clintox.csv.gz", + gout.name, + ) + with gzip.open(gout.name) as gfile: + with open(os.path.join(self.raw_dir, "clintox.csv"), "wt") as fout: + fout.write(gfile.read().decode()) + + def setup_processed(self) -> None: + """Processes and splits the dataset.""" + print("Create splits") + data = list(self._load_data_from_file(os.path.join(self.raw_dir, f"clintox.csv"))) + groups = np.array([d["group"] for d in data]) + if not all(g is None for g in groups): + split_size = int(len(set(groups)) * self.train_split) + os.makedirs(self.processed_dir, exist_ok=True) + splitter = GroupShuffleSplit(train_size=split_size, n_splits=1) + + train_split_index, temp_split_index = next( + splitter.split(data, groups=groups) + ) + + split_groups = groups[temp_split_index] + + splitter = GroupShuffleSplit( + train_size=int(len(set(split_groups)) * self.train_split), n_splits=1 + ) + test_split_index, validation_split_index = next( + splitter.split(temp_split_index, groups=split_groups) + ) + train_split = [data[i] for i in train_split_index] + test_split = [ + d + for d in (data[temp_split_index[i]] for i in test_split_index) + if d["original"] + ] + validation_split = [ + d + for d in (data[temp_split_index[i]] for i in validation_split_index) + if d["original"] + ] + else: + + train_split, test_split = train_test_split( + data, train_size=self.train_split, shuffle=True + ) + test_split, validation_split = train_test_split( + test_split, train_size=0.5, shuffle=True + ) + for k, split in [ + ("test", test_split), + ("train", train_split), + ("validation", validation_split), + ]: + print("transform", k) + torch.save( + split, + os.path.join(self.processed_dir, f"{k}.pt"), + ) + + def setup(self, **kwargs) -> None: + """Sets up the dataset by downloading and processing if necessary.""" + if any( + not os.path.isfile(os.path.join(self.raw_dir, f)) + for f in self.raw_file_names + ): + self.download() + if any( + not os.path.isfile(os.path.join(self.processed_dir, f)) + for f in self.processed_file_names + ): + self.setup_processed() + + def _load_data_from_file(self, input_file_path: str) -> List[Dict]: + """Loads data from a CSV file. + + Args: + input_file_path (str): Path to the CSV file. + + Returns: + List[Dict]: List of data dictionaries. + """ + i = 0 + with open(input_file_path, "r") as input_file: + reader = csv.DictReader(input_file) + for row in reader: + i += 1 + smiles = row["smiles"] + labels = [ + bool(int(l)) if l else None for l in (row[k] for k in self.HEADERS) + ] + yield self.reader.to_data(dict(features=smiles, labels=labels, ident=i)) + + +class ClinToxChem(ClinTox): + """Chemical data reader for Tox21MolNet dataset.""" + + READER = dr.ChemDataReader \ No newline at end of file diff --git a/chebai/preprocessing/datasets/molecule_regression.py b/chebai/preprocessing/datasets/molecule_regression.py new file mode 100644 index 00000000..6dbfa7d1 --- /dev/null +++ b/chebai/preprocessing/datasets/molecule_regression.py @@ -0,0 +1,207 @@ +from tempfile import NamedTemporaryFile, TemporaryDirectory +from urllib import request +import csv +import gzip +import os +import random +import shutil +import zipfile +from typing import Dict, Generator, List, Optional + +from rdkit import Chem +from sklearn.model_selection import GroupShuffleSplit, train_test_split +import numpy as np +import pysmiles +import torch +from sklearn.preprocessing import LabelBinarizer + +from chebai.preprocessing import reader as dr +from chebai.preprocessing.datasets.base import MergedDataset, XYBaseDataModule +from chebai.preprocessing.datasets.chebi import JCIExtendedTokenData +from chebai.preprocessing.datasets.pubchem import Hazardous + +class Lipo(XYBaseDataModule): + HEADERS = [ + "exp", + ] + + @property + def _name(self): + return "Lipo" + + @property + def label_number(self): + return 1 + + @property + def raw_file_names(self): + return ["Lipo.csv"] + + @property + def processed_file_names(self): + return ["test.pt", "train.pt", "validation.pt"] + + def download(self): + # download + with open(os.path.join(self.raw_dir, "Lipo.csv"), "ab") as dst: + with request.urlopen(f"https://deepchemdata.s3-us-west-1.amazonaws.com/datasets/Lipophilicity.csv",) as src: + shutil.copyfileobj(src, dst) + + + def setup_processed(self): + print("Create splits") + data = list(self._load_data_from_file(os.path.join(self.raw_dir, f"Lipo.csv"))) + print(len(data)) + if 0 == 0: + train_split, test_split = train_test_split( + data, train_size=self.train_split, shuffle=True + ) + test_split, validation_split = train_test_split( + test_split, train_size=0.5, shuffle=True + ) + for k, split in [ + ("test", test_split), + ("train", train_split), + ("validation", validation_split), + ]: + print("transform", k) + torch.save( + split, + os.path.join(self.processed_dir, f"{k}.pt"), + ) + + def setup(self, **kwargs): + if any( + not os.path.isfile(os.path.join(self.raw_dir, f)) + for f in self.raw_file_names + ): + self.download() + print([ + not os.path.isfile(os.path.join(self.processed_dir, f)) + for f in self.processed_file_names + ]) + if any( + not os.path.isfile(os.path.join(self.processed_dir, f)) + for f in self.processed_file_names + ): + self.setup_processed() + + def _load_data_from_file(self, input_file_path: str) -> List[Dict]: + """Loads data from a CSV file. + + Args: + input_file_path (str): Path to the CSV file. + + Returns: + List[Dict]: List of data dictionaries. + """ + smiles_l = [] + labels_l = [] + with open(input_file_path, "r") as input_file: + reader = csv.DictReader(input_file) + print(reader.fieldnames) + for row in reader: + smiles_l.append(row["smiles"]) + labels_l.append(float(row["exp"])) + + for i in range(0,len(smiles_l)): + yield self.reader.to_data(dict(features=smiles_l[i], labels=[labels_l[i]], ident=i)) + + +class FreeSolv(XYBaseDataModule): + HEADERS = [ + "expt", + ] + + @property + def _name(self): + return "FreeSolv" + + @property + def label_number(self): + return 1 + + @property + def raw_file_names(self): + return ["FreeSolv.csv"] + + @property + def processed_file_names(self): + return ["test.pt", "train.pt", "validation.pt"] + + def download(self): + # download + with open(os.path.join(self.raw_dir, "FreeSolv.csv"), "ab") as dst: + with request.urlopen(f"https://deepchemdata.s3-us-west-1.amazonaws.com/datasets/SAMPL.csv",) as src: + shutil.copyfileobj(src, dst) + + + def setup_processed(self): + print("Create splits") + data = list(self._load_data_from_file(os.path.join(self.raw_dir, f"FreeSolv.csv"))) + print(len(data)) + if 0 == 0: + train_split, test_split = train_test_split( + data, train_size=self.train_split, shuffle=True + ) + test_split, validation_split = train_test_split( + test_split, train_size=0.5, shuffle=True + ) + for k, split in [ + ("test", test_split), + ("train", train_split), + ("validation", validation_split), + ]: + print("transform", k) + torch.save( + split, + os.path.join(self.processed_dir, f"{k}.pt"), + ) + + def setup(self, **kwargs): + if any( + not os.path.isfile(os.path.join(self.raw_dir, f)) + for f in self.raw_file_names + ): + self.download() + print([ + not os.path.isfile(os.path.join(self.processed_dir, f)) + for f in self.processed_file_names + ]) + if any( + not os.path.isfile(os.path.join(self.processed_dir, f)) + for f in self.processed_file_names + ): + self.setup_processed() + + def _load_data_from_file(self, input_file_path: str) -> List[Dict]: + """Loads data from a CSV file. + + Args: + input_file_path (str): Path to the CSV file. + + Returns: + List[Dict]: List of data dictionaries. + """ + smiles_l = [] + labels_l = [] + with open(input_file_path, "r") as input_file: + reader = csv.DictReader(input_file) + print(reader.fieldnames) + for row in reader: + smiles_l.append(row["smiles"]) + labels_l.append(float(row["expt"])) + + for i in range(0,len(smiles_l)): + yield self.reader.to_data(dict(features=smiles_l[i], labels=[labels_l[i]], ident=i)) + + +class LipoChem(Lipo): + """Chemical data reader for the solubility dataset.""" + + READER = dr.ChemDataReader + +class FreeSolvChem(FreeSolv): + """Chemical data reader for the solubility dataset.""" + + READER = dr.ChemDataReader \ No newline at end of file diff --git a/chebai/preprocessing/datasets/tox21.py b/chebai/preprocessing/datasets/tox21.py index b1f9606f..12ad2060 100644 --- a/chebai/preprocessing/datasets/tox21.py +++ b/chebai/preprocessing/datasets/tox21.py @@ -10,7 +10,7 @@ import numpy as np import torch from rdkit import Chem -from sklearn.model_selection import GroupShuffleSplit, train_test_split +from sklearn.model_selection import GroupShuffleSplit, train_test_split, StratifiedShuffleSplit from chebai.preprocessing import reader as dr from chebai.preprocessing.datasets.base import XYBaseDataModule @@ -100,10 +100,13 @@ def setup_processed(self) -> None: if d["original"] ] else: - train_split, test_split = train_test_split( - data, train_size=self.train_split, shuffle=True - ) - test_split, validation_split = train_test_split( + print(self.train_split) + sss = StratifiedShuffleSplit(n_splits=5, test_size=1-self.train_split, random_state=0) + train_split, test_split = sss.get_n_splits(data) + # train_split, test_split = StratifiedShuffleSplit( + # data, train_size=self.train_split, shuffle=True + # ) + test_split, validation_split = StratifiedShuffleSplit( test_split, train_size=0.5, shuffle=True ) for k, split in [ @@ -146,7 +149,8 @@ def _load_dict(self, input_file_path: str) -> List[Dict]: labels = [ bool(int(l)) if l else None for l in (row[k] for k in self.HEADERS) ] - yield self.reader.to_data(dict(features=smiles, labels=labels, ident=row["mol_id"])) + yield dict(features=smiles, labels=labels, ident=row["mol_id"]) + # yield self.reader.to_data(dict(features=smiles, labels=labels, ident=row["mol_id"])) class Tox21Challenge(XYBaseDataModule): diff --git a/chebai/result/regression.py b/chebai/result/regression.py index e70c0ddc..a3823822 100644 --- a/chebai/result/regression.py +++ b/chebai/result/regression.py @@ -55,10 +55,11 @@ def print_metrics( mse = MeanSquaredError() mse = mse.to(labels.device) - # my_f1_macro = MacroF1(preds.shape[1]).to(device=device) - # my_bal_acc = BalancedAccuracy(preds.shape[1]).to(device=device) + rmse = MeanSquaredError(squared = False) + rmse = rmse.to(labels.device) + + return(mse(preds, labels), rmse(preds, labels)) - print(f"MSE: {mse(preds, labels)}") # print(f"Micro-F1: {f1_micro(preds, labels):3f}") # print(f"Balanced Accuracy: {my_bal_acc(preds, labels):3f}") diff --git a/configs/data/clintox_moleculenet.yml b/configs/data/clintox_moleculenet.yml new file mode 100644 index 00000000..4422bfe6 --- /dev/null +++ b/configs/data/clintox_moleculenet.yml @@ -0,0 +1,3 @@ +class_path: chebai.preprocessing.datasets.molecule_classification.ClinToxChem +init_args: + batch_size: 10 diff --git a/configs/data/freesolv_moleculenet.yml b/configs/data/freesolv_moleculenet.yml new file mode 100644 index 00000000..d7d0a708 --- /dev/null +++ b/configs/data/freesolv_moleculenet.yml @@ -0,0 +1,3 @@ +class_path: chebai.preprocessing.datasets.molecule_regression.FreeSolvChem +init_args: + batch_size: 32 diff --git a/configs/data/lipo_moleculenet.yml b/configs/data/lipo_moleculenet.yml new file mode 100644 index 00000000..b2ed0ad2 --- /dev/null +++ b/configs/data/lipo_moleculenet.yml @@ -0,0 +1,3 @@ +class_path: chebai.preprocessing.datasets.molecule_regression.LipoChem +init_args: + batch_size: 32 diff --git a/configs/data/solubilityESOL.yml b/configs/data/solubilityESOL.yml new file mode 100644 index 00000000..9a1834ac --- /dev/null +++ b/configs/data/solubilityESOL.yml @@ -0,0 +1,3 @@ +class_path: chebai.preprocessing.datasets.solCuration.SolESOLChem +init_args: + batch_size: 32 diff --git a/configs/model/electra_tox.yml b/configs/model/electra_tox.yml new file mode 100644 index 00000000..fd3d1af9 --- /dev/null +++ b/configs/model/electra_tox.yml @@ -0,0 +1,13 @@ +class_path: chebai.models.Electra +init_args: + model_type: classification + optimizer_kwargs: + lr: 1e-4 + config: + vocab_size: 1400 + max_position_embeddings: 1800 + num_attention_heads: 8 + num_hidden_layers: 6 + type_vocab_size: 1 + hidden_size: 256 + out_dim: 1 diff --git a/configs/training/wandb_logger_no_onto_clintox.yml b/configs/training/wandb_logger_no_onto_clintox.yml new file mode 100644 index 00000000..1728530b --- /dev/null +++ b/configs/training/wandb_logger_no_onto_clintox.yml @@ -0,0 +1,6 @@ +class_path: chebai.loggers.custom.CustomLogger # Extension of Wandb logger +init_args: + save_dir: /Users/ctumes/Cheb-AI/clintox/runs_no_onto/ + project: 'chebai-clintox' + entity: 'ch-tumescheit-university-of-zurich' + log_model: 'all' diff --git a/configs/training/wandb_logger_no_onto_esol.yml b/configs/training/wandb_logger_no_onto_esol.yml new file mode 100644 index 00000000..085bf40d --- /dev/null +++ b/configs/training/wandb_logger_no_onto_esol.yml @@ -0,0 +1,6 @@ +class_path: chebai.loggers.custom.CustomLogger # Extension of Wandb logger +init_args: + save_dir: /Users/ctumes/Cheb-AI/esol/runs_no_onto/ + project: 'chebai-esol' + entity: 'ch-tumescheit-university-of-zurich' + log_model: 'all' diff --git a/configs/training/wandb_logger_no_onto_freesolv.yml b/configs/training/wandb_logger_no_onto_freesolv.yml new file mode 100644 index 00000000..ed965a88 --- /dev/null +++ b/configs/training/wandb_logger_no_onto_freesolv.yml @@ -0,0 +1,6 @@ +class_path: chebai.loggers.custom.CustomLogger # Extension of Wandb logger +init_args: + save_dir: /Users/ctumes/Cheb-AI/freesolv/runs_no_onto/ + project: 'chebai-freesolv' + entity: 'ch-tumescheit-university-of-zurich' + log_model: 'all' diff --git a/configs/training/wandb_logger_no_onto_lipo.yml b/configs/training/wandb_logger_no_onto_lipo.yml new file mode 100644 index 00000000..0d8b551b --- /dev/null +++ b/configs/training/wandb_logger_no_onto_lipo.yml @@ -0,0 +1,6 @@ +class_path: chebai.loggers.custom.CustomLogger # Extension of Wandb logger +init_args: + save_dir: /Users/ctumes/Cheb-AI/lipo/runs_no_onto/ + project: 'chebai-lipo' + entity: 'ch-tumescheit-university-of-zurich' + log_model: 'all' diff --git a/configs/training/wandb_logger_no_onto_tox.yml b/configs/training/wandb_logger_no_onto_tox.yml new file mode 100644 index 00000000..2bc575dc --- /dev/null +++ b/configs/training/wandb_logger_no_onto_tox.yml @@ -0,0 +1,6 @@ +class_path: chebai.loggers.custom.CustomLogger # Extension of Wandb logger +init_args: + save_dir: /Users/ctumes/Cheb-AI/tox/runs_no_onto/ + project: 'chebai-tox' + entity: 'ch-tumescheit-university-of-zurich' + log_model: 'all' diff --git a/configs/training/wandb_logger_onto.yml b/configs/training/wandb_logger_onto.yml new file mode 100644 index 00000000..fdd4cafb --- /dev/null +++ b/configs/training/wandb_logger_onto.yml @@ -0,0 +1,6 @@ +class_path: chebai.loggers.custom.CustomLogger # Extension of Wandb logger +init_args: + save_dir: /Users/ctumes/Cheb-AI/runs_onto/ + project: 'cheb-ai-sol' + entity: 'ch-tumescheit-university-of-zurich' + log_model: 'all' diff --git a/configs/training/wandb_logger_onto_clintox.yml b/configs/training/wandb_logger_onto_clintox.yml new file mode 100644 index 00000000..f5e51eeb --- /dev/null +++ b/configs/training/wandb_logger_onto_clintox.yml @@ -0,0 +1,6 @@ +class_path: chebai.loggers.custom.CustomLogger # Extension of Wandb logger +init_args: + save_dir: /Users/ctumes/Cheb-AI/clintox/runs_onto/ + project: 'chebai-clintox' + entity: 'ch-tumescheit-university-of-zurich' + log_model: 'all' diff --git a/configs/training/wandb_logger_onto_esol.yml b/configs/training/wandb_logger_onto_esol.yml new file mode 100644 index 00000000..73b6c5be --- /dev/null +++ b/configs/training/wandb_logger_onto_esol.yml @@ -0,0 +1,6 @@ +class_path: chebai.loggers.custom.CustomLogger # Extension of Wandb logger +init_args: + save_dir: /Users/ctumes/Cheb-AI/esol/runs_onto/ + project: 'chebai-esol' + entity: 'ch-tumescheit-university-of-zurich' + log_model: 'all' diff --git a/configs/training/wandb_logger_onto_freesolv.yml b/configs/training/wandb_logger_onto_freesolv.yml new file mode 100644 index 00000000..0907ae6b --- /dev/null +++ b/configs/training/wandb_logger_onto_freesolv.yml @@ -0,0 +1,6 @@ +class_path: chebai.loggers.custom.CustomLogger # Extension of Wandb logger +init_args: + save_dir: /Users/ctumes/Cheb-AI/freesolv/runs_onto/ + project: 'chebai-freesolv' + entity: 'ch-tumescheit-university-of-zurich' + log_model: 'all' diff --git a/configs/training/wandb_logger_onto_lipo.yml b/configs/training/wandb_logger_onto_lipo.yml new file mode 100644 index 00000000..7edb4f0f --- /dev/null +++ b/configs/training/wandb_logger_onto_lipo.yml @@ -0,0 +1,6 @@ +class_path: chebai.loggers.custom.CustomLogger # Extension of Wandb logger +init_args: + save_dir: /Users/ctumes/Cheb-AI/lipo/runs_onto/ + project: 'chebai-lipo' + entity: 'ch-tumescheit-university-of-zurich' + log_model: 'all' diff --git a/configs/training/wandb_logger_onto_tox.yml b/configs/training/wandb_logger_onto_tox.yml new file mode 100644 index 00000000..8853af76 --- /dev/null +++ b/configs/training/wandb_logger_onto_tox.yml @@ -0,0 +1,6 @@ +class_path: chebai.loggers.custom.CustomLogger # Extension of Wandb logger +init_args: + save_dir: /Users/ctumes/Cheb-AI/tox/runs_onto/ + project: 'chebai-tox' + entity: 'ch-tumescheit-university-of-zurich' + log_model: 'all' From c272f453eff94e9925ebd7d20e4efcd7dc33e591 Mon Sep 17 00:00:00 2001 From: schnamo Date: Sat, 18 Jan 2025 17:04:49 +0100 Subject: [PATCH 25/54] adjust metrics for classifications, add BBBP --- chebai/preprocessing/collate.py | 2 +- .../datasets/molecule_classification.py | 117 +++++++++++++++++- .../datasets/molecule_regression.py | 6 +- chebai/preprocessing/datasets/solCuration.py | 3 +- chebai/preprocessing/datasets/tox21.py | 14 +-- chebai/result/classification.py | 44 +++++++ chebai/result/regression.py | 2 +- configs/data/bbbp_moleculenet.yml | 3 + configs/data/clintox_moleculenet.yml | 2 +- configs/data/tox21_moleculenet.yml | 2 +- configs/metrics/micro-macro-f1-roc-auc-2.yml | 13 ++ .../metrics/micro-macro-f1-roc-auc-binary.yml | 7 ++ configs/metrics/micro-macro-f1-roc-auc.yml | 13 ++ configs/training/binary_callbacks.yml | 12 ++ configs/training/binary_trainer.yml | 5 + .../training/wandb_logger_no_onto_bbbp.yml | 6 + configs/training/wandb_logger_onto_bbbp.yml | 6 + 17 files changed, 240 insertions(+), 17 deletions(-) create mode 100644 configs/data/bbbp_moleculenet.yml create mode 100644 configs/metrics/micro-macro-f1-roc-auc-2.yml create mode 100644 configs/metrics/micro-macro-f1-roc-auc-binary.yml create mode 100644 configs/metrics/micro-macro-f1-roc-auc.yml create mode 100644 configs/training/binary_callbacks.yml create mode 100644 configs/training/binary_trainer.yml create mode 100644 configs/training/wandb_logger_no_onto_bbbp.yml create mode 100644 configs/training/wandb_logger_onto_bbbp.yml diff --git a/chebai/preprocessing/collate.py b/chebai/preprocessing/collate.py index 0a93ab8e..921243f8 100644 --- a/chebai/preprocessing/collate.py +++ b/chebai/preprocessing/collate.py @@ -111,7 +111,7 @@ def __call__(self, data: List[Union[Dict, Tuple]]) -> XYData: lens = torch.tensor(list(map(len, x))) model_kwargs["mask"] = torch.arange(max(lens))[None, :] < lens[:, None] model_kwargs["lens"] = lens - + return XYData( pad_sequence([torch.tensor(a) for a in x], batch_first=True), y, diff --git a/chebai/preprocessing/datasets/molecule_classification.py b/chebai/preprocessing/datasets/molecule_classification.py index 1e329887..9d5386f4 100644 --- a/chebai/preprocessing/datasets/molecule_classification.py +++ b/chebai/preprocessing/datasets/molecule_classification.py @@ -93,7 +93,21 @@ def setup_processed(self) -> None: if d["original"] ] else: - + print(self.train_split) + print(type(data)) + print((data[0])) + print(type(data[0])) + X = [] + y = [] + for item in data: + X.append(item['ident']) + y.append(item['labels']) + sss = StratifiedShuffleSplit(n_splits=10, test_size=1-self.train_split, random_state=0) + sss.get_n_splits(np.array(X), np.array(y)) + print(sss) + train, test = sss.split(X, y) + print(train) + exit() train_split, test_split = train_test_split( data, train_size=self.train_split, shuffle=True ) @@ -124,7 +138,7 @@ def setup(self, **kwargs) -> None: ): self.setup_processed() - def _load_data_from_file(self, input_file_path: str) -> List[Dict]: + def _load_dict(self, input_file_path: str) -> List[Dict]: """Loads data from a CSV file. Args: @@ -142,7 +156,104 @@ def _load_data_from_file(self, input_file_path: str) -> List[Dict]: labels = [ bool(int(l)) if l else None for l in (row[k] for k in self.HEADERS) ] - yield self.reader.to_data(dict(features=smiles, labels=labels, ident=i)) + yield dict(features=smiles, labels=labels, ident=i) + # yield self.reader.to_data(dict(features=smiles, labels=labels, ident=i)) + + +class BBBP(XYBaseDataModule): + """Data module for ClinTox MoleculeNet dataset.""" + + HEADERS = [ + "p_np", + ] + + @property + def _name(self) -> str: + """Returns the name of the dataset.""" + return "BBBP" + + @property + def label_number(self) -> int: + """Returns the number of labels.""" + return 1 + + @property + def raw_file_names(self) -> List[str]: + """Returns a list of raw file names.""" + return ["bbbp.csv"] + + @property + def processed_file_names(self) -> List[str]: + """Returns a list of processed file names.""" + return ["test.pt", "train.pt", "validation.pt"] + + def download(self) -> None: + + """Downloads and extracts the dataset.""" + with open(os.path.join(self.raw_dir, "bbbp.csv"), "ab") as dst: + with request.urlopen(f"https://deepchemdata.s3-us-west-1.amazonaws.com/datasets/BBBP.csv",) as src: + shutil.copyfileobj(src, dst) + + + def setup_processed(self) -> None: + """Processes and splits the dataset.""" + print("Create splits") + data = list(self._load_data_from_file(os.path.join(self.raw_dir, f"bbbp.csv"))) + + train_split, test_split = train_test_split( + data, train_size=self.train_split, shuffle=True + ) + test_split, validation_split = train_test_split( + test_split, train_size=0.5, shuffle=True + ) + for k, split in [ + ("test", test_split), + ("train", train_split), + ("validation", validation_split), + ]: + print("transform", k) + torch.save( + split, + os.path.join(self.processed_dir, f"{k}.pt"), + ) + + def setup(self, **kwargs) -> None: + """Sets up the dataset by downloading and processing if necessary.""" + if any( + not os.path.isfile(os.path.join(self.raw_dir, f)) + for f in self.raw_file_names + ): + self.download() + if any( + not os.path.isfile(os.path.join(self.processed_dir, f)) + for f in self.processed_file_names + ): + self.setup_processed() + + def _load_dict(self, input_file_path: str) -> List[Dict]: + """Loads data from a CSV file. + + Args: + input_file_path (str): Path to the CSV file. + + Returns: + List[Dict]: List of data dictionaries. + """ + i = 0 + with open(input_file_path, "r") as input_file: + reader = csv.DictReader(input_file) + for row in reader: + i += 1 + smiles = row["smiles"] + labels = [int(row["p_np"])] + yield dict(features=smiles, labels=labels, ident=i) + # yield self.reader.to_data(dict(features=smiles, labels=labels, ident=i)) + + +class BBBPChem(BBBP): + """Chemical data reader for Tox21MolNet dataset.""" + + READER = dr.ChemDataReader class ClinToxChem(ClinTox): diff --git a/chebai/preprocessing/datasets/molecule_regression.py b/chebai/preprocessing/datasets/molecule_regression.py index 6dbfa7d1..7a1fb876 100644 --- a/chebai/preprocessing/datasets/molecule_regression.py +++ b/chebai/preprocessing/datasets/molecule_regression.py @@ -105,7 +105,8 @@ def _load_data_from_file(self, input_file_path: str) -> List[Dict]: labels_l.append(float(row["exp"])) for i in range(0,len(smiles_l)): - yield self.reader.to_data(dict(features=smiles_l[i], labels=[labels_l[i]], ident=i)) + yield dict(features=smiles_l[i], labels=[labels_l[i]], ident=i) + # yield self.reader.to_data(dict(features=smiles_l[i], labels=[labels_l[i]], ident=i)) class FreeSolv(XYBaseDataModule): @@ -193,7 +194,8 @@ def _load_data_from_file(self, input_file_path: str) -> List[Dict]: labels_l.append(float(row["expt"])) for i in range(0,len(smiles_l)): - yield self.reader.to_data(dict(features=smiles_l[i], labels=[labels_l[i]], ident=i)) + yield dict(features=smiles_l[i], labels=[labels_l[i]], ident=i) + # yield self.reader.to_data(dict(features=smiles_l[i], labels=[labels_l[i]], ident=i)) class LipoChem(Lipo): diff --git a/chebai/preprocessing/datasets/solCuration.py b/chebai/preprocessing/datasets/solCuration.py index 2ddf57df..9cec482c 100644 --- a/chebai/preprocessing/datasets/solCuration.py +++ b/chebai/preprocessing/datasets/solCuration.py @@ -210,7 +210,8 @@ def _load_data_from_file(self, input_file_path: str) -> List[Dict]: labels_l.append(float(row["measured log solubility in mols per litre"])) for i in range(0,len(smiles_l)): - yield self.reader.to_data(dict(features=smiles_l[i], labels=[labels_l[i]], ident=i)) + yield dict(features=smiles_l[i], labels=[labels_l[i]], ident=i) + # yield self.reader.to_data(dict(features=smiles_l[i], labels=[labels_l[i]], ident=i)) class SolCurationChem(SolCuration): diff --git a/chebai/preprocessing/datasets/tox21.py b/chebai/preprocessing/datasets/tox21.py index 12ad2060..3706febc 100644 --- a/chebai/preprocessing/datasets/tox21.py +++ b/chebai/preprocessing/datasets/tox21.py @@ -100,13 +100,13 @@ def setup_processed(self) -> None: if d["original"] ] else: - print(self.train_split) - sss = StratifiedShuffleSplit(n_splits=5, test_size=1-self.train_split, random_state=0) - train_split, test_split = sss.get_n_splits(data) - # train_split, test_split = StratifiedShuffleSplit( - # data, train_size=self.train_split, shuffle=True - # ) - test_split, validation_split = StratifiedShuffleSplit( + # print(self.train_split) + # sss = StratifiedShuffleSplit(n_splits=5, test_size=1-self.train_split, random_state=0) + # train_split, test_split = sss.get_n_splits(data) + train_split, test_split = StratifiedShuffleSplit( + data, train_size=self.train_split, shuffle=True + ) + test_split, validation_split = train_test_split( test_split, train_size=0.5, shuffle=True ) for k, split in [ diff --git a/chebai/result/classification.py b/chebai/result/classification.py index c75c7b29..14790c1d 100644 --- a/chebai/result/classification.py +++ b/chebai/result/classification.py @@ -8,8 +8,13 @@ MultilabelF1Score, MultilabelPrecision, MultilabelRecall, + MultilabelAUROC, + BinaryF1Score, + BinaryAUROC, ) +from torcheval.metrics import BinaryAUROC + from chebai.callbacks.epoch_metrics import BalancedAccuracy, MacroF1 from chebai.result.utils import * @@ -56,6 +61,8 @@ def print_metrics( top_k: The number of top classes to display based on F1 score. markdown_output: If True, print metrics in markdown format. """ + if device != labels.device: + device = labels.device f1_micro = MultilabelF1Score(preds.shape[1], average="micro").to(device=device) my_f1_macro = MacroF1(preds.shape[1]).to(device=device) my_bal_acc = BalancedAccuracy(preds.shape[1]).to(device=device) @@ -103,3 +110,40 @@ def print_metrics( print( f'Found {len(zeros)} classes with F1-score == 0 (and non-zero labels): {", ".join(zeros)}' ) + +def metrics_classification( + preds: Tensor, + labels: Tensor, + device: torch.device, + classes: Optional[List[str]] = None, + top_k: int = 10,): + + prc = 0 + auc_roc = 0 + macro_f1 = 0 + micro_f1 = 0 + bal_acc = 0 + + if device != labels.device: + device = labels.device + + print(len(labels[0]['labels'])) + + if len(labels[0]['labels']) > 1: + my_f1_macro = MultilabelF1Score(preds.shape[1], average="micro").to(device=device) + f1_micro = MacroF1(preds.shape[1]).to(device=device) + my_bal_acc = BalancedAccuracy(preds.shape[1]).to(device=device) + my_auc_rco = MultilabelAUROC(preds.shape[1]).to(device=device) + + macro_f1 = my_f1_macro(preds, labels).cpu().numpy() + micro_f1 = f1_micro(preds, labels).cpu().numpy() + bal_acc = my_bal_acc(preds, labels).cpu().numpy() + auc_roc = my_auc_rco(preds, labels).cpu().numpy() + else: + my_auc_rco = BinaryAUROC(preds.shape[1]).to(device=device) + my_f1 = BinaryF1Score(preds.shape[1]).to(device=device) + + auc_roc = my_auc_rco(preds, labels).cpu().numpy() + macro_f1 = my_f1(preds, labels).cpu().numpy() + + return prc, auc_roc, macro_f1, micro_f1, bal_acc diff --git a/chebai/result/regression.py b/chebai/result/regression.py index a3823822..bfd544db 100644 --- a/chebai/result/regression.py +++ b/chebai/result/regression.py @@ -34,7 +34,7 @@ # plt.show() -def print_metrics( +def metrics_regression( preds: Tensor, labels: Tensor, device: torch.device, diff --git a/configs/data/bbbp_moleculenet.yml b/configs/data/bbbp_moleculenet.yml new file mode 100644 index 00000000..f5b1a7a8 --- /dev/null +++ b/configs/data/bbbp_moleculenet.yml @@ -0,0 +1,3 @@ +class_path: chebai.preprocessing.datasets.molecule_classification.BBBPChem +init_args: + batch_size: 32 diff --git a/configs/data/clintox_moleculenet.yml b/configs/data/clintox_moleculenet.yml index 4422bfe6..4633c985 100644 --- a/configs/data/clintox_moleculenet.yml +++ b/configs/data/clintox_moleculenet.yml @@ -1,3 +1,3 @@ class_path: chebai.preprocessing.datasets.molecule_classification.ClinToxChem init_args: - batch_size: 10 + batch_size: 32 diff --git a/configs/data/tox21_moleculenet.yml b/configs/data/tox21_moleculenet.yml index 5579a829..0ab32e1c 100644 --- a/configs/data/tox21_moleculenet.yml +++ b/configs/data/tox21_moleculenet.yml @@ -1,3 +1,3 @@ class_path: chebai.preprocessing.datasets.tox21.Tox21MolNetChem init_args: - batch_size: 10 + batch_size: 32 diff --git a/configs/metrics/micro-macro-f1-roc-auc-2.yml b/configs/metrics/micro-macro-f1-roc-auc-2.yml new file mode 100644 index 00000000..d69bf123 --- /dev/null +++ b/configs/metrics/micro-macro-f1-roc-auc-2.yml @@ -0,0 +1,13 @@ +class_path: torchmetrics.MetricCollection +init_args: + metrics: + micro-f1: + class_path: torchmetrics.classification.MultilabelF1Score + init_args: + average: micro + macro-f1: + class_path: chebai.callbacks.epoch_metrics.MacroF1 + roc-auc: + class_path: torchmetrics.classification.MultilabelAUROC + init_args: + num_labels: 2 diff --git a/configs/metrics/micro-macro-f1-roc-auc-binary.yml b/configs/metrics/micro-macro-f1-roc-auc-binary.yml new file mode 100644 index 00000000..05834343 --- /dev/null +++ b/configs/metrics/micro-macro-f1-roc-auc-binary.yml @@ -0,0 +1,7 @@ +class_path: torchmetrics.MetricCollection +init_args: + metrics: + f1: + class_path: torchmetrics.classification.BinaryF1Score + roc-auc: + class_path: torchmetrics.classification.BinaryAUROC diff --git a/configs/metrics/micro-macro-f1-roc-auc.yml b/configs/metrics/micro-macro-f1-roc-auc.yml new file mode 100644 index 00000000..18ddfff1 --- /dev/null +++ b/configs/metrics/micro-macro-f1-roc-auc.yml @@ -0,0 +1,13 @@ +class_path: torchmetrics.MetricCollection +init_args: + metrics: + micro-f1: + class_path: torchmetrics.classification.MultilabelF1Score + init_args: + average: micro + macro-f1: + class_path: chebai.callbacks.epoch_metrics.MacroF1 + roc-auc: + class_path: torchmetrics.classification.MultilabelAUROC + init_args: + num_labels: 12 diff --git a/configs/training/binary_callbacks.yml b/configs/training/binary_callbacks.yml new file mode 100644 index 00000000..1b8e3da1 --- /dev/null +++ b/configs/training/binary_callbacks.yml @@ -0,0 +1,12 @@ +- class_path: chebai.callbacks.model_checkpoint.CustomModelCheckpoint + init_args: + monitor: val_f1 + mode: 'max' + filename: 'best_{epoch:02d}_{val_loss:.4f}_{val_f1:.4f}_{roc-auc:.4f}' + every_n_epochs: 1 + save_top_k: 3 +- class_path: chebai.callbacks.model_checkpoint.CustomModelCheckpoint + init_args: + filename: 'per_{epoch:02d}_{val_loss:.4f}_{val_f1:.4f}_{roc-auc:.4f}' + every_n_epochs: 25 + save_top_k: -1 diff --git a/configs/training/binary_trainer.yml b/configs/training/binary_trainer.yml new file mode 100644 index 00000000..a6ce374b --- /dev/null +++ b/configs/training/binary_trainer.yml @@ -0,0 +1,5 @@ +min_epochs: 100 +max_epochs: 100 +default_root_dir: &default_root_dir logs +logger: csv_logger.yml +callbacks: binary_callbacks.yml diff --git a/configs/training/wandb_logger_no_onto_bbbp.yml b/configs/training/wandb_logger_no_onto_bbbp.yml new file mode 100644 index 00000000..b4e51196 --- /dev/null +++ b/configs/training/wandb_logger_no_onto_bbbp.yml @@ -0,0 +1,6 @@ +class_path: chebai.loggers.custom.CustomLogger # Extension of Wandb logger +init_args: + save_dir: /Users/ctumes/Cheb-AI/bbbp/runs_no_onto/ + project: 'chebai-bbbp' + entity: 'ch-tumescheit-university-of-zurich' + log_model: 'all' diff --git a/configs/training/wandb_logger_onto_bbbp.yml b/configs/training/wandb_logger_onto_bbbp.yml new file mode 100644 index 00000000..d22eb8e0 --- /dev/null +++ b/configs/training/wandb_logger_onto_bbbp.yml @@ -0,0 +1,6 @@ +class_path: chebai.loggers.custom.CustomLogger # Extension of Wandb logger +init_args: + save_dir: /Users/ctumes/Cheb-AI/bbbp/runs_onto/ + project: 'chebai-bbbp' + entity: 'ch-tumescheit-university-of-zurich' + log_model: 'all' From dc9e104e48ec725111a181385542f66d5fdf7e7d Mon Sep 17 00:00:00 2001 From: schnamo Date: Sun, 19 Jan 2025 17:08:32 +0100 Subject: [PATCH 26/54] more datasets --- .../datasets/molecule_classification.py | 139 ++++++++++++++++++ chebai/result/classification.py | 70 +++++---- configs/data/sider_moleculenet.yml | 3 + configs/metrics/micro-macro-f1-roc-auc-27.yml | 13 ++ configs/training/default_callbacks.yml | 4 +- .../training/wandb_logger_no_onto_sider.yml | 6 + configs/training/wandb_logger_onto_sider.yml | 6 + 7 files changed, 208 insertions(+), 33 deletions(-) create mode 100644 configs/data/sider_moleculenet.yml create mode 100644 configs/metrics/micro-macro-f1-roc-auc-27.yml create mode 100644 configs/training/wandb_logger_no_onto_sider.yml create mode 100644 configs/training/wandb_logger_onto_sider.yml diff --git a/chebai/preprocessing/datasets/molecule_classification.py b/chebai/preprocessing/datasets/molecule_classification.py index 9d5386f4..6e230b4b 100644 --- a/chebai/preprocessing/datasets/molecule_classification.py +++ b/chebai/preprocessing/datasets/molecule_classification.py @@ -249,6 +249,145 @@ def _load_dict(self, input_file_path: str) -> List[Dict]: yield dict(features=smiles, labels=labels, ident=i) # yield self.reader.to_data(dict(features=smiles, labels=labels, ident=i)) +class Sider(XYBaseDataModule): + """Data module for ClinTox MoleculeNet dataset.""" + + HEADERS = [ + "Hepatobiliary disorders", "Metabolism and nutrition disorders", "Product issues", "Eye disorders","Investigations", + "Musculoskeletal and connective tissue disorders", "Gastrointestinal disorders", "Social circumstances", + "Immune system disorders", "Reproductive system and breast disorders", + "Neoplasms benign, malignant and unspecified (incl cysts and polyps)", + "General disorders and administration site conditions", + "Endocrine disorders", "Surgical and medical procedures", "Vascular disorders", "Blood and lymphatic system disorders", + "Skin and subcutaneous tissue disorders", "Congenital, familial and genetic disorders", + "Infections and infestations", "Respiratory, thoracic and mediastinal disorders", "Psychiatric disorders", + "Renal and urinary disorders", "Pregnancy, puerperium and perinatal conditions", + "Ear and labyrinth disorders", "Cardiac disorders", "Nervous system disorders", + "Injury, poisoning and procedural complications" + ] + + @property + def _name(self) -> str: + """Returns the name of the dataset.""" + return "Sider" + + @property + def label_number(self) -> int: + """Returns the number of labels.""" + return 27 + + @property + def raw_file_names(self) -> List[str]: + """Returns a list of raw file names.""" + return ["sider.csv"] + + @property + def processed_file_names(self) -> List[str]: + """Returns a list of processed file names.""" + return ["test.pt", "train.pt", "validation.pt"] + + def download(self) -> None: + """Downloads and extracts the dataset.""" + with NamedTemporaryFile("rb") as gout: + request.urlretrieve( + "https://deepchemdata.s3-us-west-1.amazonaws.com/datasets/sider.csv.gz", + gout.name, + ) + with gzip.open(gout.name) as gfile: + with open(os.path.join(self.raw_dir, "sider.csv"), "wt") as fout: + fout.write(gfile.read().decode()) + + def setup_processed(self) -> None: + """Processes and splits the dataset.""" + print("Create splits") + data = list(self._load_data_from_file(os.path.join(self.raw_dir, f"sider.csv"))) + groups = np.array([d["group"] for d in data]) + if not all(g is None for g in groups): + split_size = int(len(set(groups)) * self.train_split) + os.makedirs(self.processed_dir, exist_ok=True) + splitter = GroupShuffleSplit(train_size=split_size, n_splits=1) + + train_split_index, temp_split_index = next( + splitter.split(data, groups=groups) + ) + + split_groups = groups[temp_split_index] + + splitter = GroupShuffleSplit( + train_size=int(len(set(split_groups)) * self.train_split), n_splits=1 + ) + test_split_index, validation_split_index = next( + splitter.split(temp_split_index, groups=split_groups) + ) + train_split = [data[i] for i in train_split_index] + test_split = [ + d + for d in (data[temp_split_index[i]] for i in test_split_index) + if d["original"] + ] + validation_split = [ + d + for d in (data[temp_split_index[i]] for i in validation_split_index) + if d["original"] + ] + else: + train_split, test_split = train_test_split( + data, train_size=self.train_split, shuffle=True + ) + test_split, validation_split = train_test_split( + test_split, train_size=0.5, shuffle=True + ) + for k, split in [ + ("test", test_split), + ("train", train_split), + ("validation", validation_split), + ]: + print("transform", k) + torch.save( + split, + os.path.join(self.processed_dir, f"{k}.pt"), + ) + + def setup(self, **kwargs) -> None: + """Sets up the dataset by downloading and processing if necessary.""" + if any( + not os.path.isfile(os.path.join(self.raw_dir, f)) + for f in self.raw_file_names + ): + self.download() + if any( + not os.path.isfile(os.path.join(self.processed_dir, f)) + for f in self.processed_file_names + ): + self.setup_processed() + + def _load_dict(self, input_file_path: str) -> List[Dict]: + """Loads data from a CSV file. + + Args: + input_file_path (str): Path to the CSV file. + + Returns: + List[Dict]: List of data dictionaries. + """ + i = 0 + with open(input_file_path, "r") as input_file: + reader = csv.DictReader(input_file) + for row in reader: + i += 1 + smiles = row["smiles"] + labels = [ + bool(int(l)) if l else None for l in (row[k] for k in self.HEADERS) + ] + yield dict(features=smiles, labels=labels, ident=i) + # yield self.reader.to_data(dict(features=smiles, labels=labels, ident=i)) + + +class SiderChem(Sider): + """Chemical data reader for Tox21MolNet dataset.""" + + READER = dr.ChemDataReader + class BBBPChem(BBBP): """Chemical data reader for Tox21MolNet dataset.""" diff --git a/chebai/result/classification.py b/chebai/result/classification.py index 14790c1d..c3d932d6 100644 --- a/chebai/result/classification.py +++ b/chebai/result/classification.py @@ -11,10 +11,10 @@ MultilabelAUROC, BinaryF1Score, BinaryAUROC, + BinaryAveragePrecision, + MultilabelAveragePrecision ) -from torcheval.metrics import BinaryAUROC - from chebai.callbacks.epoch_metrics import BalancedAccuracy, MacroF1 from chebai.result.utils import * @@ -111,39 +111,47 @@ def print_metrics( f'Found {len(zeros)} classes with F1-score == 0 (and non-zero labels): {", ".join(zeros)}' ) -def metrics_classification( +def metrics_classification_multilabel( preds: Tensor, labels: Tensor, - device: torch.device, - classes: Optional[List[str]] = None, - top_k: int = 10,): + device: torch.device,): - prc = 0 - auc_roc = 0 - macro_f1 = 0 - micro_f1 = 0 - bal_acc = 0 + if device != labels.device: + device = labels.device + + my_bal_acc = BalancedAccuracy(preds.shape[1]).to(device=device) + + bal_acc = my_bal_acc(preds, labels).cpu().numpy() + my_f1_macro = MultilabelF1Score(preds.shape[1], average="micro").to(device=device) + f1_micro = MacroF1(preds.shape[1]).to(device=device) + my_auc_roc = MultilabelAUROC(preds.shape[1]).to(device=device) + my_av_prec = MultilabelAveragePrecision(preds.shape[1]).to(device=device) + + macro_f1 = my_f1_macro(preds, labels).cpu().numpy() + micro_f1 = f1_micro(preds, labels).cpu().numpy() + auc_roc = my_auc_roc(preds, labels).cpu().numpy() + prc_auc = my_av_prec(preds, labels).cpu().numpy() + + return auc_roc, macro_f1, micro_f1, bal_acc, prc_auc + +def metrics_classification_binary( + preds: Tensor, + labels: Tensor, + device: torch.device,): if device != labels.device: device = labels.device - print(len(labels[0]['labels'])) - - if len(labels[0]['labels']) > 1: - my_f1_macro = MultilabelF1Score(preds.shape[1], average="micro").to(device=device) - f1_micro = MacroF1(preds.shape[1]).to(device=device) - my_bal_acc = BalancedAccuracy(preds.shape[1]).to(device=device) - my_auc_rco = MultilabelAUROC(preds.shape[1]).to(device=device) - - macro_f1 = my_f1_macro(preds, labels).cpu().numpy() - micro_f1 = f1_micro(preds, labels).cpu().numpy() - bal_acc = my_bal_acc(preds, labels).cpu().numpy() - auc_roc = my_auc_rco(preds, labels).cpu().numpy() - else: - my_auc_rco = BinaryAUROC(preds.shape[1]).to(device=device) - my_f1 = BinaryF1Score(preds.shape[1]).to(device=device) - - auc_roc = my_auc_rco(preds, labels).cpu().numpy() - macro_f1 = my_f1(preds, labels).cpu().numpy() - - return prc, auc_roc, macro_f1, micro_f1, bal_acc + my_auc_roc = BinaryAUROC() + my_f1 = BinaryF1Score().to(device=device) + my_av_prec = BinaryAveragePrecision().to(device=device) + my_bal_acc = BalancedAccuracy(preds.shape[1]).to(device=device) + + bal_acc = my_bal_acc(preds, labels).cpu().numpy() + auc_roc = my_auc_roc(preds, labels).cpu().numpy() + # my_auc_roc.update(preds.cpu()[:, 0], labels.cpu()[:, 0]) + # auc_roc = my_auc_roc.compute().numpy() + f1_score = my_f1(preds, labels).cpu().numpy() + prc_auc = my_av_prec(preds, labels).cpu().numpy() + + return auc_roc, f1_score, bal_acc, prc_auc \ No newline at end of file diff --git a/configs/data/sider_moleculenet.yml b/configs/data/sider_moleculenet.yml new file mode 100644 index 00000000..d2529ad9 --- /dev/null +++ b/configs/data/sider_moleculenet.yml @@ -0,0 +1,3 @@ +class_path: chebai.preprocessing.datasets.molecule_classification.SiderChem +init_args: + batch_size: 32 diff --git a/configs/metrics/micro-macro-f1-roc-auc-27.yml b/configs/metrics/micro-macro-f1-roc-auc-27.yml new file mode 100644 index 00000000..81b2b091 --- /dev/null +++ b/configs/metrics/micro-macro-f1-roc-auc-27.yml @@ -0,0 +1,13 @@ +class_path: torchmetrics.MetricCollection +init_args: + metrics: + micro-f1: + class_path: torchmetrics.classification.MultilabelF1Score + init_args: + average: micro + macro-f1: + class_path: chebai.callbacks.epoch_metrics.MacroF1 + roc-auc: + class_path: torchmetrics.classification.MultilabelAUROC + init_args: + num_labels: 27 diff --git a/configs/training/default_callbacks.yml b/configs/training/default_callbacks.yml index 29f23d53..38a513ed 100644 --- a/configs/training/default_callbacks.yml +++ b/configs/training/default_callbacks.yml @@ -2,11 +2,11 @@ init_args: monitor: val_micro-f1 mode: 'max' - filename: 'best_{epoch:02d}_{val_loss:.4f}_{val_macro-f1:.4f}_{val_micro-f1:.4f}_{roc-auc:.4f}' + filename: 'best_{epoch:02d}_{val_loss:.4f}_{val_macro-f1:.4f}_{val_micro-f1:.4f}_{val_roc-auc:.4f}' every_n_epochs: 1 save_top_k: 3 - class_path: chebai.callbacks.model_checkpoint.CustomModelCheckpoint init_args: - filename: 'per_{epoch:02d}_{val_loss:.4f}_{val_macro-f1:.4f}_{val_micro-f1:.4f}_{roc-auc:.4f}' + filename: 'per_{epoch:02d}_{val_loss:.4f}_{val_macro-f1:.4f}_{val_micro-f1:.4f}_{val_roc-auc:.4f}' every_n_epochs: 25 save_top_k: -1 diff --git a/configs/training/wandb_logger_no_onto_sider.yml b/configs/training/wandb_logger_no_onto_sider.yml new file mode 100644 index 00000000..96690738 --- /dev/null +++ b/configs/training/wandb_logger_no_onto_sider.yml @@ -0,0 +1,6 @@ +class_path: chebai.loggers.custom.CustomLogger # Extension of Wandb logger +init_args: + save_dir: /Users/ctumes/Cheb-AI/sider/runs_no_onto/ + project: 'chebai-sider' + entity: 'ch-tumescheit-university-of-zurich' + log_model: 'all' diff --git a/configs/training/wandb_logger_onto_sider.yml b/configs/training/wandb_logger_onto_sider.yml new file mode 100644 index 00000000..69d8e6ea --- /dev/null +++ b/configs/training/wandb_logger_onto_sider.yml @@ -0,0 +1,6 @@ +class_path: chebai.loggers.custom.CustomLogger # Extension of Wandb logger +init_args: + save_dir: /Users/ctumes/Cheb-AI/sider/runs_onto/ + project: 'chebai-sider' + entity: 'ch-tumescheit-university-of-zurich' + log_model: 'all' From 9a3967dac4875e647517bc96d35fa2877f2e4700 Mon Sep 17 00:00:00 2001 From: schnamo Date: Mon, 20 Jan 2025 14:08:15 +0100 Subject: [PATCH 27/54] bug fixes and different loss and electra params --- chebai/models/electra.py | 5 +- .../datasets/molecule_classification.py | 94 +++++++++++++++++++ chebai/preprocessing/datasets/tox21.py | 2 +- configs/data/tox21_moleculenet.yml | 3 +- configs/loss/bce_new.yml | 1 + configs/model/electra_tox.yml | 2 +- configs/model/electra_tox_paper.yml | 16 ++++ 7 files changed, 118 insertions(+), 5 deletions(-) create mode 100644 configs/loss/bce_new.yml create mode 100644 configs/model/electra_tox_paper.yml diff --git a/chebai/models/electra.py b/chebai/models/electra.py index defbfaae..16b81f7f 100644 --- a/chebai/models/electra.py +++ b/chebai/models/electra.py @@ -317,11 +317,12 @@ def _get_prediction_and_labels( n = loss_kwargs["non_null_labels"] d = d[n] if self.model_type == 'classification': + d = torch.sigmoid(d) if "missing_labels" in loss_kwargs: missing_labels = loss_kwargs["missing_labels"] - labels = labels * (~missing_labels).int() + d = d * (~missing_labels).int() - return torch.sigmoid(d), labels.int() if labels is not None else None + return d, labels.int() if labels is not None else None elif self.model_type == 'regression': return d, labels else: diff --git a/chebai/preprocessing/datasets/molecule_classification.py b/chebai/preprocessing/datasets/molecule_classification.py index 6e230b4b..4b970b04 100644 --- a/chebai/preprocessing/datasets/molecule_classification.py +++ b/chebai/preprocessing/datasets/molecule_classification.py @@ -382,6 +382,100 @@ def _load_dict(self, input_file_path: str) -> List[Dict]: yield dict(features=smiles, labels=labels, ident=i) # yield self.reader.to_data(dict(features=smiles, labels=labels, ident=i)) +class Bace(XYBaseDataModule): + """Data module for ClinTox MoleculeNet dataset.""" + + HEADERS = [ + "class", + ] + + @property + def _name(self) -> str: + """Returns the name of the dataset.""" + return "Bace" + + @property + def label_number(self) -> int: + """Returns the number of labels.""" + return 1 + + @property + def raw_file_names(self) -> List[str]: + """Returns a list of raw file names.""" + return ["bace.csv"] + + @property + def processed_file_names(self) -> List[str]: + """Returns a list of processed file names.""" + return ["test.pt", "train.pt", "validation.pt"] + + def download(self) -> None: + + """Downloads and extracts the dataset.""" + with open(os.path.join(self.raw_dir, "bace.csv"), "ab") as dst: + with request.urlopen(f"https://deepchemdata.s3-us-west-1.amazonaws.com/datasets/bace.csv",) as src: + shutil.copyfileobj(src, dst) + + + def setup_processed(self) -> None: + """Processes and splits the dataset.""" + print("Create splits") + data = list(self._load_data_from_file(os.path.join(self.raw_dir, f"bace.csv"))) + + train_split, test_split = train_test_split( + data, train_size=self.train_split, shuffle=True + ) + test_split, validation_split = train_test_split( + test_split, train_size=0.5, shuffle=True + ) + for k, split in [ + ("test", test_split), + ("train", train_split), + ("validation", validation_split), + ]: + print("transform", k) + torch.save( + split, + os.path.join(self.processed_dir, f"{k}.pt"), + ) + + def setup(self, **kwargs) -> None: + """Sets up the dataset by downloading and processing if necessary.""" + if any( + not os.path.isfile(os.path.join(self.raw_dir, f)) + for f in self.raw_file_names + ): + self.download() + if any( + not os.path.isfile(os.path.join(self.processed_dir, f)) + for f in self.processed_file_names + ): + self.setup_processed() + + def _load_dict(self, input_file_path: str) -> List[Dict]: + """Loads data from a CSV file. + + Args: + input_file_path (str): Path to the CSV file. + + Returns: + List[Dict]: List of data dictionaries. + """ + i = 0 + with open(input_file_path, "r") as input_file: + reader = csv.DictReader(input_file) + for row in reader: + i += 1 + smiles = row["mol"] + labels = [int(row["Class"])] + yield dict(features=smiles, labels=labels, ident=i) + # yield self.reader.to_data(dict(features=smiles, labels=labels, ident=i)) + + +class BaceChem(Bace): + """Chemical data reader for Tox21MolNet dataset.""" + + READER = dr.ChemDataReader class SiderChem(Sider): """Chemical data reader for Tox21MolNet dataset.""" diff --git a/chebai/preprocessing/datasets/tox21.py b/chebai/preprocessing/datasets/tox21.py index 3706febc..4dab61d9 100644 --- a/chebai/preprocessing/datasets/tox21.py +++ b/chebai/preprocessing/datasets/tox21.py @@ -103,7 +103,7 @@ def setup_processed(self) -> None: # print(self.train_split) # sss = StratifiedShuffleSplit(n_splits=5, test_size=1-self.train_split, random_state=0) # train_split, test_split = sss.get_n_splits(data) - train_split, test_split = StratifiedShuffleSplit( + train_split, test_split = train_test_split( data, train_size=self.train_split, shuffle=True ) test_split, validation_split = train_test_split( diff --git a/configs/data/tox21_moleculenet.yml b/configs/data/tox21_moleculenet.yml index 0ab32e1c..31b6e791 100644 --- a/configs/data/tox21_moleculenet.yml +++ b/configs/data/tox21_moleculenet.yml @@ -1,3 +1,4 @@ class_path: chebai.preprocessing.datasets.tox21.Tox21MolNetChem init_args: - batch_size: 32 + batch_size: 10 + train_split: 0.8 diff --git a/configs/loss/bce_new.yml b/configs/loss/bce_new.yml new file mode 100644 index 00000000..f8fbe98d --- /dev/null +++ b/configs/loss/bce_new.yml @@ -0,0 +1 @@ +class_path: torch.nn.BCEWithLogitsLoss \ No newline at end of file diff --git a/configs/model/electra_tox.yml b/configs/model/electra_tox.yml index fd3d1af9..fbba5993 100644 --- a/configs/model/electra_tox.yml +++ b/configs/model/electra_tox.yml @@ -10,4 +10,4 @@ init_args: num_hidden_layers: 6 type_vocab_size: 1 hidden_size: 256 - out_dim: 1 + out_dim: 12 diff --git a/configs/model/electra_tox_paper.yml b/configs/model/electra_tox_paper.yml new file mode 100644 index 00000000..40ed4bb0 --- /dev/null +++ b/configs/model/electra_tox_paper.yml @@ -0,0 +1,16 @@ +class_path: chebai.models.Electra +init_args: + model_type: classification + optimizer_kwargs: + lr: 1e-4 + # weight_decay: 0.0001 + config: + vocab_size: 1400 + max_position_embeddings: 1800 + num_attention_heads: 8 + num_hidden_layers: 6 + type_vocab_size: 1 + hidden_size: 256 + out_dim: 12 + hidden_dropout_prob: 0.4 + word_dropout: 0.2 From 1bc873695d1aace652f8a25d8f35a719c1aa4971 Mon Sep 17 00:00:00 2001 From: schnamo Date: Tue, 21 Jan 2025 15:37:21 +0100 Subject: [PATCH 28/54] changes to missing labels: negate labels as well as logits, add them to eval fct --- chebai/models/electra.py | 12 +++++++- .../datasets/molecule_classification.py | 30 +++++++++---------- chebai/preprocessing/datasets/tox21.py | 4 +-- chebai/result/utils.py | 2 +- configs/data/bbbp_moleculenet.yml | 1 + configs/data/clintox_moleculenet.yml | 1 + configs/data/sider_moleculenet.yml | 1 + configs/data/tox21_moleculenet.yml | 2 +- configs/model/electra_tox_paper.yml | 7 +++-- configs/training/binary_callbacks.yml | 11 +++++-- configs/training/default_callbacks.yml | 9 +++++- 11 files changed, 53 insertions(+), 27 deletions(-) diff --git a/chebai/models/electra.py b/chebai/models/electra.py index 16b81f7f..8dd106c3 100644 --- a/chebai/models/electra.py +++ b/chebai/models/electra.py @@ -245,6 +245,7 @@ def __init__( self.config = ElectraConfig(**config, output_attentions=True) self.word_dropout = nn.Dropout(config.get("word_dropout", 0)) self.model_type = model_type + self.pass_loss_kwargs = True in_d = self.config.hidden_size self.output = nn.Sequential( @@ -271,6 +272,10 @@ def __init__( else: self.electra = ElectraModel(config=self.config) + # freeze parameters + # for param in self.electra.parameters(): + # param.requires_grad = False + def _process_for_loss( self, model_output: Dict[str, Tensor], @@ -295,6 +300,7 @@ def _process_for_loss( if "missing_labels" in kwargs_copy: missing_labels = kwargs_copy.pop("missing_labels") output = output * (~missing_labels).int() + labels = labels * (~missing_labels).int() return output, labels, kwargs_copy def _get_prediction_and_labels( @@ -317,10 +323,14 @@ def _get_prediction_and_labels( n = loss_kwargs["non_null_labels"] d = d[n] if self.model_type == 'classification': + # print(self.model_type, ' in electra 324') d = torch.sigmoid(d) + # for mulitclass here softmax instead of sigmoid + #print('blababababab') if "missing_labels" in loss_kwargs: + #print('bla') missing_labels = loss_kwargs["missing_labels"] - d = d * (~missing_labels).int() + d = d * (~missing_labels).int().to(device=d.device) return d, labels.int() if labels is not None else None elif self.model_type == 'regression': diff --git a/chebai/preprocessing/datasets/molecule_classification.py b/chebai/preprocessing/datasets/molecule_classification.py index 4b970b04..023405a8 100644 --- a/chebai/preprocessing/datasets/molecule_classification.py +++ b/chebai/preprocessing/datasets/molecule_classification.py @@ -93,21 +93,21 @@ def setup_processed(self) -> None: if d["original"] ] else: - print(self.train_split) - print(type(data)) - print((data[0])) - print(type(data[0])) - X = [] - y = [] - for item in data: - X.append(item['ident']) - y.append(item['labels']) - sss = StratifiedShuffleSplit(n_splits=10, test_size=1-self.train_split, random_state=0) - sss.get_n_splits(np.array(X), np.array(y)) - print(sss) - train, test = sss.split(X, y) - print(train) - exit() + # print(self.train_split) + # print(type(data)) + # print((data[0])) + # print(type(data[0])) + # X = [] + # y = [] + # for item in data: + # X.append(item['ident']) + # y.append(item['labels']) + # sss = StratifiedShuffleSplit(n_splits=10, test_size=1-self.train_split, random_state=0) + # sss.get_n_splits(np.array(X), np.array(y)) + # print(sss) + # train, test = sss.split(X, y) + # print(train) + # exit() train_split, test_split = train_test_split( data, train_size=self.train_split, shuffle=True ) diff --git a/chebai/preprocessing/datasets/tox21.py b/chebai/preprocessing/datasets/tox21.py index 4dab61d9..80b81644 100644 --- a/chebai/preprocessing/datasets/tox21.py +++ b/chebai/preprocessing/datasets/tox21.py @@ -100,9 +100,7 @@ def setup_processed(self) -> None: if d["original"] ] else: - # print(self.train_split) - # sss = StratifiedShuffleSplit(n_splits=5, test_size=1-self.train_split, random_state=0) - # train_split, test_split = sss.get_n_splits(data) + train_split, test_split = train_test_split( data, train_size=self.train_split, shuffle=True ) diff --git a/chebai/result/utils.py b/chebai/result/utils.py index f8d5cf4b..856446c2 100644 --- a/chebai/result/utils.py +++ b/chebai/result/utils.py @@ -58,7 +58,7 @@ def _run_batch(batch, model, collate): if collated.y is not None: collated.y = collated.to_y(model.device) processable_data = model._process_batch(collated, 0) - del processable_data["loss_kwargs"] + # del processable_data["loss_kwargs"] model_output = model(processable_data, **processable_data["model_kwargs"]) preds, labels = model._get_prediction_and_labels( processable_data, processable_data["labels"], model_output diff --git a/configs/data/bbbp_moleculenet.yml b/configs/data/bbbp_moleculenet.yml index f5b1a7a8..9f3b7164 100644 --- a/configs/data/bbbp_moleculenet.yml +++ b/configs/data/bbbp_moleculenet.yml @@ -1,3 +1,4 @@ class_path: chebai.preprocessing.datasets.molecule_classification.BBBPChem init_args: batch_size: 32 + train_split: 0.8 diff --git a/configs/data/clintox_moleculenet.yml b/configs/data/clintox_moleculenet.yml index 4633c985..2cfdcacf 100644 --- a/configs/data/clintox_moleculenet.yml +++ b/configs/data/clintox_moleculenet.yml @@ -1,3 +1,4 @@ class_path: chebai.preprocessing.datasets.molecule_classification.ClinToxChem init_args: batch_size: 32 + train_split: 0.8 diff --git a/configs/data/sider_moleculenet.yml b/configs/data/sider_moleculenet.yml index d2529ad9..596b1b44 100644 --- a/configs/data/sider_moleculenet.yml +++ b/configs/data/sider_moleculenet.yml @@ -1,3 +1,4 @@ class_path: chebai.preprocessing.datasets.molecule_classification.SiderChem init_args: batch_size: 32 + train_split: 0.8 \ No newline at end of file diff --git a/configs/data/tox21_moleculenet.yml b/configs/data/tox21_moleculenet.yml index 31b6e791..933b9b60 100644 --- a/configs/data/tox21_moleculenet.yml +++ b/configs/data/tox21_moleculenet.yml @@ -1,4 +1,4 @@ class_path: chebai.preprocessing.datasets.tox21.Tox21MolNetChem init_args: - batch_size: 10 + batch_size: 32 train_split: 0.8 diff --git a/configs/model/electra_tox_paper.yml b/configs/model/electra_tox_paper.yml index 40ed4bb0..bcb48fc9 100644 --- a/configs/model/electra_tox_paper.yml +++ b/configs/model/electra_tox_paper.yml @@ -11,6 +11,7 @@ init_args: num_hidden_layers: 6 type_vocab_size: 1 hidden_size: 256 - out_dim: 12 - hidden_dropout_prob: 0.4 - word_dropout: 0.2 + # output_hidden_size: 256 + # out_dim: 12 + hidden_dropout_prob: 0.3 + word_dropout: 0.1 diff --git a/configs/training/binary_callbacks.yml b/configs/training/binary_callbacks.yml index 1b8e3da1..1c8ab408 100644 --- a/configs/training/binary_callbacks.yml +++ b/configs/training/binary_callbacks.yml @@ -2,11 +2,18 @@ init_args: monitor: val_f1 mode: 'max' - filename: 'best_{epoch:02d}_{val_loss:.4f}_{val_f1:.4f}_{roc-auc:.4f}' + filename: 'best_f1_{epoch:02d}_{val_loss:.4f}_{val_f1:.4f}_{val_roc-auc:.4f}' every_n_epochs: 1 save_top_k: 3 - class_path: chebai.callbacks.model_checkpoint.CustomModelCheckpoint init_args: - filename: 'per_{epoch:02d}_{val_loss:.4f}_{val_f1:.4f}_{roc-auc:.4f}' + monitor: val_loss + mode: 'min' + filename: 'best_loss_{epoch:02d}_{val_loss:.4f}_{val_f1:.4f}_{val_roc-auc:.4f}' + every_n_epochs: 1 + save_top_k: 3 +- class_path: chebai.callbacks.model_checkpoint.CustomModelCheckpoint + init_args: + filename: 'per_{epoch:02d}_{val_loss:.4f}_{val_f1:.4f}_{val_roc-auc:.4f}' every_n_epochs: 25 save_top_k: -1 diff --git a/configs/training/default_callbacks.yml b/configs/training/default_callbacks.yml index 38a513ed..628a92b1 100644 --- a/configs/training/default_callbacks.yml +++ b/configs/training/default_callbacks.yml @@ -2,7 +2,14 @@ init_args: monitor: val_micro-f1 mode: 'max' - filename: 'best_{epoch:02d}_{val_loss:.4f}_{val_macro-f1:.4f}_{val_micro-f1:.4f}_{val_roc-auc:.4f}' + filename: 'best_micro_f1_{epoch:02d}_{val_loss:.4f}_{val_macro-f1:.4f}_{val_micro-f1:.4f}_{val_roc-auc:.4f}' + every_n_epochs: 1 + save_top_k: 3 +- class_path: chebai.callbacks.model_checkpoint.CustomModelCheckpoint + init_args: + monitor: val_loss + mode: 'min' + filename: 'best_loss_{epoch:02d}_{val_loss:.4f}_{val_macro-f1:.4f}_{val_micro-f1:.4f}_{val_roc-auc:.4f}' every_n_epochs: 1 save_top_k: 3 - class_path: chebai.callbacks.model_checkpoint.CustomModelCheckpoint From 4885960e5bfcafe6e0a1ae98b71739f984d166a2 Mon Sep 17 00:00:00 2001 From: schnamo Date: Thu, 13 Feb 2025 18:24:43 +0100 Subject: [PATCH 29/54] try different splits, remove debugging comments --- chebai/models/electra.py | 5 +- chebai/preprocessing/datasets/base.py | 2 +- .../datasets/molecule_classification.py | 95 +++++++++++++++---- .../datasets/molecule_regression.py | 8 +- chebai/preprocessing/datasets/solCuration.py | 2 +- chebai/preprocessing/datasets/tox21.py | 14 +-- chebai/result/utils.py | 2 +- chebai/train.py | 2 + configs/data/freesolv_moleculenet.yml | 1 + configs/data/lipo_moleculenet.yml | 1 + configs/data/sider_moleculenet.yml | 2 +- configs/data/solubilityCuration.yml | 1 + configs/data/solubilityESOL.yml | 1 + configs/model/electra.yml | 3 +- configs/model/electra_tox_paper.yml | 6 +- configs/training/binary_callbacks.yml | 7 ++ configs/training/default_trainer.yml | 2 +- configs/training/solCur_callbacks.yml | 11 ++- 18 files changed, 123 insertions(+), 42 deletions(-) diff --git a/chebai/models/electra.py b/chebai/models/electra.py index 8dd106c3..c84b5b30 100644 --- a/chebai/models/electra.py +++ b/chebai/models/electra.py @@ -299,8 +299,10 @@ def _process_for_loss( labels = labels.float() if "missing_labels" in kwargs_copy: missing_labels = kwargs_copy.pop("missing_labels") - output = output * (~missing_labels).int() + output = output * (~missing_labels).int() - 10000 * missing_labels.int() labels = labels * (~missing_labels).int() + if self.model_type == "classification": + assert ((labels <= torch.tensor(1.0)) & (labels >= torch.tensor(0.0))).all() return output, labels, kwargs_copy def _get_prediction_and_labels( @@ -331,7 +333,6 @@ def _get_prediction_and_labels( #print('bla') missing_labels = loss_kwargs["missing_labels"] d = d * (~missing_labels).int().to(device=d.device) - return d, labels.int() if labels is not None else None elif self.model_type == 'regression': return d, labels diff --git a/chebai/preprocessing/datasets/base.py b/chebai/preprocessing/datasets/base.py index 3d565b3b..31347fe9 100644 --- a/chebai/preprocessing/datasets/base.py +++ b/chebai/preprocessing/datasets/base.py @@ -292,7 +292,7 @@ def _load_data_from_file(self, path: str) -> List[Dict[str, Any]]: Returns: List: A list of dictionaries containing the features and labels. """ - print("what???") + lines = self._get_data_size(path) print(f"Processing {lines} lines...") data = [ diff --git a/chebai/preprocessing/datasets/molecule_classification.py b/chebai/preprocessing/datasets/molecule_classification.py index 023405a8..ca4a7aac 100644 --- a/chebai/preprocessing/datasets/molecule_classification.py +++ b/chebai/preprocessing/datasets/molecule_classification.py @@ -180,7 +180,7 @@ def label_number(self) -> int: @property def raw_file_names(self) -> List[str]: """Returns a list of raw file names.""" - return ["bbbp.csv"] + return ["bbbp_groups4.csv"] @property def processed_file_names(self) -> List[str]: @@ -198,14 +198,44 @@ def download(self) -> None: def setup_processed(self) -> None: """Processes and splits the dataset.""" print("Create splits") - data = list(self._load_data_from_file(os.path.join(self.raw_dir, f"bbbp.csv"))) + data = list(self._load_data_from_file(os.path.join(self.raw_dir, f"bbbp_groups4.csv"))) + groups = np.array([d["group"] for d in data]) + if not all(g is None for g in groups): + print('Group shuffled') + split_size = int(len(set(groups)) * self.train_split) + os.makedirs(self.processed_dir, exist_ok=True) + splitter = GroupShuffleSplit(train_size=split_size, n_splits=1) - train_split, test_split = train_test_split( - data, train_size=self.train_split, shuffle=True + train_split_index, temp_split_index = next( + splitter.split(data, groups=groups) ) - test_split, validation_split = train_test_split( - test_split, train_size=0.5, shuffle=True + + split_groups = groups[temp_split_index] + + splitter = GroupShuffleSplit( + train_size=int(len(set(split_groups)) * self.train_split), n_splits=1 ) + test_split_index, validation_split_index = next( + splitter.split(temp_split_index, groups=split_groups) + ) + train_split = [data[i] for i in train_split_index] + test_split = [ + d + for d in (data[temp_split_index[i]] for i in test_split_index) + # if d["original"] + ] + validation_split = [ + d + for d in (data[temp_split_index[i]] for i in validation_split_index) + # if d["original"] + ] + else: + train_split, test_split = train_test_split( + data, train_size=self.train_split, shuffle=True + ) + test_split, validation_split = train_test_split( + test_split, train_size=0.5, shuffle=True + ) for k, split in [ ("test", test_split), ("train", train_split), @@ -246,7 +276,8 @@ def _load_dict(self, input_file_path: str) -> List[Dict]: i += 1 smiles = row["smiles"] labels = [int(row["p_np"])] - yield dict(features=smiles, labels=labels, ident=i) + group = int(row["group"]) + yield dict(features=smiles, labels=labels, ident=i, group=group) # yield self.reader.to_data(dict(features=smiles, labels=labels, ident=i)) class Sider(XYBaseDataModule): @@ -279,7 +310,7 @@ def label_number(self) -> int: @property def raw_file_names(self) -> List[str]: """Returns a list of raw file names.""" - return ["sider.csv"] + return ["sider_groups4.csv"] @property def processed_file_names(self) -> List[str]: @@ -300,7 +331,7 @@ def download(self) -> None: def setup_processed(self) -> None: """Processes and splits the dataset.""" print("Create splits") - data = list(self._load_data_from_file(os.path.join(self.raw_dir, f"sider.csv"))) + data = list(self._load_data_from_file(os.path.join(self.raw_dir, f"sider_groups4.csv"))) groups = np.array([d["group"] for d in data]) if not all(g is None for g in groups): split_size = int(len(set(groups)) * self.train_split) @@ -323,12 +354,12 @@ def setup_processed(self) -> None: test_split = [ d for d in (data[temp_split_index[i]] for i in test_split_index) - if d["original"] + # if d["original"] ] validation_split = [ d for d in (data[temp_split_index[i]] for i in validation_split_index) - if d["original"] + # if d["original"] ] else: train_split, test_split = train_test_split( @@ -379,7 +410,8 @@ def _load_dict(self, input_file_path: str) -> List[Dict]: labels = [ bool(int(l)) if l else None for l in (row[k] for k in self.HEADERS) ] - yield dict(features=smiles, labels=labels, ident=i) + group = row["group"] + yield dict(features=smiles, labels=labels, ident=i, group=group) # yield self.reader.to_data(dict(features=smiles, labels=labels, ident=i)) class Bace(XYBaseDataModule): @@ -421,13 +453,41 @@ def setup_processed(self) -> None: """Processes and splits the dataset.""" print("Create splits") data = list(self._load_data_from_file(os.path.join(self.raw_dir, f"bace.csv"))) - + # groups = np.array([d.get("group") for d in data]) + + # if not all(g is None for g in groups): + # split_size = int(len(set(groups)) * self.train_split) + # os.makedirs(self.processed_dir, exist_ok=True) + # splitter = GroupShuffleSplit(train_size=split_size, n_splits=1) + + # train_split_index, temp_split_index = next( + # splitter.split(data, groups=groups) + # ) + + # split_groups = groups[temp_split_index] + + # splitter = GroupShuffleSplit( + # train_size=int(len(set(split_groups)) * self.train_split), n_splits=1 + # ) + # test_split_index, validation_split_index = next( + # splitter.split(temp_split_index, groups=split_groups) + # ) + # train_split = [data[i] for i in train_split_index] + # test_split = [ + # d + # for d in (data[temp_split_index[i]] for i in test_split_index) + # ] + # validation_split = [ + # d + # for d in (data[temp_split_index[i]] for i in validation_split_index) + # ] + # else: train_split, test_split = train_test_split( - data, train_size=self.train_split, shuffle=True - ) + data, train_size=self.train_split, shuffle=True + ) test_split, validation_split = train_test_split( - test_split, train_size=0.5, shuffle=True - ) + test_split, train_size=0.5, shuffle=True + ) for k, split in [ ("test", test_split), ("train", train_split), @@ -468,6 +528,7 @@ def _load_dict(self, input_file_path: str) -> List[Dict]: i += 1 smiles = row["mol"] labels = [int(row["Class"])] + # group = row["group"] yield dict(features=smiles, labels=labels, ident=i) # yield self.reader.to_data(dict(features=smiles, labels=labels, ident=i)) diff --git a/chebai/preprocessing/datasets/molecule_regression.py b/chebai/preprocessing/datasets/molecule_regression.py index 7a1fb876..435b4d68 100644 --- a/chebai/preprocessing/datasets/molecule_regression.py +++ b/chebai/preprocessing/datasets/molecule_regression.py @@ -86,7 +86,7 @@ def setup(self, **kwargs): ): self.setup_processed() - def _load_data_from_file(self, input_file_path: str) -> List[Dict]: + def _load_dict(self, input_file_path: str) -> List[Dict]: """Loads data from a CSV file. Args: @@ -143,10 +143,10 @@ def setup_processed(self): print(len(data)) if 0 == 0: train_split, test_split = train_test_split( - data, train_size=self.train_split, shuffle=True + data, train_size=self.train_split, shuffle=True, random_state=5 ) test_split, validation_split = train_test_split( - test_split, train_size=0.5, shuffle=True + test_split, train_size=0.5, shuffle=True, random_state=5 ) for k, split in [ ("test", test_split), @@ -175,7 +175,7 @@ def setup(self, **kwargs): ): self.setup_processed() - def _load_data_from_file(self, input_file_path: str) -> List[Dict]: + def _load_dict(self, input_file_path: str) -> List[Dict]: """Loads data from a CSV file. Args: diff --git a/chebai/preprocessing/datasets/solCuration.py b/chebai/preprocessing/datasets/solCuration.py index 9cec482c..b39d69de 100644 --- a/chebai/preprocessing/datasets/solCuration.py +++ b/chebai/preprocessing/datasets/solCuration.py @@ -191,7 +191,7 @@ def setup(self, **kwargs): ): self.setup_processed() - def _load_data_from_file(self, input_file_path: str) -> List[Dict]: + def _load_dict(self, input_file_path: str) -> List[Dict]: """Loads data from a CSV file. Args: diff --git a/chebai/preprocessing/datasets/tox21.py b/chebai/preprocessing/datasets/tox21.py index 80b81644..93380fbd 100644 --- a/chebai/preprocessing/datasets/tox21.py +++ b/chebai/preprocessing/datasets/tox21.py @@ -47,7 +47,7 @@ def label_number(self) -> int: @property def raw_file_names(self) -> List[str]: """Returns a list of raw file names.""" - return ["tox21.csv"] + return ["tox21_groups_04.csv"] @property def processed_file_names(self) -> List[str]: @@ -68,7 +68,7 @@ def download(self) -> None: def setup_processed(self) -> None: """Processes and splits the dataset.""" print("Create splits") - data = list(self._load_data_from_file(os.path.join(self.raw_dir, f"tox21.csv"))) + data = list(self._load_data_from_file(os.path.join(self.raw_dir, f"tox21_groups_04.csv"))) groups = np.array([d.get("group") for d in data]) if not all(g is None for g in groups): @@ -92,12 +92,12 @@ def setup_processed(self) -> None: test_split = [ d for d in (data[temp_split_index[i]] for i in test_split_index) - if d["original"] + # if d["original"] ] validation_split = [ d for d in (data[temp_split_index[i]] for i in validation_split_index) - if d["original"] + # if d["original"] ] else: @@ -143,11 +143,13 @@ def _load_dict(self, input_file_path: str) -> List[Dict]: with open(input_file_path, "r") as input_file: reader = csv.DictReader(input_file) for row in reader: + print(row) smiles = row["smiles"] labels = [ - bool(int(l)) if l else None for l in (row[k] for k in self.HEADERS) + bool(int(float(l))) if len(l) > 1 else None for l in (row[k] for k in self.HEADERS) ] - yield dict(features=smiles, labels=labels, ident=row["mol_id"]) + group = int(row["group"]) + yield dict(features=smiles, labels=labels, ident=row["mol_id"], group=group) # yield self.reader.to_data(dict(features=smiles, labels=labels, ident=row["mol_id"])) diff --git a/chebai/result/utils.py b/chebai/result/utils.py index 856446c2..e83dcf2d 100644 --- a/chebai/result/utils.py +++ b/chebai/result/utils.py @@ -102,6 +102,7 @@ def evaluate_model( Returns: Tensors with predictions and labels. """ + assert model.model_type == "classification" model.eval() collate = data_module.reader.COLLATOR() @@ -112,7 +113,6 @@ def evaluate_model( else: data_list = data_module.load_processed_data("test", filename) data_list = data_list[: data_module.data_limit] - print(data_list[2:5]) preds_list = [] labels_list = [] if buffer_dir is not None: diff --git a/chebai/train.py b/chebai/train.py index d69d6094..1af9b3db 100644 --- a/chebai/train.py +++ b/chebai/train.py @@ -47,6 +47,7 @@ def eval_model( for molecule, label in batch: model_outputs = model(molecule) # todo: this is also just for classification, adjust to regression + print("THESE SHOULD BE PROBAS (in train.py):", model_outputs) prediction = [1.0 if i > 0.5 else 0.0 for i in model_outputs] predictions.append(prediction) raw_values.append(model_outputs) @@ -148,6 +149,7 @@ def _execute( loss = loss_fn(prediction, labels) data_size += 1 # todo: this is also just for classification, adjust to regression + print("THESE SHOULD BE PROBAS (in train.py):", prediction) f1 += f1_score(prediction > 0.5, labels > 0.5, average="micro") train_running_loss += loss.item() diff --git a/configs/data/freesolv_moleculenet.yml b/configs/data/freesolv_moleculenet.yml index d7d0a708..30ed6125 100644 --- a/configs/data/freesolv_moleculenet.yml +++ b/configs/data/freesolv_moleculenet.yml @@ -1,3 +1,4 @@ class_path: chebai.preprocessing.datasets.molecule_regression.FreeSolvChem init_args: batch_size: 32 + train_split: 0.8 diff --git a/configs/data/lipo_moleculenet.yml b/configs/data/lipo_moleculenet.yml index b2ed0ad2..f3a8cfc4 100644 --- a/configs/data/lipo_moleculenet.yml +++ b/configs/data/lipo_moleculenet.yml @@ -1,3 +1,4 @@ class_path: chebai.preprocessing.datasets.molecule_regression.LipoChem init_args: batch_size: 32 + train_split: 0.8 \ No newline at end of file diff --git a/configs/data/sider_moleculenet.yml b/configs/data/sider_moleculenet.yml index 596b1b44..09fc55af 100644 --- a/configs/data/sider_moleculenet.yml +++ b/configs/data/sider_moleculenet.yml @@ -1,4 +1,4 @@ class_path: chebai.preprocessing.datasets.molecule_classification.SiderChem init_args: - batch_size: 32 + batch_size: 10 train_split: 0.8 \ No newline at end of file diff --git a/configs/data/solubilityCuration.yml b/configs/data/solubilityCuration.yml index 7e07f37a..ad633dee 100644 --- a/configs/data/solubilityCuration.yml +++ b/configs/data/solubilityCuration.yml @@ -1,3 +1,4 @@ class_path: chebai.preprocessing.datasets.solCuration.SolCurationChem init_args: batch_size: 32 + train_split: 0.8 diff --git a/configs/data/solubilityESOL.yml b/configs/data/solubilityESOL.yml index 9a1834ac..24e0a799 100644 --- a/configs/data/solubilityESOL.yml +++ b/configs/data/solubilityESOL.yml @@ -1,3 +1,4 @@ class_path: chebai.preprocessing.datasets.solCuration.SolESOLChem init_args: batch_size: 32 + train_split: 0.8 diff --git a/configs/model/electra.yml b/configs/model/electra.yml index b66b1a53..a77a985d 100644 --- a/configs/model/electra.yml +++ b/configs/model/electra.yml @@ -9,5 +9,4 @@ init_args: num_attention_heads: 8 num_hidden_layers: 6 type_vocab_size: 1 - hidden_size: 256 - out_dim: 1 + hidden_size: 256 \ No newline at end of file diff --git a/configs/model/electra_tox_paper.yml b/configs/model/electra_tox_paper.yml index bcb48fc9..9f6797c8 100644 --- a/configs/model/electra_tox_paper.yml +++ b/configs/model/electra_tox_paper.yml @@ -11,7 +11,5 @@ init_args: num_hidden_layers: 6 type_vocab_size: 1 hidden_size: 256 - # output_hidden_size: 256 - # out_dim: 12 - hidden_dropout_prob: 0.3 - word_dropout: 0.1 + hidden_dropout_prob: 0.4 + word_dropout: 0.2 diff --git a/configs/training/binary_callbacks.yml b/configs/training/binary_callbacks.yml index 1c8ab408..0cad51f4 100644 --- a/configs/training/binary_callbacks.yml +++ b/configs/training/binary_callbacks.yml @@ -17,3 +17,10 @@ filename: 'per_{epoch:02d}_{val_loss:.4f}_{val_f1:.4f}_{val_roc-auc:.4f}' every_n_epochs: 25 save_top_k: -1 +# - class_path: lightning.pytorch.callbacks.early_stopping.EarlyStopping +# init_args: +# monitor: "val_loss_epoch" +# min_delta: 0.0 +# patience: 10 +# verbose: False +# mode: "min" diff --git a/configs/training/default_trainer.yml b/configs/training/default_trainer.yml index 91aa4244..0ce68a49 100644 --- a/configs/training/default_trainer.yml +++ b/configs/training/default_trainer.yml @@ -1,4 +1,4 @@ -min_epochs: 100 +min_epochs: 20 max_epochs: 100 default_root_dir: &default_root_dir logs logger: csv_logger.yml diff --git a/configs/training/solCur_callbacks.yml b/configs/training/solCur_callbacks.yml index 97cb4b2d..f0bd45c1 100644 --- a/configs/training/solCur_callbacks.yml +++ b/configs/training/solCur_callbacks.yml @@ -1,8 +1,15 @@ - class_path: chebai.callbacks.model_checkpoint.CustomModelCheckpoint init_args: - monitor: val_mse + monitor: val_loss mode: 'min' - filename: 'best_{epoch:02d}_{val_loss:.4f}_{val_mse:.4f}_{val_rmse:.4f}_{val_r2:.4f}' + filename: 'best_loss_{epoch:02d}_{val_loss:.4f}_{val_mse:.4f}_{val_rmse:.4f}_{val_r2:.4f}' + every_n_epochs: 1 + save_top_k: 3 +- class_path: chebai.callbacks.model_checkpoint.CustomModelCheckpoint + init_args: + monitor: val_r2 + mode: 'max' + filename: 'best_r2_{epoch:02d}_{val_loss:.4f}_{val_mse:.4f}_{val_rmse:.4f}_{val_r2:.4f}' every_n_epochs: 1 save_top_k: 3 - class_path: chebai.callbacks.model_checkpoint.CustomModelCheckpoint From ba01607c831200ef2e9cca8829b0f43a4fee3c4f Mon Sep 17 00:00:00 2001 From: schnamo Date: Wed, 12 Mar 2025 14:23:08 +0100 Subject: [PATCH 30/54] fix issue with input args --- chebai/cli.py | 14 +++++++------- chebai/models/base.py | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/chebai/cli.py b/chebai/cli.py index 36245aa0..ce8f3b71 100644 --- a/chebai/cli.py +++ b/chebai/cli.py @@ -47,13 +47,13 @@ def add_arguments_to_parser(self, parser: LightningArgumentParser): parser.link_arguments( "model.init_args.out_dim", "trainer.callbacks.init_args.num_labels" ) - parser.link_arguments( - "data", "model.init_args.criterion.init_args.data_extractor" - ) - parser.link_arguments( - "data.init_args.chebi_version", - "model.init_args.criterion.init_args.data_extractor.init_args.chebi_version", - ) + # parser.link_arguments( + # "data", "model.init_args.criterion.init_args.data_extractor" + # ) + # parser.link_arguments( + # "data.init_args.chebi_version", + # "model.init_args.criterion.init_args.data_extractor.init_args.chebi_version", + # ) @staticmethod def subcommands() -> Dict[str, Set[str]]: diff --git a/chebai/models/base.py b/chebai/models/base.py index 15a5f5e7..cfd0ed79 100644 --- a/chebai/models/base.py +++ b/chebai/models/base.py @@ -246,7 +246,7 @@ def _execute( predictions, and loss (if applicable). """ assert isinstance(batch, XYData) - batch = batch.to(self.device) + batch = batch.to(self.device) # if this is in lightning why do we need to do .to(device)? see https://lightning.ai/docs/pytorch/stable/common/lightning_module.html data = self._process_batch(batch, batch_idx) labels = data["labels"] model_output = self(data, **data.get("model_kwargs", dict())) From f74964c7e057f4b3bb2598ec19da408de9dc8a4c Mon Sep 17 00:00:00 2001 From: schnamo Date: Wed, 19 Mar 2025 12:51:16 +0100 Subject: [PATCH 31/54] add missing configs --- chebai/train.py | 2 +- configs/data/bace_moleculenet.yml | 4 ++++ configs/loss/bce_try.yml | 1 + configs/model/electra_LR.yml | 12 ++++++++++++ .../model/electra_tox_paper_regression.yml | 15 +++++++++++++++ .../early_stop_callbacks_regression.yml | 19 +++++++++++++++++++ .../training/early_stop_callbacks_tox21.yml | 19 +++++++++++++++++++ .../training/wandb_logger_no_onto_bace.yml | 6 ++++++ configs/training/wandb_logger_onto_bace.yml | 6 ++++++ tutorials/data_exploration_chebi.ipynb | 6 +++--- 10 files changed, 86 insertions(+), 4 deletions(-) create mode 100644 configs/data/bace_moleculenet.yml create mode 100644 configs/loss/bce_try.yml create mode 100644 configs/model/electra_LR.yml create mode 100644 configs/model/electra_tox_paper_regression.yml create mode 100644 configs/training/early_stop_callbacks_regression.yml create mode 100644 configs/training/early_stop_callbacks_tox21.yml create mode 100644 configs/training/wandb_logger_no_onto_bace.yml create mode 100644 configs/training/wandb_logger_onto_bace.yml diff --git a/chebai/train.py b/chebai/train.py index 1af9b3db..096f6183 100644 --- a/chebai/train.py +++ b/chebai/train.py @@ -132,7 +132,7 @@ def _execute( Returns: - train_running_loss (float): Average loss over the data. - - f1 (float): Average F1 score over the data. + - f1 (float): Average F1 score over the data. -> so this is for classification tasks only? """ train_running_loss = 0.0 diff --git a/configs/data/bace_moleculenet.yml b/configs/data/bace_moleculenet.yml new file mode 100644 index 00000000..eceadc45 --- /dev/null +++ b/configs/data/bace_moleculenet.yml @@ -0,0 +1,4 @@ +class_path: chebai.preprocessing.datasets.molecule_classification.BaceChem +init_args: + batch_size: 32 + train_split: 0.8 \ No newline at end of file diff --git a/configs/loss/bce_try.yml b/configs/loss/bce_try.yml new file mode 100644 index 00000000..ff8f9d4e --- /dev/null +++ b/configs/loss/bce_try.yml @@ -0,0 +1 @@ +class_path: torch.nn.BCELoss \ No newline at end of file diff --git a/configs/model/electra_LR.yml b/configs/model/electra_LR.yml new file mode 100644 index 00000000..5e12a0ae --- /dev/null +++ b/configs/model/electra_LR.yml @@ -0,0 +1,12 @@ +class_path: chebai.models.Electra +init_args: + model_type: classification + optimizer_kwargs: + lr: 1e-5 + config: + vocab_size: 1400 + max_position_embeddings: 1800 + num_attention_heads: 8 + num_hidden_layers: 6 + type_vocab_size: 1 + hidden_size: 256 diff --git a/configs/model/electra_tox_paper_regression.yml b/configs/model/electra_tox_paper_regression.yml new file mode 100644 index 00000000..640c7ba0 --- /dev/null +++ b/configs/model/electra_tox_paper_regression.yml @@ -0,0 +1,15 @@ +class_path: chebai.models.Electra +init_args: + model_type: regression + optimizer_kwargs: + lr: 1e-4 + # weight_decay: 0.0001 + config: + vocab_size: 1400 + max_position_embeddings: 1800 + num_attention_heads: 8 + num_hidden_layers: 6 + type_vocab_size: 1 + hidden_size: 256 + hidden_dropout_prob: 0.4 + word_dropout: 0.2 diff --git a/configs/training/early_stop_callbacks_regression.yml b/configs/training/early_stop_callbacks_regression.yml new file mode 100644 index 00000000..99986469 --- /dev/null +++ b/configs/training/early_stop_callbacks_regression.yml @@ -0,0 +1,19 @@ +- class_path: chebai.callbacks.model_checkpoint.CustomModelCheckpoint + init_args: + monitor: val_loss + mode: 'min' + filename: 'best_{epoch:02d}_{val_loss:.4f}_{val_mse:.4f}' + every_n_epochs: 1 + save_top_k: 3 +- class_path: chebai.callbacks.model_checkpoint.CustomModelCheckpoint + init_args: + filename: 'per_{epoch:02d}_{val_loss:.4f}_{val_mse:.4f}' + every_n_epochs: 25 + save_top_k: -1 +- class_path: lightning.pytorch.callbacks.early_stopping.EarlyStopping + init_args: + monitor: "val_loss_epoch" + min_delta: 0.0 + patience: 5 + verbose: False + mode: "min" diff --git a/configs/training/early_stop_callbacks_tox21.yml b/configs/training/early_stop_callbacks_tox21.yml new file mode 100644 index 00000000..647a2eea --- /dev/null +++ b/configs/training/early_stop_callbacks_tox21.yml @@ -0,0 +1,19 @@ +- class_path: chebai.callbacks.model_checkpoint.CustomModelCheckpoint + init_args: + monitor: val_micro-f1 + mode: 'max' + filename: 'best_{epoch:02d}_{val_loss:.4f}_{val_macro-f1:.4f}_{val_micro-f1:.4f}' + every_n_epochs: 1 + save_top_k: 3 +- class_path: chebai.callbacks.model_checkpoint.CustomModelCheckpoint + init_args: + filename: 'per_{epoch:02d}_{val_loss:.4f}_{val_macro-f1:.4f}_{val_micro-f1:.4f}' + every_n_epochs: 25 + save_top_k: -1 +- class_path: lightning.pytorch.callbacks.early_stopping.EarlyStopping + init_args: + monitor: "val_loss_epoch" + min_delta: 0.0 + patience: 5 + verbose: False + mode: "min" diff --git a/configs/training/wandb_logger_no_onto_bace.yml b/configs/training/wandb_logger_no_onto_bace.yml new file mode 100644 index 00000000..84343ad6 --- /dev/null +++ b/configs/training/wandb_logger_no_onto_bace.yml @@ -0,0 +1,6 @@ +class_path: chebai.loggers.custom.CustomLogger # Extension of Wandb logger +init_args: + save_dir: /Users/ctumes/Cheb-AI/bace/runs_no_onto/ + project: 'chebai-bace' + entity: 'ch-tumescheit-university-of-zurich' + log_model: 'all' diff --git a/configs/training/wandb_logger_onto_bace.yml b/configs/training/wandb_logger_onto_bace.yml new file mode 100644 index 00000000..e979beb6 --- /dev/null +++ b/configs/training/wandb_logger_onto_bace.yml @@ -0,0 +1,6 @@ +class_path: chebai.loggers.custom.CustomLogger # Extension of Wandb logger +init_args: + save_dir: /Users/ctumes/Cheb-AI/bace/runs_onto/ + project: 'chebai-bace' + entity: 'ch-tumescheit-university-of-zurich' + log_model: 'all' diff --git a/tutorials/data_exploration_chebi.ipynb b/tutorials/data_exploration_chebi.ipynb index 81256f4a..e9a2dcba 100644 --- a/tutorials/data_exploration_chebi.ipynb +++ b/tutorials/data_exploration_chebi.ipynb @@ -1077,9 +1077,9 @@ ], "metadata": { "kernelspec": { - "display_name": "Python (env_chebai)", + "display_name": "Python 3 (ipykernel)", "language": "python", - "name": "env_chebai" + "name": "python3" }, "language_info": { "codemirror_mode": { @@ -1091,7 +1091,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.14" + "version": "3.12.8" } }, "nbformat": 4, From 59064afafb7e259a4174e77780bf81e8e59d4d9f Mon Sep 17 00:00:00 2001 From: schnamo Date: Fri, 4 Apr 2025 16:29:01 +0200 Subject: [PATCH 32/54] add HIV dataset handling --- .../datasets/molecule_classification.py | 262 +++++++++++++++++- configs/training/binary_trainer.yml | 2 +- 2 files changed, 257 insertions(+), 7 deletions(-) diff --git a/chebai/preprocessing/datasets/molecule_classification.py b/chebai/preprocessing/datasets/molecule_classification.py index ca4a7aac..c21e8b50 100644 --- a/chebai/preprocessing/datasets/molecule_classification.py +++ b/chebai/preprocessing/datasets/molecule_classification.py @@ -41,7 +41,7 @@ def label_number(self) -> int: @property def raw_file_names(self) -> List[str]: """Returns a list of raw file names.""" - return ["clintox.csv"] + return ["clintox_groups4.csv"] @property def processed_file_names(self) -> List[str]: @@ -56,13 +56,13 @@ def download(self) -> None: gout.name, ) with gzip.open(gout.name) as gfile: - with open(os.path.join(self.raw_dir, "clintox.csv"), "wt") as fout: + with open(os.path.join(self.raw_dir, "clintox_groups4.csv"), "wt") as fout: fout.write(gfile.read().decode()) def setup_processed(self) -> None: """Processes and splits the dataset.""" print("Create splits") - data = list(self._load_data_from_file(os.path.join(self.raw_dir, f"clintox.csv"))) + data = list(self._load_data_from_file(os.path.join(self.raw_dir, f"clintox_groups4.csv"))) groups = np.array([d["group"] for d in data]) if not all(g is None for g in groups): split_size = int(len(set(groups)) * self.train_split) @@ -85,12 +85,10 @@ def setup_processed(self) -> None: test_split = [ d for d in (data[temp_split_index[i]] for i in test_split_index) - if d["original"] ] validation_split = [ d for d in (data[temp_split_index[i]] for i in validation_split_index) - if d["original"] ] else: # print(self.train_split) @@ -156,7 +154,9 @@ def _load_dict(self, input_file_path: str) -> List[Dict]: labels = [ bool(int(l)) if l else None for l in (row[k] for k in self.HEADERS) ] - yield dict(features=smiles, labels=labels, ident=i) + group = int(row["group"]) + yield dict(features=smiles, labels=labels, ident=i, group=group) + # yield dict(features=smiles, labels=labels, ident=i) # yield self.reader.to_data(dict(features=smiles, labels=labels, ident=i)) @@ -532,6 +532,250 @@ def _load_dict(self, input_file_path: str) -> List[Dict]: yield dict(features=smiles, labels=labels, ident=i) # yield self.reader.to_data(dict(features=smiles, labels=labels, ident=i)) +class HIV(XYBaseDataModule): + """Data module for ClinTox MoleculeNet dataset.""" + + HEADERS = [ + "HIV_active", + ] + + @property + def _name(self) -> str: + """Returns the name of the dataset.""" + return "HIV" + + @property + def label_number(self) -> int: + """Returns the number of labels.""" + return 1 + + @property + def raw_file_names(self) -> List[str]: + """Returns a list of raw file names.""" + return ["HIV.csv"] + + @property + def processed_file_names(self) -> List[str]: + """Returns a list of processed file names.""" + return ["test.pt", "train.pt", "validation.pt"] + + def download(self) -> None: + + """Downloads and extracts the dataset.""" + with open(os.path.join(self.raw_dir, "HIV.csv"), "ab") as dst: + with request.urlopen(f"https://deepchemdata.s3-us-west-1.amazonaws.com/datasets/HIV.csv",) as src: + shutil.copyfileobj(src, dst) + + + def setup_processed(self) -> None: + """Processes and splits the dataset.""" + print("Create splits") + data = list(self._load_data_from_file(os.path.join(self.raw_dir, f"HIV.csv"))) + groups = np.array([d["group"] for d in data]) + if not all(g is None for g in groups): + print('Group shuffled') + split_size = int(len(set(groups)) * self.train_split) + os.makedirs(self.processed_dir, exist_ok=True) + splitter = GroupShuffleSplit(train_size=split_size, n_splits=1) + + train_split_index, temp_split_index = next( + splitter.split(data, groups=groups) + ) + + split_groups = groups[temp_split_index] + + splitter = GroupShuffleSplit( + train_size=int(len(set(split_groups)) * self.train_split), n_splits=1 + ) + test_split_index, validation_split_index = next( + splitter.split(temp_split_index, groups=split_groups) + ) + train_split = [data[i] for i in train_split_index] + test_split = [ + d + for d in (data[temp_split_index[i]] for i in test_split_index) + ] + validation_split = [ + d + for d in (data[temp_split_index[i]] for i in validation_split_index) + ] + else: + train_split, test_split = train_test_split( + data, train_size=self.train_split, shuffle=True + ) + test_split, validation_split = train_test_split( + test_split, train_size=0.5, shuffle=True + ) + for k, split in [ + ("test", test_split), + ("train", train_split), + ("validation", validation_split), + ]: + print("transform", k) + torch.save( + split, + os.path.join(self.processed_dir, f"{k}.pt"), + ) + + def setup(self, **kwargs) -> None: + """Sets up the dataset by downloading and processing if necessary.""" + if any( + not os.path.isfile(os.path.join(self.raw_dir, f)) + for f in self.raw_file_names + ): + self.download() + if any( + not os.path.isfile(os.path.join(self.processed_dir, f)) + for f in self.processed_file_names + ): + self.setup_processed() + + def _load_dict(self, input_file_path: str) -> List[Dict]: + """Loads data from a CSV file. + + Args: + input_file_path (str): Path to the CSV file. + + Returns: + List[Dict]: List of data dictionaries. + """ + i = 0 + with open(input_file_path, "r") as input_file: + reader = csv.DictReader(input_file) + for row in reader: + i += 1 + smiles = row["smiles"] + labels = [int(row["HIV_active"])] + # group = int(row["group"]) + yield dict(features=smiles, labels=labels, ident=i) #, group=group) + # yield self.reader.to_data(dict(features=smiles, labels=labels, ident=i)) + + +class MUV(XYBaseDataModule): + """Data module for ClinTox MoleculeNet dataset.""" + + HEADERS = [ + "MUV-466","MUV-548","MUV-600","MUV-644","MUV-652","MUV-689", + "MUV-692","MUV-712","MUV-713","MUV-733","MUV-737","MUV-810", + "MUV-832","MUV-846","MUV-852","MUV-858","MUV-859" + ] + + @property + def _name(self) -> str: + """Returns the name of the dataset.""" + return "MUV" + + @property + def label_number(self) -> int: + """Returns the number of labels.""" + return 17 + + @property + def raw_file_names(self) -> List[str]: + """Returns a list of raw file names.""" + return ["muv.csv"] + + @property + def processed_file_names(self) -> List[str]: + """Returns a list of processed file names.""" + return ["test.pt", "train.pt", "validation.pt"] + + def download(self) -> None: + """Downloads and extracts the dataset.""" + with NamedTemporaryFile("rb") as gout: + request.urlretrieve( + "https://deepchemdata.s3-us-west-1.amazonaws.com/datasets/muv.csv.gz", + gout.name, + ) + with gzip.open(gout.name) as gfile: + with open(os.path.join(self.raw_dir, "muv.csv"), "wt") as fout: + fout.write(gfile.read().decode()) + + def setup_processed(self) -> None: + """Processes and splits the dataset.""" + print("Create splits") + data = list(self._load_data_from_file(os.path.join(self.raw_dir, f"sider_groups4.csv"))) + groups = np.array([d["group"] for d in data]) + if not all(g is None for g in groups): + split_size = int(len(set(groups)) * self.train_split) + os.makedirs(self.processed_dir, exist_ok=True) + splitter = GroupShuffleSplit(train_size=split_size, n_splits=1) + + train_split_index, temp_split_index = next( + splitter.split(data, groups=groups) + ) + + split_groups = groups[temp_split_index] + + splitter = GroupShuffleSplit( + train_size=int(len(set(split_groups)) * self.train_split), n_splits=1 + ) + test_split_index, validation_split_index = next( + splitter.split(temp_split_index, groups=split_groups) + ) + train_split = [data[i] for i in train_split_index] + test_split = [ + d + for d in (data[temp_split_index[i]] for i in test_split_index) + # if d["original"] + ] + validation_split = [ + d + for d in (data[temp_split_index[i]] for i in validation_split_index) + # if d["original"] + ] + else: + train_split, test_split = train_test_split( + data, train_size=self.train_split, shuffle=True + ) + test_split, validation_split = train_test_split( + test_split, train_size=0.5, shuffle=True + ) + for k, split in [ + ("test", test_split), + ("train", train_split), + ("validation", validation_split), + ]: + print("transform", k) + torch.save( + split, + os.path.join(self.processed_dir, f"{k}.pt"), + ) + + def setup(self, **kwargs) -> None: + """Sets up the dataset by downloading and processing if necessary.""" + if any( + not os.path.isfile(os.path.join(self.raw_dir, f)) + for f in self.raw_file_names + ): + self.download() + if any( + not os.path.isfile(os.path.join(self.processed_dir, f)) + for f in self.processed_file_names + ): + self.setup_processed() + + def _load_dict(self, input_file_path: str) -> List[Dict]: + """Loads data from a CSV file. + + Args: + input_file_path (str): Path to the CSV file. + + Returns: + List[Dict]: List of data dictionaries. + """ + i = 0 + with open(input_file_path, "r") as input_file: + reader = csv.DictReader(input_file) + for row in reader: + i += 1 + smiles = row["smiles"] + labels = [ + bool(int(l)) if l else None for l in (row[k] for k in self.HEADERS) + ] + # group = row["group"] + yield dict(features=smiles, labels=labels, ident=i)# , group=group) + # yield self.reader.to_data(dict(features=smiles, labels=labels, ident=i)) class BaceChem(Bace): """Chemical data reader for Tox21MolNet dataset.""" @@ -553,4 +797,10 @@ class BBBPChem(BBBP): class ClinToxChem(ClinTox): """Chemical data reader for Tox21MolNet dataset.""" + READER = dr.ChemDataReader + + +class HIVChem(HIV): + """Chemical data reader for Tox21MolNet dataset.""" + READER = dr.ChemDataReader \ No newline at end of file diff --git a/configs/training/binary_trainer.yml b/configs/training/binary_trainer.yml index a6ce374b..5787a67c 100644 --- a/configs/training/binary_trainer.yml +++ b/configs/training/binary_trainer.yml @@ -1,4 +1,4 @@ -min_epochs: 100 +min_epochs: 20 max_epochs: 100 default_root_dir: &default_root_dir logs logger: csv_logger.yml From 93d47eb5cab42e0b0b4e782778c37dbb84bbb1aa Mon Sep 17 00:00:00 2001 From: schnamo Date: Fri, 4 Apr 2025 16:49:22 +0200 Subject: [PATCH 33/54] dd MUV dataset --- .../datasets/molecule_classification.py | 8 +++++++- configs/data/hiv_moleculenet.yml | 4 ++++ configs/data/muv_moleculenet.yml | 4 ++++ configs/metrics/micro-macro-f1-roc-auc-17.yml | 13 +++++++++++++ configs/training/wandb_logger_no_onto_hiv.yml | 6 ++++++ configs/training/wandb_logger_no_onto_muv.yml | 6 ++++++ configs/training/wandb_logger_onto_hiv copy.yml | 6 ++++++ configs/training/wandb_logger_onto_muv.yml | 6 ++++++ 8 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 configs/data/hiv_moleculenet.yml create mode 100644 configs/data/muv_moleculenet.yml create mode 100644 configs/metrics/micro-macro-f1-roc-auc-17.yml create mode 100644 configs/training/wandb_logger_no_onto_hiv.yml create mode 100644 configs/training/wandb_logger_no_onto_muv.yml create mode 100644 configs/training/wandb_logger_onto_hiv copy.yml create mode 100644 configs/training/wandb_logger_onto_muv.yml diff --git a/chebai/preprocessing/datasets/molecule_classification.py b/chebai/preprocessing/datasets/molecule_classification.py index c21e8b50..15762f66 100644 --- a/chebai/preprocessing/datasets/molecule_classification.py +++ b/chebai/preprocessing/datasets/molecule_classification.py @@ -694,7 +694,7 @@ def download(self) -> None: def setup_processed(self) -> None: """Processes and splits the dataset.""" print("Create splits") - data = list(self._load_data_from_file(os.path.join(self.raw_dir, f"sider_groups4.csv"))) + data = list(self._load_data_from_file(os.path.join(self.raw_dir, f"muv.csv"))) groups = np.array([d["group"] for d in data]) if not all(g is None for g in groups): split_size = int(len(set(groups)) * self.train_split) @@ -777,6 +777,7 @@ def _load_dict(self, input_file_path: str) -> List[Dict]: yield dict(features=smiles, labels=labels, ident=i)# , group=group) # yield self.reader.to_data(dict(features=smiles, labels=labels, ident=i)) + class BaceChem(Bace): """Chemical data reader for Tox21MolNet dataset.""" @@ -803,4 +804,9 @@ class ClinToxChem(ClinTox): class HIVChem(HIV): """Chemical data reader for Tox21MolNet dataset.""" + READER = dr.ChemDataReader + +class MUVChem(MUV): + """Chemical data reader for Tox21MolNet dataset.""" + READER = dr.ChemDataReader \ No newline at end of file diff --git a/configs/data/hiv_moleculenet.yml b/configs/data/hiv_moleculenet.yml new file mode 100644 index 00000000..ad2271b9 --- /dev/null +++ b/configs/data/hiv_moleculenet.yml @@ -0,0 +1,4 @@ +class_path: chebai.preprocessing.datasets.molecule_classification.HIVChem +init_args: + batch_size: 32 + train_split: 0.8 \ No newline at end of file diff --git a/configs/data/muv_moleculenet.yml b/configs/data/muv_moleculenet.yml new file mode 100644 index 00000000..bdb563e2 --- /dev/null +++ b/configs/data/muv_moleculenet.yml @@ -0,0 +1,4 @@ +class_path: chebai.preprocessing.datasets.molecule_classification.MUVChem +init_args: + batch_size: 32 + train_split: 0.8 \ No newline at end of file diff --git a/configs/metrics/micro-macro-f1-roc-auc-17.yml b/configs/metrics/micro-macro-f1-roc-auc-17.yml new file mode 100644 index 00000000..a730c129 --- /dev/null +++ b/configs/metrics/micro-macro-f1-roc-auc-17.yml @@ -0,0 +1,13 @@ +class_path: torchmetrics.MetricCollection +init_args: + metrics: + micro-f1: + class_path: torchmetrics.classification.MultilabelF1Score + init_args: + average: micro + macro-f1: + class_path: chebai.callbacks.epoch_metrics.MacroF1 + roc-auc: + class_path: torchmetrics.classification.MultilabelAUROC + init_args: + num_labels: 17 diff --git a/configs/training/wandb_logger_no_onto_hiv.yml b/configs/training/wandb_logger_no_onto_hiv.yml new file mode 100644 index 00000000..0a4681de --- /dev/null +++ b/configs/training/wandb_logger_no_onto_hiv.yml @@ -0,0 +1,6 @@ +class_path: chebai.loggers.custom.CustomLogger # Extension of Wandb logger +init_args: + save_dir: /Users/ctumes/Cheb-AI/hiv/runs_no_onto/ + project: 'chebai-hiv' + entity: 'ch-tumescheit-university-of-zurich' + log_model: 'all' diff --git a/configs/training/wandb_logger_no_onto_muv.yml b/configs/training/wandb_logger_no_onto_muv.yml new file mode 100644 index 00000000..afc4b4b1 --- /dev/null +++ b/configs/training/wandb_logger_no_onto_muv.yml @@ -0,0 +1,6 @@ +class_path: chebai.loggers.custom.CustomLogger # Extension of Wandb logger +init_args: + save_dir: /Users/ctumes/Cheb-AI/muv/runs_no_onto/ + project: 'chebai-muv' + entity: 'ch-tumescheit-university-of-zurich' + log_model: 'all' diff --git a/configs/training/wandb_logger_onto_hiv copy.yml b/configs/training/wandb_logger_onto_hiv copy.yml new file mode 100644 index 00000000..bb963da9 --- /dev/null +++ b/configs/training/wandb_logger_onto_hiv copy.yml @@ -0,0 +1,6 @@ +class_path: chebai.loggers.custom.CustomLogger # Extension of Wandb logger +init_args: + save_dir: /Users/ctumes/Cheb-AI/hiv/runs_onto/ + project: 'chebai-hiv' + entity: 'ch-tumescheit-university-of-zurich' + log_model: 'all' diff --git a/configs/training/wandb_logger_onto_muv.yml b/configs/training/wandb_logger_onto_muv.yml new file mode 100644 index 00000000..ae91c220 --- /dev/null +++ b/configs/training/wandb_logger_onto_muv.yml @@ -0,0 +1,6 @@ +class_path: chebai.loggers.custom.CustomLogger # Extension of Wandb logger +init_args: + save_dir: /Users/ctumes/Cheb-AI/muv/runs_onto/ + project: 'chebai-muv' + entity: 'ch-tumescheit-university-of-zurich' + log_model: 'all' From 87babcc42f3428c00be281807980fca2bc8426c8 Mon Sep 17 00:00:00 2001 From: schnamo Date: Fri, 18 Apr 2025 09:21:57 +0200 Subject: [PATCH 34/54] debugging --- chebai/models/electra.py | 4 ++-- .../datasets/molecule_classification.py | 17 +++++++++-------- configs/loss/bce.yml | 2 ++ configs/training/wandb_logger_onto_hiv copy.yml | 6 ------ 4 files changed, 13 insertions(+), 16 deletions(-) delete mode 100644 configs/training/wandb_logger_onto_hiv copy.yml diff --git a/chebai/models/electra.py b/chebai/models/electra.py index c84b5b30..f710de90 100644 --- a/chebai/models/electra.py +++ b/chebai/models/electra.py @@ -326,13 +326,13 @@ def _get_prediction_and_labels( d = d[n] if self.model_type == 'classification': # print(self.model_type, ' in electra 324') - d = torch.sigmoid(d) # for mulitclass here softmax instead of sigmoid - #print('blababababab') + d = torch.sigmoid(d) # changing this made a difference for the roc-auc but not the f1, why? if "missing_labels" in loss_kwargs: #print('bla') missing_labels = loss_kwargs["missing_labels"] d = d * (~missing_labels).int().to(device=d.device) + labels = labels * (~missing_labels).int().to(device=d.device) return d, labels.int() if labels is not None else None elif self.model_type == 'regression': return d, labels diff --git a/chebai/preprocessing/datasets/molecule_classification.py b/chebai/preprocessing/datasets/molecule_classification.py index 15762f66..b45509fa 100644 --- a/chebai/preprocessing/datasets/molecule_classification.py +++ b/chebai/preprocessing/datasets/molecule_classification.py @@ -552,7 +552,7 @@ def label_number(self) -> int: @property def raw_file_names(self) -> List[str]: """Returns a list of raw file names.""" - return ["HIV.csv"] + return ["hiv_groups4.csv"] @property def processed_file_names(self) -> List[str]: @@ -562,7 +562,7 @@ def processed_file_names(self) -> List[str]: def download(self) -> None: """Downloads and extracts the dataset.""" - with open(os.path.join(self.raw_dir, "HIV.csv"), "ab") as dst: + with open(os.path.join(self.raw_dir, "hiv_groups4"), "ab") as dst: with request.urlopen(f"https://deepchemdata.s3-us-west-1.amazonaws.com/datasets/HIV.csv",) as src: shutil.copyfileobj(src, dst) @@ -570,7 +570,7 @@ def download(self) -> None: def setup_processed(self) -> None: """Processes and splits the dataset.""" print("Create splits") - data = list(self._load_data_from_file(os.path.join(self.raw_dir, f"HIV.csv"))) + data = list(self._load_data_from_file(os.path.join(self.raw_dir, f"hiv_groups4.csv"))) groups = np.array([d["group"] for d in data]) if not all(g is None for g in groups): print('Group shuffled') @@ -643,11 +643,12 @@ def _load_dict(self, input_file_path: str) -> List[Dict]: with open(input_file_path, "r") as input_file: reader = csv.DictReader(input_file) for row in reader: - i += 1 - smiles = row["smiles"] - labels = [int(row["HIV_active"])] - # group = int(row["group"]) - yield dict(features=smiles, labels=labels, ident=i) #, group=group) + if len(row) > 1: + i += 1 + smiles = row["smiles"] + labels = [int(row["HIV_active"])] + group = int(row["group"]) + yield dict(features=smiles, labels=labels, ident=i, group=group) # yield self.reader.to_data(dict(features=smiles, labels=labels, ident=i)) diff --git a/configs/loss/bce.yml b/configs/loss/bce.yml index e2fc30b8..10135513 100644 --- a/configs/loss/bce.yml +++ b/configs/loss/bce.yml @@ -1 +1,3 @@ class_path: chebai.loss.bce_weighted.BCEWeighted +init_args: + beta: 1000 \ No newline at end of file diff --git a/configs/training/wandb_logger_onto_hiv copy.yml b/configs/training/wandb_logger_onto_hiv copy.yml deleted file mode 100644 index bb963da9..00000000 --- a/configs/training/wandb_logger_onto_hiv copy.yml +++ /dev/null @@ -1,6 +0,0 @@ -class_path: chebai.loggers.custom.CustomLogger # Extension of Wandb logger -init_args: - save_dir: /Users/ctumes/Cheb-AI/hiv/runs_onto/ - project: 'chebai-hiv' - entity: 'ch-tumescheit-university-of-zurich' - log_model: 'all' From ebe049e9514fd141721893c00a0c0defd4bf7f6f Mon Sep 17 00:00:00 2001 From: schnamo Date: Tue, 1 Jul 2025 22:11:37 +0200 Subject: [PATCH 35/54] final updates --- chebai/models/electra.py | 5 ++- .../datasets/molecule_classification.py | 5 +-- chebai/preprocessing/datasets/tox21.py | 2 +- .../micro-macro-f1-roc-auc-17_test.yml | 22 +++++++++++++ configs/model/electra_tox_expl.yml | 15 +++++++++ configs/training/binary_callbacks.yml | 29 ++++++++++++---- configs/training/default_callbacks.yml | 26 +++++++++++---- .../early_stop_callbacks_regression.yml | 14 ++++---- .../training/early_stop_callbacks_tox21.yml | 23 +++++++++---- configs/training/solCur_callbacks.yml | 33 ++++++++++++++----- 10 files changed, 133 insertions(+), 41 deletions(-) create mode 100644 configs/metrics/micro-macro-f1-roc-auc-17_test.yml create mode 100644 configs/model/electra_tox_expl.yml diff --git a/chebai/models/electra.py b/chebai/models/electra.py index f710de90..54ab2a97 100644 --- a/chebai/models/electra.py +++ b/chebai/models/electra.py @@ -329,10 +329,9 @@ def _get_prediction_and_labels( # for mulitclass here softmax instead of sigmoid d = torch.sigmoid(d) # changing this made a difference for the roc-auc but not the f1, why? if "missing_labels" in loss_kwargs: - #print('bla') missing_labels = loss_kwargs["missing_labels"] - d = d * (~missing_labels).int().to(device=d.device) - labels = labels * (~missing_labels).int().to(device=d.device) + d = d * (~missing_labels).int().to(device=d.device) # we set the prob of missing labels to 0 + labels = labels * (~missing_labels).int().to(device=d.device) # we set the labels of missing labels to 0 return d, labels.int() if labels is not None else None elif self.model_type == 'regression': return d, labels diff --git a/chebai/preprocessing/datasets/molecule_classification.py b/chebai/preprocessing/datasets/molecule_classification.py index b45509fa..9c92ad9f 100644 --- a/chebai/preprocessing/datasets/molecule_classification.py +++ b/chebai/preprocessing/datasets/molecule_classification.py @@ -435,6 +435,7 @@ def label_number(self) -> int: def raw_file_names(self) -> List[str]: """Returns a list of raw file names.""" return ["bace.csv"] + # return ["bace_groups4.csv"] @property def processed_file_names(self) -> List[str]: @@ -487,7 +488,7 @@ def setup_processed(self) -> None: ) test_split, validation_split = train_test_split( test_split, train_size=0.5, shuffle=True - ) + ) for k, split in [ ("test", test_split), ("train", train_split), @@ -529,7 +530,7 @@ def _load_dict(self, input_file_path: str) -> List[Dict]: smiles = row["mol"] labels = [int(row["Class"])] # group = row["group"] - yield dict(features=smiles, labels=labels, ident=i) + yield dict(features=smiles, labels=labels, ident=i) # , group=group # yield self.reader.to_data(dict(features=smiles, labels=labels, ident=i)) class HIV(XYBaseDataModule): diff --git a/chebai/preprocessing/datasets/tox21.py b/chebai/preprocessing/datasets/tox21.py index 93380fbd..a73ad800 100644 --- a/chebai/preprocessing/datasets/tox21.py +++ b/chebai/preprocessing/datasets/tox21.py @@ -47,6 +47,7 @@ def label_number(self) -> int: @property def raw_file_names(self) -> List[str]: """Returns a list of raw file names.""" + # return ["tox21.csv"] return ["tox21_groups_04.csv"] @property @@ -143,7 +144,6 @@ def _load_dict(self, input_file_path: str) -> List[Dict]: with open(input_file_path, "r") as input_file: reader = csv.DictReader(input_file) for row in reader: - print(row) smiles = row["smiles"] labels = [ bool(int(float(l))) if len(l) > 1 else None for l in (row[k] for k in self.HEADERS) diff --git a/configs/metrics/micro-macro-f1-roc-auc-17_test.yml b/configs/metrics/micro-macro-f1-roc-auc-17_test.yml new file mode 100644 index 00000000..0a42fb0e --- /dev/null +++ b/configs/metrics/micro-macro-f1-roc-auc-17_test.yml @@ -0,0 +1,22 @@ +class_path: torchmetrics.MetricCollection +init_args: + metrics: + micro-f1: + class_path: torchmetrics.classification.MultilabelF1Score + init_args: + average: micro + num_labels: 17 + macro-f1: + class_path: chebai.callbacks.epoch_metrics.MacroF1 + roc-auc: + class_path: torchmetrics.classification.MultilabelAUROC + init_args: + num_labels: 17 + precision: + class_path: torchmetrics.classification.MultilabelPrecision + init_args: + num_labels: 17 + recall: + class_path: torchmetrics.classification.MultilabelRecall + init_args: + num_labels: 17 \ No newline at end of file diff --git a/configs/model/electra_tox_expl.yml b/configs/model/electra_tox_expl.yml new file mode 100644 index 00000000..e17ad570 --- /dev/null +++ b/configs/model/electra_tox_expl.yml @@ -0,0 +1,15 @@ +class_path: chebai.models.Electra +init_args: + model_type: classification + optimizer_kwargs: + lr: 1e-4 + weight_decay: 0.0001 + config: + vocab_size: 1400 + max_position_embeddings: 1800 + num_attention_heads: 8 + num_hidden_layers: 6 + type_vocab_size: 1 + hidden_size: 256 + hidden_dropout_prob: 0.4 + word_dropout: 0.2 diff --git a/configs/training/binary_callbacks.yml b/configs/training/binary_callbacks.yml index 0cad51f4..013b8c77 100644 --- a/configs/training/binary_callbacks.yml +++ b/configs/training/binary_callbacks.yml @@ -4,19 +4,36 @@ mode: 'max' filename: 'best_f1_{epoch:02d}_{val_loss:.4f}_{val_f1:.4f}_{val_roc-auc:.4f}' every_n_epochs: 1 - save_top_k: 3 + save_top_k: 1 +# - class_path: chebai.callbacks.model_checkpoint.CustomModelCheckpoint +# init_args: +# monitor: val_loss +# mode: 'min' +# filename: 'best_loss_{epoch:02d}_{val_loss:.4f}_{val_f1:.4f}_{val_roc-auc:.4f}' +# every_n_epochs: 1 +# save_top_k: 1 - class_path: chebai.callbacks.model_checkpoint.CustomModelCheckpoint init_args: - monitor: val_loss - mode: 'min' - filename: 'best_loss_{epoch:02d}_{val_loss:.4f}_{val_f1:.4f}_{val_roc-auc:.4f}' + monitor: val_roc-auc + mode: 'max' + filename: 'best_roc-auc_{epoch:02d}_{val_loss:.4f}_{val_f1:.4f}_{val_roc-auc:.4f}' every_n_epochs: 1 - save_top_k: 3 + save_top_k: 1 - class_path: chebai.callbacks.model_checkpoint.CustomModelCheckpoint init_args: filename: 'per_{epoch:02d}_{val_loss:.4f}_{val_f1:.4f}_{val_roc-auc:.4f}' every_n_epochs: 25 - save_top_k: -1 + save_top_k: 1 + +# - class_path: lightning.pytorch.callbacks.early_stopping.EarlyStopping +# init_args: +# monitor: "val_roc-auc" +# min_delta: 0.0 +# patience: 5 +# verbose: False +# mode: "max" + + # - class_path: lightning.pytorch.callbacks.early_stopping.EarlyStopping # init_args: # monitor: "val_loss_epoch" diff --git a/configs/training/default_callbacks.yml b/configs/training/default_callbacks.yml index 628a92b1..ee76e0d5 100644 --- a/configs/training/default_callbacks.yml +++ b/configs/training/default_callbacks.yml @@ -4,16 +4,30 @@ mode: 'max' filename: 'best_micro_f1_{epoch:02d}_{val_loss:.4f}_{val_macro-f1:.4f}_{val_micro-f1:.4f}_{val_roc-auc:.4f}' every_n_epochs: 1 - save_top_k: 3 + save_top_k: 1 +# - class_path: chebai.callbacks.model_checkpoint.CustomModelCheckpoint +# init_args: +# monitor: val_loss +# mode: 'min' +# filename: 'best_loss_{epoch:02d}_{val_loss:.4f}_{val_macro-f1:.4f}_{val_micro-f1:.4f}_{val_roc-auc:.4f}' +# every_n_epochs: 1 +# save_top_k: 1 - class_path: chebai.callbacks.model_checkpoint.CustomModelCheckpoint init_args: - monitor: val_loss - mode: 'min' - filename: 'best_loss_{epoch:02d}_{val_loss:.4f}_{val_macro-f1:.4f}_{val_micro-f1:.4f}_{val_roc-auc:.4f}' + monitor: val_roc-auc + mode: 'max' + filename: 'best_roc-auc_{epoch:02d}_{val_loss:.4f}_{val_macro-f1:.4f}_{val_micro-f1:.4f}_{val_roc-auc:.4f}' every_n_epochs: 1 - save_top_k: 3 + save_top_k: 1 +# - class_path: lightning.pytorch.callbacks.early_stopping.EarlyStopping +# init_args: +# monitor: "val_roc-auc" +# min_delta: 0.0 +# patience: 5 +# verbose: False +# mode: "max" - class_path: chebai.callbacks.model_checkpoint.CustomModelCheckpoint init_args: filename: 'per_{epoch:02d}_{val_loss:.4f}_{val_macro-f1:.4f}_{val_micro-f1:.4f}_{val_roc-auc:.4f}' every_n_epochs: 25 - save_top_k: -1 + save_top_k: 1 diff --git a/configs/training/early_stop_callbacks_regression.yml b/configs/training/early_stop_callbacks_regression.yml index 99986469..ebf314aa 100644 --- a/configs/training/early_stop_callbacks_regression.yml +++ b/configs/training/early_stop_callbacks_regression.yml @@ -10,10 +10,10 @@ filename: 'per_{epoch:02d}_{val_loss:.4f}_{val_mse:.4f}' every_n_epochs: 25 save_top_k: -1 -- class_path: lightning.pytorch.callbacks.early_stopping.EarlyStopping - init_args: - monitor: "val_loss_epoch" - min_delta: 0.0 - patience: 5 - verbose: False - mode: "min" +# - class_path: lightning.pytorch.callbacks.early_stopping.EarlyStopping +# init_args: +# monitor: "val_loss_epoch" +# min_delta: 0.0 +# patience: 5 +# verbose: False +# mode: "min" diff --git a/configs/training/early_stop_callbacks_tox21.yml b/configs/training/early_stop_callbacks_tox21.yml index 647a2eea..468ca2a2 100644 --- a/configs/training/early_stop_callbacks_tox21.yml +++ b/configs/training/early_stop_callbacks_tox21.yml @@ -2,18 +2,27 @@ init_args: monitor: val_micro-f1 mode: 'max' - filename: 'best_{epoch:02d}_{val_loss:.4f}_{val_macro-f1:.4f}_{val_micro-f1:.4f}' + filename: 'best_micro_f1_{epoch:02d}_{val_loss:.4f}_{val_macro-f1:.4f}_{val_micro-f1:.4f}_{val_roc-auc:.4f}' every_n_epochs: 1 - save_top_k: 3 + save_top_k: 1 - class_path: chebai.callbacks.model_checkpoint.CustomModelCheckpoint init_args: - filename: 'per_{epoch:02d}_{val_loss:.4f}_{val_macro-f1:.4f}_{val_micro-f1:.4f}' - every_n_epochs: 25 - save_top_k: -1 + monitor: val_loss + mode: 'min' + filename: 'best_loss_{epoch:02d}_{val_loss:.4f}_{val_macro-f1:.4f}_{val_micro-f1:.4f}_{val_roc-auc:.4f}' + every_n_epochs: 1 + save_top_k: 1 +- class_path: chebai.callbacks.model_checkpoint.CustomModelCheckpoint + init_args: + monitor: val_roc-auc + mode: 'max' + filename: 'best_roc-auc_{epoch:02d}_{val_loss:.4f}_{val_macro-f1:.4f}_{val_micro-f1:.4f}_{val_roc-auc:.4f}' + every_n_epochs: 1 + save_top_k: 1 - class_path: lightning.pytorch.callbacks.early_stopping.EarlyStopping init_args: - monitor: "val_loss_epoch" + monitor: "val_roc-auc" min_delta: 0.0 patience: 5 verbose: False - mode: "min" + mode: "max" diff --git a/configs/training/solCur_callbacks.yml b/configs/training/solCur_callbacks.yml index f0bd45c1..155ab7c6 100644 --- a/configs/training/solCur_callbacks.yml +++ b/configs/training/solCur_callbacks.yml @@ -1,19 +1,34 @@ -- class_path: chebai.callbacks.model_checkpoint.CustomModelCheckpoint - init_args: - monitor: val_loss - mode: 'min' - filename: 'best_loss_{epoch:02d}_{val_loss:.4f}_{val_mse:.4f}_{val_rmse:.4f}_{val_r2:.4f}' - every_n_epochs: 1 - save_top_k: 3 +# - class_path: chebai.callbacks.model_checkpoint.CustomModelCheckpoint +# init_args: +# monitor: val_loss +# mode: 'min' +# filename: 'best_loss_{epoch:02d}_{val_loss:.4f}_{val_mse:.4f}_{val_rmse:.4f}_{val_r2:.4f}' +# every_n_epochs: 1 +# save_top_k: 1 - class_path: chebai.callbacks.model_checkpoint.CustomModelCheckpoint init_args: monitor: val_r2 mode: 'max' filename: 'best_r2_{epoch:02d}_{val_loss:.4f}_{val_mse:.4f}_{val_rmse:.4f}_{val_r2:.4f}' every_n_epochs: 1 - save_top_k: 3 + save_top_k: 1 +- class_path: chebai.callbacks.model_checkpoint.CustomModelCheckpoint + init_args: + monitor: val_rmse + mode: 'min' + filename: 'best_rmse_{epoch:02d}_{val_loss:.4f}_{val_mse:.4f}_{val_rmse:.4f}_{val_r2:.4f}' + every_n_epochs: 1 + save_top_k: 1 +# - class_path: lightning.pytorch.callbacks.early_stopping.EarlyStopping +# init_args: +# monitor: "val_rmse" +# min_delta: 0.0 +# patience: 5 +# verbose: False +# mode: "min" + - class_path: chebai.callbacks.model_checkpoint.CustomModelCheckpoint init_args: filename: 'per_{epoch:02d}_{val_loss:.4f}_{val_mse:.4f}_{val_rmse:.4f}_{val_r2:.4f}' every_n_epochs: 25 - save_top_k: -1 + save_top_k: 1 From 188f32fd6c106eceaae824e690a36d68f3fe6fdc Mon Sep 17 00:00:00 2001 From: schnamo Date: Mon, 29 Sep 2025 10:42:21 +0200 Subject: [PATCH 36/54] add focal loss --- configs/loss/focal_loss_12.yml | 4 ++++ configs/training/wandb_logger.yml | 7 ------- configs/training/wandb_logger_no_onto_bace.yml | 6 ------ configs/training/wandb_logger_no_onto_bbbp.yml | 6 ------ configs/training/wandb_logger_no_onto_clintox.yml | 6 ------ configs/training/wandb_logger_no_onto_esol.yml | 6 ------ configs/training/wandb_logger_no_onto_freesolv.yml | 6 ------ configs/training/wandb_logger_no_onto_hiv.yml | 6 ------ configs/training/wandb_logger_no_onto_lipo.yml | 6 ------ configs/training/wandb_logger_no_onto_muv.yml | 6 ------ configs/training/wandb_logger_no_onto_sider.yml | 6 ------ configs/training/wandb_logger_no_onto_tox.yml | 6 ------ configs/training/wandb_logger_onto.yml | 6 ------ configs/training/wandb_logger_onto_bace.yml | 6 ------ configs/training/wandb_logger_onto_bbbp.yml | 6 ------ configs/training/wandb_logger_onto_clintox.yml | 6 ------ configs/training/wandb_logger_onto_esol.yml | 6 ------ configs/training/wandb_logger_onto_freesolv.yml | 6 ------ configs/training/wandb_logger_onto_lipo.yml | 6 ------ configs/training/wandb_logger_onto_muv.yml | 6 ------ configs/training/wandb_logger_onto_sider.yml | 6 ------ configs/training/wandb_logger_onto_tox.yml | 6 ------ 22 files changed, 4 insertions(+), 127 deletions(-) create mode 100644 configs/loss/focal_loss_12.yml delete mode 100644 configs/training/wandb_logger.yml delete mode 100644 configs/training/wandb_logger_no_onto_bace.yml delete mode 100644 configs/training/wandb_logger_no_onto_bbbp.yml delete mode 100644 configs/training/wandb_logger_no_onto_clintox.yml delete mode 100644 configs/training/wandb_logger_no_onto_esol.yml delete mode 100644 configs/training/wandb_logger_no_onto_freesolv.yml delete mode 100644 configs/training/wandb_logger_no_onto_hiv.yml delete mode 100644 configs/training/wandb_logger_no_onto_lipo.yml delete mode 100644 configs/training/wandb_logger_no_onto_muv.yml delete mode 100644 configs/training/wandb_logger_no_onto_sider.yml delete mode 100644 configs/training/wandb_logger_no_onto_tox.yml delete mode 100644 configs/training/wandb_logger_onto.yml delete mode 100644 configs/training/wandb_logger_onto_bace.yml delete mode 100644 configs/training/wandb_logger_onto_bbbp.yml delete mode 100644 configs/training/wandb_logger_onto_clintox.yml delete mode 100644 configs/training/wandb_logger_onto_esol.yml delete mode 100644 configs/training/wandb_logger_onto_freesolv.yml delete mode 100644 configs/training/wandb_logger_onto_lipo.yml delete mode 100644 configs/training/wandb_logger_onto_muv.yml delete mode 100644 configs/training/wandb_logger_onto_sider.yml delete mode 100644 configs/training/wandb_logger_onto_tox.yml diff --git a/configs/loss/focal_loss_12.yml b/configs/loss/focal_loss_12.yml new file mode 100644 index 00000000..0351a942 --- /dev/null +++ b/configs/loss/focal_loss_12.yml @@ -0,0 +1,4 @@ +class_path: chebai.loss.focal_loss.FocalLoss +init_args: + task_type: multi-label + num_classes: 12 \ No newline at end of file diff --git a/configs/training/wandb_logger.yml b/configs/training/wandb_logger.yml deleted file mode 100644 index f883f387..00000000 --- a/configs/training/wandb_logger.yml +++ /dev/null @@ -1,7 +0,0 @@ -class_path: chebai.loggers.custom.CustomLogger # Extension of Wandb logger -init_args: - save_dir: /Users/ctumes/Cheb-AI/runs_no_onto/ - # version: no-onto - project: 'cheb-ai-sol' - entity: 'ch-tumescheit-university-of-zurich' - log_model: 'all' diff --git a/configs/training/wandb_logger_no_onto_bace.yml b/configs/training/wandb_logger_no_onto_bace.yml deleted file mode 100644 index 84343ad6..00000000 --- a/configs/training/wandb_logger_no_onto_bace.yml +++ /dev/null @@ -1,6 +0,0 @@ -class_path: chebai.loggers.custom.CustomLogger # Extension of Wandb logger -init_args: - save_dir: /Users/ctumes/Cheb-AI/bace/runs_no_onto/ - project: 'chebai-bace' - entity: 'ch-tumescheit-university-of-zurich' - log_model: 'all' diff --git a/configs/training/wandb_logger_no_onto_bbbp.yml b/configs/training/wandb_logger_no_onto_bbbp.yml deleted file mode 100644 index b4e51196..00000000 --- a/configs/training/wandb_logger_no_onto_bbbp.yml +++ /dev/null @@ -1,6 +0,0 @@ -class_path: chebai.loggers.custom.CustomLogger # Extension of Wandb logger -init_args: - save_dir: /Users/ctumes/Cheb-AI/bbbp/runs_no_onto/ - project: 'chebai-bbbp' - entity: 'ch-tumescheit-university-of-zurich' - log_model: 'all' diff --git a/configs/training/wandb_logger_no_onto_clintox.yml b/configs/training/wandb_logger_no_onto_clintox.yml deleted file mode 100644 index 1728530b..00000000 --- a/configs/training/wandb_logger_no_onto_clintox.yml +++ /dev/null @@ -1,6 +0,0 @@ -class_path: chebai.loggers.custom.CustomLogger # Extension of Wandb logger -init_args: - save_dir: /Users/ctumes/Cheb-AI/clintox/runs_no_onto/ - project: 'chebai-clintox' - entity: 'ch-tumescheit-university-of-zurich' - log_model: 'all' diff --git a/configs/training/wandb_logger_no_onto_esol.yml b/configs/training/wandb_logger_no_onto_esol.yml deleted file mode 100644 index 085bf40d..00000000 --- a/configs/training/wandb_logger_no_onto_esol.yml +++ /dev/null @@ -1,6 +0,0 @@ -class_path: chebai.loggers.custom.CustomLogger # Extension of Wandb logger -init_args: - save_dir: /Users/ctumes/Cheb-AI/esol/runs_no_onto/ - project: 'chebai-esol' - entity: 'ch-tumescheit-university-of-zurich' - log_model: 'all' diff --git a/configs/training/wandb_logger_no_onto_freesolv.yml b/configs/training/wandb_logger_no_onto_freesolv.yml deleted file mode 100644 index ed965a88..00000000 --- a/configs/training/wandb_logger_no_onto_freesolv.yml +++ /dev/null @@ -1,6 +0,0 @@ -class_path: chebai.loggers.custom.CustomLogger # Extension of Wandb logger -init_args: - save_dir: /Users/ctumes/Cheb-AI/freesolv/runs_no_onto/ - project: 'chebai-freesolv' - entity: 'ch-tumescheit-university-of-zurich' - log_model: 'all' diff --git a/configs/training/wandb_logger_no_onto_hiv.yml b/configs/training/wandb_logger_no_onto_hiv.yml deleted file mode 100644 index 0a4681de..00000000 --- a/configs/training/wandb_logger_no_onto_hiv.yml +++ /dev/null @@ -1,6 +0,0 @@ -class_path: chebai.loggers.custom.CustomLogger # Extension of Wandb logger -init_args: - save_dir: /Users/ctumes/Cheb-AI/hiv/runs_no_onto/ - project: 'chebai-hiv' - entity: 'ch-tumescheit-university-of-zurich' - log_model: 'all' diff --git a/configs/training/wandb_logger_no_onto_lipo.yml b/configs/training/wandb_logger_no_onto_lipo.yml deleted file mode 100644 index 0d8b551b..00000000 --- a/configs/training/wandb_logger_no_onto_lipo.yml +++ /dev/null @@ -1,6 +0,0 @@ -class_path: chebai.loggers.custom.CustomLogger # Extension of Wandb logger -init_args: - save_dir: /Users/ctumes/Cheb-AI/lipo/runs_no_onto/ - project: 'chebai-lipo' - entity: 'ch-tumescheit-university-of-zurich' - log_model: 'all' diff --git a/configs/training/wandb_logger_no_onto_muv.yml b/configs/training/wandb_logger_no_onto_muv.yml deleted file mode 100644 index afc4b4b1..00000000 --- a/configs/training/wandb_logger_no_onto_muv.yml +++ /dev/null @@ -1,6 +0,0 @@ -class_path: chebai.loggers.custom.CustomLogger # Extension of Wandb logger -init_args: - save_dir: /Users/ctumes/Cheb-AI/muv/runs_no_onto/ - project: 'chebai-muv' - entity: 'ch-tumescheit-university-of-zurich' - log_model: 'all' diff --git a/configs/training/wandb_logger_no_onto_sider.yml b/configs/training/wandb_logger_no_onto_sider.yml deleted file mode 100644 index 96690738..00000000 --- a/configs/training/wandb_logger_no_onto_sider.yml +++ /dev/null @@ -1,6 +0,0 @@ -class_path: chebai.loggers.custom.CustomLogger # Extension of Wandb logger -init_args: - save_dir: /Users/ctumes/Cheb-AI/sider/runs_no_onto/ - project: 'chebai-sider' - entity: 'ch-tumescheit-university-of-zurich' - log_model: 'all' diff --git a/configs/training/wandb_logger_no_onto_tox.yml b/configs/training/wandb_logger_no_onto_tox.yml deleted file mode 100644 index 2bc575dc..00000000 --- a/configs/training/wandb_logger_no_onto_tox.yml +++ /dev/null @@ -1,6 +0,0 @@ -class_path: chebai.loggers.custom.CustomLogger # Extension of Wandb logger -init_args: - save_dir: /Users/ctumes/Cheb-AI/tox/runs_no_onto/ - project: 'chebai-tox' - entity: 'ch-tumescheit-university-of-zurich' - log_model: 'all' diff --git a/configs/training/wandb_logger_onto.yml b/configs/training/wandb_logger_onto.yml deleted file mode 100644 index fdd4cafb..00000000 --- a/configs/training/wandb_logger_onto.yml +++ /dev/null @@ -1,6 +0,0 @@ -class_path: chebai.loggers.custom.CustomLogger # Extension of Wandb logger -init_args: - save_dir: /Users/ctumes/Cheb-AI/runs_onto/ - project: 'cheb-ai-sol' - entity: 'ch-tumescheit-university-of-zurich' - log_model: 'all' diff --git a/configs/training/wandb_logger_onto_bace.yml b/configs/training/wandb_logger_onto_bace.yml deleted file mode 100644 index e979beb6..00000000 --- a/configs/training/wandb_logger_onto_bace.yml +++ /dev/null @@ -1,6 +0,0 @@ -class_path: chebai.loggers.custom.CustomLogger # Extension of Wandb logger -init_args: - save_dir: /Users/ctumes/Cheb-AI/bace/runs_onto/ - project: 'chebai-bace' - entity: 'ch-tumescheit-university-of-zurich' - log_model: 'all' diff --git a/configs/training/wandb_logger_onto_bbbp.yml b/configs/training/wandb_logger_onto_bbbp.yml deleted file mode 100644 index d22eb8e0..00000000 --- a/configs/training/wandb_logger_onto_bbbp.yml +++ /dev/null @@ -1,6 +0,0 @@ -class_path: chebai.loggers.custom.CustomLogger # Extension of Wandb logger -init_args: - save_dir: /Users/ctumes/Cheb-AI/bbbp/runs_onto/ - project: 'chebai-bbbp' - entity: 'ch-tumescheit-university-of-zurich' - log_model: 'all' diff --git a/configs/training/wandb_logger_onto_clintox.yml b/configs/training/wandb_logger_onto_clintox.yml deleted file mode 100644 index f5e51eeb..00000000 --- a/configs/training/wandb_logger_onto_clintox.yml +++ /dev/null @@ -1,6 +0,0 @@ -class_path: chebai.loggers.custom.CustomLogger # Extension of Wandb logger -init_args: - save_dir: /Users/ctumes/Cheb-AI/clintox/runs_onto/ - project: 'chebai-clintox' - entity: 'ch-tumescheit-university-of-zurich' - log_model: 'all' diff --git a/configs/training/wandb_logger_onto_esol.yml b/configs/training/wandb_logger_onto_esol.yml deleted file mode 100644 index 73b6c5be..00000000 --- a/configs/training/wandb_logger_onto_esol.yml +++ /dev/null @@ -1,6 +0,0 @@ -class_path: chebai.loggers.custom.CustomLogger # Extension of Wandb logger -init_args: - save_dir: /Users/ctumes/Cheb-AI/esol/runs_onto/ - project: 'chebai-esol' - entity: 'ch-tumescheit-university-of-zurich' - log_model: 'all' diff --git a/configs/training/wandb_logger_onto_freesolv.yml b/configs/training/wandb_logger_onto_freesolv.yml deleted file mode 100644 index 0907ae6b..00000000 --- a/configs/training/wandb_logger_onto_freesolv.yml +++ /dev/null @@ -1,6 +0,0 @@ -class_path: chebai.loggers.custom.CustomLogger # Extension of Wandb logger -init_args: - save_dir: /Users/ctumes/Cheb-AI/freesolv/runs_onto/ - project: 'chebai-freesolv' - entity: 'ch-tumescheit-university-of-zurich' - log_model: 'all' diff --git a/configs/training/wandb_logger_onto_lipo.yml b/configs/training/wandb_logger_onto_lipo.yml deleted file mode 100644 index 7edb4f0f..00000000 --- a/configs/training/wandb_logger_onto_lipo.yml +++ /dev/null @@ -1,6 +0,0 @@ -class_path: chebai.loggers.custom.CustomLogger # Extension of Wandb logger -init_args: - save_dir: /Users/ctumes/Cheb-AI/lipo/runs_onto/ - project: 'chebai-lipo' - entity: 'ch-tumescheit-university-of-zurich' - log_model: 'all' diff --git a/configs/training/wandb_logger_onto_muv.yml b/configs/training/wandb_logger_onto_muv.yml deleted file mode 100644 index ae91c220..00000000 --- a/configs/training/wandb_logger_onto_muv.yml +++ /dev/null @@ -1,6 +0,0 @@ -class_path: chebai.loggers.custom.CustomLogger # Extension of Wandb logger -init_args: - save_dir: /Users/ctumes/Cheb-AI/muv/runs_onto/ - project: 'chebai-muv' - entity: 'ch-tumescheit-university-of-zurich' - log_model: 'all' diff --git a/configs/training/wandb_logger_onto_sider.yml b/configs/training/wandb_logger_onto_sider.yml deleted file mode 100644 index 69d8e6ea..00000000 --- a/configs/training/wandb_logger_onto_sider.yml +++ /dev/null @@ -1,6 +0,0 @@ -class_path: chebai.loggers.custom.CustomLogger # Extension of Wandb logger -init_args: - save_dir: /Users/ctumes/Cheb-AI/sider/runs_onto/ - project: 'chebai-sider' - entity: 'ch-tumescheit-university-of-zurich' - log_model: 'all' diff --git a/configs/training/wandb_logger_onto_tox.yml b/configs/training/wandb_logger_onto_tox.yml deleted file mode 100644 index 8853af76..00000000 --- a/configs/training/wandb_logger_onto_tox.yml +++ /dev/null @@ -1,6 +0,0 @@ -class_path: chebai.loggers.custom.CustomLogger # Extension of Wandb logger -init_args: - save_dir: /Users/ctumes/Cheb-AI/tox/runs_onto/ - project: 'chebai-tox' - entity: 'ch-tumescheit-university-of-zurich' - log_model: 'all' From dccc2e39e18306888fe59122a37c13e004637647 Mon Sep 17 00:00:00 2001 From: schnamo Date: Mon, 29 Sep 2025 10:53:20 +0200 Subject: [PATCH 37/54] add focal loss --- chebai/loss/focal_loss.py | 137 ++++++++++++++++++++++++++++++++ chebai/preprocessing/collate.py | 4 +- 2 files changed, 140 insertions(+), 1 deletion(-) create mode 100644 chebai/loss/focal_loss.py diff --git a/chebai/loss/focal_loss.py b/chebai/loss/focal_loss.py new file mode 100644 index 00000000..ceafa934 --- /dev/null +++ b/chebai/loss/focal_loss.py @@ -0,0 +1,137 @@ +import torch +import torch.nn as nn +import torch.nn.functional as F + + +# from https://github.com/itakurah/Focal-loss-PyTorch + +class FocalLoss(nn.Module): + def __init__(self, gamma=2, alpha=None, reduction='mean', task_type='binary', num_classes=None): + """ + Unified Focal Loss class for binary, multi-class, and multi-label classification tasks. + :param gamma: Focusing parameter, controls the strength of the modulating factor (1 - p_t)^gamma + :param alpha: Balancing factor, can be a scalar or a tensor for class-wise weights. If None, no class balancing is used. + :param reduction: Specifies the reduction method: 'none' | 'mean' | 'sum' + :param task_type: Specifies the type of task: 'binary', 'multi-class', or 'multi-label' + :param num_classes: Number of classes (only required for multi-class classification) + """ + super(FocalLoss, self).__init__() + self.gamma = gamma + self.alpha = alpha + self.reduction = reduction + self.task_type = task_type + self.num_classes = num_classes + + # Handle alpha for class balancing in multi-class tasks + if task_type == 'multi-class' and alpha is not None and isinstance(alpha, (list, torch.Tensor)): + assert num_classes is not None, "num_classes must be specified for multi-class classification" + if isinstance(alpha, list): + self.alpha = torch.Tensor(alpha) + else: + self.alpha = alpha + + def forward(self, inputs, targets): + """ + Forward pass to compute the Focal Loss based on the specified task type. + :param inputs: Predictions (logits) from the model. + Shape: + - binary/multi-label: (batch_size, num_classes) + - multi-class: (batch_size, num_classes) + :param targets: Ground truth labels. + Shape: + - binary: (batch_size,) + - multi-label: (batch_size, num_classes) + - multi-class: (batch_size,) + """ + if self.task_type == 'binary': + return self.binary_focal_loss(inputs, targets) + elif self.task_type == 'multi-class': + return self.multi_class_focal_loss(inputs, targets) + elif self.task_type == 'multi-label': + return self.multi_label_focal_loss(inputs, targets) + else: + raise ValueError( + f"Unsupported task_type '{self.task_type}'. Use 'binary', 'multi-class', or 'multi-label'.") + + def binary_focal_loss(self, inputs, targets): + """ Focal loss for binary classification. """ + probs = torch.sigmoid(inputs) + targets = targets.float() + + # Compute binary cross entropy + bce_loss = F.binary_cross_entropy_with_logits(inputs, targets, reduction='none') + + # Compute focal weight + p_t = probs * targets + (1 - probs) * (1 - targets) + focal_weight = (1 - p_t) ** self.gamma + + # Apply alpha if provided + if self.alpha is not None: + alpha_t = self.alpha * targets + (1 - self.alpha) * (1 - targets) + bce_loss = alpha_t * bce_loss + + # Apply focal loss weighting + loss = focal_weight * bce_loss + + if self.reduction == 'mean': + return loss.mean() + elif self.reduction == 'sum': + return loss.sum() + return loss + + def multi_class_focal_loss(self, inputs, targets): + """ Focal loss for multi-class classification. """ + if self.alpha is not None: + alpha = self.alpha.to(inputs.device) + + # Convert logits to probabilities with softmax + probs = F.softmax(inputs, dim=1) + + # One-hot encode the targets + targets_one_hot = F.one_hot(targets, num_classes=self.num_classes).float() + + # Compute cross-entropy for each class + ce_loss = -targets_one_hot * torch.log(probs) + + # Compute focal weight + p_t = torch.sum(probs * targets_one_hot, dim=1) # p_t for each sample + focal_weight = (1 - p_t) ** self.gamma + + # Apply alpha if provided (per-class weighting) + if self.alpha is not None: + alpha_t = alpha.gather(0, targets) + ce_loss = alpha_t.unsqueeze(1) * ce_loss + + # Apply focal loss weight + loss = focal_weight.unsqueeze(1) * ce_loss + + if self.reduction == 'mean': + return loss.mean() + elif self.reduction == 'sum': + return loss.sum() + return loss + + def multi_label_focal_loss(self, inputs, targets): + """ Focal loss for multi-label classification. """ + probs = torch.sigmoid(inputs) + + # Compute binary cross entropy + bce_loss = F.binary_cross_entropy_with_logits(inputs, targets, reduction='none') + + # Compute focal weight + p_t = probs * targets + (1 - probs) * (1 - targets) + focal_weight = (1 - p_t) ** self.gamma + + # Apply alpha if provided + if self.alpha is not None: + alpha_t = self.alpha * targets + (1 - self.alpha) * (1 - targets) + bce_loss = alpha_t * bce_loss + + # Apply focal loss weight + loss = focal_weight * bce_loss + + if self.reduction == 'mean': + return loss.mean() + elif self.reduction == 'sum': + return loss.sum() + return loss \ No newline at end of file diff --git a/chebai/preprocessing/collate.py b/chebai/preprocessing/collate.py index 921243f8..0e1f4c0e 100644 --- a/chebai/preprocessing/collate.py +++ b/chebai/preprocessing/collate.py @@ -86,7 +86,9 @@ def __call__(self, data: List[Union[Dict, Tuple]]) -> XYData: x, y, idents = zip( *((d["features"], d["labels"], d.get("ident")) for d in data) ) - missing_labels = [d.get("missing_labels", [False for _ in y[0]]) for d in data] + missing_labels = [ + d.get("missing_labels", [False for _ in y[0]]) for d in data + ] if any(x is not None for x in y): # If any label is not None: (None, None, `1`, None) From d57016f54db414016567dd2b714d62e769fc8eff Mon Sep 17 00:00:00 2001 From: schnamo Date: Mon, 29 Sep 2025 11:10:47 +0200 Subject: [PATCH 38/54] format for lint --- chebai/loss/focal_loss.py | 50 +++++++++++++++++++++------------ chebai/models/base.py | 5 ++-- chebai/models/electra.py | 4 +-- chebai/preprocessing/collate.py | 2 +- 4 files changed, 38 insertions(+), 23 deletions(-) diff --git a/chebai/loss/focal_loss.py b/chebai/loss/focal_loss.py index ceafa934..32cb58c0 100644 --- a/chebai/loss/focal_loss.py +++ b/chebai/loss/focal_loss.py @@ -6,7 +6,14 @@ # from https://github.com/itakurah/Focal-loss-PyTorch class FocalLoss(nn.Module): - def __init__(self, gamma=2, alpha=None, reduction='mean', task_type='binary', num_classes=None): + def __init__( + self, + gamma=2, + alpha=None, + reduction='mean', + task_type='binary', + num_classes=None, + ): """ Unified Focal Loss class for binary, multi-class, and multi-label classification tasks. :param gamma: Focusing parameter, controls the strength of the modulating factor (1 - p_t)^gamma @@ -23,8 +30,14 @@ def __init__(self, gamma=2, alpha=None, reduction='mean', task_type='binary', nu self.num_classes = num_classes # Handle alpha for class balancing in multi-class tasks - if task_type == 'multi-class' and alpha is not None and isinstance(alpha, (list, torch.Tensor)): - assert num_classes is not None, "num_classes must be specified for multi-class classification" + if ( + task_type == 'multi-class' + and alpha is not None + and isinstance(alpha, (list, torch.Tensor)) + ): + assert ( + num_classes is not None + ), "num_classes must be specified for multi-class classification" if isinstance(alpha, list): self.alpha = torch.Tensor(alpha) else: @@ -43,23 +56,24 @@ def forward(self, inputs, targets): - multi-label: (batch_size, num_classes) - multi-class: (batch_size,) """ - if self.task_type == 'binary': + if self.task_type == "binary": return self.binary_focal_loss(inputs, targets) - elif self.task_type == 'multi-class': + elif self.task_type == "multi-class": return self.multi_class_focal_loss(inputs, targets) - elif self.task_type == 'multi-label': + elif self.task_type == "multi-label": return self.multi_label_focal_loss(inputs, targets) else: raise ValueError( - f"Unsupported task_type '{self.task_type}'. Use 'binary', 'multi-class', or 'multi-label'.") + f"Unsupported task_type '{self.task_type}'. Use 'binary', 'multi-class', or 'multi-label'." + ) def binary_focal_loss(self, inputs, targets): - """ Focal loss for binary classification. """ + """Focal loss for binary classification.""" probs = torch.sigmoid(inputs) targets = targets.float() # Compute binary cross entropy - bce_loss = F.binary_cross_entropy_with_logits(inputs, targets, reduction='none') + bce_loss = F.binary_cross_entropy_with_logits(inputs, targets, reduction="none") # Compute focal weight p_t = probs * targets + (1 - probs) * (1 - targets) @@ -73,14 +87,14 @@ def binary_focal_loss(self, inputs, targets): # Apply focal loss weighting loss = focal_weight * bce_loss - if self.reduction == 'mean': + if self.reduction == "mean": return loss.mean() - elif self.reduction == 'sum': + elif self.reduction == "sum": return loss.sum() return loss def multi_class_focal_loss(self, inputs, targets): - """ Focal loss for multi-class classification. """ + """Focal loss for multi-class classification.""" if self.alpha is not None: alpha = self.alpha.to(inputs.device) @@ -105,18 +119,18 @@ def multi_class_focal_loss(self, inputs, targets): # Apply focal loss weight loss = focal_weight.unsqueeze(1) * ce_loss - if self.reduction == 'mean': + if self.reduction == "mean": return loss.mean() - elif self.reduction == 'sum': + elif self.reduction == "sum": return loss.sum() return loss def multi_label_focal_loss(self, inputs, targets): - """ Focal loss for multi-label classification. """ + """Focal loss for multi-label classification.""" probs = torch.sigmoid(inputs) # Compute binary cross entropy - bce_loss = F.binary_cross_entropy_with_logits(inputs, targets, reduction='none') + bce_loss = F.binary_cross_entropy_with_logits(inputs, targets, reduction="none") # Compute focal weight p_t = probs * targets + (1 - probs) * (1 - targets) @@ -130,8 +144,8 @@ def multi_label_focal_loss(self, inputs, targets): # Apply focal loss weight loss = focal_weight * bce_loss - if self.reduction == 'mean': + if self.reduction == "mean": return loss.mean() - elif self.reduction == 'sum': + elif self.reduction == "sum": return loss.sum() return loss \ No newline at end of file diff --git a/chebai/models/base.py b/chebai/models/base.py index cfd0ed79..84493771 100644 --- a/chebai/models/base.py +++ b/chebai/models/base.py @@ -246,7 +246,9 @@ def _execute( predictions, and loss (if applicable). """ assert isinstance(batch, XYData) - batch = batch.to(self.device) # if this is in lightning why do we need to do .to(device)? see https://lightning.ai/docs/pytorch/stable/common/lightning_module.html + batch = batch.to( + self.device + ) data = self._process_batch(batch, batch_idx) labels = data["labels"] model_output = self(data, **data.get("model_kwargs", dict())) @@ -260,7 +262,6 @@ def _execute( loss_kwargs = dict() if self.pass_loss_kwargs: loss_kwargs = loss_kwargs_candidates - # todo: fix this and make it conditional # loss_kwargs["current_epoch"] = self.trainer.current_epoch loss = self.criterion(loss_data, loss_labels, **loss_kwargs) if isinstance(loss, tuple): diff --git a/chebai/models/electra.py b/chebai/models/electra.py index 54ab2a97..90b21e20 100644 --- a/chebai/models/electra.py +++ b/chebai/models/electra.py @@ -231,7 +231,7 @@ def __init__( config: Optional[Dict[str, Any]] = None, pretrained_checkpoint: Optional[str] = None, load_prefix: Optional[str] = None, - model_type='classification', + model_type="classification", **kwargs: Any, ): # Remove this property in order to prevent it from being stored as a @@ -324,7 +324,7 @@ def _get_prediction_and_labels( if "non_null_labels" in loss_kwargs: n = loss_kwargs["non_null_labels"] d = d[n] - if self.model_type == 'classification': + if self.model_type == "classification": # print(self.model_type, ' in electra 324') # for mulitclass here softmax instead of sigmoid d = torch.sigmoid(d) # changing this made a difference for the roc-auc but not the f1, why? diff --git a/chebai/preprocessing/collate.py b/chebai/preprocessing/collate.py index 0e1f4c0e..2d1a03cb 100644 --- a/chebai/preprocessing/collate.py +++ b/chebai/preprocessing/collate.py @@ -88,7 +88,7 @@ def __call__(self, data: List[Union[Dict, Tuple]]) -> XYData: ) missing_labels = [ d.get("missing_labels", [False for _ in y[0]]) for d in data - ] + ] if any(x is not None for x in y): # If any label is not None: (None, None, `1`, None) From 41c0b1ca16db4eb5991d720e8ed7cd2bc73cbd1e Mon Sep 17 00:00:00 2001 From: schnamo Date: Mon, 29 Sep 2025 11:13:14 +0200 Subject: [PATCH 39/54] lint fix --- chebai/loss/focal_loss.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/chebai/loss/focal_loss.py b/chebai/loss/focal_loss.py index 32cb58c0..9bdf7a7c 100644 --- a/chebai/loss/focal_loss.py +++ b/chebai/loss/focal_loss.py @@ -7,12 +7,12 @@ class FocalLoss(nn.Module): def __init__( - self, - gamma=2, - alpha=None, - reduction='mean', - task_type='binary', - num_classes=None, + self, + gamma=2, + alpha=None, + reduction='mean', + task_type='binary', + num_classes=None, ): """ Unified Focal Loss class for binary, multi-class, and multi-label classification tasks. @@ -31,7 +31,7 @@ def __init__( # Handle alpha for class balancing in multi-class tasks if ( - task_type == 'multi-class' + task_type == "multi-class" and alpha is not None and isinstance(alpha, (list, torch.Tensor)) ): @@ -148,4 +148,5 @@ def multi_label_focal_loss(self, inputs, targets): return loss.mean() elif self.reduction == "sum": return loss.sum() - return loss \ No newline at end of file + return loss + \ No newline at end of file From 4c993a22cdc514f0787e3db86287126a2790bc1b Mon Sep 17 00:00:00 2001 From: schnamo Date: Mon, 29 Sep 2025 11:26:00 +0200 Subject: [PATCH 40/54] lint fix --- chebai/callbacks.py | 1 - chebai/loss/focal_loss.py | 16 +- chebai/models/base.py | 4 +- chebai/models/electra.py | 17 +- .../datasets/molecule_classification.py | 158 ++++++++++++------ .../datasets/molecule_regression.py | 48 +++--- chebai/preprocessing/datasets/solCuration.py | 65 ++++--- chebai/preprocessing/datasets/tox21.py | 19 ++- chebai/result/classification.py | 16 +- chebai/result/pretraining.py | 1 - chebai/result/regression.py | 8 +- chebai/result/utils.py | 1 + chebai/train.py | 6 +- chebai/trainer/CustomTrainer.py | 6 +- 14 files changed, 223 insertions(+), 143 deletions(-) diff --git a/chebai/callbacks.py b/chebai/callbacks.py index 52029dd2..764db443 100644 --- a/chebai/callbacks.py +++ b/chebai/callbacks.py @@ -79,7 +79,6 @@ def write_on_epoch_end( labels = labels.tolist() else: labels = [None for _ in idents] - # todo: here adjust for regression !!! output = torch.sigmoid(p["output"]["logits"]).tolist() for i, l, o in zip(idents, labels, output): pred_list.append(dict(ident=i, labels=l, predictions=o)) diff --git a/chebai/loss/focal_loss.py b/chebai/loss/focal_loss.py index 9bdf7a7c..0fcc3c61 100644 --- a/chebai/loss/focal_loss.py +++ b/chebai/loss/focal_loss.py @@ -5,13 +5,14 @@ # from https://github.com/itakurah/Focal-loss-PyTorch + class FocalLoss(nn.Module): def __init__( - self, - gamma=2, - alpha=None, - reduction='mean', - task_type='binary', + self, + gamma=2, + alpha=None, + reduction="mean", + task_type="binary", num_classes=None, ): """ @@ -31,8 +32,8 @@ def __init__( # Handle alpha for class balancing in multi-class tasks if ( - task_type == "multi-class" - and alpha is not None + task_type == "multi-class" + and alpha is not None and isinstance(alpha, (list, torch.Tensor)) ): assert ( @@ -149,4 +150,3 @@ def multi_label_focal_loss(self, inputs, targets): elif self.reduction == "sum": return loss.sum() return loss - \ No newline at end of file diff --git a/chebai/models/base.py b/chebai/models/base.py index 84493771..51494664 100644 --- a/chebai/models/base.py +++ b/chebai/models/base.py @@ -246,9 +246,7 @@ def _execute( predictions, and loss (if applicable). """ assert isinstance(batch, XYData) - batch = batch.to( - self.device - ) + batch = batch.to(self.device) data = self._process_batch(batch, batch_idx) labels = data["labels"] model_output = self(data, **data.get("model_kwargs", dict())) diff --git a/chebai/models/electra.py b/chebai/models/electra.py index 90b21e20..fbfa7832 100644 --- a/chebai/models/electra.py +++ b/chebai/models/electra.py @@ -327,17 +327,22 @@ def _get_prediction_and_labels( if self.model_type == "classification": # print(self.model_type, ' in electra 324') # for mulitclass here softmax instead of sigmoid - d = torch.sigmoid(d) # changing this made a difference for the roc-auc but not the f1, why? + d = torch.sigmoid( + d + ) # changing this made a difference for the roc-auc but not the f1, why? if "missing_labels" in loss_kwargs: missing_labels = loss_kwargs["missing_labels"] - d = d * (~missing_labels).int().to(device=d.device) # we set the prob of missing labels to 0 - labels = labels * (~missing_labels).int().to(device=d.device) # we set the labels of missing labels to 0 + d = d * (~missing_labels).int().to( + device=d.device + ) # we set the prob of missing labels to 0 + labels = labels * (~missing_labels).int().to( + device=d.device + ) # we set the labels of missing labels to 0 return d, labels.int() if labels is not None else None - elif self.model_type == 'regression': + elif self.model_type == "regression": return d, labels else: - raise ValueError('Please specify a valid model type in your model config.') - + raise ValueError("Please specify a valid model type in your model config.") def forward(self, data: Dict[str, Tensor], **kwargs: Any) -> Dict[str, Any]: """ diff --git a/chebai/preprocessing/datasets/molecule_classification.py b/chebai/preprocessing/datasets/molecule_classification.py index 9c92ad9f..20bbca30 100644 --- a/chebai/preprocessing/datasets/molecule_classification.py +++ b/chebai/preprocessing/datasets/molecule_classification.py @@ -9,17 +9,22 @@ from typing import Dict, Generator, List, Optional from rdkit import Chem -from sklearn.model_selection import GroupShuffleSplit, train_test_split, StratifiedShuffleSplit +from sklearn.model_selection import ( + GroupShuffleSplit, + train_test_split, + StratifiedShuffleSplit, +) import numpy as np import pysmiles import torch -from sklearn.preprocessing import LabelBinarizer +from sklearn.preprocessing import LabelBinarizer from chebai.preprocessing import reader as dr from chebai.preprocessing.datasets.base import MergedDataset, XYBaseDataModule from chebai.preprocessing.datasets.chebi import JCIExtendedTokenData from chebai.preprocessing.datasets.pubchem import Hazardous + class ClinTox(XYBaseDataModule): """Data module for ClinTox MoleculeNet dataset.""" @@ -56,13 +61,19 @@ def download(self) -> None: gout.name, ) with gzip.open(gout.name) as gfile: - with open(os.path.join(self.raw_dir, "clintox_groups4.csv"), "wt") as fout: + with open( + os.path.join(self.raw_dir, "clintox_groups4.csv"), "wt" + ) as fout: fout.write(gfile.read().decode()) def setup_processed(self) -> None: """Processes and splits the dataset.""" print("Create splits") - data = list(self._load_data_from_file(os.path.join(self.raw_dir, f"clintox_groups4.csv"))) + data = list( + self._load_data_from_file( + os.path.join(self.raw_dir, f"clintox_groups4.csv") + ) + ) groups = np.array([d["group"] for d in data]) if not all(g is None for g in groups): split_size = int(len(set(groups)) * self.train_split) @@ -83,12 +94,10 @@ def setup_processed(self) -> None: ) train_split = [data[i] for i in train_split_index] test_split = [ - d - for d in (data[temp_split_index[i]] for i in test_split_index) + d for d in (data[temp_split_index[i]] for i in test_split_index) ] validation_split = [ - d - for d in (data[temp_split_index[i]] for i in validation_split_index) + d for d in (data[temp_split_index[i]] for i in validation_split_index) ] else: # print(self.train_split) @@ -188,20 +197,22 @@ def processed_file_names(self) -> List[str]: return ["test.pt", "train.pt", "validation.pt"] def download(self) -> None: - """Downloads and extracts the dataset.""" with open(os.path.join(self.raw_dir, "bbbp.csv"), "ab") as dst: - with request.urlopen(f"https://deepchemdata.s3-us-west-1.amazonaws.com/datasets/BBBP.csv",) as src: + with request.urlopen( + f"https://deepchemdata.s3-us-west-1.amazonaws.com/datasets/BBBP.csv", + ) as src: shutil.copyfileobj(src, dst) - def setup_processed(self) -> None: """Processes and splits the dataset.""" print("Create splits") - data = list(self._load_data_from_file(os.path.join(self.raw_dir, f"bbbp_groups4.csv"))) + data = list( + self._load_data_from_file(os.path.join(self.raw_dir, f"bbbp_groups4.csv")) + ) groups = np.array([d["group"] for d in data]) if not all(g is None for g in groups): - print('Group shuffled') + print("Group shuffled") split_size = int(len(set(groups)) * self.train_split) os.makedirs(self.processed_dir, exist_ok=True) splitter = GroupShuffleSplit(train_size=split_size, n_splits=1) @@ -231,11 +242,11 @@ def setup_processed(self) -> None: ] else: train_split, test_split = train_test_split( - data, train_size=self.train_split, shuffle=True - ) + data, train_size=self.train_split, shuffle=True + ) test_split, validation_split = train_test_split( - test_split, train_size=0.5, shuffle=True - ) + test_split, train_size=0.5, shuffle=True + ) for k, split in [ ("test", test_split), ("train", train_split), @@ -280,21 +291,38 @@ def _load_dict(self, input_file_path: str) -> List[Dict]: yield dict(features=smiles, labels=labels, ident=i, group=group) # yield self.reader.to_data(dict(features=smiles, labels=labels, ident=i)) + class Sider(XYBaseDataModule): """Data module for ClinTox MoleculeNet dataset.""" HEADERS = [ - "Hepatobiliary disorders", "Metabolism and nutrition disorders", "Product issues", "Eye disorders","Investigations", - "Musculoskeletal and connective tissue disorders", "Gastrointestinal disorders", "Social circumstances", - "Immune system disorders", "Reproductive system and breast disorders", - "Neoplasms benign, malignant and unspecified (incl cysts and polyps)", + "Hepatobiliary disorders", + "Metabolism and nutrition disorders", + "Product issues", + "Eye disorders", + "Investigations", + "Musculoskeletal and connective tissue disorders", + "Gastrointestinal disorders", + "Social circumstances", + "Immune system disorders", + "Reproductive system and breast disorders", + "Neoplasms benign, malignant and unspecified (incl cysts and polyps)", "General disorders and administration site conditions", - "Endocrine disorders", "Surgical and medical procedures", "Vascular disorders", "Blood and lymphatic system disorders", - "Skin and subcutaneous tissue disorders", "Congenital, familial and genetic disorders", - "Infections and infestations", "Respiratory, thoracic and mediastinal disorders", "Psychiatric disorders", - "Renal and urinary disorders", "Pregnancy, puerperium and perinatal conditions", - "Ear and labyrinth disorders", "Cardiac disorders", "Nervous system disorders", - "Injury, poisoning and procedural complications" + "Endocrine disorders", + "Surgical and medical procedures", + "Vascular disorders", + "Blood and lymphatic system disorders", + "Skin and subcutaneous tissue disorders", + "Congenital, familial and genetic disorders", + "Infections and infestations", + "Respiratory, thoracic and mediastinal disorders", + "Psychiatric disorders", + "Renal and urinary disorders", + "Pregnancy, puerperium and perinatal conditions", + "Ear and labyrinth disorders", + "Cardiac disorders", + "Nervous system disorders", + "Injury, poisoning and procedural complications", ] @property @@ -331,7 +359,9 @@ def download(self) -> None: def setup_processed(self) -> None: """Processes and splits the dataset.""" print("Create splits") - data = list(self._load_data_from_file(os.path.join(self.raw_dir, f"sider_groups4.csv"))) + data = list( + self._load_data_from_file(os.path.join(self.raw_dir, f"sider_groups4.csv")) + ) groups = np.array([d["group"] for d in data]) if not all(g is None for g in groups): split_size = int(len(set(groups)) * self.train_split) @@ -414,6 +444,7 @@ def _load_dict(self, input_file_path: str) -> List[Dict]: yield dict(features=smiles, labels=labels, ident=i, group=group) # yield self.reader.to_data(dict(features=smiles, labels=labels, ident=i)) + class Bace(XYBaseDataModule): """Data module for ClinTox MoleculeNet dataset.""" @@ -443,13 +474,13 @@ def processed_file_names(self) -> List[str]: return ["test.pt", "train.pt", "validation.pt"] def download(self) -> None: - """Downloads and extracts the dataset.""" with open(os.path.join(self.raw_dir, "bace.csv"), "ab") as dst: - with request.urlopen(f"https://deepchemdata.s3-us-west-1.amazonaws.com/datasets/bace.csv",) as src: + with request.urlopen( + f"https://deepchemdata.s3-us-west-1.amazonaws.com/datasets/bace.csv", + ) as src: shutil.copyfileobj(src, dst) - def setup_processed(self) -> None: """Processes and splits the dataset.""" print("Create splits") @@ -484,11 +515,11 @@ def setup_processed(self) -> None: # ] # else: train_split, test_split = train_test_split( - data, train_size=self.train_split, shuffle=True - ) + data, train_size=self.train_split, shuffle=True + ) test_split, validation_split = train_test_split( - test_split, train_size=0.5, shuffle=True - ) + test_split, train_size=0.5, shuffle=True + ) for k, split in [ ("test", test_split), ("train", train_split), @@ -530,9 +561,10 @@ def _load_dict(self, input_file_path: str) -> List[Dict]: smiles = row["mol"] labels = [int(row["Class"])] # group = row["group"] - yield dict(features=smiles, labels=labels, ident=i) # , group=group + yield dict(features=smiles, labels=labels, ident=i) # , group=group # yield self.reader.to_data(dict(features=smiles, labels=labels, ident=i)) + class HIV(XYBaseDataModule): """Data module for ClinTox MoleculeNet dataset.""" @@ -561,20 +593,22 @@ def processed_file_names(self) -> List[str]: return ["test.pt", "train.pt", "validation.pt"] def download(self) -> None: - """Downloads and extracts the dataset.""" with open(os.path.join(self.raw_dir, "hiv_groups4"), "ab") as dst: - with request.urlopen(f"https://deepchemdata.s3-us-west-1.amazonaws.com/datasets/HIV.csv",) as src: + with request.urlopen( + f"https://deepchemdata.s3-us-west-1.amazonaws.com/datasets/HIV.csv", + ) as src: shutil.copyfileobj(src, dst) - def setup_processed(self) -> None: """Processes and splits the dataset.""" print("Create splits") - data = list(self._load_data_from_file(os.path.join(self.raw_dir, f"hiv_groups4.csv"))) + data = list( + self._load_data_from_file(os.path.join(self.raw_dir, f"hiv_groups4.csv")) + ) groups = np.array([d["group"] for d in data]) if not all(g is None for g in groups): - print('Group shuffled') + print("Group shuffled") split_size = int(len(set(groups)) * self.train_split) os.makedirs(self.processed_dir, exist_ok=True) splitter = GroupShuffleSplit(train_size=split_size, n_splits=1) @@ -593,20 +627,18 @@ def setup_processed(self) -> None: ) train_split = [data[i] for i in train_split_index] test_split = [ - d - for d in (data[temp_split_index[i]] for i in test_split_index) + d for d in (data[temp_split_index[i]] for i in test_split_index) ] validation_split = [ - d - for d in (data[temp_split_index[i]] for i in validation_split_index) + d for d in (data[temp_split_index[i]] for i in validation_split_index) ] else: train_split, test_split = train_test_split( - data, train_size=self.train_split, shuffle=True - ) + data, train_size=self.train_split, shuffle=True + ) test_split, validation_split = train_test_split( - test_split, train_size=0.5, shuffle=True - ) + test_split, train_size=0.5, shuffle=True + ) for k, split in [ ("test", test_split), ("train", train_split), @@ -657,9 +689,23 @@ class MUV(XYBaseDataModule): """Data module for ClinTox MoleculeNet dataset.""" HEADERS = [ - "MUV-466","MUV-548","MUV-600","MUV-644","MUV-652","MUV-689", - "MUV-692","MUV-712","MUV-713","MUV-733","MUV-737","MUV-810", - "MUV-832","MUV-846","MUV-852","MUV-858","MUV-859" + "MUV-466", + "MUV-548", + "MUV-600", + "MUV-644", + "MUV-652", + "MUV-689", + "MUV-692", + "MUV-712", + "MUV-713", + "MUV-733", + "MUV-737", + "MUV-810", + "MUV-832", + "MUV-846", + "MUV-852", + "MUV-858", + "MUV-859", ] @property @@ -776,14 +822,15 @@ def _load_dict(self, input_file_path: str) -> List[Dict]: bool(int(l)) if l else None for l in (row[k] for k in self.HEADERS) ] # group = row["group"] - yield dict(features=smiles, labels=labels, ident=i)# , group=group) + yield dict(features=smiles, labels=labels, ident=i) # , group=group) # yield self.reader.to_data(dict(features=smiles, labels=labels, ident=i)) class BaceChem(Bace): """Chemical data reader for Tox21MolNet dataset.""" - READER = dr.ChemDataReader + READER = dr.ChemDataReader + class SiderChem(Sider): """Chemical data reader for Tox21MolNet dataset.""" @@ -808,7 +855,8 @@ class HIVChem(HIV): READER = dr.ChemDataReader + class MUVChem(MUV): """Chemical data reader for Tox21MolNet dataset.""" - READER = dr.ChemDataReader \ No newline at end of file + READER = dr.ChemDataReader diff --git a/chebai/preprocessing/datasets/molecule_regression.py b/chebai/preprocessing/datasets/molecule_regression.py index 435b4d68..f6d428cd 100644 --- a/chebai/preprocessing/datasets/molecule_regression.py +++ b/chebai/preprocessing/datasets/molecule_regression.py @@ -13,13 +13,14 @@ import numpy as np import pysmiles import torch -from sklearn.preprocessing import LabelBinarizer +from sklearn.preprocessing import LabelBinarizer from chebai.preprocessing import reader as dr from chebai.preprocessing.datasets.base import MergedDataset, XYBaseDataModule from chebai.preprocessing.datasets.chebi import JCIExtendedTokenData from chebai.preprocessing.datasets.pubchem import Hazardous + class Lipo(XYBaseDataModule): HEADERS = [ "exp", @@ -42,11 +43,12 @@ def processed_file_names(self): return ["test.pt", "train.pt", "validation.pt"] def download(self): - # download + # download with open(os.path.join(self.raw_dir, "Lipo.csv"), "ab") as dst: - with request.urlopen(f"https://deepchemdata.s3-us-west-1.amazonaws.com/datasets/Lipophilicity.csv",) as src: + with request.urlopen( + f"https://deepchemdata.s3-us-west-1.amazonaws.com/datasets/Lipophilicity.csv", + ) as src: shutil.copyfileobj(src, dst) - def setup_processed(self): print("Create splits") @@ -76,10 +78,12 @@ def setup(self, **kwargs): for f in self.raw_file_names ): self.download() - print([ - not os.path.isfile(os.path.join(self.processed_dir, f)) - for f in self.processed_file_names - ]) + print( + [ + not os.path.isfile(os.path.join(self.processed_dir, f)) + for f in self.processed_file_names + ] + ) if any( not os.path.isfile(os.path.join(self.processed_dir, f)) for f in self.processed_file_names @@ -104,7 +108,7 @@ def _load_dict(self, input_file_path: str) -> List[Dict]: smiles_l.append(row["smiles"]) labels_l.append(float(row["exp"])) - for i in range(0,len(smiles_l)): + for i in range(0, len(smiles_l)): yield dict(features=smiles_l[i], labels=[labels_l[i]], ident=i) # yield self.reader.to_data(dict(features=smiles_l[i], labels=[labels_l[i]], ident=i)) @@ -131,15 +135,18 @@ def processed_file_names(self): return ["test.pt", "train.pt", "validation.pt"] def download(self): - # download + # download with open(os.path.join(self.raw_dir, "FreeSolv.csv"), "ab") as dst: - with request.urlopen(f"https://deepchemdata.s3-us-west-1.amazonaws.com/datasets/SAMPL.csv",) as src: + with request.urlopen( + f"https://deepchemdata.s3-us-west-1.amazonaws.com/datasets/SAMPL.csv", + ) as src: shutil.copyfileobj(src, dst) - def setup_processed(self): print("Create splits") - data = list(self._load_data_from_file(os.path.join(self.raw_dir, f"FreeSolv.csv"))) + data = list( + self._load_data_from_file(os.path.join(self.raw_dir, f"FreeSolv.csv")) + ) print(len(data)) if 0 == 0: train_split, test_split = train_test_split( @@ -165,10 +172,12 @@ def setup(self, **kwargs): for f in self.raw_file_names ): self.download() - print([ - not os.path.isfile(os.path.join(self.processed_dir, f)) - for f in self.processed_file_names - ]) + print( + [ + not os.path.isfile(os.path.join(self.processed_dir, f)) + for f in self.processed_file_names + ] + ) if any( not os.path.isfile(os.path.join(self.processed_dir, f)) for f in self.processed_file_names @@ -193,7 +202,7 @@ def _load_dict(self, input_file_path: str) -> List[Dict]: smiles_l.append(row["smiles"]) labels_l.append(float(row["expt"])) - for i in range(0,len(smiles_l)): + for i in range(0, len(smiles_l)): yield dict(features=smiles_l[i], labels=[labels_l[i]], ident=i) # yield self.reader.to_data(dict(features=smiles_l[i], labels=[labels_l[i]], ident=i)) @@ -203,7 +212,8 @@ class LipoChem(Lipo): READER = dr.ChemDataReader + class FreeSolvChem(FreeSolv): """Chemical data reader for the solubility dataset.""" - READER = dr.ChemDataReader \ No newline at end of file + READER = dr.ChemDataReader diff --git a/chebai/preprocessing/datasets/solCuration.py b/chebai/preprocessing/datasets/solCuration.py index b39d69de..85154eb1 100644 --- a/chebai/preprocessing/datasets/solCuration.py +++ b/chebai/preprocessing/datasets/solCuration.py @@ -13,7 +13,7 @@ import numpy as np import pysmiles import torch -from sklearn.preprocessing import LabelBinarizer +from sklearn.preprocessing import LabelBinarizer from chebai.preprocessing import reader as dr from chebai.preprocessing.datasets.base import MergedDataset, XYBaseDataModule @@ -44,20 +44,23 @@ def processed_file_names(self): def download(self): # download and combine all the available curated datasets from xxx - db_sol = ['aqsol','aqua','esol','ochem','phys'] + db_sol = ["aqsol", "aqua", "esol", "ochem", "phys"] with open(os.path.join(self.raw_dir, "solCuration.csv"), "ab") as dst: for i, db in enumerate(db_sol): - with request.urlopen(f"https://raw.githubusercontent.com/Mengjintao/SolCuration/master/cure/{db}_cure.csv",) as src: + with request.urlopen( + f"https://raw.githubusercontent.com/Mengjintao/SolCuration/master/cure/{db}_cure.csv", + ) as src: if i > 0: src.readline() shutil.copyfileobj(src, dst) - def setup_processed(self): print("Create splits") print(self.train_split) print(os.path.join(self.raw_dir, f"solCuration.csv")) - data = list(self._load_data_from_file(os.path.join(self.raw_dir, f"solCuration.csv"))) + data = list( + self._load_data_from_file(os.path.join(self.raw_dir, f"solCuration.csv")) + ) print(len(data)) # data = self._load_data_from_file(os.path.join(self.raw_dir, f"solCuration.csv")) if 0 == 0: @@ -84,10 +87,12 @@ def setup(self, **kwargs): for f in self.raw_file_names ): self.download() - print([ - not os.path.isfile(os.path.join(self.processed_dir, f)) - for f in self.processed_file_names - ]) + print( + [ + not os.path.isfile(os.path.join(self.processed_dir, f)) + for f in self.processed_file_names + ] + ) if any( not os.path.isfile(os.path.join(self.processed_dir, f)) for f in self.processed_file_names @@ -113,16 +118,19 @@ def _load_data_from_file(self, input_file_path: str) -> List[Dict]: smiles_l.append(row["smiles"]) labels_l.append(float(row["logS"])) # print(len(smiles_l), len(labels_l)) - # labels_l.append(np.floor(float(row["logS"]))) - # onehotencoding - # label_binarizer = LabelBinarizer() - # label_binarizer.fit(labels_l) - # onehot_label_l = label_binarizer.transform(labels_l) + # labels_l.append(np.floor(float(row["logS"]))) + # onehotencoding + # label_binarizer = LabelBinarizer() + # label_binarizer.fit(labels_l) + # onehot_label_l = label_binarizer.transform(labels_l) # normalise data to be between 0 and 1 # labels_norm = [(float(label)-min(labels_l))/(max(labels_l)-min(labels_l)) for label in labels_l] - for i in range(0,len(smiles_l)): - yield self.reader.to_data(dict(features=smiles_l[i], labels=[labels_l[i]], ident=i)) + for i in range(0, len(smiles_l)): + yield self.reader.to_data( + dict(features=smiles_l[i], labels=[labels_l[i]], ident=i) + ) + class SolESOL(XYBaseDataModule): HEADERS = [ @@ -146,15 +154,18 @@ def processed_file_names(self): return ["test.pt", "train.pt", "validation.pt"] def download(self): - # download + # download with open(os.path.join(self.raw_dir, "solESOL.csv"), "ab") as dst: - with request.urlopen(f"https://deepchemdata.s3-us-west-1.amazonaws.com/datasets/delaney-processed.csv",) as src: + with request.urlopen( + f"https://deepchemdata.s3-us-west-1.amazonaws.com/datasets/delaney-processed.csv", + ) as src: shutil.copyfileobj(src, dst) - def setup_processed(self): print("Create splits") - data = list(self._load_data_from_file(os.path.join(self.raw_dir, f"solESOL.csv"))) + data = list( + self._load_data_from_file(os.path.join(self.raw_dir, f"solESOL.csv")) + ) print(len(data)) # data = self._load_data_from_file(os.path.join(self.raw_dir, f"solCuration.csv")) if 0 == 0: @@ -181,10 +192,12 @@ def setup(self, **kwargs): for f in self.raw_file_names ): self.download() - print([ - not os.path.isfile(os.path.join(self.processed_dir, f)) - for f in self.processed_file_names - ]) + print( + [ + not os.path.isfile(os.path.join(self.processed_dir, f)) + for f in self.processed_file_names + ] + ) if any( not os.path.isfile(os.path.join(self.processed_dir, f)) for f in self.processed_file_names @@ -209,7 +222,7 @@ def _load_dict(self, input_file_path: str) -> List[Dict]: smiles_l.append(row["smiles"]) labels_l.append(float(row["measured log solubility in mols per litre"])) - for i in range(0,len(smiles_l)): + for i in range(0, len(smiles_l)): yield dict(features=smiles_l[i], labels=[labels_l[i]], ident=i) # yield self.reader.to_data(dict(features=smiles_l[i], labels=[labels_l[i]], ident=i)) @@ -223,4 +236,4 @@ class SolCurationChem(SolCuration): class SolESOLChem(SolESOL): """Chemical data reader for the solubility dataset.""" - READER = dr.ChemDataReader \ No newline at end of file + READER = dr.ChemDataReader diff --git a/chebai/preprocessing/datasets/tox21.py b/chebai/preprocessing/datasets/tox21.py index a73ad800..9bf1866e 100644 --- a/chebai/preprocessing/datasets/tox21.py +++ b/chebai/preprocessing/datasets/tox21.py @@ -10,7 +10,11 @@ import numpy as np import torch from rdkit import Chem -from sklearn.model_selection import GroupShuffleSplit, train_test_split, StratifiedShuffleSplit +from sklearn.model_selection import ( + GroupShuffleSplit, + train_test_split, + StratifiedShuffleSplit, +) from chebai.preprocessing import reader as dr from chebai.preprocessing.datasets.base import XYBaseDataModule @@ -69,7 +73,11 @@ def download(self) -> None: def setup_processed(self) -> None: """Processes and splits the dataset.""" print("Create splits") - data = list(self._load_data_from_file(os.path.join(self.raw_dir, f"tox21_groups_04.csv"))) + data = list( + self._load_data_from_file( + os.path.join(self.raw_dir, f"tox21_groups_04.csv") + ) + ) groups = np.array([d.get("group") for d in data]) if not all(g is None for g in groups): @@ -146,10 +154,13 @@ def _load_dict(self, input_file_path: str) -> List[Dict]: for row in reader: smiles = row["smiles"] labels = [ - bool(int(float(l))) if len(l) > 1 else None for l in (row[k] for k in self.HEADERS) + bool(int(float(l))) if len(l) > 1 else None + for l in (row[k] for k in self.HEADERS) ] group = int(row["group"]) - yield dict(features=smiles, labels=labels, ident=row["mol_id"], group=group) + yield dict( + features=smiles, labels=labels, ident=row["mol_id"], group=group + ) # yield self.reader.to_data(dict(features=smiles, labels=labels, ident=row["mol_id"])) diff --git a/chebai/result/classification.py b/chebai/result/classification.py index c3d932d6..67cebe08 100644 --- a/chebai/result/classification.py +++ b/chebai/result/classification.py @@ -12,7 +12,7 @@ BinaryF1Score, BinaryAUROC, BinaryAveragePrecision, - MultilabelAveragePrecision + MultilabelAveragePrecision, ) from chebai.callbacks.epoch_metrics import BalancedAccuracy, MacroF1 @@ -111,16 +111,18 @@ def print_metrics( f'Found {len(zeros)} classes with F1-score == 0 (and non-zero labels): {", ".join(zeros)}' ) + def metrics_classification_multilabel( preds: Tensor, labels: Tensor, - device: torch.device,): + device: torch.device, +): if device != labels.device: device = labels.device my_bal_acc = BalancedAccuracy(preds.shape[1]).to(device=device) - + bal_acc = my_bal_acc(preds, labels).cpu().numpy() my_f1_macro = MultilabelF1Score(preds.shape[1], average="micro").to(device=device) f1_micro = MacroF1(preds.shape[1]).to(device=device) @@ -134,10 +136,12 @@ def metrics_classification_multilabel( return auc_roc, macro_f1, micro_f1, bal_acc, prc_auc + def metrics_classification_binary( preds: Tensor, labels: Tensor, - device: torch.device,): + device: torch.device, +): if device != labels.device: device = labels.device @@ -146,7 +150,7 @@ def metrics_classification_binary( my_f1 = BinaryF1Score().to(device=device) my_av_prec = BinaryAveragePrecision().to(device=device) my_bal_acc = BalancedAccuracy(preds.shape[1]).to(device=device) - + bal_acc = my_bal_acc(preds, labels).cpu().numpy() auc_roc = my_auc_roc(preds, labels).cpu().numpy() # my_auc_roc.update(preds.cpu()[:, 0], labels.cpu()[:, 0]) @@ -154,4 +158,4 @@ def metrics_classification_binary( f1_score = my_f1(preds, labels).cpu().numpy() prc_auc = my_av_prec(preds, labels).cpu().numpy() - return auc_roc, f1_score, bal_acc, prc_auc \ No newline at end of file + return auc_roc, f1_score, bal_acc, prc_auc diff --git a/chebai/result/pretraining.py b/chebai/result/pretraining.py index 9ec90e83..8d712f21 100644 --- a/chebai/result/pretraining.py +++ b/chebai/result/pretraining.py @@ -41,7 +41,6 @@ def evaluate_model(logs_base_path, model_filename, data_module): for row in tqdm.tqdm(data_list): processable_data = model._process_batch(collate([row]), 0) model_output = model(processable_data, **processable_data["model_kwargs"]) - # todo fix this preds, labels = model._get_prediction_and_labels( processable_data, processable_data["labels"], model_output ) diff --git a/chebai/result/regression.py b/chebai/result/regression.py index bfd544db..0ea2ee1c 100644 --- a/chebai/result/regression.py +++ b/chebai/result/regression.py @@ -54,15 +54,15 @@ def metrics_regression( """ mse = MeanSquaredError() mse = mse.to(labels.device) - - rmse = MeanSquaredError(squared = False) + + rmse = MeanSquaredError(squared=False) rmse = rmse.to(labels.device) - return(mse(preds, labels), rmse(preds, labels)) + return (mse(preds, labels), rmse(preds, labels)) # print(f"Micro-F1: {f1_micro(preds, labels):3f}") # print(f"Balanced Accuracy: {my_bal_acc(preds, labels):3f}") - + # if markdown_output: # print( # f"| Model | MSE | RMSE | Macro-Precision | Micro-Precision | Macro-Recall | Micro-Recall | Balanced Accuracy" diff --git a/chebai/result/utils.py b/chebai/result/utils.py index dce9cb42..9517f415 100644 --- a/chebai/result/utils.py +++ b/chebai/result/utils.py @@ -168,6 +168,7 @@ def evaluate_model( os.path.join(buffer_dir, f"labels{save_ind:03d}.pt"), ) + def evaluate_model_regression( model: ChebaiBaseNet, data_module: XYBaseDataModule, diff --git a/chebai/train.py b/chebai/train.py index 096f6183..4a2c0b27 100644 --- a/chebai/train.py +++ b/chebai/train.py @@ -46,8 +46,6 @@ def eval_model( for batch in dataset: for molecule, label in batch: model_outputs = model(molecule) - # todo: this is also just for classification, adjust to regression - print("THESE SHOULD BE PROBAS (in train.py):", model_outputs) prediction = [1.0 if i > 0.5 else 0.0 for i in model_outputs] predictions.append(prediction) raw_values.append(model_outputs) @@ -95,7 +93,7 @@ def crawl_info( def collate( - batch: List[Tuple[Molecule, torch.Tensor]] + batch: List[Tuple[Molecule, torch.Tensor]], ) -> Tuple[List[Molecule], torch.Tensor]: """ Collate function for DataLoader. @@ -148,8 +146,6 @@ def _execute( prediction = model(molecules) loss = loss_fn(prediction, labels) data_size += 1 - # todo: this is also just for classification, adjust to regression - print("THESE SHOULD BE PROBAS (in train.py):", prediction) f1 += f1_score(prediction > 0.5, labels > 0.5, average="micro") train_running_loss += loss.item() diff --git a/chebai/trainer/CustomTrainer.py b/chebai/trainer/CustomTrainer.py index 1638c450..403debb8 100644 --- a/chebai/trainer/CustomTrainer.py +++ b/chebai/trainer/CustomTrainer.py @@ -123,15 +123,11 @@ def _predict_smiles( ) features = torch.cat((cls_tokens, x), dim=1) model_output = model({"features": features}) - print(model.model_type) - # todo: check again - if model.model_type == 'regression': - # todo: do we actually have logits here? + if model.model_type == "regression": preds = model_output["logits"] else: preds = torch.sigmoid(model_output["logits"]) - print(preds.shape) return preds @property From 4aa1771b4fa64077c3ef40272f860827914e5ac7 Mon Sep 17 00:00:00 2001 From: schnamo Date: Mon, 29 Sep 2025 11:34:06 +0200 Subject: [PATCH 41/54] add regression to readme --- README.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 3448206e..a3c5cd77 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,10 @@ ChEBai is a deep learning library designed for the integration of deep learning methods with chemical ontologies, particularly ChEBI. The library emphasizes the incorporation of the semantic qualities of the ontology into the learning process. +## News + +We now support regression tasks! + ## Note for developers If you have used ChEBai before PR #39, the file structure in which your ChEBI-data is saved has changed. This means that @@ -57,11 +61,16 @@ A command with additional options may look like this: python3 -m chebai fit --trainer=configs/training/default_trainer.yml --model=configs/model/electra.yml --model.train_metrics=configs/metrics/micro-macro-f1.yml --model.test_metrics=configs/metrics/micro-macro-f1.yml --model.val_metrics=configs/metrics/micro-macro-f1.yml --model.pretrained_checkpoint=electra_pretrained.ckpt --model.load_prefix=generator. --data=configs/data/chebi50.yml --model.out_dim=1446 --model.criterion=configs/loss/bce.yml --data.init_args.batch_size=10 --trainer.logger.init_args.name=chebi50_bce_unweighted --data.init_args.num_workers=9 --model.pass_loss_kwargs=false --data.init_args.chebi_version=231 --data.init_args.data_limit=1000 ``` -### Fine-tuning for Toxicity prediction +### Fine-tuning for classification tasks, e.g. Toxicity prediction ``` python -m chebai fit --config=[path-to-your-tox21-config] --trainer.callbacks=configs/training/default_callbacks.yml --model.pretrained_checkpoint=[path-to-pretrained-model] ``` +### Fine-tuning for regression tasks, e.g. solubility prediction +``` +python -m chebai fit --config=[path-to-your-esol-config] --trainer.callbacks=configs/training/solCur_callbacks.yml --model.pretrained_checkpoint=[path-to-pretrained-model] +``` + ### Predicting classes given SMILES strings ``` python3 -m chebai predict_from_file --model=[path-to-model-config] --checkpoint_path=[path-to-model] --input_path={path-to-file-containing-smiles] [--classes_path=[path-to-classes-file]] [--save_to=[path-to-output]] From d411c9eb3fd32274446392d037fe278e5e3102ef Mon Sep 17 00:00:00 2001 From: schnamo Date: Wed, 8 Jan 2025 16:48:14 +0100 Subject: [PATCH 42/54] fix union expression --- chebai/loss/semantic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chebai/loss/semantic.py b/chebai/loss/semantic.py index 89abb175..a9e78964 100644 --- a/chebai/loss/semantic.py +++ b/chebai/loss/semantic.py @@ -62,7 +62,7 @@ def __init__( pos_epsilon: float = 0.01, multiply_by_softmax: bool = False, use_sigmoidal_implication: bool = False, - weight_epoch_dependent: Union[bool | tuple[int, int]] = False, + weight_epoch_dependent: Union[bool, tuple[int, int]] = False, start_at_epoch: int = 0, violations_per_cls_aggregator: Literal[ "sum", "max", "mean", "log-sum", "log-max", "log-mean" From 18d8e02d24fcff78ba19f32172054e316ece8d35 Mon Sep 17 00:00:00 2001 From: schnamo Date: Wed, 8 Jan 2025 17:04:27 +0100 Subject: [PATCH 43/54] fix tuple issue to make it backwards compatible --- chebai/loss/semantic.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/chebai/loss/semantic.py b/chebai/loss/semantic.py index a9e78964..3fef3085 100644 --- a/chebai/loss/semantic.py +++ b/chebai/loss/semantic.py @@ -2,7 +2,7 @@ import math import os import pickle -from typing import TYPE_CHECKING, List, Literal, Union +from typing import TYPE_CHECKING, List, Literal, Union, Tuple import torch @@ -62,7 +62,7 @@ def __init__( pos_epsilon: float = 0.01, multiply_by_softmax: bool = False, use_sigmoidal_implication: bool = False, - weight_epoch_dependent: Union[bool, tuple[int, int]] = False, + weight_epoch_dependent: Union[bool, Tuple[int, int]] = False, start_at_epoch: int = 0, violations_per_cls_aggregator: Literal[ "sum", "max", "mean", "log-sum", "log-max", "log-mean" From ed1d4b4a51182eb087a70999a30eb3ed42c935ae Mon Sep 17 00:00:00 2001 From: Charlotte Tumescheit Date: Wed, 29 Oct 2025 14:23:36 +0100 Subject: [PATCH 44/54] adjust to current dev branch --- chebai/cli.py | 28 ++++---- chebai/models/electra.py | 4 +- chebai/preprocessing/datasets/tox21.py | 2 +- chebai/result/molplot.py | 5 +- tutorials/demo_process_results.ipynb | 92 +++++++++++++++++++++++--- 5 files changed, 105 insertions(+), 26 deletions(-) diff --git a/chebai/cli.py b/chebai/cli.py index 3b0cede2..379d5e80 100644 --- a/chebai/cli.py +++ b/chebai/cli.py @@ -46,18 +46,18 @@ def call_data_methods(data: Type[XYBaseDataModule]): data.setup() return data.num_of_labels - # parser.link_arguments( - # "data", - # "model.init_args.out_dim", - # apply_on="instantiate", - # compute_fn=call_data_methods, - # ) + parser.link_arguments( + "data", + "model.init_args.out_dim", + apply_on="instantiate", + compute_fn=call_data_methods, + ) - # parser.link_arguments( - # "data.feature_vector_size", - # "model.init_args.input_dim", - # apply_on="instantiate", - # ) + parser.link_arguments( + "data.feature_vector_size", + "model.init_args.input_dim", + apply_on="instantiate", + ) for kind in ("train", "val", "test"): for average in ("micro-f1", "macro-f1", "balanced-accuracy", "f1", "mse", "rmse","r2"): @@ -66,10 +66,14 @@ def call_data_methods(data: Type[XYBaseDataModule]): f"model.init_args.{kind}_metrics.init_args.metrics.{average}.init_args.num_labels", apply_on="instantiate", ) + parser.link_arguments( - "model.init_args.out_dim", "trainer.callbacks.init_args.num_labels" + "data.num_of_labels", "trainer.callbacks.init_args.num_labels" ) # parser.link_arguments( + # "model.init_args.out_dim", "trainer.callbacks.init_args.num_labels" + # ) + # parser.link_arguments( # "data", "model.init_args.criterion.init_args.data_extractor" # ) # parser.link_arguments( diff --git a/chebai/models/electra.py b/chebai/models/electra.py index c1c69661..45b615a2 100644 --- a/chebai/models/electra.py +++ b/chebai/models/electra.py @@ -19,7 +19,8 @@ logging.getLogger("pysmiles").setLevel(logging.CRITICAL) -from chebai.loss.semantic import DisjointLoss as ElectraChEBIDisjointLoss # noqa +# TODO: put back in before pull request +# from chebai.loss.semantic import DisjointLoss as ElectraChEBIDisjointLoss # noqa class ElectraPre(ChebaiBaseNet): @@ -40,6 +41,7 @@ class ElectraPre(ChebaiBaseNet): def __init__(self, config: Dict[str, Any] = None, **kwargs: Any): super().__init__(config=config, **kwargs) + self.generator_config = ElectraConfig(**config["generator"]) self.generator = ElectraForMaskedLM(self.generator_config) self.discriminator_config = ElectraConfig(**config["discriminator"]) diff --git a/chebai/preprocessing/datasets/tox21.py b/chebai/preprocessing/datasets/tox21.py index 81647163..712558e2 100644 --- a/chebai/preprocessing/datasets/tox21.py +++ b/chebai/preprocessing/datasets/tox21.py @@ -163,7 +163,7 @@ def _load_dict(self, input_file_path: str) -> List[Dict]: features=smiles, labels=labels, ident=row["mol_id"], group=group ) # yield self.reader.to_data(dict(features=smiles, labels=labels, ident=row["mol_id"])) - def _set_processed_data_props(self): + def _set_processed_data_props(self): """ Load processed data and extract metadata. diff --git a/chebai/result/molplot.py b/chebai/result/molplot.py index 6f8d1e79..055c3b26 100644 --- a/chebai/result/molplot.py +++ b/chebai/result/molplot.py @@ -11,9 +11,10 @@ from networkx.algorithms.isomorphism import GraphMatcher from pysmiles.read_smiles import LOGGER, TokenType, _tokenize from rdkit import Chem -from rdkit.Chem.Draw import MolToMPL, rdMolDraw2D +from rdkit.Chem.Draw import rdMolDraw2D +# from rdkit.Chem.Draw import MolToMPL, rdMolDraw2D -from chebai.preprocessing.datasets import JCI_500_COLUMNS_INT +# from chebai.preprocessing.datasets import JCI_500_COLUMNS_INT from chebai.result.base import ResultProcessor diff --git a/tutorials/demo_process_results.ipynb b/tutorials/demo_process_results.ipynb index b62af78e..6a3870be 100644 --- a/tutorials/demo_process_results.ipynb +++ b/tutorials/demo_process_results.ipynb @@ -8,7 +8,10 @@ "end_time": "2023-11-29T08:17:25.832642900Z", "start_time": "2023-11-29T08:17:25.816890700Z" }, - "collapsed": true + "collapsed": true, + "jupyter": { + "outputs_hidden": true + } }, "outputs": [], "source": [ @@ -37,7 +40,10 @@ "end_time": "2023-11-24T09:13:26.387885900Z", "start_time": "2023-11-24T09:06:23.191727Z" }, - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "outputs": [ { @@ -109,7 +115,10 @@ "end_time": "2023-11-29T08:33:48.374202Z", "start_time": "2023-11-29T08:33:48.261436600Z" }, - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "outputs": [ { @@ -239,7 +248,10 @@ "end_time": "2023-11-24T09:55:24.187152800Z", "start_time": "2023-11-24T09:55:21.580572700Z" }, - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "outputs": [ { @@ -275,6 +287,9 @@ "execution_count": 2, "metadata": { "collapsed": false, + "jupyter": { + "outputs_hidden": false + }, "pycharm": { "name": "#%%\n" } @@ -299,6 +314,9 @@ "execution_count": 4, "metadata": { "collapsed": false, + "jupyter": { + "outputs_hidden": false + }, "pycharm": { "name": "#%%\n" } @@ -338,6 +356,9 @@ "execution_count": 7, "metadata": { "collapsed": false, + "jupyter": { + "outputs_hidden": false + }, "pycharm": { "name": "#%%\n" } @@ -360,6 +381,9 @@ "execution_count": 3, "metadata": { "collapsed": false, + "jupyter": { + "outputs_hidden": false + }, "pycharm": { "name": "#%%\n" } @@ -382,6 +406,9 @@ "execution_count": 4, "metadata": { "collapsed": false, + "jupyter": { + "outputs_hidden": false + }, "pycharm": { "name": "#%%\n" } @@ -403,6 +430,9 @@ "execution_count": 9, "metadata": { "collapsed": false, + "jupyter": { + "outputs_hidden": false + }, "pycharm": { "name": "#%%\n" } @@ -428,6 +458,9 @@ "execution_count": 11, "metadata": { "collapsed": false, + "jupyter": { + "outputs_hidden": false + }, "pycharm": { "name": "#%%\n" } @@ -451,6 +484,9 @@ "execution_count": 5, "metadata": { "collapsed": false, + "jupyter": { + "outputs_hidden": false + }, "pycharm": { "name": "#%%\n" } @@ -483,6 +519,9 @@ "execution_count": 58, "metadata": { "collapsed": false, + "jupyter": { + "outputs_hidden": false + }, "pycharm": { "name": "#%%\n" } @@ -643,6 +682,9 @@ "execution_count": 12, "metadata": { "collapsed": false, + "jupyter": { + "outputs_hidden": false + }, "pycharm": { "name": "#%%\n" } @@ -700,6 +742,9 @@ "execution_count": 11, "metadata": { "collapsed": false, + "jupyter": { + "outputs_hidden": false + }, "pycharm": { "name": "#%%\n" } @@ -730,7 +775,10 @@ { "cell_type": "markdown", "metadata": { - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "source": [ "Results:\n", @@ -762,6 +810,9 @@ "execution_count": 40, "metadata": { "collapsed": false, + "jupyter": { + "outputs_hidden": false + }, "pycharm": { "name": "#%%\n" } @@ -794,6 +845,9 @@ "execution_count": 41, "metadata": { "collapsed": false, + "jupyter": { + "outputs_hidden": false + }, "pycharm": { "name": "#%%\n" } @@ -826,6 +880,9 @@ "execution_count": 42, "metadata": { "collapsed": false, + "jupyter": { + "outputs_hidden": false + }, "pycharm": { "name": "#%%\n" } @@ -858,6 +915,9 @@ "execution_count": 13, "metadata": { "collapsed": false, + "jupyter": { + "outputs_hidden": false + }, "pycharm": { "name": "#%%\n" } @@ -912,6 +972,9 @@ "start_time": "2023-11-24T07:36:43.594504200Z" }, "collapsed": false, + "jupyter": { + "outputs_hidden": false + }, "pycharm": { "name": "#%%\n" } @@ -958,6 +1021,9 @@ "start_time": "2023-11-24T07:36:51.800819200Z" }, "collapsed": false, + "jupyter": { + "outputs_hidden": false + }, "pycharm": { "name": "#%%\n" } @@ -984,6 +1050,9 @@ "execution_count": null, "metadata": { "collapsed": false, + "jupyter": { + "outputs_hidden": false + }, "pycharm": { "name": "#%%\n" } @@ -1010,6 +1079,9 @@ "execution_count": null, "metadata": { "collapsed": false, + "jupyter": { + "outputs_hidden": false + }, "pycharm": { "name": "#%%\n" } @@ -1035,23 +1107,23 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", - "version": 2 + "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", - "pygments_lexer": "ipython2", - "version": "2.7.6" + "pygments_lexer": "ipython3", + "version": "3.12.11" } }, "nbformat": 4, - "nbformat_minor": 0 + "nbformat_minor": 4 } From dca60a3662a013c0553cb037d85daaefbdb11e07 Mon Sep 17 00:00:00 2001 From: Charlotte Tumescheit Date: Wed, 29 Oct 2025 16:44:45 +0100 Subject: [PATCH 45/54] adjust all regression tasks to new logic --- chebai/cli.py | 8 +- .../preprocessing/bin/smiles_token/tokens.txt | 408 ++++++++++++++++++ .../datasets/molecule_classification.py | 227 ++++++++-- .../datasets/molecule_regression.py | 41 +- chebai/preprocessing/datasets/solCuration.py | 95 +++- chebai/preprocessing/datasets/tox21.py | 14 +- configs/data/bace_moleculenet.yml | 3 +- configs/data/bbbp_moleculenet.yml | 3 +- configs/data/clintox_moleculenet.yml | 3 +- configs/data/hiv_moleculenet.yml | 3 +- configs/data/lipo_moleculenet.yml | 3 +- configs/data/muv_moleculenet.yml | 3 +- configs/data/sider_moleculenet.yml | 3 +- configs/data/solubilityCuration.yml | 3 +- configs/data/solubilityESOL.yml | 3 +- tutorials/eval_model_basic.ipynb | 2 +- 16 files changed, 750 insertions(+), 72 deletions(-) diff --git a/chebai/cli.py b/chebai/cli.py index 379d5e80..866ee4cb 100644 --- a/chebai/cli.py +++ b/chebai/cli.py @@ -43,7 +43,7 @@ def add_arguments_to_parser(self, parser: LightningArgumentParser): def call_data_methods(data: Type[XYBaseDataModule]): if data._num_of_labels is None: data.prepare_data() - data.setup() + data.setup() return data.num_of_labels parser.link_arguments( @@ -60,7 +60,9 @@ def call_data_methods(data: Type[XYBaseDataModule]): ) for kind in ("train", "val", "test"): - for average in ("micro-f1", "macro-f1", "balanced-accuracy", "f1", "mse", "rmse","r2"): + # todo: fix this + # for average in ("mse", "rmse","r2"): # for regression + for average in ("micro-f1", "macro-f1", "balanced-accuracy", "f1"): # for classification parser.link_arguments( "data.num_of_labels", f"model.init_args.{kind}_metrics.init_args.metrics.{average}.init_args.num_labels", @@ -79,7 +81,7 @@ def call_data_methods(data: Type[XYBaseDataModule]): # parser.link_arguments( # "data.init_args.chebi_version", # "model.init_args.criterion.init_args.data_extractor.init_args.chebi_version", - # ) + # ) @staticmethod def subcommands() -> Dict[str, Set[str]]: diff --git a/chebai/preprocessing/bin/smiles_token/tokens.txt b/chebai/preprocessing/bin/smiles_token/tokens.txt index 9ce39f9d..7999d974 100644 --- a/chebai/preprocessing/bin/smiles_token/tokens.txt +++ b/chebai/preprocessing/bin/smiles_token/tokens.txt @@ -984,3 +984,411 @@ p [ClH2+] [BrH2+] [IH2+] +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 diff --git a/chebai/preprocessing/datasets/molecule_classification.py b/chebai/preprocessing/datasets/molecule_classification.py index 883a94d7..c79943ab 100644 --- a/chebai/preprocessing/datasets/molecule_classification.py +++ b/chebai/preprocessing/datasets/molecule_classification.py @@ -48,10 +48,18 @@ def raw_file_names(self) -> List[str]: """Returns a list of raw file names.""" return ["clintox_groups.csv"] + # @property + # def processed_file_names(self) -> List[str]: + # """Returns a list of processed file names.""" + # return ["test.pt", "train.pt", "validation.pt"] + @property - def processed_file_names(self) -> List[str]: - """Returns a list of processed file names.""" - return ["test.pt", "train.pt", "validation.pt"] + def processed_file_names_dict(self) -> dict: + return { + "test": "test.pt", + "train": "train.pt", + "validation": "validation.pt", + } def download(self) -> None: """Downloads and extracts the dataset.""" @@ -100,21 +108,6 @@ def setup_processed(self) -> None: d for d in (data[temp_split_index[i]] for i in validation_split_index) ] else: - # print(self.train_split) - # print(type(data)) - # print((data[0])) - # print(type(data[0])) - # X = [] - # y = [] - # for item in data: - # X.append(item['ident']) - # y.append(item['labels']) - # sss = StratifiedShuffleSplit(n_splits=10, test_size=1-self.train_split, random_state=0) - # sss.get_n_splits(np.array(X), np.array(y)) - # print(sss) - # train, test = sss.split(X, y) - # print(train) - # exit() train_split, test_split = train_test_split( data, train_size=self.train_split, shuffle=True ) @@ -145,6 +138,24 @@ def setup(self, **kwargs) -> None: ): self.setup_processed() + self._after_setup() + + def _set_processed_data_props(self): + """ + Load processed data and extract metadata. + + Sets: + - self._num_of_labels: Number of target labels in the dataset. + - self._feature_vector_size: Maximum feature vector length across all data points. + """ + pt_file_path = os.path.join( + self.processed_dir, self.processed_file_names_dict["train"] + ) + data_pt = torch.load(pt_file_path, weights_only=False) + + self._num_of_labels = len(data_pt[0]["labels"]) + self._feature_vector_size = max(len(d["features"]) for d in data_pt) + def _load_dict(self, input_file_path: str) -> List[Dict]: """Loads data from a CSV file. @@ -168,6 +179,9 @@ def _load_dict(self, input_file_path: str) -> List[Dict]: # yield dict(features=smiles, labels=labels, ident=i) # yield self.reader.to_data(dict(features=smiles, labels=labels, ident=i)) + def _perform_data_preparation(self, *args, **kwargs) -> None: + pass + class BBBP(XYBaseDataModule): """Data module for ClinTox MoleculeNet dataset.""" @@ -191,10 +205,18 @@ def raw_file_names(self) -> List[str]: """Returns a list of raw file names.""" return ["bbbp_groups.csv"] + # @property + # def processed_file_names(self) -> List[str]: + # """Returns a list of processed file names.""" + # return ["test.pt", "train.pt", "validation.pt"] + @property - def processed_file_names(self) -> List[str]: - """Returns a list of processed file names.""" - return ["test.pt", "train.pt", "validation.pt"] + def processed_file_names_dict(self) -> dict: + return { + "test": "test.pt", + "train": "train.pt", + "validation": "validation.pt", + } def download(self) -> None: """Downloads and extracts the dataset.""" @@ -270,6 +292,26 @@ def setup(self, **kwargs) -> None: for f in self.processed_file_names ): self.setup_processed() + + self._after_setup() + + + def _set_processed_data_props(self): + """ + Load processed data and extract metadata. + + Sets: + - self._num_of_labels: Number of target labels in the dataset. + - self._feature_vector_size: Maximum feature vector length across all data points. + """ + pt_file_path = os.path.join( + self.processed_dir, self.processed_file_names_dict["train"] + ) + data_pt = torch.load(pt_file_path, weights_only=False) + + self._num_of_labels = len(data_pt[0]["labels"]) + self._feature_vector_size = max(len(d["features"]) for d in data_pt) + def _load_dict(self, input_file_path: str) -> List[Dict]: """Loads data from a CSV file. @@ -291,6 +333,9 @@ def _load_dict(self, input_file_path: str) -> List[Dict]: yield dict(features=smiles, labels=labels, ident=i, group=group) # yield self.reader.to_data(dict(features=smiles, labels=labels, ident=i)) + def _perform_data_preparation(self, *args, **kwargs) -> None: + pass + class Sider(XYBaseDataModule): """Data module for ClinTox MoleculeNet dataset.""" @@ -340,10 +385,18 @@ def raw_file_names(self) -> List[str]: """Returns a list of raw file names.""" return ["sider_groups.csv"] + # @property + # def processed_file_names(self) -> List[str]: + # """Returns a list of processed file names.""" + # return ["test.pt", "train.pt", "validation.pt"] + @property - def processed_file_names(self) -> List[str]: - """Returns a list of processed file names.""" - return ["test.pt", "train.pt", "validation.pt"] + def processed_file_names_dict(self) -> dict: + return { + "test": "test.pt", + "train": "train.pt", + "validation": "validation.pt", + } def download(self) -> None: """Downloads and extracts the dataset.""" @@ -421,6 +474,24 @@ def setup(self, **kwargs) -> None: for f in self.processed_file_names ): self.setup_processed() + + self._after_setup() + + def _set_processed_data_props(self): + """ + Load processed data and extract metadata. + + Sets: + - self._num_of_labels: Number of target labels in the dataset. + - self._feature_vector_size: Maximum feature vector length across all data points. + """ + pt_file_path = os.path.join( + self.processed_dir, self.processed_file_names_dict["train"] + ) + data_pt = torch.load(pt_file_path, weights_only=False) + + self._num_of_labels = len(data_pt[0]["labels"]) + self._feature_vector_size = max(len(d["features"]) for d in data_pt) def _load_dict(self, input_file_path: str) -> List[Dict]: """Loads data from a CSV file. @@ -444,6 +515,8 @@ def _load_dict(self, input_file_path: str) -> List[Dict]: yield dict(features=smiles, labels=labels, ident=i, group=group) # yield self.reader.to_data(dict(features=smiles, labels=labels, ident=i)) + def _perform_data_preparation(self, *args, **kwargs) -> None: + pass class Bace(XYBaseDataModule): """Data module for ClinTox MoleculeNet dataset.""" @@ -467,10 +540,18 @@ def raw_file_names(self) -> List[str]: """Returns a list of raw file names.""" return ["bace.csv"] + # @property + # def processed_file_names(self) -> List[str]: + # """Returns a list of processed file names.""" + # return ["test.pt", "train.pt", "validation.pt"] + @property - def processed_file_names(self) -> List[str]: - """Returns a list of processed file names.""" - return ["test.pt", "train.pt", "validation.pt"] + def processed_file_names_dict(self) -> dict: + return { + "test": "test.pt", + "train": "train.pt", + "validation": "validation.pt", + } def download(self) -> None: """Downloads and extracts the dataset.""" @@ -543,6 +624,24 @@ def setup(self, **kwargs) -> None: ): self.setup_processed() + self._after_setup() + + def _set_processed_data_props(self): + """ + Load processed data and extract metadata. + + Sets: + - self._num_of_labels: Number of target labels in the dataset. + - self._feature_vector_size: Maximum feature vector length across all data points. + """ + pt_file_path = os.path.join( + self.processed_dir, self.processed_file_names_dict["train"] + ) + data_pt = torch.load(pt_file_path, weights_only=False) + + self._num_of_labels = len(data_pt[0]["labels"]) + self._feature_vector_size = max(len(d["features"]) for d in data_pt) + def _load_dict(self, input_file_path: str) -> List[Dict]: """Loads data from a CSV file. @@ -563,6 +662,9 @@ def _load_dict(self, input_file_path: str) -> List[Dict]: yield dict(features=smiles, labels=labels, ident=i) # , group=group # yield self.reader.to_data(dict(features=smiles, labels=labels, ident=i)) + def _perform_data_preparation(self, *args, **kwargs) -> None: + pass + class HIV(XYBaseDataModule): """Data module for ClinTox MoleculeNet dataset.""" @@ -586,10 +688,18 @@ def raw_file_names(self) -> List[str]: """Returns a list of raw file names.""" return ["hiv_groups.csv"] + # @property + # def processed_file_names(self) -> List[str]: + # """Returns a list of processed file names.""" + # return ["test.pt", "train.pt", "validation.pt"] + @property - def processed_file_names(self) -> List[str]: - """Returns a list of processed file names.""" - return ["test.pt", "train.pt", "validation.pt"] + def processed_file_names_dict(self) -> dict: + return { + "test": "test.pt", + "train": "train.pt", + "validation": "validation.pt", + } def download(self) -> None: """Downloads and extracts the dataset.""" @@ -662,6 +772,24 @@ def setup(self, **kwargs) -> None: ): self.setup_processed() + self._after_setup() + + def _set_processed_data_props(self): + """ + Load processed data and extract metadata. + + Sets: + - self._num_of_labels: Number of target labels in the dataset. + - self._feature_vector_size: Maximum feature vector length across all data points. + """ + pt_file_path = os.path.join( + self.processed_dir, self.processed_file_names_dict["train"] + ) + data_pt = torch.load(pt_file_path, weights_only=False) + + self._num_of_labels = len(data_pt[0]["labels"]) + self._feature_vector_size = max(len(d["features"]) for d in data_pt) + def _load_dict(self, input_file_path: str) -> List[Dict]: """Loads data from a CSV file. @@ -683,6 +811,9 @@ def _load_dict(self, input_file_path: str) -> List[Dict]: yield dict(features=smiles, labels=labels, ident=i, group=group) # yield self.reader.to_data(dict(features=smiles, labels=labels, ident=i)) + def _perform_data_preparation(self, *args, **kwargs) -> None: + pass + class MUV(XYBaseDataModule): """Data module for ClinTox MoleculeNet dataset.""" @@ -722,10 +853,18 @@ def raw_file_names(self) -> List[str]: """Returns a list of raw file names.""" return ["muv.csv"] + # @property + # def processed_file_names(self) -> List[str]: + # """Returns a list of processed file names.""" + # return ["test.pt", "train.pt", "validation.pt"] + @property - def processed_file_names(self) -> List[str]: - """Returns a list of processed file names.""" - return ["test.pt", "train.pt", "validation.pt"] + def processed_file_names_dict(self) -> dict: + return { + "test": "test.pt", + "train": "train.pt", + "validation": "validation.pt", + } def download(self) -> None: """Downloads and extracts the dataset.""" @@ -802,6 +941,25 @@ def setup(self, **kwargs) -> None: ): self.setup_processed() + self._after_setup() + + def _set_processed_data_props(self): + """ + Load processed data and extract metadata. + + Sets: + - self._num_of_labels: Number of target labels in the dataset. + - self._feature_vector_size: Maximum feature vector length across all data points. + """ + pt_file_path = os.path.join( + self.processed_dir, self.processed_file_names_dict["train"] + ) + data_pt = torch.load(pt_file_path, weights_only=False) + + self._num_of_labels = len(data_pt[0]["labels"]) + self._feature_vector_size = max(len(d["features"]) for d in data_pt) + + def _load_dict(self, input_file_path: str) -> List[Dict]: """Loads data from a CSV file. @@ -824,6 +982,9 @@ def _load_dict(self, input_file_path: str) -> List[Dict]: yield dict(features=smiles, labels=labels, ident=i) # , group=group) # yield self.reader.to_data(dict(features=smiles, labels=labels, ident=i)) + def _perform_data_preparation(self, *args, **kwargs) -> None: + pass + class BaceChem(Bace): """Chemical data reader for Tox21MolNet dataset.""" diff --git a/chebai/preprocessing/datasets/molecule_regression.py b/chebai/preprocessing/datasets/molecule_regression.py index 3fcc73c9..900037be 100644 --- a/chebai/preprocessing/datasets/molecule_regression.py +++ b/chebai/preprocessing/datasets/molecule_regression.py @@ -38,9 +38,17 @@ def label_number(self): def raw_file_names(self): return ["Lipo.csv"] + # @property + # def processed_file_names(self): + # return ["test.pt", "train.pt", "validation.pt"] + @property - def processed_file_names(self): - return ["test.pt", "train.pt", "validation.pt"] + def processed_file_names_dict(self) -> dict: + return { + "test": "test.pt", + "train": "train.pt", + "validation": "validation.pt", + } def download(self): # download @@ -96,6 +104,23 @@ def setup(self, **kwargs): self._after_setup() + def _set_processed_data_props(self): + """ + Load processed data and extract metadata. + + Sets: + - self._num_of_labels: Number of target labels in the dataset. + - self._feature_vector_size: Maximum feature vector length across all data points. + """ + pt_file_path = os.path.join( + self.processed_dir, self.processed_file_names_dict["train"] + ) + data_pt = torch.load(pt_file_path, weights_only=False) + + self._num_of_labels = len(data_pt[0]["labels"]) + self._feature_vector_size = max(len(d["features"]) for d in data_pt) + + def _load_dict(self, input_file_path: str) -> List[Dict]: """Loads data from a CSV file. @@ -117,7 +142,14 @@ def _load_dict(self, input_file_path: str) -> List[Dict]: for i in range(0, len(smiles_l)): yield dict(features=smiles_l[i], labels=[labels_l[i]], ident=i) # yield self.reader.to_data(dict(features=smiles_l[i], labels=[labels_l[i]], ident=i)) + + def _perform_data_preparation(self, *args, **kwargs) -> None: + pass +class LipoChem(Lipo): + """Chemical data reader for the solubility dataset.""" + + READER = dr.ChemDataReader class FreeSolv(XYBaseDataModule): HEADERS = [ @@ -244,11 +276,6 @@ def _load_dict(self, input_file_path: str) -> List[Dict]: def _perform_data_preparation(self, *args, **kwargs) -> None: pass -class LipoChem(Lipo): - """Chemical data reader for the solubility dataset.""" - - READER = dr.ChemDataReader - class FreeSolvChem(FreeSolv): """Chemical data reader for the solubility dataset.""" diff --git a/chebai/preprocessing/datasets/solCuration.py b/chebai/preprocessing/datasets/solCuration.py index 85154eb1..b8ab6dd3 100644 --- a/chebai/preprocessing/datasets/solCuration.py +++ b/chebai/preprocessing/datasets/solCuration.py @@ -38,9 +38,17 @@ def label_number(self): def raw_file_names(self): return ["solCuration.csv"] + # @property + # def processed_file_names(self): + # return ["test.pt", "train.pt", "validation.pt"] + @property - def processed_file_names(self): - return ["test.pt", "train.pt", "validation.pt"] + def processed_file_names_dict(self) -> dict: + return { + "test": "test.pt", + "train": "train.pt", + "validation": "validation.pt", + } def download(self): # download and combine all the available curated datasets from xxx @@ -56,14 +64,16 @@ def download(self): def setup_processed(self): print("Create splits") - print(self.train_split) - print(os.path.join(self.raw_dir, f"solCuration.csv")) data = list( self._load_data_from_file(os.path.join(self.raw_dir, f"solCuration.csv")) ) print(len(data)) - # data = self._load_data_from_file(os.path.join(self.raw_dir, f"solCuration.csv")) - if 0 == 0: + + train_split, test_split = train_test_split(data, test_size=self.test_split, shuffle=True) + train_split, validation_split = train_test_split(train_split, test_size=self.validation_split, shuffle=True) + + + if False: train_split, test_split = train_test_split( data, train_size=self.train_split, shuffle=True ) @@ -99,6 +109,24 @@ def setup(self, **kwargs): ): self.setup_processed() + self._after_setup() + + def _set_processed_data_props(self): + """ + Load processed data and extract metadata. + + Sets: + - self._num_of_labels: Number of target labels in the dataset. + - self._feature_vector_size: Maximum feature vector length across all data points. + """ + pt_file_path = os.path.join( + self.processed_dir, self.processed_file_names_dict["train"] + ) + data_pt = torch.load(pt_file_path, weights_only=False) + + self._num_of_labels = len(data_pt[0]["labels"]) + self._feature_vector_size = max(len(d["features"]) for d in data_pt) + def _load_data_from_file(self, input_file_path: str) -> List[Dict]: """Loads data from a CSV file. @@ -108,7 +136,6 @@ def _load_data_from_file(self, input_file_path: str) -> List[Dict]: Returns: List[Dict]: List of data dictionaries. """ - print("!!!!!!!!!!!!!!!!") smiles_l = [] labels_l = [] with open(input_file_path, "r") as input_file: @@ -131,6 +158,14 @@ def _load_data_from_file(self, input_file_path: str) -> List[Dict]: dict(features=smiles_l[i], labels=[labels_l[i]], ident=i) ) + def _perform_data_preparation(self, *args, **kwargs) -> None: + pass + +class SolCurationChem(SolCuration): + """Chemical data reader for the solubility dataset.""" + + READER = dr.ChemDataReader + class SolESOL(XYBaseDataModule): HEADERS = [ @@ -149,9 +184,17 @@ def label_number(self): def raw_file_names(self): return ["solESOL.csv"] + # @property + # def processed_file_names(self): + # return ["test.pt", "train.pt", "validation.pt"] + @property - def processed_file_names(self): - return ["test.pt", "train.pt", "validation.pt"] + def processed_file_names_dict(self) -> dict: + return { + "test": "test.pt", + "train": "train.pt", + "validation": "validation.pt", + } def download(self): # download @@ -167,8 +210,13 @@ def setup_processed(self): self._load_data_from_file(os.path.join(self.raw_dir, f"solESOL.csv")) ) print(len(data)) - # data = self._load_data_from_file(os.path.join(self.raw_dir, f"solCuration.csv")) - if 0 == 0: + + + train_split, test_split = train_test_split(data, test_size=self.test_split, shuffle=True) + train_split, validation_split = train_test_split(train_split, test_size=self.validation_split, shuffle=True) + + + if False: train_split, test_split = train_test_split( data, train_size=self.train_split, shuffle=True ) @@ -203,6 +251,24 @@ def setup(self, **kwargs): for f in self.processed_file_names ): self.setup_processed() + + self._after_setup() + + def _set_processed_data_props(self): + """ + Load processed data and extract metadata. + + Sets: + - self._num_of_labels: Number of target labels in the dataset. + - self._feature_vector_size: Maximum feature vector length across all data points. + """ + pt_file_path = os.path.join( + self.processed_dir, self.processed_file_names_dict["train"] + ) + data_pt = torch.load(pt_file_path, weights_only=False) + + self._num_of_labels = len(data_pt[0]["labels"]) + self._feature_vector_size = max(len(d["features"]) for d in data_pt) def _load_dict(self, input_file_path: str) -> List[Dict]: """Loads data from a CSV file. @@ -226,11 +292,8 @@ def _load_dict(self, input_file_path: str) -> List[Dict]: yield dict(features=smiles_l[i], labels=[labels_l[i]], ident=i) # yield self.reader.to_data(dict(features=smiles_l[i], labels=[labels_l[i]], ident=i)) - -class SolCurationChem(SolCuration): - """Chemical data reader for the solubility dataset.""" - - READER = dr.ChemDataReader + def _perform_data_preparation(self, *args, **kwargs) -> None: + pass class SolESOLChem(SolESOL): diff --git a/chebai/preprocessing/datasets/tox21.py b/chebai/preprocessing/datasets/tox21.py index 712558e2..6e86b057 100644 --- a/chebai/preprocessing/datasets/tox21.py +++ b/chebai/preprocessing/datasets/tox21.py @@ -49,10 +49,18 @@ def raw_file_names(self) -> List[str]: # return ["tox21.csv"] return ["tox21.csv"] + # @property + # def processed_file_names(self) -> List[str]: + # """Returns a list of processed file names.""" + # return ["test.pt", "train.pt", "validation.pt"] + @property - def processed_file_names(self) -> List[str]: - """Returns a list of processed file names.""" - return ["test.pt", "train.pt", "validation.pt"] + def processed_file_names_dict(self) -> dict: + return { + "test": "test.pt", + "train": "train.pt", + "validation": "validation.pt", + } def download(self) -> None: """Downloads and extracts the dataset.""" diff --git a/configs/data/bace_moleculenet.yml b/configs/data/bace_moleculenet.yml index eceadc45..e5d4bdb7 100644 --- a/configs/data/bace_moleculenet.yml +++ b/configs/data/bace_moleculenet.yml @@ -1,4 +1,5 @@ class_path: chebai.preprocessing.datasets.molecule_classification.BaceChem init_args: batch_size: 32 - train_split: 0.8 \ No newline at end of file + validation_split: 0.05 + test_split: 0.15 \ No newline at end of file diff --git a/configs/data/bbbp_moleculenet.yml b/configs/data/bbbp_moleculenet.yml index 9f3b7164..01479443 100644 --- a/configs/data/bbbp_moleculenet.yml +++ b/configs/data/bbbp_moleculenet.yml @@ -1,4 +1,5 @@ class_path: chebai.preprocessing.datasets.molecule_classification.BBBPChem init_args: batch_size: 32 - train_split: 0.8 + validation_split: 0.05 + test_split: 0.15 diff --git a/configs/data/clintox_moleculenet.yml b/configs/data/clintox_moleculenet.yml index 2cfdcacf..d7b7c3be 100644 --- a/configs/data/clintox_moleculenet.yml +++ b/configs/data/clintox_moleculenet.yml @@ -1,4 +1,5 @@ class_path: chebai.preprocessing.datasets.molecule_classification.ClinToxChem init_args: batch_size: 32 - train_split: 0.8 + validation_split: 0.05 + test_split: 0.15 diff --git a/configs/data/hiv_moleculenet.yml b/configs/data/hiv_moleculenet.yml index ad2271b9..70c74434 100644 --- a/configs/data/hiv_moleculenet.yml +++ b/configs/data/hiv_moleculenet.yml @@ -1,4 +1,5 @@ class_path: chebai.preprocessing.datasets.molecule_classification.HIVChem init_args: batch_size: 32 - train_split: 0.8 \ No newline at end of file + validation_split: 0.05 + test_split: 0.15 \ No newline at end of file diff --git a/configs/data/lipo_moleculenet.yml b/configs/data/lipo_moleculenet.yml index f3a8cfc4..c246db5b 100644 --- a/configs/data/lipo_moleculenet.yml +++ b/configs/data/lipo_moleculenet.yml @@ -1,4 +1,5 @@ class_path: chebai.preprocessing.datasets.molecule_regression.LipoChem init_args: batch_size: 32 - train_split: 0.8 \ No newline at end of file + validation_split: 0.05 + test_split: 0.15 \ No newline at end of file diff --git a/configs/data/muv_moleculenet.yml b/configs/data/muv_moleculenet.yml index bdb563e2..f4eba3e1 100644 --- a/configs/data/muv_moleculenet.yml +++ b/configs/data/muv_moleculenet.yml @@ -1,4 +1,5 @@ class_path: chebai.preprocessing.datasets.molecule_classification.MUVChem init_args: batch_size: 32 - train_split: 0.8 \ No newline at end of file + validation_split: 0.05 + test_split: 0.15 \ No newline at end of file diff --git a/configs/data/sider_moleculenet.yml b/configs/data/sider_moleculenet.yml index 09fc55af..a1d635c5 100644 --- a/configs/data/sider_moleculenet.yml +++ b/configs/data/sider_moleculenet.yml @@ -1,4 +1,5 @@ class_path: chebai.preprocessing.datasets.molecule_classification.SiderChem init_args: batch_size: 10 - train_split: 0.8 \ No newline at end of file + validation_split: 0.05 + test_split: 0.15 \ No newline at end of file diff --git a/configs/data/solubilityCuration.yml b/configs/data/solubilityCuration.yml index ad633dee..89145905 100644 --- a/configs/data/solubilityCuration.yml +++ b/configs/data/solubilityCuration.yml @@ -1,4 +1,5 @@ class_path: chebai.preprocessing.datasets.solCuration.SolCurationChem init_args: batch_size: 32 - train_split: 0.8 + validation_split: 0.05 + test_split: 0.15 diff --git a/configs/data/solubilityESOL.yml b/configs/data/solubilityESOL.yml index 24e0a799..a58c4ba0 100644 --- a/configs/data/solubilityESOL.yml +++ b/configs/data/solubilityESOL.yml @@ -1,4 +1,5 @@ class_path: chebai.preprocessing.datasets.solCuration.SolESOLChem init_args: batch_size: 32 - train_split: 0.8 + validation_split: 0.05 + test_split: 0.15 diff --git a/tutorials/eval_model_basic.ipynb b/tutorials/eval_model_basic.ipynb index dd97dc59..776a3d3c 100644 --- a/tutorials/eval_model_basic.ipynb +++ b/tutorials/eval_model_basic.ipynb @@ -234,7 +234,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.19" + "version": "3.12.11" } }, "nbformat": 4, From b6f0d239ea611108ffa11edaf5772d0ace5e3f8d Mon Sep 17 00:00:00 2001 From: Charlotte Tumescheit Date: Wed, 29 Oct 2025 17:33:17 +0100 Subject: [PATCH 46/54] adjust classification tasks for new logic --- chebai/cli.py | 4 +- .../preprocessing/bin/smiles_token/tokens.txt | 1186 +++++++++++++++++ .../datasets/molecule_classification.py | 128 +- chebai/preprocessing/datasets/tox21.py | 23 +- 4 files changed, 1256 insertions(+), 85 deletions(-) diff --git a/chebai/cli.py b/chebai/cli.py index 866ee4cb..c4f7401d 100644 --- a/chebai/cli.py +++ b/chebai/cli.py @@ -62,7 +62,9 @@ def call_data_methods(data: Type[XYBaseDataModule]): for kind in ("train", "val", "test"): # todo: fix this # for average in ("mse", "rmse","r2"): # for regression - for average in ("micro-f1", "macro-f1", "balanced-accuracy", "f1"): # for classification + for average in ("f1", "roc-auc"): # for binary classification + # for average in ("micro-f1", "macro-f1", "roc-auc"): # for multilabel classification + # for average in ("micro-f1", "macro-f1", "balanced-accuracy", "roc-auc"): # for multilabel classification using balanced-accuracy parser.link_arguments( "data.num_of_labels", f"model.init_args.{kind}_metrics.init_args.metrics.{average}.init_args.num_labels", diff --git a/chebai/preprocessing/bin/smiles_token/tokens.txt b/chebai/preprocessing/bin/smiles_token/tokens.txt index 7999d974..1cc8bade 100644 --- a/chebai/preprocessing/bin/smiles_token/tokens.txt +++ b/chebai/preprocessing/bin/smiles_token/tokens.txt @@ -1392,3 +1392,1189 @@ p 301 302 303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +326 +327 +328 +329 +330 +331 +332 +333 +334 +335 +336 +337 +338 +339 +340 +341 +342 +343 +344 +345 +346 +347 +348 +349 +350 +351 +352 +353 +354 +355 +356 +357 +358 +359 +360 +361 +362 +363 +364 +365 +366 +367 +368 +369 +370 +371 +372 +373 +374 +375 +376 +377 +378 +379 +380 +381 +382 +383 +384 +385 +386 +387 +388 +389 +390 +391 +392 +393 +394 +395 +396 +397 +398 +399 +400 +401 +402 +403 +404 +405 +406 +407 +408 +409 +410 +411 +412 +413 +414 +415 +416 +417 +418 +419 +420 +421 +422 +423 +424 +425 +426 +427 +428 +429 +430 +431 +432 +433 +434 +435 +436 +437 +438 +439 +440 +441 +442 +443 +444 +445 +446 +447 +448 +449 +450 +451 +452 +453 +454 +455 +456 +457 +458 +459 +460 +461 +462 +463 +464 +465 +466 +467 +468 +469 +470 +471 +472 +473 +474 +475 +476 +477 +478 +479 +480 +481 +482 +483 +484 +485 +486 +487 +488 +489 +490 +491 +492 +493 +494 +495 +496 +497 +498 +499 +500 +501 +502 +503 +504 +505 +506 +507 +508 +509 +510 +511 +512 +513 +514 +515 +516 +517 +518 +519 +520 +521 +522 +523 +524 +525 +526 +527 +528 +529 +530 +531 +532 +533 +534 +535 +536 +537 +538 +539 +540 +541 +542 +543 +544 +545 +546 +547 +548 +549 +550 +551 +552 +553 +554 +555 +556 +557 +558 +559 +560 +561 +562 +563 +564 +565 +566 +567 +568 +569 +570 +571 +572 +573 +574 +575 +576 +577 +578 +579 +580 +581 +582 +583 +584 +585 +586 +587 +588 +589 +590 +591 +592 +593 +594 +595 +596 +597 +598 +599 +600 +601 +602 +603 +604 +605 +606 +607 +608 +609 +610 +611 +612 +613 +614 +615 +616 +617 +618 +619 +620 +621 +622 +623 +624 +625 +626 +627 +628 +629 +630 +631 +632 +633 +634 +635 +636 +637 +638 +639 +640 +641 +642 +643 +644 +645 +646 +647 +648 +649 +650 +651 +652 +653 +654 +655 +656 +657 +658 +659 +660 +661 +662 +663 +664 +665 +666 +667 +668 +669 +670 +671 +672 +673 +674 +675 +676 +677 +678 +679 +680 +681 +682 +683 +684 +685 +686 +687 +688 +689 +690 +691 +692 +693 +694 +695 +696 +697 +698 +699 +700 +701 +702 +703 +704 +705 +706 +707 +708 +709 +710 +711 +712 +713 +714 +715 +716 +717 +718 +719 +720 +721 +722 +723 +724 +725 +726 +727 +728 +729 +730 +731 +732 +733 +734 +735 +736 +737 +738 +739 +740 +741 +742 +743 +744 +745 +746 +747 +748 +749 +750 +751 +752 +753 +754 +755 +756 +757 +758 +759 +760 +761 +762 +763 +764 +765 +766 +767 +768 +769 +770 +771 +772 +773 +774 +775 +776 +777 +778 +779 +780 +781 +782 +783 +784 +785 +786 +787 +788 +789 +790 +791 +792 +793 +794 +795 +796 +797 +798 +799 +800 +801 +802 +803 +804 +805 +806 +807 +808 +809 +810 +811 +812 +813 +814 +815 +816 +817 +818 +819 +820 +821 +822 +823 +824 +825 +826 +827 +828 +829 +830 +831 +832 +833 +834 +835 +836 +837 +838 +839 +840 +841 +842 +843 +844 +845 +846 +847 +848 +849 +850 +851 +852 +853 +854 +855 +856 +857 +858 +859 +860 +861 +862 +863 +864 +865 +866 +867 +868 +869 +870 +871 +872 +873 +874 +875 +876 +877 +878 +879 +880 +881 +882 +883 +884 +885 +886 +887 +888 +889 +890 +891 +892 +893 +894 +895 +896 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +326 +327 +328 +329 +330 +331 +332 +333 +334 +335 +336 +337 +338 +339 +340 +341 +342 +343 +344 +345 +346 +347 +348 +349 +350 +351 +352 +353 +354 +355 +356 +357 +358 +359 +360 +361 +362 +363 +364 +365 +366 +367 +368 +369 +370 +371 +372 +373 +374 +375 +376 +377 +378 +379 +380 +381 +382 +383 +384 +385 +386 +387 +388 +389 +390 +391 +392 +393 +394 +395 +396 +397 +398 +399 +400 +401 +402 +403 +404 +405 +406 +407 +408 +409 +410 +411 +412 +413 +414 +415 +416 +417 +418 +419 +420 +421 +422 +423 +424 +425 +426 +427 +428 +429 +430 +431 +432 +433 +434 +435 +436 +437 +438 +439 +440 +441 +442 +443 +444 +445 +446 +447 +448 +449 +450 +451 +452 +453 +454 +455 +456 +457 +458 +459 +460 +461 +462 +463 +464 +465 +466 +467 +468 +469 +470 +471 +472 +473 +474 +475 +476 +477 +478 +479 +480 +481 +482 +483 +484 +485 +486 +487 +488 +489 +490 +491 +492 +493 +494 +495 +496 +497 +498 +499 +500 +501 +502 +503 +504 +505 +506 +507 +508 +509 +510 +511 +512 +513 +514 +515 +516 +517 +518 +519 +520 +521 +522 +523 +524 +525 +526 +527 +528 +529 +530 +531 +532 +533 +534 +535 +536 +537 +538 +539 +540 +541 +542 +543 +544 +545 +546 +547 +548 +549 +550 +551 +552 +553 +554 +555 +556 +557 +558 +559 +560 +561 +562 +563 +564 +565 +566 +567 +568 +569 +570 +571 +572 +573 +574 +575 +576 +577 +578 +579 +580 +581 +582 +583 +584 +585 +586 +587 +588 +589 +590 +591 +592 +593 +594 +595 +596 +597 +598 +599 +600 +601 +602 +603 +604 +605 +606 +607 +608 +609 +610 +611 +612 +613 +614 +615 +616 +617 +618 +619 +620 +621 +622 +623 +624 +625 +626 +627 +628 +629 +630 +631 +632 +633 +634 +635 +636 +637 +638 +639 +640 +641 +642 +643 +644 +645 +646 +647 +648 +649 +650 +651 +652 +653 +654 +655 +656 +657 +658 +659 +660 +661 +662 +663 +664 +665 +666 +667 +668 +669 +670 +671 +672 +673 +674 +675 +676 +677 +678 +679 +680 +681 +682 +683 +684 +685 +686 +687 +688 +689 +690 +691 +692 +693 +694 +695 +696 +697 +698 +699 +700 +701 +702 +703 +704 +705 +706 +707 +708 +709 +710 +711 +712 +713 +714 +715 +716 +717 +718 +719 +720 +721 +722 +723 +724 +725 +726 +727 +728 +729 +730 +731 +732 +733 +734 +735 +736 +737 +738 +739 +740 +741 +742 +743 +744 +745 +746 +747 +748 +749 +750 +751 +752 +753 +754 +755 +756 +757 +758 +759 +760 +761 +762 +763 +764 +765 +766 +767 +768 +769 +770 +771 +772 +773 +774 +775 +776 +777 +778 +779 +780 +781 +782 +783 +784 +785 +786 +787 +788 +789 +790 +791 +792 +793 +794 +795 +796 +797 +798 +799 +800 +801 +802 +803 +804 +805 +806 +807 +808 +809 +810 +811 +812 +813 +814 +815 +816 +817 +818 +819 +820 +821 +822 +823 +824 +825 +826 +827 +828 +829 +830 +831 +832 +833 +834 +835 +836 +837 +838 +839 +840 +841 +842 +843 +844 +845 +846 +847 +848 +849 +850 +851 +852 +853 +854 +855 +856 +857 +858 +859 +860 +861 +862 +863 +864 +865 +866 +867 +868 +869 +870 +871 +872 +873 +874 +875 +876 +877 +878 +879 +880 +881 +882 +883 +884 +885 +886 +887 +888 +889 +890 +891 +892 +893 +894 +895 +896 diff --git a/chebai/preprocessing/datasets/molecule_classification.py b/chebai/preprocessing/datasets/molecule_classification.py index c79943ab..2fd06661 100644 --- a/chebai/preprocessing/datasets/molecule_classification.py +++ b/chebai/preprocessing/datasets/molecule_classification.py @@ -46,7 +46,7 @@ def label_number(self) -> int: @property def raw_file_names(self) -> List[str]: """Returns a list of raw file names.""" - return ["clintox_groups.csv"] + return ["clintox.csv"] # @property # def processed_file_names(self) -> List[str]: @@ -70,7 +70,7 @@ def download(self) -> None: ) with gzip.open(gout.name) as gfile: with open( - os.path.join(self.raw_dir, "clintox_groups.csv"), "wt" + os.path.join(self.raw_dir, "clintox.csv"), "wt" ) as fout: fout.write(gfile.read().decode()) @@ -79,12 +79,12 @@ def setup_processed(self) -> None: print("Create splits") data = list( self._load_data_from_file( - os.path.join(self.raw_dir, f"clintox_groups.csv") + os.path.join(self.raw_dir, f"clintox.csv") ) ) groups = np.array([d["group"] for d in data]) if not all(g is None for g in groups): - split_size = int(len(set(groups)) * self.train_split) + split_size = int(len(set(groups)) * (1 - self.test_split - self.validation_split)) os.makedirs(self.processed_dir, exist_ok=True) splitter = GroupShuffleSplit(train_size=split_size, n_splits=1) @@ -95,7 +95,7 @@ def setup_processed(self) -> None: split_groups = groups[temp_split_index] splitter = GroupShuffleSplit( - train_size=int(len(set(split_groups)) * self.train_split), n_splits=1 + train_size=int(len(set(split_groups)) * (1 - self.test_split - self.validation_split)), n_splits=1 ) test_split_index, validation_split_index = next( splitter.split(temp_split_index, groups=split_groups) @@ -108,12 +108,8 @@ def setup_processed(self) -> None: d for d in (data[temp_split_index[i]] for i in validation_split_index) ] else: - train_split, test_split = train_test_split( - data, train_size=self.train_split, shuffle=True - ) - test_split, validation_split = train_test_split( - test_split, train_size=0.5, shuffle=True - ) + train_split, test_split = train_test_split(data, test_size=self.test_split, shuffle=True) + train_split, validation_split = train_test_split(train_split, test_size=self.validation_split, shuffle=True) for k, split in [ ("test", test_split), ("train", train_split), @@ -140,7 +136,7 @@ def setup(self, **kwargs) -> None: self._after_setup() - def _set_processed_data_props(self): + def _set_processed_data_props(self): """ Load processed data and extract metadata. @@ -174,8 +170,10 @@ def _load_dict(self, input_file_path: str) -> List[Dict]: labels = [ bool(int(l)) if l else None for l in (row[k] for k in self.HEADERS) ] - group = int(row["group"]) - yield dict(features=smiles, labels=labels, ident=i, group=group) + # group = int(row["group"]) + yield dict(features=smiles, labels=labels, ident=i, + # group=group + ) # yield dict(features=smiles, labels=labels, ident=i) # yield self.reader.to_data(dict(features=smiles, labels=labels, ident=i)) @@ -203,7 +201,7 @@ def label_number(self) -> int: @property def raw_file_names(self) -> List[str]: """Returns a list of raw file names.""" - return ["bbbp_groups.csv"] + return ["bbbp.csv"] # @property # def processed_file_names(self) -> List[str]: @@ -230,12 +228,12 @@ def setup_processed(self) -> None: """Processes and splits the dataset.""" print("Create splits") data = list( - self._load_data_from_file(os.path.join(self.raw_dir, f"bbbp_groups.csv")) + self._load_data_from_file(os.path.join(self.raw_dir, f"bbbp.csv")) ) groups = np.array([d["group"] for d in data]) if not all(g is None for g in groups): print("Group shuffled") - split_size = int(len(set(groups)) * self.train_split) + split_size = int(len(set(groups)) * (1 - self.test_split - self.validation_split)) os.makedirs(self.processed_dir, exist_ok=True) splitter = GroupShuffleSplit(train_size=split_size, n_splits=1) @@ -246,7 +244,7 @@ def setup_processed(self) -> None: split_groups = groups[temp_split_index] splitter = GroupShuffleSplit( - train_size=int(len(set(split_groups)) * self.train_split), n_splits=1 + train_size=int(len(set(split_groups)) * (1 - self.test_split - self.validation_split)), n_splits=1 ) test_split_index, validation_split_index = next( splitter.split(temp_split_index, groups=split_groups) @@ -263,12 +261,8 @@ def setup_processed(self) -> None: # if d["original"] ] else: - train_split, test_split = train_test_split( - data, train_size=self.train_split, shuffle=True - ) - test_split, validation_split = train_test_split( - test_split, train_size=0.5, shuffle=True - ) + train_split, test_split = train_test_split(data, test_size=self.test_split, shuffle=True) + train_split, validation_split = train_test_split(train_split, test_size=self.validation_split, shuffle=True) for k, split in [ ("test", test_split), ("train", train_split), @@ -296,7 +290,7 @@ def setup(self, **kwargs) -> None: self._after_setup() - def _set_processed_data_props(self): + def _set_processed_data_props(self): """ Load processed data and extract metadata. @@ -329,8 +323,10 @@ def _load_dict(self, input_file_path: str) -> List[Dict]: i += 1 smiles = row["smiles"] labels = [int(row["p_np"])] - group = int(row["group"]) - yield dict(features=smiles, labels=labels, ident=i, group=group) + # group = int(row["group"]) + yield dict(features=smiles, labels=labels, ident=i + # , group=group + ) # yield self.reader.to_data(dict(features=smiles, labels=labels, ident=i)) def _perform_data_preparation(self, *args, **kwargs) -> None: @@ -383,7 +379,7 @@ def label_number(self) -> int: @property def raw_file_names(self) -> List[str]: """Returns a list of raw file names.""" - return ["sider_groups.csv"] + return ["sider.csv"] # @property # def processed_file_names(self) -> List[str]: @@ -413,11 +409,11 @@ def setup_processed(self) -> None: """Processes and splits the dataset.""" print("Create splits") data = list( - self._load_data_from_file(os.path.join(self.raw_dir, f"sider_groups.csv")) + self._load_data_from_file(os.path.join(self.raw_dir, f"sider.csv")) ) groups = np.array([d["group"] for d in data]) if not all(g is None for g in groups): - split_size = int(len(set(groups)) * self.train_split) + split_size = int(len(set(groups)) * (1 - self.test_split - self.validation_split)) os.makedirs(self.processed_dir, exist_ok=True) splitter = GroupShuffleSplit(train_size=split_size, n_splits=1) @@ -428,7 +424,7 @@ def setup_processed(self) -> None: split_groups = groups[temp_split_index] splitter = GroupShuffleSplit( - train_size=int(len(set(split_groups)) * self.train_split), n_splits=1 + train_size=int(len(set(split_groups)) * (1 - self.test_split - self.validation_split)), n_splits=1 ) test_split_index, validation_split_index = next( splitter.split(temp_split_index, groups=split_groups) @@ -445,12 +441,8 @@ def setup_processed(self) -> None: # if d["original"] ] else: - train_split, test_split = train_test_split( - data, train_size=self.train_split, shuffle=True - ) - test_split, validation_split = train_test_split( - test_split, train_size=0.5, shuffle=True - ) + train_split, test_split = train_test_split(data, test_size=self.test_split, shuffle=True) + train_split, validation_split = train_test_split(train_split, test_size=self.validation_split, shuffle=True) for k, split in [ ("test", test_split), ("train", train_split), @@ -477,7 +469,7 @@ def setup(self, **kwargs) -> None: self._after_setup() - def _set_processed_data_props(self): + def _set_processed_data_props(self): """ Load processed data and extract metadata. @@ -511,8 +503,10 @@ def _load_dict(self, input_file_path: str) -> List[Dict]: labels = [ bool(int(l)) if l else None for l in (row[k] for k in self.HEADERS) ] - group = row["group"] - yield dict(features=smiles, labels=labels, ident=i, group=group) + # group = row["group"] + yield dict(features=smiles, labels=labels, ident=i + # , group=group + ) # yield self.reader.to_data(dict(features=smiles, labels=labels, ident=i)) def _perform_data_preparation(self, *args, **kwargs) -> None: @@ -568,7 +562,7 @@ def setup_processed(self) -> None: # groups = np.array([d.get("group") for d in data]) # if not all(g is None for g in groups): - # split_size = int(len(set(groups)) * self.train_split) + # split_size = int(len(set(groups)) * (1 - self.test_split - self.validation_split)) # os.makedirs(self.processed_dir, exist_ok=True) # splitter = GroupShuffleSplit(train_size=split_size, n_splits=1) @@ -579,7 +573,7 @@ def setup_processed(self) -> None: # split_groups = groups[temp_split_index] # splitter = GroupShuffleSplit( - # train_size=int(len(set(split_groups)) * self.train_split), n_splits=1 + # train_size=int(len(set(split_groups)) * (1 - self.test_split - self.validation_split)), n_splits=1 # ) # test_split_index, validation_split_index = next( # splitter.split(temp_split_index, groups=split_groups) @@ -594,12 +588,8 @@ def setup_processed(self) -> None: # for d in (data[temp_split_index[i]] for i in validation_split_index) # ] # else: - train_split, test_split = train_test_split( - data, train_size=self.train_split, shuffle=True - ) - test_split, validation_split = train_test_split( - test_split, train_size=0.5, shuffle=True - ) + train_split, test_split = train_test_split(data, test_size=self.test_split, shuffle=True) + train_split, validation_split = train_test_split(train_split, test_size=self.validation_split, shuffle=True) for k, split in [ ("test", test_split), ("train", train_split), @@ -626,7 +616,7 @@ def setup(self, **kwargs) -> None: self._after_setup() - def _set_processed_data_props(self): + def _set_processed_data_props(self): """ Load processed data and extract metadata. @@ -686,7 +676,7 @@ def label_number(self) -> int: @property def raw_file_names(self) -> List[str]: """Returns a list of raw file names.""" - return ["hiv_groups.csv"] + return ["hiv.csv"] # @property # def processed_file_names(self) -> List[str]: @@ -703,7 +693,7 @@ def processed_file_names_dict(self) -> dict: def download(self) -> None: """Downloads and extracts the dataset.""" - with open(os.path.join(self.raw_dir, "hiv_groups.csv"), "ab") as dst: + with open(os.path.join(self.raw_dir, "hiv.csv"), "ab") as dst: with request.urlopen( f"https://deepchemdata.s3-us-west-1.amazonaws.com/datasets/HIV.csv", ) as src: @@ -713,12 +703,12 @@ def setup_processed(self) -> None: """Processes and splits the dataset.""" print("Create splits") data = list( - self._load_data_from_file(os.path.join(self.raw_dir, f"hiv_groups.csv")) + self._load_data_from_file(os.path.join(self.raw_dir, f"hiv.csv")) ) groups = np.array([d["group"] for d in data]) if not all(g is None for g in groups): print("Group shuffled") - split_size = int(len(set(groups)) * self.train_split) + split_size = int(len(set(groups)) * (1 - self.test_split - self.validation_split)) os.makedirs(self.processed_dir, exist_ok=True) splitter = GroupShuffleSplit(train_size=split_size, n_splits=1) @@ -729,7 +719,7 @@ def setup_processed(self) -> None: split_groups = groups[temp_split_index] splitter = GroupShuffleSplit( - train_size=int(len(set(split_groups)) * self.train_split), n_splits=1 + train_size=int(len(set(split_groups)) * (1 - self.test_split - self.validation_split)), n_splits=1 ) test_split_index, validation_split_index = next( splitter.split(temp_split_index, groups=split_groups) @@ -742,12 +732,8 @@ def setup_processed(self) -> None: d for d in (data[temp_split_index[i]] for i in validation_split_index) ] else: - train_split, test_split = train_test_split( - data, train_size=self.train_split, shuffle=True - ) - test_split, validation_split = train_test_split( - test_split, train_size=0.5, shuffle=True - ) + train_split, test_split = train_test_split(data, test_size=self.test_split, shuffle=True) + train_split, validation_split = train_test_split(train_split, test_size=self.validation_split, shuffle=True) for k, split in [ ("test", test_split), ("train", train_split), @@ -774,7 +760,7 @@ def setup(self, **kwargs) -> None: self._after_setup() - def _set_processed_data_props(self): + def _set_processed_data_props(self): """ Load processed data and extract metadata. @@ -807,8 +793,10 @@ def _load_dict(self, input_file_path: str) -> List[Dict]: i += 1 smiles = row["smiles"] labels = [int(row["HIV_active"])] - group = int(row["group"]) - yield dict(features=smiles, labels=labels, ident=i, group=group) + # group = int(row["group"]) + yield dict(features=smiles, labels=labels, ident=i + # , group=group + ) # yield self.reader.to_data(dict(features=smiles, labels=labels, ident=i)) def _perform_data_preparation(self, *args, **kwargs) -> None: @@ -883,7 +871,7 @@ def setup_processed(self) -> None: data = list(self._load_data_from_file(os.path.join(self.raw_dir, f"muv.csv"))) groups = np.array([d["group"] for d in data]) if not all(g is None for g in groups): - split_size = int(len(set(groups)) * self.train_split) + split_size = int(len(set(groups)) * (1 - self.test_split - self.validation_split)) os.makedirs(self.processed_dir, exist_ok=True) splitter = GroupShuffleSplit(train_size=split_size, n_splits=1) @@ -894,7 +882,7 @@ def setup_processed(self) -> None: split_groups = groups[temp_split_index] splitter = GroupShuffleSplit( - train_size=int(len(set(split_groups)) * self.train_split), n_splits=1 + train_size=int(len(set(split_groups)) * (1 - self.test_split - self.validation_split)), n_splits=1 ) test_split_index, validation_split_index = next( splitter.split(temp_split_index, groups=split_groups) @@ -911,12 +899,8 @@ def setup_processed(self) -> None: # if d["original"] ] else: - train_split, test_split = train_test_split( - data, train_size=self.train_split, shuffle=True - ) - test_split, validation_split = train_test_split( - test_split, train_size=0.5, shuffle=True - ) + train_split, test_split = train_test_split(data, test_size=self.test_split, shuffle=True) + train_split, validation_split = train_test_split(train_split, test_size=self.validation_split, shuffle=True) for k, split in [ ("test", test_split), ("train", train_split), @@ -943,7 +927,7 @@ def setup(self, **kwargs) -> None: self._after_setup() - def _set_processed_data_props(self): + def _set_processed_data_props(self): """ Load processed data and extract metadata. diff --git a/chebai/preprocessing/datasets/tox21.py b/chebai/preprocessing/datasets/tox21.py index 6e86b057..b4a639bf 100644 --- a/chebai/preprocessing/datasets/tox21.py +++ b/chebai/preprocessing/datasets/tox21.py @@ -46,7 +46,6 @@ def _name(self) -> str: @property def raw_file_names(self) -> List[str]: """Returns a list of raw file names.""" - # return ["tox21.csv"] return ["tox21.csv"] # @property @@ -84,7 +83,7 @@ def setup_processed(self) -> None: groups = np.array([d.get("group") for d in data]) if not all(g is None for g in groups): - split_size = int(len(set(groups)) * self.train_split) + split_size = int(len(set(groups)) * (1 - self.test_split - self.validation_split)) os.makedirs(self.processed_dir, exist_ok=True) splitter = GroupShuffleSplit(train_size=split_size, n_splits=1) @@ -95,7 +94,7 @@ def setup_processed(self) -> None: split_groups = groups[temp_split_index] splitter = GroupShuffleSplit( - train_size=int(len(set(split_groups)) * self.train_split), n_splits=1 + train_size=int(len(set(split_groups)) * (1 - self.test_split - self.validation_split)), n_splits=1 ) test_split_index, validation_split_index = next( splitter.split(temp_split_index, groups=split_groups) @@ -112,13 +111,9 @@ def setup_processed(self) -> None: # if d["original"] ] else: + train_split, test_split = train_test_split(data, test_size=self.test_split, shuffle=True) + train_split, validation_split = train_test_split(train_split, test_size=self.validation_split, shuffle=True) - train_split, test_split = train_test_split( - data, train_size=self.train_split, shuffle=True - ) - test_split, validation_split = train_test_split( - test_split, train_size=0.5, shuffle=True - ) for k, split in [ ("test", test_split), ("train", train_split), @@ -147,7 +142,8 @@ def setup(self, **kwargs) -> None: ): self.setup_processed() - self._set_processed_data_props() + # self._set_processed_data_props() + self._after_setup() def _load_dict(self, input_file_path: str) -> List[Dict]: """Loads data from a CSV file. @@ -166,9 +162,10 @@ def _load_dict(self, input_file_path: str) -> List[Dict]: bool(int(float(l))) if len(l) > 1 else None for l in (row[k] for k in self.HEADERS) ] - group = int(row["group"]) + # group = int(row["group"]) yield dict( - features=smiles, labels=labels, ident=row["mol_id"], group=group + features=smiles, labels=labels, ident=row["mol_id"] + # group=group ) # yield self.reader.to_data(dict(features=smiles, labels=labels, ident=row["mol_id"])) def _set_processed_data_props(self): @@ -187,6 +184,8 @@ def _set_processed_data_props(self): self._num_of_labels = len(data_pt[0]["labels"]) self._feature_vector_size = max(len(d["features"]) for d in data_pt) + def _perform_data_preparation(self, *args, **kwargs) -> None: + pass class Tox21Challenge(XYBaseDataModule): """Data module for Tox21Challenge dataset.""" From 9b29411ddaccdf1ff678036cb30c449ef418c4f9 Mon Sep 17 00:00:00 2001 From: Charlotte Tumescheit Date: Wed, 29 Oct 2025 17:38:24 +0100 Subject: [PATCH 47/54] lightning cli issue --- chebai/cli.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/chebai/cli.py b/chebai/cli.py index c4f7401d..b1fbee28 100644 --- a/chebai/cli.py +++ b/chebai/cli.py @@ -60,9 +60,10 @@ def call_data_methods(data: Type[XYBaseDataModule]): ) for kind in ("train", "val", "test"): - # todo: fix this + for average in ("micro-f1", "macro-f1", "balanced-accuracy", "roc-auc", "f1", "mse", "rmse", "r2"): + # When using lightning > 2.5.1 then need to uncomment all metrics that are not used # for average in ("mse", "rmse","r2"): # for regression - for average in ("f1", "roc-auc"): # for binary classification + # for average in ("f1", "roc-auc"): # for binary classification # for average in ("micro-f1", "macro-f1", "roc-auc"): # for multilabel classification # for average in ("micro-f1", "macro-f1", "balanced-accuracy", "roc-auc"): # for multilabel classification using balanced-accuracy parser.link_arguments( From d56e226b172cae35db5aa34eaf5efbfaf5a7aadc Mon Sep 17 00:00:00 2001 From: schnamo Date: Wed, 29 Oct 2025 18:18:54 +0100 Subject: [PATCH 48/54] black-lint fix --- chebai/cli.py | 27 ++- chebai/models/electra.py | 2 +- .../datasets/molecule_classification.py | 178 ++++++++++++------ .../datasets/molecule_regression.py | 29 ++- chebai/preprocessing/datasets/solCuration.py | 30 +-- chebai/preprocessing/datasets/tox21.py | 34 ++-- chebai/result/molplot.py | 1 + 7 files changed, 195 insertions(+), 106 deletions(-) diff --git a/chebai/cli.py b/chebai/cli.py index b1fbee28..502a5834 100644 --- a/chebai/cli.py +++ b/chebai/cli.py @@ -43,7 +43,7 @@ def add_arguments_to_parser(self, parser: LightningArgumentParser): def call_data_methods(data: Type[XYBaseDataModule]): if data._num_of_labels is None: data.prepare_data() - data.setup() + data.setup() return data.num_of_labels parser.link_arguments( @@ -60,18 +60,27 @@ def call_data_methods(data: Type[XYBaseDataModule]): ) for kind in ("train", "val", "test"): - for average in ("micro-f1", "macro-f1", "balanced-accuracy", "roc-auc", "f1", "mse", "rmse", "r2"): - # When using lightning > 2.5.1 then need to uncomment all metrics that are not used - # for average in ("mse", "rmse","r2"): # for regression - # for average in ("f1", "roc-auc"): # for binary classification - # for average in ("micro-f1", "macro-f1", "roc-auc"): # for multilabel classification - # for average in ("micro-f1", "macro-f1", "balanced-accuracy", "roc-auc"): # for multilabel classification using balanced-accuracy + for average in ( + "micro-f1", + "macro-f1", + "balanced-accuracy", + "roc-auc", + "f1", + "mse", + "rmse", + "r2", + ): + # When using lightning > 2.5.1 then need to uncomment all metrics that are not used + # for average in ("mse", "rmse","r2"): # for regression + # for average in ("f1", "roc-auc"): # for binary classification + # for average in ("micro-f1", "macro-f1", "roc-auc"): # for multilabel classification + # for average in ("micro-f1", "macro-f1", "balanced-accuracy", "roc-auc"): # for multilabel classification using balanced-accuracy parser.link_arguments( "data.num_of_labels", f"model.init_args.{kind}_metrics.init_args.metrics.{average}.init_args.num_labels", apply_on="instantiate", ) - + parser.link_arguments( "data.num_of_labels", "trainer.callbacks.init_args.num_labels" ) @@ -84,7 +93,7 @@ def call_data_methods(data: Type[XYBaseDataModule]): # parser.link_arguments( # "data.init_args.chebi_version", # "model.init_args.criterion.init_args.data_extractor.init_args.chebi_version", - # ) + # ) @staticmethod def subcommands() -> Dict[str, Set[str]]: diff --git a/chebai/models/electra.py b/chebai/models/electra.py index 45b615a2..103b9114 100644 --- a/chebai/models/electra.py +++ b/chebai/models/electra.py @@ -41,7 +41,7 @@ class ElectraPre(ChebaiBaseNet): def __init__(self, config: Dict[str, Any] = None, **kwargs: Any): super().__init__(config=config, **kwargs) - + self.generator_config = ElectraConfig(**config["generator"]) self.generator = ElectraForMaskedLM(self.generator_config) self.discriminator_config = ElectraConfig(**config["discriminator"]) diff --git a/chebai/preprocessing/datasets/molecule_classification.py b/chebai/preprocessing/datasets/molecule_classification.py index 2fd06661..3abe94a3 100644 --- a/chebai/preprocessing/datasets/molecule_classification.py +++ b/chebai/preprocessing/datasets/molecule_classification.py @@ -56,8 +56,8 @@ def raw_file_names(self) -> List[str]: @property def processed_file_names_dict(self) -> dict: return { - "test": "test.pt", - "train": "train.pt", + "test": "test.pt", + "train": "train.pt", "validation": "validation.pt", } @@ -69,22 +69,20 @@ def download(self) -> None: gout.name, ) with gzip.open(gout.name) as gfile: - with open( - os.path.join(self.raw_dir, "clintox.csv"), "wt" - ) as fout: + with open(os.path.join(self.raw_dir, "clintox.csv"), "wt") as fout: fout.write(gfile.read().decode()) def setup_processed(self) -> None: """Processes and splits the dataset.""" print("Create splits") data = list( - self._load_data_from_file( - os.path.join(self.raw_dir, f"clintox.csv") - ) + self._load_data_from_file(os.path.join(self.raw_dir, f"clintox.csv")) ) groups = np.array([d["group"] for d in data]) if not all(g is None for g in groups): - split_size = int(len(set(groups)) * (1 - self.test_split - self.validation_split)) + split_size = int( + len(set(groups)) * (1 - self.test_split - self.validation_split) + ) os.makedirs(self.processed_dir, exist_ok=True) splitter = GroupShuffleSplit(train_size=split_size, n_splits=1) @@ -95,7 +93,11 @@ def setup_processed(self) -> None: split_groups = groups[temp_split_index] splitter = GroupShuffleSplit( - train_size=int(len(set(split_groups)) * (1 - self.test_split - self.validation_split)), n_splits=1 + train_size=int( + len(set(split_groups)) + * (1 - self.test_split - self.validation_split) + ), + n_splits=1, ) test_split_index, validation_split_index = next( splitter.split(temp_split_index, groups=split_groups) @@ -108,8 +110,12 @@ def setup_processed(self) -> None: d for d in (data[temp_split_index[i]] for i in validation_split_index) ] else: - train_split, test_split = train_test_split(data, test_size=self.test_split, shuffle=True) - train_split, validation_split = train_test_split(train_split, test_size=self.validation_split, shuffle=True) + train_split, test_split = train_test_split( + data, test_size=self.test_split, shuffle=True + ) + train_split, validation_split = train_test_split( + train_split, test_size=self.validation_split, shuffle=True + ) for k, split in [ ("test", test_split), ("train", train_split), @@ -171,8 +177,11 @@ def _load_dict(self, input_file_path: str) -> List[Dict]: bool(int(l)) if l else None for l in (row[k] for k in self.HEADERS) ] # group = int(row["group"]) - yield dict(features=smiles, labels=labels, ident=i, - # group=group + yield dict( + features=smiles, + labels=labels, + ident=i, + # group=group ) # yield dict(features=smiles, labels=labels, ident=i) # yield self.reader.to_data(dict(features=smiles, labels=labels, ident=i)) @@ -211,8 +220,8 @@ def raw_file_names(self) -> List[str]: @property def processed_file_names_dict(self) -> dict: return { - "test": "test.pt", - "train": "train.pt", + "test": "test.pt", + "train": "train.pt", "validation": "validation.pt", } @@ -227,13 +236,13 @@ def download(self) -> None: def setup_processed(self) -> None: """Processes and splits the dataset.""" print("Create splits") - data = list( - self._load_data_from_file(os.path.join(self.raw_dir, f"bbbp.csv")) - ) + data = list(self._load_data_from_file(os.path.join(self.raw_dir, f"bbbp.csv"))) groups = np.array([d["group"] for d in data]) if not all(g is None for g in groups): print("Group shuffled") - split_size = int(len(set(groups)) * (1 - self.test_split - self.validation_split)) + split_size = int( + len(set(groups)) * (1 - self.test_split - self.validation_split) + ) os.makedirs(self.processed_dir, exist_ok=True) splitter = GroupShuffleSplit(train_size=split_size, n_splits=1) @@ -244,7 +253,11 @@ def setup_processed(self) -> None: split_groups = groups[temp_split_index] splitter = GroupShuffleSplit( - train_size=int(len(set(split_groups)) * (1 - self.test_split - self.validation_split)), n_splits=1 + train_size=int( + len(set(split_groups)) + * (1 - self.test_split - self.validation_split) + ), + n_splits=1, ) test_split_index, validation_split_index = next( splitter.split(temp_split_index, groups=split_groups) @@ -261,8 +274,12 @@ def setup_processed(self) -> None: # if d["original"] ] else: - train_split, test_split = train_test_split(data, test_size=self.test_split, shuffle=True) - train_split, validation_split = train_test_split(train_split, test_size=self.validation_split, shuffle=True) + train_split, test_split = train_test_split( + data, test_size=self.test_split, shuffle=True + ) + train_split, validation_split = train_test_split( + train_split, test_size=self.validation_split, shuffle=True + ) for k, split in [ ("test", test_split), ("train", train_split), @@ -286,9 +303,8 @@ def setup(self, **kwargs) -> None: for f in self.processed_file_names ): self.setup_processed() - - self._after_setup() + self._after_setup() def _set_processed_data_props(self): """ @@ -306,7 +322,6 @@ def _set_processed_data_props(self): self._num_of_labels = len(data_pt[0]["labels"]) self._feature_vector_size = max(len(d["features"]) for d in data_pt) - def _load_dict(self, input_file_path: str) -> List[Dict]: """Loads data from a CSV file. @@ -324,8 +339,11 @@ def _load_dict(self, input_file_path: str) -> List[Dict]: smiles = row["smiles"] labels = [int(row["p_np"])] # group = int(row["group"]) - yield dict(features=smiles, labels=labels, ident=i - # , group=group + yield dict( + features=smiles, + labels=labels, + ident=i, + # , group=group ) # yield self.reader.to_data(dict(features=smiles, labels=labels, ident=i)) @@ -389,8 +407,8 @@ def raw_file_names(self) -> List[str]: @property def processed_file_names_dict(self) -> dict: return { - "test": "test.pt", - "train": "train.pt", + "test": "test.pt", + "train": "train.pt", "validation": "validation.pt", } @@ -408,12 +426,12 @@ def download(self) -> None: def setup_processed(self) -> None: """Processes and splits the dataset.""" print("Create splits") - data = list( - self._load_data_from_file(os.path.join(self.raw_dir, f"sider.csv")) - ) + data = list(self._load_data_from_file(os.path.join(self.raw_dir, f"sider.csv"))) groups = np.array([d["group"] for d in data]) if not all(g is None for g in groups): - split_size = int(len(set(groups)) * (1 - self.test_split - self.validation_split)) + split_size = int( + len(set(groups)) * (1 - self.test_split - self.validation_split) + ) os.makedirs(self.processed_dir, exist_ok=True) splitter = GroupShuffleSplit(train_size=split_size, n_splits=1) @@ -424,7 +442,11 @@ def setup_processed(self) -> None: split_groups = groups[temp_split_index] splitter = GroupShuffleSplit( - train_size=int(len(set(split_groups)) * (1 - self.test_split - self.validation_split)), n_splits=1 + train_size=int( + len(set(split_groups)) + * (1 - self.test_split - self.validation_split) + ), + n_splits=1, ) test_split_index, validation_split_index = next( splitter.split(temp_split_index, groups=split_groups) @@ -441,8 +463,12 @@ def setup_processed(self) -> None: # if d["original"] ] else: - train_split, test_split = train_test_split(data, test_size=self.test_split, shuffle=True) - train_split, validation_split = train_test_split(train_split, test_size=self.validation_split, shuffle=True) + train_split, test_split = train_test_split( + data, test_size=self.test_split, shuffle=True + ) + train_split, validation_split = train_test_split( + train_split, test_size=self.validation_split, shuffle=True + ) for k, split in [ ("test", test_split), ("train", train_split), @@ -466,7 +492,7 @@ def setup(self, **kwargs) -> None: for f in self.processed_file_names ): self.setup_processed() - + self._after_setup() def _set_processed_data_props(self): @@ -504,14 +530,18 @@ def _load_dict(self, input_file_path: str) -> List[Dict]: bool(int(l)) if l else None for l in (row[k] for k in self.HEADERS) ] # group = row["group"] - yield dict(features=smiles, labels=labels, ident=i - # , group=group + yield dict( + features=smiles, + labels=labels, + ident=i, + # , group=group ) # yield self.reader.to_data(dict(features=smiles, labels=labels, ident=i)) def _perform_data_preparation(self, *args, **kwargs) -> None: pass + class Bace(XYBaseDataModule): """Data module for ClinTox MoleculeNet dataset.""" @@ -542,8 +572,8 @@ def raw_file_names(self) -> List[str]: @property def processed_file_names_dict(self) -> dict: return { - "test": "test.pt", - "train": "train.pt", + "test": "test.pt", + "train": "train.pt", "validation": "validation.pt", } @@ -588,8 +618,12 @@ def setup_processed(self) -> None: # for d in (data[temp_split_index[i]] for i in validation_split_index) # ] # else: - train_split, test_split = train_test_split(data, test_size=self.test_split, shuffle=True) - train_split, validation_split = train_test_split(train_split, test_size=self.validation_split, shuffle=True) + train_split, test_split = train_test_split( + data, test_size=self.test_split, shuffle=True + ) + train_split, validation_split = train_test_split( + train_split, test_size=self.validation_split, shuffle=True + ) for k, split in [ ("test", test_split), ("train", train_split), @@ -686,8 +720,8 @@ def raw_file_names(self) -> List[str]: @property def processed_file_names_dict(self) -> dict: return { - "test": "test.pt", - "train": "train.pt", + "test": "test.pt", + "train": "train.pt", "validation": "validation.pt", } @@ -702,13 +736,13 @@ def download(self) -> None: def setup_processed(self) -> None: """Processes and splits the dataset.""" print("Create splits") - data = list( - self._load_data_from_file(os.path.join(self.raw_dir, f"hiv.csv")) - ) + data = list(self._load_data_from_file(os.path.join(self.raw_dir, f"hiv.csv"))) groups = np.array([d["group"] for d in data]) if not all(g is None for g in groups): print("Group shuffled") - split_size = int(len(set(groups)) * (1 - self.test_split - self.validation_split)) + split_size = int( + len(set(groups)) * (1 - self.test_split - self.validation_split) + ) os.makedirs(self.processed_dir, exist_ok=True) splitter = GroupShuffleSplit(train_size=split_size, n_splits=1) @@ -719,7 +753,11 @@ def setup_processed(self) -> None: split_groups = groups[temp_split_index] splitter = GroupShuffleSplit( - train_size=int(len(set(split_groups)) * (1 - self.test_split - self.validation_split)), n_splits=1 + train_size=int( + len(set(split_groups)) + * (1 - self.test_split - self.validation_split) + ), + n_splits=1, ) test_split_index, validation_split_index = next( splitter.split(temp_split_index, groups=split_groups) @@ -732,8 +770,12 @@ def setup_processed(self) -> None: d for d in (data[temp_split_index[i]] for i in validation_split_index) ] else: - train_split, test_split = train_test_split(data, test_size=self.test_split, shuffle=True) - train_split, validation_split = train_test_split(train_split, test_size=self.validation_split, shuffle=True) + train_split, test_split = train_test_split( + data, test_size=self.test_split, shuffle=True + ) + train_split, validation_split = train_test_split( + train_split, test_size=self.validation_split, shuffle=True + ) for k, split in [ ("test", test_split), ("train", train_split), @@ -794,8 +836,11 @@ def _load_dict(self, input_file_path: str) -> List[Dict]: smiles = row["smiles"] labels = [int(row["HIV_active"])] # group = int(row["group"]) - yield dict(features=smiles, labels=labels, ident=i - # , group=group + yield dict( + features=smiles, + labels=labels, + ident=i, + # , group=group ) # yield self.reader.to_data(dict(features=smiles, labels=labels, ident=i)) @@ -849,8 +894,8 @@ def raw_file_names(self) -> List[str]: @property def processed_file_names_dict(self) -> dict: return { - "test": "test.pt", - "train": "train.pt", + "test": "test.pt", + "train": "train.pt", "validation": "validation.pt", } @@ -871,7 +916,9 @@ def setup_processed(self) -> None: data = list(self._load_data_from_file(os.path.join(self.raw_dir, f"muv.csv"))) groups = np.array([d["group"] for d in data]) if not all(g is None for g in groups): - split_size = int(len(set(groups)) * (1 - self.test_split - self.validation_split)) + split_size = int( + len(set(groups)) * (1 - self.test_split - self.validation_split) + ) os.makedirs(self.processed_dir, exist_ok=True) splitter = GroupShuffleSplit(train_size=split_size, n_splits=1) @@ -882,7 +929,11 @@ def setup_processed(self) -> None: split_groups = groups[temp_split_index] splitter = GroupShuffleSplit( - train_size=int(len(set(split_groups)) * (1 - self.test_split - self.validation_split)), n_splits=1 + train_size=int( + len(set(split_groups)) + * (1 - self.test_split - self.validation_split) + ), + n_splits=1, ) test_split_index, validation_split_index = next( splitter.split(temp_split_index, groups=split_groups) @@ -899,8 +950,12 @@ def setup_processed(self) -> None: # if d["original"] ] else: - train_split, test_split = train_test_split(data, test_size=self.test_split, shuffle=True) - train_split, validation_split = train_test_split(train_split, test_size=self.validation_split, shuffle=True) + train_split, test_split = train_test_split( + data, test_size=self.test_split, shuffle=True + ) + train_split, validation_split = train_test_split( + train_split, test_size=self.validation_split, shuffle=True + ) for k, split in [ ("test", test_split), ("train", train_split), @@ -943,7 +998,6 @@ def _set_processed_data_props(self): self._num_of_labels = len(data_pt[0]["labels"]) self._feature_vector_size = max(len(d["features"]) for d in data_pt) - def _load_dict(self, input_file_path: str) -> List[Dict]: """Loads data from a CSV file. diff --git a/chebai/preprocessing/datasets/molecule_regression.py b/chebai/preprocessing/datasets/molecule_regression.py index 900037be..725b1eec 100644 --- a/chebai/preprocessing/datasets/molecule_regression.py +++ b/chebai/preprocessing/datasets/molecule_regression.py @@ -45,8 +45,8 @@ def raw_file_names(self): @property def processed_file_names_dict(self) -> dict: return { - "test": "test.pt", - "train": "train.pt", + "test": "test.pt", + "train": "train.pt", "validation": "validation.pt", } @@ -63,8 +63,12 @@ def setup_processed(self): data = list(self._load_data_from_file(os.path.join(self.raw_dir, f"Lipo.csv"))) print(len(data)) - train_split, test_split = train_test_split(data, test_size=self.test_split, shuffle=True) - train_split, validation_split = train_test_split(train_split, test_size=self.validation_split, shuffle=True) + train_split, test_split = train_test_split( + data, test_size=self.test_split, shuffle=True + ) + train_split, validation_split = train_test_split( + train_split, test_size=self.validation_split, shuffle=True + ) if False: train_split, test_split = train_test_split( @@ -120,7 +124,6 @@ def _set_processed_data_props(self): self._num_of_labels = len(data_pt[0]["labels"]) self._feature_vector_size = max(len(d["features"]) for d in data_pt) - def _load_dict(self, input_file_path: str) -> List[Dict]: """Loads data from a CSV file. @@ -142,15 +145,17 @@ def _load_dict(self, input_file_path: str) -> List[Dict]: for i in range(0, len(smiles_l)): yield dict(features=smiles_l[i], labels=[labels_l[i]], ident=i) # yield self.reader.to_data(dict(features=smiles_l[i], labels=[labels_l[i]], ident=i)) - + def _perform_data_preparation(self, *args, **kwargs) -> None: pass + class LipoChem(Lipo): """Chemical data reader for the solubility dataset.""" READER = dr.ChemDataReader + class FreeSolv(XYBaseDataModule): HEADERS = [ "expt", @@ -175,8 +180,8 @@ def raw_file_names(self): @property def processed_file_names_dict(self) -> dict: return { - "test": "test.pt", - "train": "train.pt", + "test": "test.pt", + "train": "train.pt", "validation": "validation.pt", } @@ -194,8 +199,12 @@ def setup_processed(self): self._load_data_from_file(os.path.join(self.raw_dir, f"FreeSolv.csv")) ) print(len(data)) - train_split, test_split = train_test_split(data, test_size=self.test_split, shuffle=True) - train_split, validation_split = train_test_split(train_split, test_size=self.validation_split, shuffle=True) + train_split, test_split = train_test_split( + data, test_size=self.test_split, shuffle=True + ) + train_split, validation_split = train_test_split( + train_split, test_size=self.validation_split, shuffle=True + ) if False: train_split, test_split = train_test_split( diff --git a/chebai/preprocessing/datasets/solCuration.py b/chebai/preprocessing/datasets/solCuration.py index b8ab6dd3..35cd3b27 100644 --- a/chebai/preprocessing/datasets/solCuration.py +++ b/chebai/preprocessing/datasets/solCuration.py @@ -45,8 +45,8 @@ def raw_file_names(self): @property def processed_file_names_dict(self) -> dict: return { - "test": "test.pt", - "train": "train.pt", + "test": "test.pt", + "train": "train.pt", "validation": "validation.pt", } @@ -69,9 +69,12 @@ def setup_processed(self): ) print(len(data)) - train_split, test_split = train_test_split(data, test_size=self.test_split, shuffle=True) - train_split, validation_split = train_test_split(train_split, test_size=self.validation_split, shuffle=True) - + train_split, test_split = train_test_split( + data, test_size=self.test_split, shuffle=True + ) + train_split, validation_split = train_test_split( + train_split, test_size=self.validation_split, shuffle=True + ) if False: train_split, test_split = train_test_split( @@ -161,6 +164,7 @@ def _load_data_from_file(self, input_file_path: str) -> List[Dict]: def _perform_data_preparation(self, *args, **kwargs) -> None: pass + class SolCurationChem(SolCuration): """Chemical data reader for the solubility dataset.""" @@ -191,8 +195,8 @@ def raw_file_names(self): @property def processed_file_names_dict(self) -> dict: return { - "test": "test.pt", - "train": "train.pt", + "test": "test.pt", + "train": "train.pt", "validation": "validation.pt", } @@ -211,10 +215,12 @@ def setup_processed(self): ) print(len(data)) - - train_split, test_split = train_test_split(data, test_size=self.test_split, shuffle=True) - train_split, validation_split = train_test_split(train_split, test_size=self.validation_split, shuffle=True) - + train_split, test_split = train_test_split( + data, test_size=self.test_split, shuffle=True + ) + train_split, validation_split = train_test_split( + train_split, test_size=self.validation_split, shuffle=True + ) if False: train_split, test_split = train_test_split( @@ -251,7 +257,7 @@ def setup(self, **kwargs): for f in self.processed_file_names ): self.setup_processed() - + self._after_setup() def _set_processed_data_props(self): diff --git a/chebai/preprocessing/datasets/tox21.py b/chebai/preprocessing/datasets/tox21.py index b4a639bf..5d65684e 100644 --- a/chebai/preprocessing/datasets/tox21.py +++ b/chebai/preprocessing/datasets/tox21.py @@ -56,8 +56,8 @@ def raw_file_names(self) -> List[str]: @property def processed_file_names_dict(self) -> dict: return { - "test": "test.pt", - "train": "train.pt", + "test": "test.pt", + "train": "train.pt", "validation": "validation.pt", } @@ -75,15 +75,13 @@ def download(self) -> None: def setup_processed(self) -> None: """Processes and splits the dataset.""" print("Create splits") - data = list( - self._load_data_from_file( - os.path.join(self.raw_dir, f"tox21.csv") - ) - ) + data = list(self._load_data_from_file(os.path.join(self.raw_dir, f"tox21.csv"))) groups = np.array([d.get("group") for d in data]) if not all(g is None for g in groups): - split_size = int(len(set(groups)) * (1 - self.test_split - self.validation_split)) + split_size = int( + len(set(groups)) * (1 - self.test_split - self.validation_split) + ) os.makedirs(self.processed_dir, exist_ok=True) splitter = GroupShuffleSplit(train_size=split_size, n_splits=1) @@ -94,7 +92,11 @@ def setup_processed(self) -> None: split_groups = groups[temp_split_index] splitter = GroupShuffleSplit( - train_size=int(len(set(split_groups)) * (1 - self.test_split - self.validation_split)), n_splits=1 + train_size=int( + len(set(split_groups)) + * (1 - self.test_split - self.validation_split) + ), + n_splits=1, ) test_split_index, validation_split_index = next( splitter.split(temp_split_index, groups=split_groups) @@ -111,8 +113,12 @@ def setup_processed(self) -> None: # if d["original"] ] else: - train_split, test_split = train_test_split(data, test_size=self.test_split, shuffle=True) - train_split, validation_split = train_test_split(train_split, test_size=self.validation_split, shuffle=True) + train_split, test_split = train_test_split( + data, test_size=self.test_split, shuffle=True + ) + train_split, validation_split = train_test_split( + train_split, test_size=self.validation_split, shuffle=True + ) for k, split in [ ("test", test_split), @@ -164,10 +170,13 @@ def _load_dict(self, input_file_path: str) -> List[Dict]: ] # group = int(row["group"]) yield dict( - features=smiles, labels=labels, ident=row["mol_id"] + features=smiles, + labels=labels, + ident=row["mol_id"], # group=group ) # yield self.reader.to_data(dict(features=smiles, labels=labels, ident=row["mol_id"])) + def _set_processed_data_props(self): """ Load processed data and extract metadata. @@ -187,6 +196,7 @@ def _set_processed_data_props(self): def _perform_data_preparation(self, *args, **kwargs) -> None: pass + class Tox21Challenge(XYBaseDataModule): """Data module for Tox21Challenge dataset.""" diff --git a/chebai/result/molplot.py b/chebai/result/molplot.py index 055c3b26..5495663d 100644 --- a/chebai/result/molplot.py +++ b/chebai/result/molplot.py @@ -12,6 +12,7 @@ from pysmiles.read_smiles import LOGGER, TokenType, _tokenize from rdkit import Chem from rdkit.Chem.Draw import rdMolDraw2D + # from rdkit.Chem.Draw import MolToMPL, rdMolDraw2D # from chebai.preprocessing.datasets import JCI_500_COLUMNS_INT From fc444e0a06b408821c55d528e6eaaa0e4a4b1273 Mon Sep 17 00:00:00 2001 From: schnamo Date: Mon, 3 Nov 2025 18:05:47 +0100 Subject: [PATCH 49/54] fix load from checkpoint issues for pretrained models --- tutorials/data_exploration_chebi.ipynb | 2 +- tutorials/demo_process_results.ipynb | 6 +-- tutorials/eval_model_basic.ipynb | 2 +- tutorials/process_results_old_chebi.ipynb | 45 ++++++++++++++++++++--- 4 files changed, 44 insertions(+), 11 deletions(-) diff --git a/tutorials/data_exploration_chebi.ipynb b/tutorials/data_exploration_chebi.ipynb index e9a2dcba..03285b56 100644 --- a/tutorials/data_exploration_chebi.ipynb +++ b/tutorials/data_exploration_chebi.ipynb @@ -1091,7 +1091,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.8" + "version": "3.12.11" } }, "nbformat": 4, diff --git a/tutorials/demo_process_results.ipynb b/tutorials/demo_process_results.ipynb index 6a3870be..76a181b6 100644 --- a/tutorials/demo_process_results.ipynb +++ b/tutorials/demo_process_results.ipynb @@ -338,9 +338,9 @@ " \"per_epoch=99_val_loss=0.0167_val_micro-f1=0.91.ckpt\",\n", ")\n", "model_path_v200 = \"electra_c100_bce_unweighted.ckpt\"\n", - "model_v148 = Electra.load_from_checkpoint(model_path_v148).to(\"cpu\")\n", - "model_v200 = Electra.load_from_checkpoint(model_path_v200).to(\"cpu\")\n", - "model_v227 = Electra.load_from_checkpoint(model_path_v227).to(\"cpu\")\n", + "model_v148 = Electra.load_from_checkpoint(model_path_v148, pretrained_checkpoint=None).to(\"cpu\")\n", + "model_v200 = Electra.load_from_checkpoint(model_path_v200, pretrained_checkpoint=None).to(\"cpu\")\n", + "model_v227 = Electra.load_from_checkpoint(model_path_v227, pretrained_checkpoint=None).to(\"cpu\")\n", "\n", "data_module_v200 = ChEBIOver100()\n", "data_module_v148 = ChEBIOver100(chebi_version_train=148)\n", diff --git a/tutorials/eval_model_basic.ipynb b/tutorials/eval_model_basic.ipynb index 776a3d3c..a2c570e1 100644 --- a/tutorials/eval_model_basic.ipynb +++ b/tutorials/eval_model_basic.ipynb @@ -126,7 +126,7 @@ ], "source": [ "# evaluates model, stores results in buffer_dir\n", - "model = model_class.load_from_checkpoint(checkpoint_path)\n", + "model = model_class.load_from_checkpoint(checkpoint_path, pretrained_checkpoint=None)\n", "if buffer_dir is None:\n", " preds, labels = evaluate_model(\n", " model,\n", diff --git a/tutorials/process_results_old_chebi.ipynb b/tutorials/process_results_old_chebi.ipynb index 9b05883a..cb3ec3be 100644 --- a/tutorials/process_results_old_chebi.ipynb +++ b/tutorials/process_results_old_chebi.ipynb @@ -3,7 +3,10 @@ { "cell_type": "markdown", "metadata": { - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "source": [ "This script evaluates two models trained on the datasets $ChEBI_{v200}^{854}$ and $ChEBI_{v148}^{709}$." @@ -17,7 +20,10 @@ "end_time": "2023-12-01T09:09:32.987478800Z", "start_time": "2023-12-01T09:09:32.979311Z" }, - "collapsed": true + "collapsed": true, + "jupyter": { + "outputs_hidden": true + } }, "outputs": [], "source": [ @@ -43,6 +49,9 @@ "start_time": "2023-12-01T09:09:34.063840600Z" }, "collapsed": false, + "jupyter": { + "outputs_hidden": false + }, "pycharm": { "name": "#%%\n" } @@ -52,8 +61,8 @@ "model_path_v200 = os.path.join(\"models\", \"electra_c100_bce_unweighted.ckpt\")\n", "model_path_v148 = os.path.join(\"models\", \"electra_c100_bce_unweighted_v148.ckpt\")\n", "\n", - "model_v200 = Electra.load_from_checkpoint(model_path_v200).to(DEVICE)\n", - "model_v148 = Electra.load_from_checkpoint(model_path_v148).to(DEVICE)\n", + "model_v200 = Electra.load_from_checkpoint(model_path_v200, pretrained_checkpoint=None).to(DEVICE)\n", + "model_v148 = Electra.load_from_checkpoint(model_path_v148, pretrained_checkpoint=None).to(DEVICE)\n", "\n", "data_module_v200 = ChEBIOver100(chebi_version=200)\n", "data_module_v148 = ChEBIOver100(chebi_version=200, chebi_version_train=148)" @@ -68,6 +77,9 @@ "start_time": "2023-12-01T09:09:35.195490300Z" }, "collapsed": false, + "jupyter": { + "outputs_hidden": false + }, "pycharm": { "name": "#%%\n" } @@ -91,6 +103,9 @@ "start_time": "2023-12-01T09:09:37.598008300Z" }, "collapsed": false, + "jupyter": { + "outputs_hidden": false + }, "pycharm": { "name": "#%%\n" } @@ -116,6 +131,9 @@ "start_time": "2023-12-01T09:11:07.914456300Z" }, "collapsed": false, + "jupyter": { + "outputs_hidden": false + }, "pycharm": { "name": "#%%\n" } @@ -142,6 +160,9 @@ "execution_count": 12, "metadata": { "collapsed": false, + "jupyter": { + "outputs_hidden": false + }, "pycharm": { "name": "#%%\n" } @@ -196,6 +217,9 @@ "execution_count": 11, "metadata": { "collapsed": false, + "jupyter": { + "outputs_hidden": false + }, "pycharm": { "name": "#%%\n" } @@ -228,6 +252,9 @@ "execution_count": 40, "metadata": { "collapsed": false, + "jupyter": { + "outputs_hidden": false + }, "pycharm": { "name": "#%%\n" } @@ -260,6 +287,9 @@ "execution_count": 41, "metadata": { "collapsed": false, + "jupyter": { + "outputs_hidden": false + }, "pycharm": { "name": "#%%\n" } @@ -292,6 +322,9 @@ "execution_count": 42, "metadata": { "collapsed": false, + "jupyter": { + "outputs_hidden": false + }, "pycharm": { "name": "#%%\n" } @@ -336,9 +369,9 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.10" + "version": "3.12.11" } }, "nbformat": 4, - "nbformat_minor": 0 + "nbformat_minor": 4 } From 5304b3e7293a53b8000a9b66d2af4f122cc3006a Mon Sep 17 00:00:00 2001 From: schnamo Date: Tue, 11 Nov 2025 16:24:37 +0100 Subject: [PATCH 50/54] adding decoding of encoded tokens function restructering of config files fixing small issues from merging --- chebai/loss/semantic.py | 1 - chebai/models/base.py | 1 - chebai/models/electra.py | 4 +- .../preprocessing/bin/smiles_token/tokens.txt | 1596 +---------------- chebai/preprocessing/reader.py | 19 + chebai/result/utils.py | 112 ++ .../{ => moleculenet}/bace_moleculenet.yml | 0 .../{ => moleculenet}/bbbp_moleculenet.yml | 0 .../{ => moleculenet}/clintox_moleculenet.yml | 0 .../freesolv_moleculenet.yml | 0 .../{ => moleculenet}/hiv_moleculenet.yml | 0 .../{ => moleculenet}/lipo_moleculenet.yml | 0 .../{ => moleculenet}/muv_moleculenet.yml | 0 .../data/{ => moleculenet}/pubchem_kmeans.yml | 0 .../{ => moleculenet}/sider_moleculenet.yml | 0 .../{ => moleculenet}/solubilityCuration.yml | 0 .../data/{ => moleculenet}/solubilityESOL.yml | 0 .../{ => OPT_experiments}/electra_LR.yml | 0 .../electra_tox_expl.yml | 0 .../electra_tox_paper.yml | 0 .../electra_tox_paper_regression.yml | 0 configs/training/wandb_logger.yml | 6 + 22 files changed, 140 insertions(+), 1599 deletions(-) rename configs/data/{ => moleculenet}/bace_moleculenet.yml (100%) rename configs/data/{ => moleculenet}/bbbp_moleculenet.yml (100%) rename configs/data/{ => moleculenet}/clintox_moleculenet.yml (100%) rename configs/data/{ => moleculenet}/freesolv_moleculenet.yml (100%) rename configs/data/{ => moleculenet}/hiv_moleculenet.yml (100%) rename configs/data/{ => moleculenet}/lipo_moleculenet.yml (100%) rename configs/data/{ => moleculenet}/muv_moleculenet.yml (100%) rename configs/data/{ => moleculenet}/pubchem_kmeans.yml (100%) rename configs/data/{ => moleculenet}/sider_moleculenet.yml (100%) rename configs/data/{ => moleculenet}/solubilityCuration.yml (100%) rename configs/data/{ => moleculenet}/solubilityESOL.yml (100%) rename configs/model/{ => OPT_experiments}/electra_LR.yml (100%) rename configs/model/{ => OPT_experiments}/electra_tox_expl.yml (100%) rename configs/model/{ => OPT_experiments}/electra_tox_paper.yml (100%) rename configs/model/{ => OPT_experiments}/electra_tox_paper_regression.yml (100%) create mode 100644 configs/training/wandb_logger.yml diff --git a/chebai/loss/semantic.py b/chebai/loss/semantic.py index dbcda85c..3fef3085 100644 --- a/chebai/loss/semantic.py +++ b/chebai/loss/semantic.py @@ -63,7 +63,6 @@ def __init__( multiply_by_softmax: bool = False, use_sigmoidal_implication: bool = False, weight_epoch_dependent: Union[bool, Tuple[int, int]] = False, - weight_epoch_dependent: Union[bool, Tuple[int, int]] = False, start_at_epoch: int = 0, violations_per_cls_aggregator: Literal[ "sum", "max", "mean", "log-sum", "log-max", "log-mean" diff --git a/chebai/models/base.py b/chebai/models/base.py index 9876d005..9a386e74 100644 --- a/chebai/models/base.py +++ b/chebai/models/base.py @@ -265,7 +265,6 @@ def _execute( loss_kwargs = dict() if self.pass_loss_kwargs: loss_kwargs = loss_kwargs_candidates - # loss_kwargs["current_epoch"] = self.trainer.current_epoch loss = self.criterion(loss_data, loss_labels, **loss_kwargs) if isinstance(loss, tuple): unnamed_loss_index = 1 diff --git a/chebai/models/electra.py b/chebai/models/electra.py index 103b9114..327e0b66 100644 --- a/chebai/models/electra.py +++ b/chebai/models/electra.py @@ -19,8 +19,8 @@ logging.getLogger("pysmiles").setLevel(logging.CRITICAL) -# TODO: put back in before pull request -# from chebai.loss.semantic import DisjointLoss as ElectraChEBIDisjointLoss # noqa + +from chebai.loss.semantic import DisjointLoss as ElectraChEBIDisjointLoss # noqa class ElectraPre(ChebaiBaseNet): diff --git a/chebai/preprocessing/bin/smiles_token/tokens.txt b/chebai/preprocessing/bin/smiles_token/tokens.txt index 1cc8bade..b084b1be 100644 --- a/chebai/preprocessing/bin/smiles_token/tokens.txt +++ b/chebai/preprocessing/bin/smiles_token/tokens.txt @@ -983,1598 +983,4 @@ p [FH2+] [ClH2+] [BrH2+] -[IH2+] -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133 -134 -135 -136 -137 -138 -139 -140 -141 -142 -143 -144 -145 -146 -147 -148 -149 -150 -151 -152 -153 -154 -155 -156 -157 -158 -159 -160 -161 -162 -163 -164 -165 -166 -167 -168 -169 -170 -171 -172 -173 -174 -175 -176 -177 -178 -179 -180 -181 -182 -183 -184 -185 -186 -187 -188 -189 -190 -191 -192 -193 -194 -195 -196 -197 -198 -199 -200 -201 -202 -203 -204 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133 -134 -135 -136 -137 -138 -139 -140 -141 -142 -143 -144 -145 -146 -147 -148 -149 -150 -151 -152 -153 -154 -155 -156 -157 -158 -159 -160 -161 -162 -163 -164 -165 -166 -167 -168 -169 -170 -171 -172 -173 -174 -175 -176 -177 -178 -179 -180 -181 -182 -183 -184 -185 -186 -187 -188 -189 -190 -191 -192 -193 -194 -195 -196 -197 -198 -199 -200 -201 -202 -203 -204 -205 -206 -207 -208 -209 -210 -211 -212 -213 -214 -215 -216 -217 -218 -219 -220 -221 -222 -223 -224 -225 -226 -227 -228 -229 -230 -231 -232 -233 -234 -235 -236 -237 -238 -239 -240 -241 -242 -243 -244 -245 -246 -247 -248 -249 -250 -251 -252 -253 -254 -255 -256 -257 -258 -259 -260 -261 -262 -263 -264 -265 -266 -267 -268 -269 -270 -271 -272 -273 -274 -275 -276 -277 -278 -279 -280 -281 -282 -283 -284 -285 -286 -287 -288 -289 -290 -291 -292 -293 -294 -295 -296 -297 -298 -299 -300 -301 -302 -303 -205 -206 -207 -208 -209 -210 -211 -212 -213 -214 -215 -216 -217 -218 -219 -220 -221 -222 -223 -224 -225 -226 -227 -228 -229 -230 -231 -232 -233 -234 -235 -236 -237 -238 -239 -240 -241 -242 -243 -244 -245 -246 -247 -248 -249 -250 -251 -252 -253 -254 -255 -256 -257 -258 -259 -260 -261 -262 -263 -264 -265 -266 -267 -268 -269 -270 -271 -272 -273 -274 -275 -276 -277 -278 -279 -280 -281 -282 -283 -284 -285 -286 -287 -288 -289 -290 -291 -292 -293 -294 -295 -296 -297 -298 -299 -300 -301 -302 -303 -304 -305 -306 -307 -308 -309 -310 -311 -312 -313 -314 -315 -316 -317 -318 -319 -320 -321 -322 -323 -324 -325 -326 -327 -328 -329 -330 -331 -332 -333 -334 -335 -336 -337 -338 -339 -340 -341 -342 -343 -344 -345 -346 -347 -348 -349 -350 -351 -352 -353 -354 -355 -356 -357 -358 -359 -360 -361 -362 -363 -364 -365 -366 -367 -368 -369 -370 -371 -372 -373 -374 -375 -376 -377 -378 -379 -380 -381 -382 -383 -384 -385 -386 -387 -388 -389 -390 -391 -392 -393 -394 -395 -396 -397 -398 -399 -400 -401 -402 -403 -404 -405 -406 -407 -408 -409 -410 -411 -412 -413 -414 -415 -416 -417 -418 -419 -420 -421 -422 -423 -424 -425 -426 -427 -428 -429 -430 -431 -432 -433 -434 -435 -436 -437 -438 -439 -440 -441 -442 -443 -444 -445 -446 -447 -448 -449 -450 -451 -452 -453 -454 -455 -456 -457 -458 -459 -460 -461 -462 -463 -464 -465 -466 -467 -468 -469 -470 -471 -472 -473 -474 -475 -476 -477 -478 -479 -480 -481 -482 -483 -484 -485 -486 -487 -488 -489 -490 -491 -492 -493 -494 -495 -496 -497 -498 -499 -500 -501 -502 -503 -504 -505 -506 -507 -508 -509 -510 -511 -512 -513 -514 -515 -516 -517 -518 -519 -520 -521 -522 -523 -524 -525 -526 -527 -528 -529 -530 -531 -532 -533 -534 -535 -536 -537 -538 -539 -540 -541 -542 -543 -544 -545 -546 -547 -548 -549 -550 -551 -552 -553 -554 -555 -556 -557 -558 -559 -560 -561 -562 -563 -564 -565 -566 -567 -568 -569 -570 -571 -572 -573 -574 -575 -576 -577 -578 -579 -580 -581 -582 -583 -584 -585 -586 -587 -588 -589 -590 -591 -592 -593 -594 -595 -596 -597 -598 -599 -600 -601 -602 -603 -604 -605 -606 -607 -608 -609 -610 -611 -612 -613 -614 -615 -616 -617 -618 -619 -620 -621 -622 -623 -624 -625 -626 -627 -628 -629 -630 -631 -632 -633 -634 -635 -636 -637 -638 -639 -640 -641 -642 -643 -644 -645 -646 -647 -648 -649 -650 -651 -652 -653 -654 -655 -656 -657 -658 -659 -660 -661 -662 -663 -664 -665 -666 -667 -668 -669 -670 -671 -672 -673 -674 -675 -676 -677 -678 -679 -680 -681 -682 -683 -684 -685 -686 -687 -688 -689 -690 -691 -692 -693 -694 -695 -696 -697 -698 -699 -700 -701 -702 -703 -704 -705 -706 -707 -708 -709 -710 -711 -712 -713 -714 -715 -716 -717 -718 -719 -720 -721 -722 -723 -724 -725 -726 -727 -728 -729 -730 -731 -732 -733 -734 -735 -736 -737 -738 -739 -740 -741 -742 -743 -744 -745 -746 -747 -748 -749 -750 -751 -752 -753 -754 -755 -756 -757 -758 -759 -760 -761 -762 -763 -764 -765 -766 -767 -768 -769 -770 -771 -772 -773 -774 -775 -776 -777 -778 -779 -780 -781 -782 -783 -784 -785 -786 -787 -788 -789 -790 -791 -792 -793 -794 -795 -796 -797 -798 -799 -800 -801 -802 -803 -804 -805 -806 -807 -808 -809 -810 -811 -812 -813 -814 -815 -816 -817 -818 -819 -820 -821 -822 -823 -824 -825 -826 -827 -828 -829 -830 -831 -832 -833 -834 -835 -836 -837 -838 -839 -840 -841 -842 -843 -844 -845 -846 -847 -848 -849 -850 -851 -852 -853 -854 -855 -856 -857 -858 -859 -860 -861 -862 -863 -864 -865 -866 -867 -868 -869 -870 -871 -872 -873 -874 -875 -876 -877 -878 -879 -880 -881 -882 -883 -884 -885 -886 -887 -888 -889 -890 -891 -892 -893 -894 -895 -896 -304 -305 -306 -307 -308 -309 -310 -311 -312 -313 -314 -315 -316 -317 -318 -319 -320 -321 -322 -323 -324 -325 -326 -327 -328 -329 -330 -331 -332 -333 -334 -335 -336 -337 -338 -339 -340 -341 -342 -343 -344 -345 -346 -347 -348 -349 -350 -351 -352 -353 -354 -355 -356 -357 -358 -359 -360 -361 -362 -363 -364 -365 -366 -367 -368 -369 -370 -371 -372 -373 -374 -375 -376 -377 -378 -379 -380 -381 -382 -383 -384 -385 -386 -387 -388 -389 -390 -391 -392 -393 -394 -395 -396 -397 -398 -399 -400 -401 -402 -403 -404 -405 -406 -407 -408 -409 -410 -411 -412 -413 -414 -415 -416 -417 -418 -419 -420 -421 -422 -423 -424 -425 -426 -427 -428 -429 -430 -431 -432 -433 -434 -435 -436 -437 -438 -439 -440 -441 -442 -443 -444 -445 -446 -447 -448 -449 -450 -451 -452 -453 -454 -455 -456 -457 -458 -459 -460 -461 -462 -463 -464 -465 -466 -467 -468 -469 -470 -471 -472 -473 -474 -475 -476 -477 -478 -479 -480 -481 -482 -483 -484 -485 -486 -487 -488 -489 -490 -491 -492 -493 -494 -495 -496 -497 -498 -499 -500 -501 -502 -503 -504 -505 -506 -507 -508 -509 -510 -511 -512 -513 -514 -515 -516 -517 -518 -519 -520 -521 -522 -523 -524 -525 -526 -527 -528 -529 -530 -531 -532 -533 -534 -535 -536 -537 -538 -539 -540 -541 -542 -543 -544 -545 -546 -547 -548 -549 -550 -551 -552 -553 -554 -555 -556 -557 -558 -559 -560 -561 -562 -563 -564 -565 -566 -567 -568 -569 -570 -571 -572 -573 -574 -575 -576 -577 -578 -579 -580 -581 -582 -583 -584 -585 -586 -587 -588 -589 -590 -591 -592 -593 -594 -595 -596 -597 -598 -599 -600 -601 -602 -603 -604 -605 -606 -607 -608 -609 -610 -611 -612 -613 -614 -615 -616 -617 -618 -619 -620 -621 -622 -623 -624 -625 -626 -627 -628 -629 -630 -631 -632 -633 -634 -635 -636 -637 -638 -639 -640 -641 -642 -643 -644 -645 -646 -647 -648 -649 -650 -651 -652 -653 -654 -655 -656 -657 -658 -659 -660 -661 -662 -663 -664 -665 -666 -667 -668 -669 -670 -671 -672 -673 -674 -675 -676 -677 -678 -679 -680 -681 -682 -683 -684 -685 -686 -687 -688 -689 -690 -691 -692 -693 -694 -695 -696 -697 -698 -699 -700 -701 -702 -703 -704 -705 -706 -707 -708 -709 -710 -711 -712 -713 -714 -715 -716 -717 -718 -719 -720 -721 -722 -723 -724 -725 -726 -727 -728 -729 -730 -731 -732 -733 -734 -735 -736 -737 -738 -739 -740 -741 -742 -743 -744 -745 -746 -747 -748 -749 -750 -751 -752 -753 -754 -755 -756 -757 -758 -759 -760 -761 -762 -763 -764 -765 -766 -767 -768 -769 -770 -771 -772 -773 -774 -775 -776 -777 -778 -779 -780 -781 -782 -783 -784 -785 -786 -787 -788 -789 -790 -791 -792 -793 -794 -795 -796 -797 -798 -799 -800 -801 -802 -803 -804 -805 -806 -807 -808 -809 -810 -811 -812 -813 -814 -815 -816 -817 -818 -819 -820 -821 -822 -823 -824 -825 -826 -827 -828 -829 -830 -831 -832 -833 -834 -835 -836 -837 -838 -839 -840 -841 -842 -843 -844 -845 -846 -847 -848 -849 -850 -851 -852 -853 -854 -855 -856 -857 -858 -859 -860 -861 -862 -863 -864 -865 -866 -867 -868 -869 -870 -871 -872 -873 -874 -875 -876 -877 -878 -879 -880 -881 -882 -883 -884 -885 -886 -887 -888 -889 -890 -891 -892 -893 -894 -895 -896 +[IH2+] \ No newline at end of file diff --git a/chebai/preprocessing/reader.py b/chebai/preprocessing/reader.py index f7af5d71..e5c6ee2f 100644 --- a/chebai/preprocessing/reader.py +++ b/chebai/preprocessing/reader.py @@ -142,6 +142,8 @@ def __init__(self, *args, **kwargs): def _get_token_index(self, token: str) -> int: """Returns a unique number for each token, automatically adds new tokens.""" + print(str(token)) + print(self.cache[str(token)] + EMBEDDING_OFFSET) if str(token) not in self.cache: self.cache[(str(token))] = len(self.cache) return self.cache[str(token)] + EMBEDDING_OFFSET @@ -210,6 +212,23 @@ def _read_data(self, raw_data: str) -> List[int]: return [self._get_token_index(v[1]) for v in _tokenize(raw_data)] + def _back_to_smiles(smiles_encoded): + + token_file = self.reader.token_path + token_coding = {} + counter = 0 + smiles_decoded = '' + + # todo: for now just copied over from a notebook but ideally do this using the cache + with open(token_file, 'r') as file: + for line in file: + token_coding[counter] = line.strip() + counter += 1 + + for token in smiles_encoded: + smiles_decoded += token_coding[token - EMBEDDING_OFFSET] + + return smiles_decoded class DeepChemDataReader(ChemDataReader): """ diff --git a/chebai/result/utils.py b/chebai/result/utils.py index 8e05d699..c563bd10 100644 --- a/chebai/result/utils.py +++ b/chebai/result/utils.py @@ -69,6 +69,18 @@ def _run_batch(batch, model, collate): ) return preds, labels +def _run_batch_give_attention(batch, model, collate): + collated = collate(batch) + collated.x = collated.to_x(model.device) + if collated.y is not None: + collated.y = collated.to_y(model.device) + processable_data = model._process_batch(collated, 0) + # del processable_data["loss_kwargs"] + model_output = model(processable_data, **processable_data["model_kwargs"]) + preds, labels = model._get_prediction_and_labels( + processable_data, processable_data["labels"], model_output + ) + return preds, labels, model_output def _concat_tuple(l_): if isinstance(l_[0], tuple): @@ -267,6 +279,106 @@ def evaluate_model_regression( ) return torch.cat(preds_list_all), torch.cat(labels_list_all) +def evaluate_model_regression_attention( + model: ChebaiBaseNet, + data_module: XYBaseDataModule, + filename: Optional[str] = None, + buffer_dir: Optional[str] = None, + batch_size: int = 32, + skip_existing_preds: bool = False, + kind: str = "test", +) -> Tuple[torch.Tensor, Optional[torch.Tensor], list, list]: + """ + Runs the model on the test set of the data module or on the dataset found in the specified file. + If buffer_dir is set, results will be saved in buffer_dir. + + Note: + No need to provide "filename" parameter for Chebi dataset, "kind" parameter should be provided. + + Args: + model: The model to evaluate. + data_module: The data module containing the dataset. + filename: Optional file name for the dataset. + buffer_dir: Optional directory to save the results. + batch_size: The batch size for evaluation. + skip_existing_preds: Whether to skip evaluation if predictions already exist. + kind: Kind of split of the data to be used for testing the model. Default is `test`. + + Returns: + Tensors with predictions and labels. + """ + model.eval() + collate = data_module.reader.COLLATOR() + + if isinstance(data_module, _ChEBIDataExtractor): + # As the dynamic split change is implemented only for chebi-dataset as of now + data_df = data_module.dynamic_split_dfs[kind] + data_list = data_df.to_dict(orient="records") + else: + data_list = data_module.load_processed_data("test", filename) + data_list = data_list[: data_module.data_limit] + preds_list = [] + labels_list = [] + preds_list_all = [] + labels_list_all = [] + features_list_all = [] + attention_list_all = [] + if buffer_dir is not None: + os.makedirs(buffer_dir, exist_ok=True) + save_ind = 0 + save_batch_size = 128 + n_saved = 1 + + print(f"") + for i in tqdm.tqdm(range(0, len(data_list), batch_size)): + if not ( + skip_existing_preds + and os.path.isfile(os.path.join(buffer_dir, f"preds{save_ind:03d}.pt")) + ): + preds, labels, model_output = _run_batch_give_attention(data_list[i : i + batch_size], model, collate) + preds_list.append(preds) + labels_list.append(labels) + preds_list_all.append(preds) + labels_list_all.append(labels) + attention_list_all.append(model_output) + features_list_all.append(data_list[i : i + batch_size]) + if buffer_dir is not None: + if n_saved * batch_size >= save_batch_size: + torch.save( + _concat_tuple(preds_list), + os.path.join(buffer_dir, f"preds{save_ind:03d}.pt"), + ) + if labels_list[0] is not None: + torch.save( + _concat_tuple(labels_list), + os.path.join(buffer_dir, f"labels{save_ind:03d}.pt"), + ) + preds_list = [] + labels_list = [] + if n_saved * batch_size >= save_batch_size: + save_ind += 1 + n_saved = 0 + n_saved += 1 + + if buffer_dir is None: + test_preds = _concat_tuple(preds_list) + if labels_list is not None: + test_labels = _concat_tuple(labels_list) + + return test_preds, test_labels, features_list_all, attention_list_all + return test_preds, None + else: + torch.save( + _concat_tuple(preds_list), + os.path.join(buffer_dir, f"preds{save_ind:03d}.pt"), + ) + if labels_list[0] is not None: + torch.save( + _concat_tuple(labels_list), + os.path.join(buffer_dir, f"labels{save_ind:03d}.pt"), + ) + return torch.cat(preds_list_all), torch.cat(labels_list_all), features_list_all, attention_list_all + def load_results_from_buffer( buffer_dir: str, device: torch.device diff --git a/configs/data/bace_moleculenet.yml b/configs/data/moleculenet/bace_moleculenet.yml similarity index 100% rename from configs/data/bace_moleculenet.yml rename to configs/data/moleculenet/bace_moleculenet.yml diff --git a/configs/data/bbbp_moleculenet.yml b/configs/data/moleculenet/bbbp_moleculenet.yml similarity index 100% rename from configs/data/bbbp_moleculenet.yml rename to configs/data/moleculenet/bbbp_moleculenet.yml diff --git a/configs/data/clintox_moleculenet.yml b/configs/data/moleculenet/clintox_moleculenet.yml similarity index 100% rename from configs/data/clintox_moleculenet.yml rename to configs/data/moleculenet/clintox_moleculenet.yml diff --git a/configs/data/freesolv_moleculenet.yml b/configs/data/moleculenet/freesolv_moleculenet.yml similarity index 100% rename from configs/data/freesolv_moleculenet.yml rename to configs/data/moleculenet/freesolv_moleculenet.yml diff --git a/configs/data/hiv_moleculenet.yml b/configs/data/moleculenet/hiv_moleculenet.yml similarity index 100% rename from configs/data/hiv_moleculenet.yml rename to configs/data/moleculenet/hiv_moleculenet.yml diff --git a/configs/data/lipo_moleculenet.yml b/configs/data/moleculenet/lipo_moleculenet.yml similarity index 100% rename from configs/data/lipo_moleculenet.yml rename to configs/data/moleculenet/lipo_moleculenet.yml diff --git a/configs/data/muv_moleculenet.yml b/configs/data/moleculenet/muv_moleculenet.yml similarity index 100% rename from configs/data/muv_moleculenet.yml rename to configs/data/moleculenet/muv_moleculenet.yml diff --git a/configs/data/pubchem_kmeans.yml b/configs/data/moleculenet/pubchem_kmeans.yml similarity index 100% rename from configs/data/pubchem_kmeans.yml rename to configs/data/moleculenet/pubchem_kmeans.yml diff --git a/configs/data/sider_moleculenet.yml b/configs/data/moleculenet/sider_moleculenet.yml similarity index 100% rename from configs/data/sider_moleculenet.yml rename to configs/data/moleculenet/sider_moleculenet.yml diff --git a/configs/data/solubilityCuration.yml b/configs/data/moleculenet/solubilityCuration.yml similarity index 100% rename from configs/data/solubilityCuration.yml rename to configs/data/moleculenet/solubilityCuration.yml diff --git a/configs/data/solubilityESOL.yml b/configs/data/moleculenet/solubilityESOL.yml similarity index 100% rename from configs/data/solubilityESOL.yml rename to configs/data/moleculenet/solubilityESOL.yml diff --git a/configs/model/electra_LR.yml b/configs/model/OPT_experiments/electra_LR.yml similarity index 100% rename from configs/model/electra_LR.yml rename to configs/model/OPT_experiments/electra_LR.yml diff --git a/configs/model/electra_tox_expl.yml b/configs/model/OPT_experiments/electra_tox_expl.yml similarity index 100% rename from configs/model/electra_tox_expl.yml rename to configs/model/OPT_experiments/electra_tox_expl.yml diff --git a/configs/model/electra_tox_paper.yml b/configs/model/OPT_experiments/electra_tox_paper.yml similarity index 100% rename from configs/model/electra_tox_paper.yml rename to configs/model/OPT_experiments/electra_tox_paper.yml diff --git a/configs/model/electra_tox_paper_regression.yml b/configs/model/OPT_experiments/electra_tox_paper_regression.yml similarity index 100% rename from configs/model/electra_tox_paper_regression.yml rename to configs/model/OPT_experiments/electra_tox_paper_regression.yml diff --git a/configs/training/wandb_logger.yml b/configs/training/wandb_logger.yml new file mode 100644 index 00000000..b7c51418 --- /dev/null +++ b/configs/training/wandb_logger.yml @@ -0,0 +1,6 @@ +class_path: chebai.loggers.custom.CustomLogger # Extension of Wandb logger +init_args: + save_dir: logs + project: 'chebai' + entity: 'chebai' + log_model: 'all' \ No newline at end of file From 426f1b06567bf178723e97f882a28c9df78fd2a7 Mon Sep 17 00:00:00 2001 From: schnamo Date: Tue, 11 Nov 2025 17:20:50 +0100 Subject: [PATCH 51/54] remove print statements from debugging --- chebai/preprocessing/reader.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/chebai/preprocessing/reader.py b/chebai/preprocessing/reader.py index e5c6ee2f..f5242994 100644 --- a/chebai/preprocessing/reader.py +++ b/chebai/preprocessing/reader.py @@ -142,8 +142,6 @@ def __init__(self, *args, **kwargs): def _get_token_index(self, token: str) -> int: """Returns a unique number for each token, automatically adds new tokens.""" - print(str(token)) - print(self.cache[str(token)] + EMBEDDING_OFFSET) if str(token) not in self.cache: self.cache[(str(token))] = len(self.cache) return self.cache[str(token)] + EMBEDDING_OFFSET From fb6fdb7721454c254715960048d2aadae9998c93 Mon Sep 17 00:00:00 2001 From: schnamo Date: Thu, 13 Nov 2025 17:57:38 +0100 Subject: [PATCH 52/54] lint fixes --- chebai/preprocessing/reader.py | 7 ++++--- chebai/result/utils.py | 21 +++++++++++++++++---- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/chebai/preprocessing/reader.py b/chebai/preprocessing/reader.py index 42ddf17a..08904a20 100644 --- a/chebai/preprocessing/reader.py +++ b/chebai/preprocessing/reader.py @@ -219,10 +219,10 @@ def _back_to_smiles(smiles_encoded): token_file = self.reader.token_path token_coding = {} counter = 0 - smiles_decoded = '' - + smiles_decoded = "" + # todo: for now just copied over from a notebook but ideally do this using the cache - with open(token_file, 'r') as file: + with open(token_file, "r") as file: for line in file: token_coding[counter] = line.strip() counter += 1 @@ -232,6 +232,7 @@ def _back_to_smiles(smiles_encoded): return smiles_decoded + class DeepChemDataReader(ChemDataReader): """ Data reader for chemical data using DeepSMILES tokens. diff --git a/chebai/result/utils.py b/chebai/result/utils.py index 5fd1589a..89e83250 100644 --- a/chebai/result/utils.py +++ b/chebai/result/utils.py @@ -69,6 +69,7 @@ def _run_batch(batch, model, collate): ) return preds, labels + def _run_batch_give_attention(batch, model, collate): collated = collate(batch) collated.x = collated.to_x(model.device) @@ -82,6 +83,7 @@ def _run_batch_give_attention(batch, model, collate): ) return preds, labels, model_output + def _concat_tuple(l_): if isinstance(l_[0], tuple): print(l_[0]) @@ -170,7 +172,7 @@ def evaluate_model( test_labels = _concat_tuple(labels_list) return test_preds, test_labels return test_preds, None - elif len(preds_list) > 0 : + elif len(preds_list) > 0: if preds_list[0] is not None: torch.save( _concat_tuple(preds_list), @@ -280,6 +282,7 @@ def evaluate_model_regression( ) return torch.cat(preds_list_all), torch.cat(labels_list_all) + def evaluate_model_regression_attention( model: ChebaiBaseNet, data_module: XYBaseDataModule, @@ -336,7 +339,9 @@ def evaluate_model_regression_attention( skip_existing_preds and os.path.isfile(os.path.join(buffer_dir, f"preds{save_ind:03d}.pt")) ): - preds, labels, model_output = _run_batch_give_attention(data_list[i : i + batch_size], model, collate) + preds, labels, model_output = _run_batch_give_attention( + data_list[i : i + batch_size], model, collate + ) preds_list.append(preds) labels_list.append(labels) preds_list_all.append(preds) @@ -477,6 +482,7 @@ def evaluate_model_regression( ) return torch.cat(preds_list_all), torch.cat(labels_list_all) + def evaluate_model_regression_attention( model: ChebaiBaseNet, data_module: XYBaseDataModule, @@ -533,7 +539,9 @@ def evaluate_model_regression_attention( skip_existing_preds and os.path.isfile(os.path.join(buffer_dir, f"preds{save_ind:03d}.pt")) ): - preds, labels, model_output = _run_batch_give_attention(data_list[i : i + batch_size], model, collate) + preds, labels, model_output = _run_batch_give_attention( + data_list[i : i + batch_size], model, collate + ) preds_list.append(preds) labels_list.append(labels) preds_list_all.append(preds) @@ -575,7 +583,12 @@ def evaluate_model_regression_attention( _concat_tuple(labels_list), os.path.join(buffer_dir, f"labels{save_ind:03d}.pt"), ) - return torch.cat(preds_list_all), torch.cat(labels_list_all), features_list_all, attention_list_all + return ( + torch.cat(preds_list_all), + torch.cat(labels_list_all), + features_list_all, + attention_list_all, + ) def load_results_from_buffer( From 81f8025fbbe1233d443871da0771076d83702978 Mon Sep 17 00:00:00 2001 From: schnamo Date: Thu, 13 Nov 2025 18:02:28 +0100 Subject: [PATCH 53/54] ruff fixes --- .../datasets/molecule_classification.py | 32 +++++++------------ .../datasets/molecule_regression.py | 24 ++++---------- chebai/preprocessing/datasets/solCuration.py | 24 ++++---------- chebai/preprocessing/datasets/tox21.py | 3 +- chebai/result/regression.py | 4 --- chebai/result/utils.py | 8 ++--- 6 files changed, 31 insertions(+), 64 deletions(-) diff --git a/chebai/preprocessing/datasets/molecule_classification.py b/chebai/preprocessing/datasets/molecule_classification.py index 3abe94a3..91d6b11d 100644 --- a/chebai/preprocessing/datasets/molecule_classification.py +++ b/chebai/preprocessing/datasets/molecule_classification.py @@ -1,28 +1,20 @@ -from tempfile import NamedTemporaryFile, TemporaryDirectory +from tempfile import NamedTemporaryFile from urllib import request import csv import gzip import os -import random import shutil -import zipfile -from typing import Dict, Generator, List, Optional +from typing import Dict, List -from rdkit import Chem from sklearn.model_selection import ( GroupShuffleSplit, train_test_split, - StratifiedShuffleSplit, ) import numpy as np -import pysmiles import torch -from sklearn.preprocessing import LabelBinarizer from chebai.preprocessing import reader as dr -from chebai.preprocessing.datasets.base import MergedDataset, XYBaseDataModule -from chebai.preprocessing.datasets.chebi import JCIExtendedTokenData -from chebai.preprocessing.datasets.pubchem import Hazardous +from chebai.preprocessing.datasets.base import XYBaseDataModule class ClinTox(XYBaseDataModule): @@ -76,7 +68,7 @@ def setup_processed(self) -> None: """Processes and splits the dataset.""" print("Create splits") data = list( - self._load_data_from_file(os.path.join(self.raw_dir, f"clintox.csv")) + self._load_data_from_file(os.path.join(self.raw_dir, "clintox.csv")) ) groups = np.array([d["group"] for d in data]) if not all(g is None for g in groups): @@ -229,14 +221,14 @@ def download(self) -> None: """Downloads and extracts the dataset.""" with open(os.path.join(self.raw_dir, "bbbp.csv"), "ab") as dst: with request.urlopen( - f"https://deepchemdata.s3-us-west-1.amazonaws.com/datasets/BBBP.csv", + "https://deepchemdata.s3-us-west-1.amazonaws.com/datasets/BBBP.csv", ) as src: shutil.copyfileobj(src, dst) def setup_processed(self) -> None: """Processes and splits the dataset.""" print("Create splits") - data = list(self._load_data_from_file(os.path.join(self.raw_dir, f"bbbp.csv"))) + data = list(self._load_data_from_file(os.path.join(self.raw_dir, "bbbp.csv"))) groups = np.array([d["group"] for d in data]) if not all(g is None for g in groups): print("Group shuffled") @@ -426,7 +418,7 @@ def download(self) -> None: def setup_processed(self) -> None: """Processes and splits the dataset.""" print("Create splits") - data = list(self._load_data_from_file(os.path.join(self.raw_dir, f"sider.csv"))) + data = list(self._load_data_from_file(os.path.join(self.raw_dir, "sider.csv"))) groups = np.array([d["group"] for d in data]) if not all(g is None for g in groups): split_size = int( @@ -581,14 +573,14 @@ def download(self) -> None: """Downloads and extracts the dataset.""" with open(os.path.join(self.raw_dir, "bace.csv"), "ab") as dst: with request.urlopen( - f"https://deepchemdata.s3-us-west-1.amazonaws.com/datasets/bace.csv", + "https://deepchemdata.s3-us-west-1.amazonaws.com/datasets/bace.csv", ) as src: shutil.copyfileobj(src, dst) def setup_processed(self) -> None: """Processes and splits the dataset.""" print("Create splits") - data = list(self._load_data_from_file(os.path.join(self.raw_dir, f"bace.csv"))) + data = list(self._load_data_from_file(os.path.join(self.raw_dir, "bace.csv"))) # groups = np.array([d.get("group") for d in data]) # if not all(g is None for g in groups): @@ -729,14 +721,14 @@ def download(self) -> None: """Downloads and extracts the dataset.""" with open(os.path.join(self.raw_dir, "hiv.csv"), "ab") as dst: with request.urlopen( - f"https://deepchemdata.s3-us-west-1.amazonaws.com/datasets/HIV.csv", + "https://deepchemdata.s3-us-west-1.amazonaws.com/datasets/HIV.csv", ) as src: shutil.copyfileobj(src, dst) def setup_processed(self) -> None: """Processes and splits the dataset.""" print("Create splits") - data = list(self._load_data_from_file(os.path.join(self.raw_dir, f"hiv.csv"))) + data = list(self._load_data_from_file(os.path.join(self.raw_dir, "hiv.csv"))) groups = np.array([d["group"] for d in data]) if not all(g is None for g in groups): print("Group shuffled") @@ -913,7 +905,7 @@ def download(self) -> None: def setup_processed(self) -> None: """Processes and splits the dataset.""" print("Create splits") - data = list(self._load_data_from_file(os.path.join(self.raw_dir, f"muv.csv"))) + data = list(self._load_data_from_file(os.path.join(self.raw_dir, "muv.csv"))) groups = np.array([d["group"] for d in data]) if not all(g is None for g in groups): split_size = int( diff --git a/chebai/preprocessing/datasets/molecule_regression.py b/chebai/preprocessing/datasets/molecule_regression.py index 725b1eec..bc74df34 100644 --- a/chebai/preprocessing/datasets/molecule_regression.py +++ b/chebai/preprocessing/datasets/molecule_regression.py @@ -1,24 +1,14 @@ -from tempfile import NamedTemporaryFile, TemporaryDirectory from urllib import request import csv -import gzip import os -import random import shutil -import zipfile -from typing import Dict, Generator, List, Optional +from typing import Dict, List -from rdkit import Chem -from sklearn.model_selection import GroupShuffleSplit, train_test_split -import numpy as np -import pysmiles +from sklearn.model_selection import train_test_split import torch -from sklearn.preprocessing import LabelBinarizer from chebai.preprocessing import reader as dr -from chebai.preprocessing.datasets.base import MergedDataset, XYBaseDataModule -from chebai.preprocessing.datasets.chebi import JCIExtendedTokenData -from chebai.preprocessing.datasets.pubchem import Hazardous +from chebai.preprocessing.datasets.base import XYBaseDataModule class Lipo(XYBaseDataModule): @@ -54,13 +44,13 @@ def download(self): # download with open(os.path.join(self.raw_dir, "Lipo.csv"), "ab") as dst: with request.urlopen( - f"https://deepchemdata.s3-us-west-1.amazonaws.com/datasets/Lipophilicity.csv", + "https://deepchemdata.s3-us-west-1.amazonaws.com/datasets/Lipophilicity.csv", ) as src: shutil.copyfileobj(src, dst) def setup_processed(self): print("Create splits") - data = list(self._load_data_from_file(os.path.join(self.raw_dir, f"Lipo.csv"))) + data = list(self._load_data_from_file(os.path.join(self.raw_dir, "Lipo.csv"))) print(len(data)) train_split, test_split = train_test_split( @@ -189,14 +179,14 @@ def download(self): # download with open(os.path.join(self.raw_dir, "FreeSolv.csv"), "ab") as dst: with request.urlopen( - f"https://deepchemdata.s3-us-west-1.amazonaws.com/datasets/SAMPL.csv", + "https://deepchemdata.s3-us-west-1.amazonaws.com/datasets/SAMPL.csv", ) as src: shutil.copyfileobj(src, dst) def setup_processed(self): print("Create splits") data = list( - self._load_data_from_file(os.path.join(self.raw_dir, f"FreeSolv.csv")) + self._load_data_from_file(os.path.join(self.raw_dir, "FreeSolv.csv")) ) print(len(data)) train_split, test_split = train_test_split( diff --git a/chebai/preprocessing/datasets/solCuration.py b/chebai/preprocessing/datasets/solCuration.py index 35cd3b27..61b88ce1 100644 --- a/chebai/preprocessing/datasets/solCuration.py +++ b/chebai/preprocessing/datasets/solCuration.py @@ -1,24 +1,14 @@ -from tempfile import NamedTemporaryFile, TemporaryDirectory from urllib import request import csv -import gzip import os -import random import shutil -import zipfile -from typing import Dict, Generator, List, Optional +from typing import Dict, List -from rdkit import Chem -from sklearn.model_selection import GroupShuffleSplit, train_test_split -import numpy as np -import pysmiles +from sklearn.model_selection import train_test_split import torch -from sklearn.preprocessing import LabelBinarizer from chebai.preprocessing import reader as dr -from chebai.preprocessing.datasets.base import MergedDataset, XYBaseDataModule -from chebai.preprocessing.datasets.chebi import JCIExtendedTokenData -from chebai.preprocessing.datasets.pubchem import Hazardous +from chebai.preprocessing.datasets.base import XYBaseDataModule class SolCuration(XYBaseDataModule): @@ -65,7 +55,7 @@ def download(self): def setup_processed(self): print("Create splits") data = list( - self._load_data_from_file(os.path.join(self.raw_dir, f"solCuration.csv")) + self._load_data_from_file(os.path.join(self.raw_dir, "solCuration.csv")) ) print(len(data)) @@ -144,7 +134,7 @@ def _load_data_from_file(self, input_file_path: str) -> List[Dict]: with open(input_file_path, "r") as input_file: reader = csv.DictReader(input_file) for row in reader: - if not row["smiles"] in smiles_l: + if row["smiles"] not in smiles_l: smiles_l.append(row["smiles"]) labels_l.append(float(row["logS"])) # print(len(smiles_l), len(labels_l)) @@ -204,14 +194,14 @@ def download(self): # download with open(os.path.join(self.raw_dir, "solESOL.csv"), "ab") as dst: with request.urlopen( - f"https://deepchemdata.s3-us-west-1.amazonaws.com/datasets/delaney-processed.csv", + "https://deepchemdata.s3-us-west-1.amazonaws.com/datasets/delaney-processed.csv", ) as src: shutil.copyfileobj(src, dst) def setup_processed(self): print("Create splits") data = list( - self._load_data_from_file(os.path.join(self.raw_dir, f"solESOL.csv")) + self._load_data_from_file(os.path.join(self.raw_dir, "solESOL.csv")) ) print(len(data)) diff --git a/chebai/preprocessing/datasets/tox21.py b/chebai/preprocessing/datasets/tox21.py index 5d65684e..4f0731a9 100644 --- a/chebai/preprocessing/datasets/tox21.py +++ b/chebai/preprocessing/datasets/tox21.py @@ -13,7 +13,6 @@ from sklearn.model_selection import ( GroupShuffleSplit, train_test_split, - StratifiedShuffleSplit, ) from chebai.preprocessing import reader as dr @@ -75,7 +74,7 @@ def download(self) -> None: def setup_processed(self) -> None: """Processes and splits the dataset.""" print("Create splits") - data = list(self._load_data_from_file(os.path.join(self.raw_dir, f"tox21.csv"))) + data = list(self._load_data_from_file(os.path.join(self.raw_dir, "tox21.csv"))) groups = np.array([d.get("group") for d in data]) if not all(g is None for g in groups): diff --git a/chebai/result/regression.py b/chebai/result/regression.py index 0ea2ee1c..4ce5084e 100644 --- a/chebai/result/regression.py +++ b/chebai/result/regression.py @@ -1,8 +1,4 @@ -from typing import List -import matplotlib.pyplot as plt -import pandas as pd -import seaborn as sns from torch import Tensor from torchmetrics.regression import ( MeanSquaredError, diff --git a/chebai/result/utils.py b/chebai/result/utils.py index 89e83250..f6c0f4eb 100644 --- a/chebai/result/utils.py +++ b/chebai/result/utils.py @@ -234,7 +234,7 @@ def evaluate_model_regression( save_batch_size = 128 n_saved = 1 - print(f"") + print("") for i in tqdm.tqdm(range(0, len(data_list), batch_size)): if not ( skip_existing_preds @@ -333,7 +333,7 @@ def evaluate_model_regression_attention( save_batch_size = 128 n_saved = 1 - print(f"") + print("") for i in tqdm.tqdm(range(0, len(data_list), batch_size)): if not ( skip_existing_preds @@ -434,7 +434,7 @@ def evaluate_model_regression( save_batch_size = 128 n_saved = 1 - print(f"") + print("") for i in tqdm.tqdm(range(0, len(data_list), batch_size)): if not ( skip_existing_preds @@ -533,7 +533,7 @@ def evaluate_model_regression_attention( save_batch_size = 128 n_saved = 1 - print(f"") + print("") for i in tqdm.tqdm(range(0, len(data_list), batch_size)): if not ( skip_existing_preds From 9a24fd7a7b4e833814f10494b23f1477cd0ba483 Mon Sep 17 00:00:00 2001 From: schnamo Date: Thu, 13 Nov 2025 18:18:26 +0100 Subject: [PATCH 54/54] black fixes --- chebai/result/regression.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/chebai/result/regression.py b/chebai/result/regression.py index 4ce5084e..ed660f12 100644 --- a/chebai/result/regression.py +++ b/chebai/result/regression.py @@ -1,12 +1,9 @@ - +import torch from torch import Tensor -from torchmetrics.regression import ( - MeanSquaredError, -) +from torchmetrics.regression import MeanSquaredError # from chebai.callbacks.epoch_metrics import BalancedAccuracy, MacroF1 -from chebai.result.utils import * - +# from chebai.result.utils import * # def visualise_f1(logs_path: str) -> None: # """