diff --git a/.dockerignore b/.dockerignore old mode 100644 new mode 100755 diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml old mode 100644 new mode 100755 index 27f43c34..5bbea609 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -101,5 +101,17 @@ jobs: exit 1 fi + dfpl interpretgnn -f example/interpret.json + if [ "$(cat interpretations.csv | wc -l)" -lt "6" ]; then + echo "predict result should have at least 5 lines. But had only $(cat interpretations.csv | wc -l)" >&2 + exit 1 + fi + + dfpl convert -f tests/data + if [ "$(find tests/data -name '*.csv' | wc -l)" -ne "$(find tests/data -name '*.pkl' | wc -l)" ]; then + echo "not all csv files are converted to pickle ones" >&2 + exit 1 + fi + - dfpl convert -f tests/data \ No newline at end of file + echo "All tests passed" \ No newline at end of file diff --git a/.github/workflows/push-to-gitlab.yml b/.github/workflows/push-to-gitlab.yml old mode 100644 new mode 100755 diff --git a/.gitignore b/.gitignore old mode 100644 new mode 100755 diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml old mode 100644 new mode 100755 diff --git a/LICENSE.pdf b/LICENSE.pdf old mode 100644 new mode 100755 diff --git a/README.md b/README.md old mode 100644 new mode 100755 index bf3ed816..64d330c0 --- a/README.md +++ b/README.md @@ -111,22 +111,23 @@ In order to use the environment it needs to be activated with `. ENV_PATH/bin/ac To use this tool in a conda environment: -1. Create the conda env from scratch +1. Install mamba. For details follow the installation guide here. https://mamba.readthedocs.io/en/latest/mamba-installation.html#mamba-install} +2. Create the mamba env from scratch From within the `deepFPlearn` directory, you can create the conda environment with the provided yaml file that contains all information and necessary packages ```shell - conda env create -f environment.yml + mamba env create -f environment.yml ``` -2. Activate the `dfpl_env` environment with +3. Activate the `dfpl_env` environment with ```shell - conda activate dfpl_env + mamba activate dfpl_env ``` -3. Install the local `dfpl` package by calling +4. Install the local `dfpl` package by calling ```shell pip install --no-deps ./ @@ -327,7 +328,6 @@ Kyriakos Soulios, Patrick Scheibe, Matthias Bernt, Jörg Hackermüller, and Jana deepFPlearn+: Enhancing Toxicity Prediction Across the Chemical Universe Using Graph Neural Networks. Submitted to a scientific journal, currently under review. -[2] Jana Schor, Patrick Scheibe, Matthias Bernt, Wibke Busch, Chih Lai, and Jörg Hackermüller. AI for predicting chemical-effect associations at the chemical universe level—deepFPlearn. Briefings in Bioinformatics, Volume 23, Issue 5, September 2022, bbac257, https://doi.org/10.1093/bib/bbac257 diff --git a/container/Dockerfile b/container/Dockerfile old mode 100644 new mode 100755 index 566685ad..d0165580 --- a/container/Dockerfile +++ b/container/Dockerfile @@ -50,6 +50,7 @@ RUN sh -c 'echo "APT { Get { AllowUnauthenticated \"1\"; }; };" > /etc/apt/apt.c RUN apt -o Acquire::AllowInsecureRepositories=true -o Acquire::AllowDowngradeToInsecureRepositories=true update RUN apt-get install -y curl wget +RUN apt-get install -y git RUN apt-key del 7fa2af80 RUN wget https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2004/x86_64/cuda-keyring_1.0-1_all.deb @@ -118,7 +119,7 @@ RUN ln -s $(which python3) /usr/local/bin/python # does not work sind it just copies the files in dfpl COPY ./ /deepFPlearn/ -# install dfpl +# install dfpl RUN python -m pip install --no-cache-dir /deepFPlearn && pip install --no-cache-dir pytest # The code to run when container is started. diff --git a/container/README.md b/container/README.md old mode 100644 new mode 100755 diff --git a/dfpl/__init__.py b/dfpl/__init__.py old mode 100644 new mode 100755 diff --git a/dfpl/__main__.py b/dfpl/__main__.py index 7896d451..f9aa715e 100755 --- a/dfpl/__main__.py +++ b/dfpl/__main__.py @@ -1,12 +1,10 @@ import dataclasses import logging -import os.path -import pathlib +import os from argparse import Namespace from os import path - -import chemprop as cp -import pandas as pd +import wandb +import chemprop from keras.models import load_model from dfpl import autoencoder as ac @@ -17,43 +15,8 @@ from dfpl import vae as vae from dfpl.utils import createArgsFromJson, createDirectory, makePathAbsolute -project_directory = pathlib.Path(".").parent.parent.absolute() -test_train_opts = options.Options( - inputFile=f"{project_directory}/input_datasets/S_dataset.pkl", - outputDir=f"{project_directory}/output_data/console_test", - ecWeightsFile=f"{project_directory}/output_data/case_00/AE_S/ae_S.encoder.hdf5", - ecModelDir=f"{project_directory}/output_data/case_00/AE_S/saved_model", - type="smiles", - fpType="topological", - epochs=100, - batchSize=1024, - fpSize=2048, - encFPSize=256, - enableMultiLabel=False, - testSize=0.2, - kFolds=2, - verbose=2, - trainAC=False, - trainFNN=True, - compressFeatures=True, - activationFunction="selu", - lossFunction="bce", - optimizer="Adam", - fnnType="FNN", -) - -test_pred_opts = options.Options( - inputFile=f"{project_directory}/input_datasets/S_dataset.pkl", - outputDir=f"{project_directory}/output_data/console_test", - outputFile=f"{project_directory}/output_data/console_test/S_dataset.predictions_ER.csv", - ecModelDir=f"{project_directory}/output_data/case_00/AE_S/saved_model", - fnnModelDir=f"{project_directory}/output_data/console_test/ER_saved_model", - type="smiles", - fpType="topological", -) - - -def traindmpnn(opts: options.GnnOptions): + +def traindmpnn(opts: options.GnnOptions) -> None: """ Train a D-MPNN model using the given options. Args: @@ -61,54 +24,46 @@ def traindmpnn(opts: options.GnnOptions): Returns: - None """ - os.environ["CUDA_VISIBLE_DEVICES"] = f"{opts.gpu}" - ignore_elements = ["py/object"] # Load options from a JSON file and replace the relevant attributes in `opts` - arguments = createArgsFromJson( - opts.configFile, ignore_elements, return_json_object=False - ) - opts = cp.args.TrainArgs().parse_args(arguments) + arguments = createArgsFromJson(jsonFile=opts.configFile) + opts = chemprop.args.TrainArgs().parse_args(arguments) logging.info("Training DMPNN...") - # Train the model and get the mean and standard deviation of AUC score from cross-validation - mean_score, std_score = cp.train.cross_validate( - args=opts, train_func=cp.train.run_training + mean_score, std_score = chemprop.train.cross_validate( + args=opts, train_func=chemprop.train.run_training ) logging.info(f"Results: {mean_score:.5f} +/- {std_score:.5f}") -def predictdmpnn(opts: options.GnnOptions, json_arg_path: str) -> None: +def predictdmpnn(opts: options.GnnOptions) -> None: """ Predict the values using a trained D-MPNN model with the given options. Args: - opts: options.GnnOptions instance containing the details of the prediction - - JSON_ARG_PATH: path to a JSON file containing additional arguments for prediction Returns: - None """ - ignore_elements = [ - "py/object", - "checkpoint_paths", - "save_dir", - "saving_name", - ] # Load options and additional arguments from a JSON file - arguments, data = createArgsFromJson( - json_arg_path, ignore_elements, return_json_object=True + arguments = createArgsFromJson(jsonFile=opts.configFile) + opts = chemprop.args.PredictArgs().parse_args(arguments) + + chemprop.train.make_predictions(args=opts) + + +def interpretdmpnn(opts: options.GnnOptions) -> None: + """ + Interpret the predictions of a trained D-MPNN model with the given options. + Args: + - opts: options.GnnOptions instance containing the details of the prediction + Returns: + - None + """ + # Load options and additional arguments from a JSON file + arguments = createArgsFromJson(jsonFile=opts.configFile) + opts = chemprop.args.InterpretArgs().parse_args(arguments) + + chemprop.interpret.interpret( + args=opts, save_to_csv=True ) - arguments.append("--preds_path") - arguments.append("") - save_dir = data.get("save_dir") - name = data.get("saving_name") - # Replace relevant attributes in `opts` with loaded options - opts = cp.args.PredictArgs().parse_args(arguments) - opts.preds_path = save_dir + "/" + name - df = pd.read_csv(opts.test_path) - smiles = [] - for index, rows in df.iterrows(): - my_list = [rows.smiles] - smiles.append(my_list) - # Make predictions and return the result - cp.train.make_predictions(args=opts, smiles=smiles) def train(opts: options.Options): @@ -116,9 +71,6 @@ def train(opts: options.Options): Run the main training procedure :param opts: Options defining the details of the training """ - - os.environ["CUDA_VISIBLE_DEVICES"] = f"{opts.gpu}" - # import data from file and create DataFrame if "tsv" in opts.inputFile: df = fp.importDataFile( @@ -128,7 +80,7 @@ def train(opts: options.Options): df = fp.importDataFile( opts.inputFile, import_function=fp.importSmilesCSV, fp_size=opts.fpSize ) - # initialize encoders to None + # initialize (auto)encoders to None encoder = None autoencoder = None if opts.trainAC: @@ -142,26 +94,32 @@ def train(opts: options.Options): # if feature compression is enabled if opts.compressFeatures: if not opts.trainAC: - if opts.aeType == "deterministic": - (autoencoder, encoder) = ac.define_ac_model(opts=options.Options()) - elif opts.aeType == "variational": + if opts.aeType == "variational": (autoencoder, encoder) = vae.define_vae_model(opts=options.Options()) - elif opts.ecWeightsFile == "": + else: + (autoencoder, encoder) = ac.define_ac_model(opts=options.Options()) + + if opts.ecWeightsFile == "": encoder = load_model(opts.ecModelDir) else: autoencoder.load_weights( os.path.join(opts.ecModelDir, opts.ecWeightsFile) ) + # compress the fingerprints using the autoencoder df = ac.compress_fingerprints(df, encoder) - # ac.visualize_fingerprints( - # df, - # before_col="fp", - # after_col="fpcompressed", - # train_indices=train_indices, - # test_indices=test_indices, - # save_as=f"UMAP_{opts.aeSplitType}.png", - # ) + if opts.visualizeLatent and opts.trainAC: + ac.visualize_fingerprints( + df, + save_as=f"{opts.ecModelDir}/TSNE_{opts.aeType}_{opts.aeSplitType}.png", + ) + elif opts.visualizeLatent: + logging.info( + "Visualizing latent space is only available if you train the autoencoder. Skipping visualization." + ) + if opts.trainFNN and opts.finetuneEncoder: + sl.train_single_label_models(df=df, opts=opts) + # train single label models if requested if opts.trainFNN and not opts.enableMultiLabel: sl.train_single_label_models(df=df, opts=opts) @@ -257,29 +215,36 @@ def main(): raise ValueError("Input directory is not a directory") elif prog_args.method == "traingnn": traingnn_opts = options.GnnOptions.fromCmdArgs(prog_args) - + createLogger("traingnn.log") traindmpnn(traingnn_opts) elif prog_args.method == "predictgnn": - predictgnn_opts = options.GnnOptions.fromCmdArgs(prog_args) - fixed_opts = dataclasses.replace( - predictgnn_opts, - test_path=makePathAbsolute(predictgnn_opts.test_path), - preds_path=makePathAbsolute(predictgnn_opts.preds_path), - ) - - logging.info( - f"The following arguments are received or filled with default values:\n{prog_args}" - ) - - predictdmpnn(fixed_opts, prog_args.configFile) + predictgnn_opts = options.PredictGnnOptions.fromCmdArgs(prog_args) + createLogger("predictgnn.log") + predictdmpnn(predictgnn_opts) + elif prog_args.method == "interpretgnn": + interpretgnn_opts = options.InterpretGNNoptions.fromCmdArgs(prog_args) + createLogger("interpretgnn.log") + interpretdmpnn(interpretgnn_opts) elif prog_args.method == "train": + if prog_args.configFile is None and prog_args.inputFile is None: + parser.error("Either --configFile or --inputFile must be provided.") + train_opts = options.Options.fromCmdArgs(prog_args) + # Access wandb configuration + # wandb.init(project="dfpl") + # config = wandb.config + fixed_opts = dataclasses.replace( train_opts, inputFile=makePathAbsolute(train_opts.inputFile), outputDir=makePathAbsolute(train_opts.outputDir), + # learningRate=config.learningRate, + # learningRateDecay=config.learningRateDecay, + # dropout=config.dropout, + # batchSize=config.batchSize, + # l2reg=config.l2reg ) createDirectory(fixed_opts.outputDir) createLogger(path.join(fixed_opts.outputDir, "train.log")) @@ -288,6 +253,8 @@ def main(): ) train(fixed_opts) elif prog_args.method == "predict": + if prog_args.configFile is None and prog_args.inputFile is None: + parser.error("Either --configFile or --inputFile must be provided.") predict_opts = options.Options.fromCmdArgs(prog_args) fixed_opts = dataclasses.replace( predict_opts, @@ -298,8 +265,6 @@ def main(): ), ecModelDir=makePathAbsolute(predict_opts.ecModelDir), fnnModelDir=makePathAbsolute(predict_opts.fnnModelDir), - trainAC=False, - trainFNN=False, ) createDirectory(fixed_opts.outputDir) createLogger(path.join(fixed_opts.outputDir, "predict.log")) diff --git a/dfpl/autoencoder.py b/dfpl/autoencoder.py old mode 100644 new mode 100755 index 99bf4578..d96cd48c --- a/dfpl/autoencoder.py +++ b/dfpl/autoencoder.py @@ -1,19 +1,18 @@ import logging import math import os.path -from os.path import basename from typing import Tuple import matplotlib.pyplot as plt import numpy as np import pandas as pd import seaborn as sns -import umap +import umap.umap_ as umap import wandb from sklearn.model_selection import train_test_split from tensorflow.keras import initializers, losses, optimizers from tensorflow.keras.layers import Dense, Input -from tensorflow.keras.models import Model +from tensorflow.keras.models import Model, load_model from dfpl import callbacks from dfpl import history as ht @@ -32,9 +31,13 @@ def define_ac_model(opts: options.Options, output_bias=None) -> Tuple[Model, Mod """ input_size = opts.fpSize encoding_dim = opts.encFPSize - ac_optimizer = optimizers.Adam( - learning_rate=opts.aeLearningRate, decay=opts.aeLearningRateDecay + lr_schedule = optimizers.schedules.ExponentialDecay( + opts.aeLearningRate, + decay_steps=1000, + decay_rate=opts.aeLearningRateDecay, + staircase=True, ) + ac_optimizer = optimizers.legacy.Adam(learning_rate=lr_schedule) if output_bias is not None: output_bias = initializers.Constant(output_bias) @@ -104,7 +107,6 @@ def define_ac_model(opts: options.Options, output_bias=None) -> Tuple[Model, Mod )(decoded) # output layer - # to either 0 or 1 and hence we use sigmoid activation function. decoded = Dense( units=input_size, activation="sigmoid", bias_initializer=output_bias )(decoded) @@ -119,19 +121,13 @@ def define_ac_model(opts: options.Options, output_bias=None) -> Tuple[Model, Mod encoder = Model(input_vec, encoded) autoencoder.summary(print_fn=logging.info) - autoencoder.compile( - optimizer=ac_optimizer, - loss=losses.BinaryCrossentropy(), - # metrics=[ - # metrics.AUC(), - # metrics.Precision(), - # metrics.Recall() - # ] - ) + autoencoder.compile(optimizer=ac_optimizer, loss=losses.BinaryCrossentropy()) return autoencoder, encoder -def train_full_ac(df: pd.DataFrame, opts: options.Options) -> Model: +def train_full_ac( + df: pd.DataFrame, opts: options.Options +) -> Tuple[Model, np.ndarray, np.ndarray]: """ Trains an autoencoder on the given feature matrix X. The response matrix is only used to split the data into meaningful test and train sets. @@ -145,37 +141,8 @@ def train_full_ac(df: pd.DataFrame, opts: options.Options) -> Model: if opts.aeWabTracking and not opts.wabTracking: wandb.init(project=f"AE_{opts.aeSplitType}") - # Define output files for autoencoder and encoder weights - if opts.ecWeightsFile == "": - # If no encoder weights file is specified, use the input file name to generate a default file name - logging.info("No AE encoder weights file specified") - base_file_name = ( - os.path.splitext(basename(opts.inputFile))[0] + opts.aeSplitType - ) - logging.info( - f"(auto)encoder weights will be saved in {base_file_name}.autoencoder.hdf5" - ) - ac_weights_file = os.path.join( - opts.outputDir, base_file_name + ".autoencoder.weights.hdf5" - ) - # ec_weights_file = os.path.join( - # opts.outputDir, base_file_name + ".encoder.weights.hdf5" - # ) - else: - # If an encoder weights file is specified, use it as the encoder weights file name - logging.info(f"AE encoder will be saved in {opts.ecWeightsFile}") - base_file_name = ( - os.path.splitext(basename(opts.ecWeightsFile))[0] + opts.aeSplitType - ) - ac_weights_file = os.path.join( - opts.outputDir, base_file_name + ".autoencoder.weights.hdf5" - ) - # ec_weights_file = os.path.join(opts.outputDir, opts.ecWeightsFile) - + save_path = os.path.join(opts.ecModelDir, f"{opts.aeSplitType}_split_autoencoder") # Collect the callbacks for training - callback_list = callbacks.autoencoder_callback( - checkpoint_path=ac_weights_file, opts=opts - ) # Select all fingerprints that are valid and turn them into a numpy array fp_matrix = np.array( @@ -257,7 +224,6 @@ def train_full_ac(df: pd.DataFrame, opts: options.Options) -> Model: # Find the corresponding indices for train_data, val_data, and test_data in the sorted DataFrame train_indices = sorted_indices[df.index.isin(train_data.index)] - # val_indices = sorted_indices[df.index.isin(val_data.index)] test_indices = sorted_indices[df.index.isin(test_data.index)] else: x_train = fp_matrix @@ -286,33 +252,39 @@ def train_full_ac(df: pd.DataFrame, opts: options.Options) -> Model: # Set up the model of the AC w.r.t. the input size and the dimension of the bottle neck (z!) (autoencoder, encoder) = define_ac_model(opts, output_bias=initial_bias) - + callback_list = callbacks.autoencoder_callback(checkpoint_path=save_path, opts=opts) # Train the autoencoder on the training data auto_hist = autoencoder.fit( x_train, x_train, - callbacks=callback_list, + callbacks=[callback_list], epochs=opts.aeEpochs, batch_size=opts.aeBatchSize, verbose=opts.verbose, validation_data=(x_test, x_test) if opts.testSize > 0.0 else None, ) - logging.info(f"Autoencoder weights stored in file: {ac_weights_file}") # Store the autoencoder training history and plot the metrics ht.store_and_plot_history( - base_file_name=os.path.join(opts.outputDir, base_file_name + ".AC"), + base_file_name=save_path, hist=auto_hist, ) # Save the autoencoder callback model to disk - save_path = os.path.join(opts.ecModelDir, f"{opts.aeSplitType}_autoencoder") if opts.testSize > 0.0: - (callback_autoencoder, callback_encoder) = define_ac_model(opts) - callback_encoder.save(filepath=save_path) + # Re-define autoencoder and encoder using your function + callback_autoencoder = load_model(filepath=save_path) + _, callback_encoder = define_ac_model(opts) + for i, layer in enumerate(callback_encoder.layers): + layer.set_weights(callback_autoencoder.layers[i].get_weights()) + + # Save the encoder model + encoder_save_path = os.path.join(save_path, "encoder_model") + callback_encoder.save(filepath=encoder_save_path) else: encoder.save(filepath=save_path) # Return the encoder model of the trained autoencoder + autoencoder.summary(print_fn=logging.info) return encoder, train_indices, test_indices @@ -341,75 +313,161 @@ def compress_fingerprints(dataframe: pd.DataFrame, encoder: Model) -> pd.DataFra return dataframe -def visualize_fingerprints( - df: pd.DataFrame, - before_col: str, - after_col: str, - train_indices: np.ndarray, - test_indices: np.ndarray, - save_as: str, -): - # Calculate the number of samples to be taken from each set - num_samples = 1000 - train_samples = int(num_samples * len(train_indices) / len(df)) - test_samples = num_samples - train_samples - - # Assign train and test data points separately - train_data = df.loc[train_indices] - test_data = df.loc[test_indices] - - # Sample train and test data points - train_data_sampled = train_data.sample(n=train_samples, random_state=42) - test_data_sampled = test_data.sample(n=test_samples, random_state=42) - - # Concatenate the sampled train and test data - df_sampled = pd.concat([train_data_sampled, test_data_sampled]) - - # Convert the boolean values in the after_col column to floats - df_sampled[after_col] = df_sampled[after_col].apply( - lambda x: np.array(x, dtype=float) - ) - - df_sampled.loc[train_data_sampled.index, "set"] = "train" - df_sampled.loc[test_data_sampled.index, "set"] = "test" - # Apply UMAP - umap_model = umap.UMAP( - n_neighbors=15, min_dist=0.1, metric="euclidean", random_state=42 - ) - # Filter out the rows with invalid arrays - umap_results = umap_model.fit_transform(df_sampled[after_col].tolist()) - # Add UMAP results to the DataFrame - df_sampled["umap_x"] = umap_results[:, 0] - df_sampled["umap_y"] = umap_results[:, 1] - - # Define custom color palette - palette = {"train": "blue", "test": "red"} - - # Create the scatter plot - sns.set(style="white") - fig, ax = plt.subplots(figsize=(10, 8)) - split = save_as.split("_", 1) - part_after_underscore = split[1] - split_type = part_after_underscore.split(".")[0] - # Plot the UMAP results - for label, grp in df_sampled.groupby("set"): - set_label = label - color = palette[set_label] - alpha = ( - 0.09 if set_label == "train" else 0.9 - ) # Set different opacities for train and test - ax.scatter( - grp["umap_x"], grp["umap_y"], label=f"{set_label}", c=color, alpha=alpha - ) - - # Customize the plot - ax.set_title( - f"UMAP visualization of molecular fingerprints using {split_type} split", - fontsize=14, - ) - ax.set_xlabel("UMAP 1") - ax.set_ylabel("UMAP 2") - ax.legend(title="", loc="upper right") - sns.despine(ax=ax, offset=10) - save_path = os.path.join(os.getcwd(), save_as) - plt.savefig(save_path) +# def visualize_fingerprints( +# df: pd.DataFrame, +# train_indices: np.ndarray, +# test_indices: np.ndarray, +# save_as: str, +# ): +# """ +# Visualize fingerprints using UMAP and save the visualization to a file. +# +# This function takes a Pandas DataFrame containing fingerprint data, calculates +# the appropriate number of samples based on the size of the dataset, applies UMAP +# for dimensionality reduction, and saves the resulting visualization. +# +# Parameters: +# - df (pd.DataFrame): A Pandas DataFrame containing fingerprint data. +# - train_indices (np.ndarray): An array containing indices of training samples. +# - test_indices (np.ndarray): An array containing indices of test samples. +# - save_as (str): The filename or path where the UMAP visualization will be saved. +# Note: +# - If the DataFrame size exceeds 50,000 rows, the function logs a message and skips +# the UMAP visualization step. +# """ +# if len(df) <= 10000: +# num_samples = len(df) +# elif len(df) > 50000: +# logging.info( +# "Cannot return the UMAP due to the large dataset size. Skipping the function." +# ) +# return +# else: +# num_samples = len(df) // 2 +# +# after_col = "fpcompressed" +# # Calculate the number of samples to be taken from each set +# train_samples = int(num_samples * len(train_indices) / len(df)) +# test_samples = num_samples - train_samples +# +# # Assign train and test data points separately +# train_data = df.loc[train_indices] +# test_data = df.loc[test_indices] +# +# # Sample train and test data points +# train_data_sampled = train_data.sample(n=train_samples, random_state=42) +# test_data_sampled = test_data.sample(n=test_samples, random_state=42) +# +# # Concatenate the sampled train and test data +# df_sampled = pd.concat([train_data_sampled, test_data_sampled]) +# +# # Convert the boolean values in the after_col column to floats +# df_sampled[after_col] = df_sampled[after_col].apply( +# lambda x: np.array(x, dtype=float) +# ) +# +# df_sampled.loc[train_data_sampled.index, "set"] = "train" +# df_sampled.loc[test_data_sampled.index, "set"] = "test" +# # Apply UMAP +# umap_model = umap.UMAP( +# n_neighbors=15, min_dist=0.1, metric="euclidean", random_state=42 +# ) +# # Filter out the rows with invalid arrays +# umap_results = umap_model.fit_transform(df_sampled[after_col].tolist()) +# # Add UMAP results to the DataFrame +# df_sampled["umap_x"] = umap_results[:, 0] +# df_sampled["umap_y"] = umap_results[:, 1] +# +# # Define custom color palette +# palette = {"train": "blue", "test": "red"} +# +# # Create the scatter plot +# sns.set(style="white") +# fig, ax = plt.subplots(figsize=(10, 8)) +# split = save_as.split("_", 1) +# part_after_underscore = split[1] +# split_type = part_after_underscore.split(".")[0] +# # Plot the UMAP results +# for label, grp in df_sampled.groupby("set"): +# set_label = label +# color = palette[set_label] +# alpha = ( +# 0.09 if set_label == "train" else 0.9 +# ) # Set different opacities for train and test +# ax.scatter( +# grp["umap_x"], grp["umap_y"], label=f"{set_label}", c=color, alpha=alpha +# ) +# +# # Customize the plot +# ax.set_title( +# f"UMAP visualization of molecular fingerprints using {split_type} split", +# fontsize=14, +# ) +# ax.set_xlabel("UMAP 1") +# ax.set_ylabel("UMAP 2") +# ax.legend(title="", loc="upper right") +# sns.despine(ax=ax, offset=10) +# save_path = os.path.join(os.getcwd(), save_as) +# plt.savefig(save_path) +import math +from sklearn.cluster import MiniBatchKMeans +from sklearn.manifold import TSNE +from sklearn.metrics import adjusted_rand_score, adjusted_mutual_info_score +import umap +import matplotlib.pyplot as plt +from sklearn.neighbors import NearestNeighbors + +def knn_preservation(original_fp, compressed_fp, n_neighbors=5): + original_nn = NearestNeighbors(n_neighbors=n_neighbors).fit(original_fp) + compressed_nn = NearestNeighbors(n_neighbors=n_neighbors).fit(compressed_fp) + + original_neighbors = original_nn.kneighbors(original_fp, return_distance=False) + compressed_neighbors = compressed_nn.kneighbors(compressed_fp, return_distance=False) + + intersection = np.array([len(np.intersect1d(original_neighbors[i], compressed_neighbors[i])) for i in range(len(original_fp))]) + preservation_score = np.mean(intersection / n_neighbors) + return preservation_score +def knn_preservation(original_fp, compressed_fp, n_neighbors=5): + original_nn = NearestNeighbors(n_neighbors=n_neighbors).fit(original_fp) + compressed_nn = NearestNeighbors(n_neighbors=n_neighbors).fit(compressed_fp) + + original_neighbors = original_nn.kneighbors(original_fp, return_distance=False) + compressed_neighbors = compressed_nn.kneighbors(compressed_fp, return_distance=False) + + intersection = np.array([len(np.intersect1d(original_neighbors[i], compressed_neighbors[i])) for i in range(len(original_fp))]) + preservation_score = np.mean(intersection / n_neighbors) + return preservation_score + +def visualize_fingerprints(dataframe, n_clusters=7, save_as=None): + # Extract original and compressed fingerprints + original_fp = np.array(dataframe['fp'].to_list()) + compressed_fp = np.array(dataframe['fpcompressed'].to_list()) + + original_tsne = TSNE(n_components=2, random_state=42).fit_transform(original_fp) + compressed_tsne = TSNE(n_components=2, random_state=42).fit_transform(compressed_fp) + + + # Cluster using MiniBatchKMeans + kmeans = MiniBatchKMeans(n_clusters=n_clusters, random_state=42) + original_clusters = kmeans.fit_predict(original_tsne) + compressed_clusters = kmeans.fit_predict(compressed_tsne) + + # Calculate metrics + ars = adjusted_rand_score(original_clusters, compressed_clusters) + ami = adjusted_mutual_info_score(original_clusters, compressed_clusters) + knn_preservation_score = knn_preservation(original_fp, compressed_fp) + + # Visualize + plt.figure(figsize=(12, 6)) + plt.subplot(1, 2, 1) + plt.scatter(original_tsne[:, 0], original_tsne[:, 1], c=original_clusters, cmap='Spectral', s=5) + plt.title('Original TSNE Visualization') + + plt.subplot(1, 2, 2) + plt.scatter(compressed_tsne[:, 0], compressed_tsne[:, 1], c=compressed_clusters, cmap='Spectral', s=5) + plt.title('Deterministic Autoencoder Latent Space TSNE Visualization') + metrics_text = f"ARS: {ars:.2f}\nAMI: {ami:.2f}\nkNN: {knn_preservation_score:.2f}" + plt.text(0.95, 0.02, metrics_text, verticalalignment='bottom', horizontalalignment='right', transform=plt.gca().transAxes, color='black', fontsize=9, bbox=dict(facecolor='white', alpha=0.8, edgecolor='black')) + + plt.suptitle('Deterministic Autoencoder Latent Space') + plt.savefig(save_as) \ No newline at end of file diff --git a/dfpl/callbacks.py b/dfpl/callbacks.py old mode 100644 new mode 100755 index 6eae7965..8bf157fd --- a/dfpl/callbacks.py +++ b/dfpl/callbacks.py @@ -22,15 +22,25 @@ def autoencoder_callback(checkpoint_path: str, opts: options.Options) -> list: else: target = "loss" # enable this checkpoint to restore the weights of the best performing model - checkpoint = ModelCheckpoint( - checkpoint_path, - monitor=target, - mode="min", - verbose=1, - period=settings.ac_train_check_period, - save_best_only=True, - save_weights_only=True, - ) + if opts.aeType == "deterministic": + checkpoint = ModelCheckpoint( + checkpoint_path, + monitor=target, + mode="min", + verbose=1, + save_freq="epoch", + save_best_only=True, + ) + else: + checkpoint = ModelCheckpoint( + checkpoint_path, + monitor=target, + mode="min", + verbose=1, + save_freq="epoch", + save_best_only=True, + save_weights_only=True, + ) callbacks.append(checkpoint) # enable early stopping if val_loss is not improving anymore @@ -43,7 +53,6 @@ def autoencoder_callback(checkpoint_path: str, opts: options.Options) -> list: restore_best_weights=True, ) callbacks.append(early_stop) - if opts.aeWabTracking and not opts.wabTracking: callbacks.append(WandbCallback(save_model=False)) return callbacks @@ -65,7 +74,7 @@ def nn_callback(checkpoint_path: str, opts: options.Options) -> list: checkpoint = ModelCheckpoint( checkpoint_path, verbose=1, - period=settings.nn_train_check_period, + save_freq="epoch", save_best_only=True, monitor="val_loss", mode="min", diff --git a/dfpl/deepFPlearn-HyperParameterTuning.py b/dfpl/deepFPlearn-HyperParameterTuning.py old mode 100644 new mode 100755 diff --git a/dfpl/feedforwardNN.py b/dfpl/feedforwardNN.py old mode 100644 new mode 100755 index e9c88776..3f8ad261 --- a/dfpl/feedforwardNN.py +++ b/dfpl/feedforwardNN.py @@ -69,10 +69,16 @@ def define_out_file_names(path_prefix: str, target: str, fold: int = -1) -> tupl def define_nn_multi_label_model( input_size: int, output_size: int, opts: options.Options ) -> Model: + lr_schedule = optimizers.schedules.ExponentialDecay( + opts.aeLearningRate, + decay_steps=1000, + decay_rate=opts.aeLearningRateDecay, + staircase=True, + ) if opts.optimizer == "Adam": - my_optimizer = optimizers.Adam(learning_rate=opts.learningRate) + my_optimizer = optimizers.legacy.Adam(learning_rate=lr_schedule) elif opts.optimizer == "SGD": - my_optimizer = optimizers.SGD(lr=opts.learningRate, momentum=0.9) + my_optimizer = optimizers.legacy.SGD(lr=lr_schedule, momentum=0.9) else: logging.error(f"Your selected optimizer is not supported:{opts.optimizer}.") sys.exit("Unsupported optimizer.") @@ -132,9 +138,9 @@ def define_nn_model_multi( decay: float = 0.01, ) -> Model: if optimizer == "Adam": - my_optimizer = optimizers.Adam(learning_rate=lr, decay=decay) + my_optimizer = optimizers.legacy.Adam(learning_rate=lr, decay=decay) elif optimizer == "SGD": - my_optimizer = optimizers.SGD(lr=lr, momentum=0.9, decay=decay) + my_optimizer = optimizers.legacy.SGD(lr=lr, momentum=0.9, decay=decay) else: my_optimizer = optimizer @@ -282,7 +288,7 @@ def train_nn_models_multi(df: pd.DataFrame, opts: options.Options) -> None: "f1_trained", ] ) # F1 scores of predictions - + all_scores_data = [] fold_no = 1 # split the data @@ -294,6 +300,8 @@ def train_nn_models_multi(df: pd.DataFrame, opts: options.Options) -> None: model_file_path_weights, model_file_path_json, model_hist_path, + model_hist_csv_path, + model_predict_valset_csv_path, model_validation, model_auc_file, model_auc_file_data, @@ -351,6 +359,16 @@ def train_nn_models_multi(df: pd.DataFrame, opts: options.Options) -> None: ) idx = hist.history["val_loss"].index(min(hist.history["val_loss"])) + row_data = { + "fold_no": fold_no, + "loss": hist.history["loss"][idx], + "val_loss": hist.history["val_loss"][idx], + "acc": hist.history["accuracy"][idx], + "val_acc": hist.history["val_accuracy"][idx], + "f1_random": scores[0], + "f1_trained": scores[1], + } + all_scores_data.append(row_data) row_df = pd.DataFrame( [ [ @@ -375,12 +393,13 @@ def train_nn_models_multi(df: pd.DataFrame, opts: options.Options) -> None: ) logging.info(row_df) - all_scores = all_scores.append(row_df, ignore_index=True) + all_scores = all_scores_data.append(row_data)#, ignore_index=True) fold_no += 1 del model logging.info(all_scores) + all_scores = pd.DataFrame(all_scores_data) # finalize model # 1. provide best performing fold variant @@ -389,11 +408,11 @@ def train_nn_models_multi(df: pd.DataFrame, opts: options.Options) -> None: fold_no = all_scores.iloc[idx2]["fold_no"] model_name = ( - "multi_compressed-" + str(opts.compressFeatures) + ".Fold-" + str(fold_no) + "multi_compressed-" + str(opts.compressFeatures) + ".Fold-" + str(int(fold_no)) ) checkpoint_path = opts.outputDir + "/" + model_name + ".checkpoint.model.hdf5" best_model_file = checkpoint_path.replace( - "Fold-" + str(fold_no) + ".checkpoint.", "best.FNN-" + "Fold-" + str(int(fold_no)) + ".checkpoint.", "best.FNN-" ) file = re.sub( diff --git a/dfpl/fingerprint.py b/dfpl/fingerprint.py old mode 100644 new mode 100755 diff --git a/dfpl/history.py b/dfpl/history.py old mode 100644 new mode 100755 diff --git a/dfpl/options.py b/dfpl/options.py old mode 100644 new mode 100755 index 6d84dbc4..347e6058 --- a/dfpl/options.py +++ b/dfpl/options.py @@ -3,12 +3,13 @@ import argparse from dataclasses import dataclass from pathlib import Path +from typing import Optional, Literal, List import jsonpickle import torch -from chemprop.args import TrainArgs +from chemprop.args import TrainArgs, PredictArgs, InterpretArgs -from dfpl.utils import makePathAbsolute +from dfpl.utils import parseCmdArgs @dataclass @@ -17,51 +18,52 @@ class Options: Dataclass for all options necessary for training the neural nets """ - configFile: str = "./example/train.json" - inputFile: str = "/deepFPlearn/CMPNN/data/tox21.csv" - outputDir: str = "." - outputFile: str = "" - ecWeightsFile: str = "AE.encoder.weights.hdf5" - ecModelDir: str = "AE_encoder" - fnnModelDir: str = "modeltraining" + configFile: str = None + inputFile: str = "tests/data/smiles.csv" + outputDir: str = "example/results_train/" # changes according to mode + outputFile: str = "results.csv" + ecWeightsFile: str = "" + ecModelDir: str = "example/results_train/AE_encoder/" + fnnModelDir: str = "example/results_train/AR_saved_model/" type: str = "smiles" fpType: str = "topological" # also "MACCS", "atompairs" - epochs: int = 512 + epochs: int = 100 fpSize: int = 2048 encFPSize: int = 256 - kFolds: int = 0 + kFolds: int = 1 testSize: float = 0.2 enableMultiLabel: bool = False - verbose: int = 0 - trainAC: bool = True # if set to False, an AC weight file must be provided! + verbose: int = 2 + trainAC: bool = False trainFNN: bool = True - compressFeatures: bool = True - sampleFractionOnes: float = 0.5 # Only used when value is in [0,1] + finetuneEncoder: bool = False + compressFeatures: bool = False + sampleFractionOnes: float = 0.5 sampleDown: bool = False split_type: str = "random" aeSplitType: str = "random" aeType: str = "deterministic" - aeEpochs: int = 3000 + aeEpochs: int = 100 aeBatchSize: int = 512 aeLearningRate: float = 0.001 - aeLearningRateDecay: float = 0.01 - aeActivationFunction: str = "relu" + aeLearningRateDecay: float = 0.96 + aeActivationFunction: str = "selu" aeOptimizer: str = "Adam" fnnType: str = "FNN" batchSize: int = 128 optimizer: str = "Adam" learningRate: float = 0.001 + learningRateDecay: float = 0.96 lossFunction: str = "bce" activationFunction: str = "relu" l2reg: float = 0.001 dropout: float = 0.2 threshold: float = 0.5 - gpu: str = "" - snnDepth = 8 - snnWidth = 50 - aeWabTracking: str = "" # Wand & Biases autoencoder tracking - wabTracking: str = "" # Wand & Biases FNN tracking - wabTarget: str = "ER" # Wand & Biases target used for showing training progress + visualizeLatent: bool = False # only if autoencoder is trained or loaded + gpu: int = None + aeWabTracking: bool = False # Wand & Biases autoencoder tracking + wabTracking: bool = False # Wand & Biases FNN tracking + wabTarget: str = "AR" # Wand & Biases target used for showing training progress def saveToFile(self, file: str) -> None: """ @@ -72,42 +74,8 @@ def saveToFile(self, file: str) -> None: f.write(jsonpickle.encode(self)) @classmethod - def fromJson(cls, file: str) -> Options: - """ - Create an instance from a JSON file - """ - jsonFile = Path(file) - if jsonFile.exists() and jsonFile.is_file(): - with jsonFile.open() as f: - content = f.read() - return jsonpickle.decode(content) - raise ValueError("JSON file does not exist or is not readable") - - @classmethod - def fromCmdArgs(cls, args: argparse.Namespace) -> Options: - """ - Creates Options instance from cmdline arguments. - - If a training file (JSON) is provided, the values from that file are used. - However, additional commandline arguments will be preferred. If, e.g., "fpSize" is specified both in the - JSON file and on the commandline, then the value of the commandline argument will be used. - """ - result = Options() - if "configFile" in vars(args).keys(): - jsonFile = Path(makePathAbsolute(args.configFile)) - if jsonFile.exists() and jsonFile.is_file(): - with jsonFile.open() as f: - content = f.read() - result = jsonpickle.decode(content) - else: - raise ValueError("Could not find JSON input file") - - for key, value in vars(args).items(): - # The args dict will contain a "method" key from the subparser. - # We don't use this. - if key != "method": - result.__setattr__(key, value) - return result + def fromCmdArgs(cls, args: argparse.Namespace) -> "Options": + return parseCmdArgs(cls, args) @dataclass @@ -118,8 +86,8 @@ class GnnOptions(TrainArgs): total_epochs: int = 30 save: bool = True - configFile: str = "./example/traingnn.json" - data_path: str = "./example/data/tox21.csv" + configFile: str = "" + data_path: str = "" use_compound_names: bool = False save_dir: str = "" no_cache: bool = False @@ -129,42 +97,118 @@ class GnnOptions(TrainArgs): num_lrs: int = 2 minimize_score: bool = False num_tasks: int = 12 - preds_path: str = "./tox21dmpnn.csv" + preds_path: str = "" test_path: str = "" - save_preds: bool = True + save_preds: bool = False + calibration_method: str = "" + uncertainty_method: str = "" + calibration_path: str = "" + evaluation_methods: str = "" + evaluation_scores_path: str = "" + wabTracking: bool = False + split_sizes: List[float] = None + show_individual_scores: bool = False + # save_smiles_splits: bool = False @classmethod - def fromCmdArgs(cls, args: argparse.Namespace) -> GnnOptions: - """ - Creates Options instance from cmdline arguments. + def fromCmdArgs(cls, args: argparse.Namespace, json_config: Optional[dict] = None): + # Initialize with JSON config if provided + if json_config: + opts = cls(**json_config) + else: + opts = cls() - If a training file (JSON) is provided, the values from that file are used. - However, additional commandline arguments will be preferred. If, e.g., "fpSize" is specified both in the - JSON file and on the commandline, then the value of the commandline argument will be used. - """ - result = GnnOptions() - if "configFile" in vars(args).keys(): - jsonFile = Path(makePathAbsolute(args.configFile)) - if jsonFile.exists() and jsonFile.is_file(): - with jsonFile.open() as f: - content = f.read() - result = jsonpickle.decode(content) - else: - raise ValueError("Could not find JSON input file") - - return result + # Update with command-line arguments + for key, value in vars(args).items(): + if value is not None: + setattr(opts, key, value) + + return opts + +class PredictGnnOptions(PredictArgs): + """ + Dataclass to hold all options used for training the graph models + """ + + configFile: str = "./example/predictgnn.json" + calibration_atom_descriptors_path: str = None + calibration_features_path: str = None + calibration_interval_percentile: float = 95 + calibration_method: Literal[ + "zscaling", + "tscaling", + "zelikman_interval", + "mve_weighting", + "platt", + "isotonic", + ] = None + calibration_path: str = None + calibration_phase_features_path: str = None + drop_extra_columns: bool = False + dropout_sampling_size: int = 10 + evaluation_methods: List[str] = None + evaluation_scores_path: str = None + # no_features_scaling: bool = True + individual_ensemble_predictions: bool = False + preds_path: str = None + regression_calibrator_metric: Literal["stdev", "interval"] = None + test_path: str = None + uncertainty_dropout_p: float = 0.1 + uncertainty_method: Literal[ + "mve", + "ensemble", + "evidential_epistemic", + "evidential_aleatoric", + "evidential_total", + "classification", + "dropout", + ] = None @classmethod - def fromJson(cls, file: str) -> GnnOptions: - """ - Create an instance from a JSON file - """ - jsonFile = Path(file) - if jsonFile.exists() and jsonFile.is_file(): - with jsonFile.open() as f: - content = f.read() - return jsonpickle.decode(content) - raise ValueError("JSON file does not exist or is not readable") + def fromCmdArgs(cls, args: argparse.Namespace, json_config: Optional[dict] = None): + # Initialize with JSON config if provided + if json_config: + opts = cls(**json_config) + else: + opts = cls() + + # Update with command-line arguments + for key, value in vars(args).items(): + if value is not None: + setattr(opts, key, value) + + return opts + + +class InterpretGNNoptions(InterpretArgs): + """ + Dataclass to hold all options used for training the graph models + """ + + configFile: str = "./example/interpret.json" + data_path: str = "./example/data/smiles.csv" + batch_size: int = 500 + c_puct: float = 10.0 + max_atoms: int = 20 + min_atoms: int = 8 + prop_delta: float = 0.5 + property_id: List[int] = None + rollout: int = 20 + + @classmethod + def fromCmdArgs(cls, args: argparse.Namespace, json_config: Optional[dict] = None): + # Initialize with JSON config if provided + if json_config: + opts = cls(**json_config) + else: + opts = cls() + + # Update with command-line arguments + for key, value in vars(args).items(): + if value is not None: + setattr(opts, key, value) + + return opts def createCommandlineParser() -> argparse.ArgumentParser: @@ -186,6 +230,12 @@ def createCommandlineParser() -> argparse.ArgumentParser: parser_predict_gnn.set_defaults(method="predictgnn") parsePredictGnn(parser_predict_gnn) + parser_interpret_gnn = subparsers.add_parser( + "interpretgnn", help="Interpret your GNN models" + ) + parser_interpret_gnn.set_defaults(method="interpretgnn") + parseInterpretGnn(parser_interpret_gnn) + parser_train = subparsers.add_parser( "train", help="Train new models with your data" ) @@ -225,7 +275,7 @@ def parseInputTrain(parser: argparse.ArgumentParser) -> None: metavar="FILE", type=str, help="Input JSON file that contains all information for training/predicting.", - default=argparse.SUPPRESS, + default="example/train.json", ) general_args.add_argument( "-i", @@ -234,7 +284,7 @@ def parseInputTrain(parser: argparse.ArgumentParser) -> None: type=str, help="The file containing the data for training in " "comma separated CSV format.The first column should be smiles.", - default=argparse.SUPPRESS, + default="tests/data/smiles.csv", ) general_args.add_argument( "-o", @@ -243,8 +293,10 @@ def parseInputTrain(parser: argparse.ArgumentParser) -> None: type=str, help="Prefix of output file name. Trained model and " "respective stats will be returned in this directory.", - default=argparse.SUPPRESS, + default="example/results_train/", ) + + # TODO CHECK WHAT IS TYPE DOING? general_args.add_argument( "-t", "--type", @@ -252,7 +304,7 @@ def parseInputTrain(parser: argparse.ArgumentParser) -> None: type=str, choices=["fp", "smiles"], help="Type of the chemical representation. Choices: 'fp', 'smiles'.", - default=argparse.SUPPRESS, + default="fp", ) general_args.add_argument( "-thr", @@ -260,47 +312,41 @@ def parseInputTrain(parser: argparse.ArgumentParser) -> None: type=float, metavar="FLOAT", help="Threshold for binary classification.", - default=argparse.SUPPRESS, + default=0.5, ) general_args.add_argument( "-gpu", "--gpu", metavar="INT", type=int, - help="Select which gpu to use. If not available, leave empty.", - default=argparse.SUPPRESS, + help="Select which gpu to use by index. If not available, leave empty", + default=None, ) general_args.add_argument( - "-k", "--fpType", metavar="STR", type=str, - choices=["topological", "MACCS"], # , 'atompairs', 'torsions'], - help="The type of fingerprint to be generated/used in input file.", - default=argparse.SUPPRESS, + choices=["topological", "MACCS"], + help="The type of fingerprint to be generated/used in input file. MACCS or topological are available.", + default="topological", ) general_args.add_argument( - "-s", "--fpSize", type=int, - help="Size of fingerprint that should be generated.", - default=argparse.SUPPRESS, + help="Length of the fingerprint that should be generated.", + default=2048, ) general_args.add_argument( - "-c", "--compressFeatures", - metavar="BOOL", - type=bool, - help="Should the fingerprints be compressed or not. Activates the autoencoder. ", - default=argparse.SUPPRESS, + action="store_true", + help="Should the fingerprints be compressed or not. Needs a path of a trained autoencoder or needs the trainAC also set to True.", + default=False, ) general_args.add_argument( - "-m", "--enableMultiLabel", - metavar="BOOL", - type=bool, + action="store_true", help="Train multi-label classification model in addition to the individual models.", - default=argparse.SUPPRESS, + default=False, ) # Autoencoder Configuration autoencoder_args.add_argument( @@ -309,14 +355,14 @@ def parseInputTrain(parser: argparse.ArgumentParser) -> None: type=str, metavar="FILE", help="The .hdf5 file of a trained encoder", - default=argparse.SUPPRESS, + default="", ) autoencoder_args.add_argument( "--ecModelDir", type=str, metavar="DIR", help="The directory where the full model of the encoder will be saved", - default=argparse.SUPPRESS, + default="example/results_train/AE_encoder/", ) autoencoder_args.add_argument( "--aeType", @@ -324,21 +370,21 @@ def parseInputTrain(parser: argparse.ArgumentParser) -> None: type=str, choices=["variational", "deterministic"], help="Autoencoder type, variational or deterministic.", - default=argparse.SUPPRESS, + default="deterministic", ) autoencoder_args.add_argument( "--aeEpochs", metavar="INT", type=int, help="Number of epochs for autoencoder training.", - default=argparse.SUPPRESS, + default=100, ) autoencoder_args.add_argument( "--aeBatchSize", metavar="INT", type=int, help="Batch size in autoencoder training.", - default=argparse.SUPPRESS, + default=512, ) autoencoder_args.add_argument( "--aeActivationFunction", @@ -346,21 +392,21 @@ def parseInputTrain(parser: argparse.ArgumentParser) -> None: type=str, choices=["relu", "selu"], help="The activation function for the hidden layers in the autoencoder.", - default=argparse.SUPPRESS, + default="relu", ) autoencoder_args.add_argument( "--aeLearningRate", metavar="FLOAT", type=float, help="Learning rate for autoencoder training.", - default=argparse.SUPPRESS, + default=0.001, ) autoencoder_args.add_argument( "--aeLearningRateDecay", metavar="FLOAT", type=float, help="Learning rate decay for autoencoder training.", - default=argparse.SUPPRESS, + default=0.96, ) autoencoder_args.add_argument( "--aeSplitType", @@ -368,7 +414,7 @@ def parseInputTrain(parser: argparse.ArgumentParser) -> None: type=str, choices=["scaffold_balanced", "random", "molecular_weight"], help="Set how the data is going to be split for the autoencoder", - default=argparse.SUPPRESS, + default="random", ) autoencoder_args.add_argument( "-d", @@ -376,7 +422,13 @@ def parseInputTrain(parser: argparse.ArgumentParser) -> None: metavar="INT", type=int, help="Size of encoded fingerprint (z-layer of autoencoder).", - default=argparse.SUPPRESS, + default=256, + ) + autoencoder_args.add_argument( + "--visualizeLatent", + action="store_true", + help="UMAP the latent space for exploration", + default=False, ) # Training Configuration training_args.add_argument( @@ -385,15 +437,14 @@ def parseInputTrain(parser: argparse.ArgumentParser) -> None: type=str, choices=["scaffold_balanced", "random", "molecular_weight"], help="Set how the data is going to be split for the feedforward neural network", - default=argparse.SUPPRESS, + default="random", ) training_args.add_argument( - "-l", "--testSize", metavar="FLOAT", type=float, help="Fraction of the dataset that should be used for testing. Value in [0,1].", - default=argparse.SUPPRESS, + default=0.2, ) training_args.add_argument( "-K", @@ -401,7 +452,7 @@ def parseInputTrain(parser: argparse.ArgumentParser) -> None: metavar="INT", type=int, help="K that is used for K-fold cross-validation in the training procedure.", - default=argparse.SUPPRESS, + default=1, ) training_args.add_argument( "-v", @@ -411,21 +462,19 @@ def parseInputTrain(parser: argparse.ArgumentParser) -> None: choices=[0, 1, 2], help="Verbosity level. O: No additional output, " + "1: Some additional output, 2: full additional output", - default=argparse.SUPPRESS, + default=2, ) training_args.add_argument( "--trainAC", - metavar="BOOL", - type=bool, + action="store_true", help="Choose to train or not, the autoencoder based on the input file", - default=argparse.SUPPRESS, + default=False, ) training_args.add_argument( "--trainFNN", - metavar="BOOL", - type=bool, - help="Train the feedforward network either with provided weights.", - default=argparse.SUPPRESS, + action="store_false", + help="When called it deactivates the training.", + default=True, ) training_args.add_argument( "--sampleFractionOnes", @@ -433,14 +482,14 @@ def parseInputTrain(parser: argparse.ArgumentParser) -> None: type=float, help="This is the fraction of positive target associations (1s) in comparison to the majority class(0s)." "only works if --sampleDown is enabled", - default=argparse.SUPPRESS, + default=0.5, ) training_args.add_argument( "--sampleDown", metavar="BOOL", type=bool, help="Enable automatic down sampling of the 0 valued samples.", - default=argparse.SUPPRESS, + default=False, ) training_args.add_argument( "-e", @@ -448,52 +497,66 @@ def parseInputTrain(parser: argparse.ArgumentParser) -> None: metavar="INT", type=int, help="Number of epochs that should be used for the FNN training", - default=argparse.SUPPRESS, + default=100, ) - + training_args.add_argument( + "--finetuneEncoder", + action="store_true", + help="Finetune the encoder with the FNN", + default=False, + ) + # TODO CHECK IF ALL LOSSES MAKE SENSE HERE training_args.add_argument( "--lossFunction", metavar="STRING", type=str, choices=["mse", "bce", "focal"], help="Loss function to use during training. mse - mean squared error, bce - binary cross entropy.", - default=argparse.SUPPRESS, + default="bce", ) + # TODO DO I NEED ALL ARGUMENTS TO BE USER SPECIFIED? WHAT DOES THE USER KNOW ABOUT OPTIMIZERS? training_args.add_argument( "--optimizer", metavar="STRING", type=str, choices=["Adam", "SGD"], help='Optimizer to use for backpropagation in the FNN. Possible values: "Adam", "SGD"', - default=argparse.SUPPRESS, + default="Adam", ) training_args.add_argument( "--batchSize", metavar="INT", type=int, help="Batch size in FNN training.", - default=argparse.SUPPRESS, + default=128, ) training_args.add_argument( "--l2reg", metavar="FLOAT", type=float, help="Value for l2 kernel regularizer.", - default=argparse.SUPPRESS, + default=0.001, ) training_args.add_argument( "--dropout", metavar="FLOAT", type=float, help="The fraction of data that is dropped out in each dropout layer.", - default=argparse.SUPPRESS, + default=0.2, ) training_args.add_argument( "--learningRate", metavar="FLOAT", type=float, help="Learning rate size in FNN training.", - default=argparse.SUPPRESS, + default=0.000022, + ) + training_args.add_argument( + "--learningRateDecay", + metavar="FLOAT", + type=float, + help="Learning rate size in FNN training.", + default=0.96, ) training_args.add_argument( "--activationFunction", @@ -501,7 +564,7 @@ def parseInputTrain(parser: argparse.ArgumentParser) -> None: type=str, choices=["relu", "selu"], help="The activation function for hidden layers in the FNN.", - default=argparse.SUPPRESS, + default="relu", ) # Tracking Configuration tracking_args.add_argument( @@ -509,14 +572,14 @@ def parseInputTrain(parser: argparse.ArgumentParser) -> None: metavar="BOOL", type=bool, help="Track autoencoder performance via Weights & Biases, see https://wandb.ai.", - default=argparse.SUPPRESS, + default=False, ) tracking_args.add_argument( "--wabTracking", metavar="BOOL", type=bool, help="Track FNN performance via Weights & Biases, see https://wandb.ai.", - default=argparse.SUPPRESS, + default=False, ) tracking_args.add_argument( "--wabTarget", @@ -524,7 +587,112 @@ def parseInputTrain(parser: argparse.ArgumentParser) -> None: type=str, choices=["AR", "ER", "ED", "GR", "TR", "PPARg", "Aromatase"], help="Which target to use for tracking performance via Weights & Biases, see https://wandb.ai.", - default=argparse.SUPPRESS, + default="AR", + ) + + +def parseInputPredict(parser: argparse.ArgumentParser) -> None: + """ + Parse the input arguments. + + :return: A namespace object built up from attributes parsed out of the cmd line. + """ + + general_args = parser.add_argument_group("General Configuration") + files_args = parser.add_argument_group("Files") + files_args.add_argument( + "-f", + "--configFile", + metavar="FILE", + type=str, + help="Input JSON file that contains all information for training/predicting.", + ) + files_args.add_argument( + "-i", + "--inputFile", + metavar="FILE", + type=str, + help="The file containing the data for the prediction in (unquoted) " + "comma separated CSV format. The column named 'smiles' or 'fp'" + "contains the field to be predicted. Please adjust the type " + "that should be predicted (fp or smile) with -t option appropriately." + "An optional column 'id' is used to assign the outcomes to the" + "original identifiers. If this column is missing, the results are" + "numbered in the order of their appearance in the input file." + "A header is expected and respective column names are used.", + default="tests/data/smiles.csv", + ) + files_args.add_argument( + "-o", + "--outputDir", + metavar="DIR", + type=str, + help="Prefix of output directory. It will contain a log file and the file specified" + "with --outputFile.", + default="example/results_predict/", + ) + files_args.add_argument( + "--outputFile", + metavar="FILE", + type=str, + help="Output .CSV file name which will contain one prediction per input line. " + "Default: prefix of input file name.", + default="results.csv", + ) + # TODO AGAIN THIS TRASH HERE? CAN WE EVEN PROCESS SMILES? + general_args.add_argument( + "-t", + "--type", + metavar="STR", + type=str, + choices=["fp", "smiles"], + help="Type of the chemical representation. Choices: 'fp', 'smiles'.", + default="fp", + ) + general_args.add_argument( + "-k", + "--fpType", + metavar="STR", + type=str, + choices=["topological", "MACCS"], + help="The type of fingerprint to be generated/used in input file. Should be the same as the type of the fps that the model was trained upon.", + default="topological", + ) + files_args.add_argument( + "--ecModelDir", + type=str, + metavar="DIR", + help="The directory where the full model of the encoder will be saved (if trainAE=True) or " + "loaded from (if trainAE=False). Provide a full path here.", + default="", + ) + files_args.add_argument( + "--ecWeightsFile", + type=str, + metavar="STR", + help="The file where the full model of the encoder will be loaded from, to compress the fingerprints. Provide a full path here.", + default="", + ) + files_args.add_argument( + "--fnnModelDir", + type=str, + metavar="DIR", + help="The directory where the full model of the fnn is loaded from. " + "Provide a full path here.", + default="example/results_train/AR_saved_model", + ) + general_args.add_argument( + "-c", "--compressFeatures", action="store_true", default=False + ) + ( + general_args.add_argument( + "--aeType", + metavar="STRING", + type=str, + choices=["variational", "deterministic"], + help="Autoencoder type, variational or deterministic.", + default="deterministic", + ) ) @@ -534,21 +702,62 @@ def parseTrainGnn(parser: argparse.ArgumentParser) -> None: files_args = parser.add_argument_group("Files") model_args = parser.add_argument_group("Model arguments") training_args = parser.add_argument_group("Training Configuration") + uncertainty_args = parser.add_argument_group("Uncertainty Configuration") + uncertainty_args.add_argument( + "--uncertainty_method", + type=str, + metavar="STRING", + choices=[ + "mve", + "ensemble", + "evidential_epistemic", + "evidential_aleatoric", + "evidential_total", + "classification", + "dropout", + "dirichlet", + ], + help="Method to use for uncertainty estimation", + default="none", + ) + # Uncertainty arguments + uncertainty_args.add_argument( + "--calibration_method", + type=str, + metavar="STRING", + choices=[ + "zscaling", + "tscaling", + "zelikman_interval", + "mve_weighting", + "platt", + "isotonic", + ], + help="Method to use for calibration", + default="none", + ) + uncertainty_args.add_argument( + "--calibration_path", + type=str, + metavar="FILE", + help="Path to file with calibration data", + ) # General arguments general_args.add_argument("--split_key_molecule", type=int) general_args.add_argument("--pytorch_seed", type=int) general_args.add_argument("--cache_cutoff", type=float) - general_args.add_argument("--save_preds", type=bool) + general_args.add_argument("--save_preds", action="store_true", default=False) + general_args.add_argument("--wabTracking", action="store_true", default=False) general_args.add_argument( "--cuda", action="store_true", default=False, help="Turn on cuda" ) - general_args.add_argument( - "--save_smiles_splits", - action="store_true", - default=False, - help="Save smiles for each train/val/test splits for prediction convenience later", - ) + # general_args.add_argument( + # "--save_smiles_splits", + # action="store_true", + # default=False, + # help="Save smiles for each train/val/test splits for prediction convenience later", + # ) general_args.add_argument( "--test", action="store_true", @@ -575,9 +784,6 @@ def parseTrainGnn(parser: argparse.ArgumentParser) -> None: default=10, help="The number of batches between each logging of the training loss", ) - general_args.add_argument( - "--no_cuda", action="store_true", default=True, help="Turn off cuda" - ) general_args.add_argument( "--no_cache", action="store_true", @@ -593,13 +799,6 @@ def parseTrainGnn(parser: argparse.ArgumentParser) -> None: type=str, help="Input JSON file that contains all information for training/predicting.", ) - files_args.add_argument( - "--config_path", - type=str, - metavar="FILE", - help="Path to a .json file containing arguments. Any arguments present in the config" - "file will override arguments specified via the command line or by the defaults.", - ) files_args.add_argument( "--save_dir", type=str, @@ -917,7 +1116,6 @@ def parseTrainGnn(parser: argparse.ArgumentParser) -> None: model_args.add_argument( "--show_individual_scores", action="store_true", - default=True, help="Show all scores for individual targets, not just average, at the end", ) model_args.add_argument("--aggregation", choices=["mean", "sum", "norm"]) @@ -1034,141 +1232,151 @@ def parseTrainGnn(parser: argparse.ArgumentParser) -> None: ) -def parseInputPredict(parser: argparse.ArgumentParser) -> None: - """ - Parse the input arguments. - - :return: A namespace object built up from attributes parsed out of the cmd line. - """ - +def parsePredictGnn(parser: argparse.ArgumentParser) -> None: general_args = parser.add_argument_group("General Configuration") files_args = parser.add_argument_group("Files") + uncertainty_args = parser.add_argument_group("Uncertainty Configuration") + + general_args.add_argument( + "--checkpoint_path", + type=str, + metavar="FILE", + help="Path to model checkpoint (.pt file)", + ) + # general_args.add_argument( + # "--no_features_scaling", + # action="store_true", + # help="Turn on scaling of features", + # ) files_args.add_argument( "-f", "--configFile", - metavar="FILE", type=str, - help="Input JSON file that contains all information for training/predicting.", - default=argparse.SUPPRESS, - ) - files_args.add_argument( - "-i", - "--inputFile", metavar="FILE", - type=str, - help="The file containing the data for the prediction in (unquoted) " - "comma separated CSV format. The column named 'smiles' or 'fp'" - "contains the field to be predicted. Please adjust the type " - "that should be predicted (fp or smile) with -t option appropriately." - "An optional column 'id' is used to assign the outcomes to the" - "original identifiers. If this column is missing, the results are" - "numbered in the order of their appearance in the input file." - "A header is expected and respective column names are used.", - default=argparse.SUPPRESS, + help="Path to a .json file containing arguments. Any arguments present in the config" + "file will override arguments specified via the command line or by the defaults.", ) files_args.add_argument( - "-o", - "--outputDir", - metavar="DIR", + "--test_path", type=str, - help="Prefix of output directory. It will contain a log file and the file specified" - "with --outputFile.", - default=argparse.SUPPRESS, + help="Path to CSV file containing testing data for which predictions will be made.", ) files_args.add_argument( - "--outputFile", - metavar="FILE", + "--preds_path", type=str, - help="Output .CSV file name which will contain one prediction per input line. " - "Default: prefix of input file name.", - default=argparse.SUPPRESS, + help="Path to CSV or PICKLE file where predictions will be saved.", ) - general_args.add_argument( - "-t", - "--type", - metavar="STR", + files_args.add_argument( + "--calibration_path", type=str, - choices=["fp", "smiles"], - help="Type of the chemical representation. Choices: 'fp', 'smiles'.", - default=argparse.SUPPRESS, + help="Path to data file to be used for uncertainty calibration.", ) - general_args.add_argument( - "-k", - "--fpType", - metavar="STR", + files_args.add_argument( + "--calibration_features_path", type=str, - choices=["topological", "MACCS"], # , 'atompairs', 'torsions'], - help="The type of fingerprint to be generated/used in input file.", - default=argparse.SUPPRESS, + nargs="+", + help="Path to features data to be used with the uncertainty calibration dataset.", ) + files_args.add_argument("--calibration_phase_features_path", type=str, help="") files_args.add_argument( - "--ecModelDir", + "--calibration_atom_descriptors_path", type=str, - metavar="DIR", - help="The directory where the full model of the encoder will be saved (if trainAE=True) or " - "loaded from (if trainAE=False). Provide a full path here.", - default=argparse.SUPPRESS, + help="Path to the extra atom descriptors.", ) files_args.add_argument( - "--fnnModelDir", + "--calibration_bond_descriptors_path", type=str, - metavar="DIR", - help="The directory where the full model of the fnn is loaded from. " - "Provide a full path here.", - default=argparse.SUPPRESS, + help="Path to the extra bond descriptors that will be used as bond features to featurize a given molecule.", ) + general_args.add_argument( + "--drop_extra_columns", + action="store_true", + help="Whether to drop all columns from the test data file besides the SMILES columns and the new prediction columns.", + ) -def parsePredictGnn(parser: argparse.ArgumentParser) -> None: - general_args = parser.add_argument_group("General Configuration") - data_args = parser.add_argument_group("Data Configuration") - files_args = parser.add_argument_group("Files") - training_args = parser.add_argument_group("Training Configuration") - files_args.add_argument( - "-f", - "--configFile", - metavar="FILE", + uncertainty_args.add_argument( + "--uncertainty_method", type=str, - help="Input JSON file that contains all information for training/predicting.", - default=argparse.SUPPRESS, + choices=[ + "mve", + "ensemble", + "evidential_epistemic", + "evidential_aleatoric", + "evidential_total", + "classification", + "dropout", + "spectra_roundrobin", + "dirichlet", + ], + help="The method of calculating uncertainty.", ) - general_args.add_argument( - "--gpu", - type=int, - metavar="INT", - choices=list(range(torch.cuda.device_count())), - help="Which GPU to use", + uncertainty_args.add_argument( + "--calibration_method", + type=str, + nargs="+", + choices=[ + "zscaling", + "tscaling", + "zelikman_interval", + "mve_weighting", + "platt", + "isotonic", + ], + help="Methods used for calibrating the uncertainty calculated with uncertainty method.", ) - general_args.add_argument( - "--no_cuda", action="store_true", default=False, help="Turn off cuda" + uncertainty_args.add_argument( + "--individual_ensemble_predictions", + action="store_true", + default=False, + help="Whether to save individual ensemble predictions.", ) - general_args.add_argument( - "--num_workers", + uncertainty_args.add_argument( + "--evaluation_methods", + type=str, + nargs="+", + help="The methods used for evaluating the uncertainty performance if the test data provided includes targets. Available methods are [nll, miscalibration_area, ence, spearman] or any available classification or multiclass metric.", + ) + uncertainty_args.add_argument( + "--evaluation_scores_path", + type=str, + help="Location to save the results of uncertainty evaluations.", + ) + uncertainty_args.add_argument( + "--uncertainty_dropout_p", + type=float, + default=0.1, + help="The probability to use for Monte Carlo dropout uncertainty estimation.", + ) + uncertainty_args.add_argument( + "--dropout_sampling_size", type=int, - metavar="INT", - help="Number of workers for the parallel data loading 0 means sequential", + default=10, + help="The number of samples to use for Monte Carlo dropout uncertainty estimation. Distinct from the dropout used during training.", ) - general_args.add_argument( - "--no_cache", - type=bool, - metavar="BOOL", - default=False, - help="Turn off caching mol2graph computation", + uncertainty_args.add_argument( + "--calibration_interval_percentile", + type=float, + default=95, + help="Sets the percentile used in the calibration methods. Must be in the range (1,100).", ) - general_args.add_argument( - "--no_cache_mol", - type=bool, - metavar="BOOL", - default=False, - help="Whether to not cache the RDKit molecule for each SMILES string to reduce memory\ - usage cached by default", + uncertainty_args.add_argument( + "--regression_calibrator_metric", + type=str, + choices=["stdev", "interval"], + help="Regression calibrators can output either a stdev or an inverval.", ) - general_args.add_argument( - "--empty_cache", - type=bool, - metavar="BOOL", - help="Whether to empty all caches before training or predicting. This is necessary if\ - multiple jobs are run within a single script and the atom or bond features change", + + +def parseInterpretGnn(parser: argparse.ArgumentParser) -> None: + files_args = parser.add_argument_group("Files") + interpret_args = parser.add_argument_group("Interpretation Configuration") + files_args.add_argument( + "-f", + "--configFile", + metavar="FILE", + type=str, + help="Input JSON file that contains all information for interpretation.", ) files_args.add_argument( "--preds_path", @@ -1191,89 +1399,44 @@ def parsePredictGnn(parser: argparse.ArgumentParser) -> None: metavar="DIR", help="Path to model checkpoint (.pt file)", ) - files_args.add_argument( - "--checkpoint_paths", - type=str, - metavar="FILE", - nargs="*", - help="Path to model checkpoint (.pt file)", - ) files_args.add_argument( "--data_path", type=str, metavar="FILE", help="Path to CSV file containing testing data for which predictions will be made", - default="", ) - files_args.add_argument( - "--test_path", - type=str, - metavar="FILE", - help="Path to CSV file containing testing data for which predictions will be made", - default="", - ) - files_args.add_argument( - "--features_path", - type=str, - metavar="FILE", - nargs="*", - help="Path to features to use in FNN (instead of features_generator)", - ) - files_args.add_argument( - "--atom_descriptors_path", - type=str, - metavar="FILE", - help="Path to the extra atom descriptors.", - ) - data_args.add_argument( - "--use_compound_names", - action="store_true", - default=False, - help="Use when test data file contains compound names in addition to SMILES strings", - ) - data_args.add_argument( - "--no_features_scaling", - action="store_true", - default=False, - help="Turn off scaling of features", - ) - data_args.add_argument( - "--max_data_size", + interpret_args.add_argument( + "--max_atoms", type=int, metavar="INT", - help="Maximum number of data points to load", + help="Maximum number of atoms to use for interpretation", ) - data_args.add_argument( - "--smiles_columns", - type=str, - metavar="STRING", - help="List of names of the columns containing SMILES strings.By default, uses the first\ - number_of_molecules columns.", - ) - data_args.add_argument( - "--number_of_molecules", + + interpret_args.add_argument( + "--min_atoms", type=int, metavar="INT", - help="Number of molecules in each input to the model.This must equal the length of\ - smiles_columns if not None", + help="Minimum number of atoms to use for interpretation", ) - data_args.add_argument( - "--atom_descriptors", - type=bool, - metavar="Bool", - help="Use or not atom descriptors", + interpret_args.add_argument( + "--prop_delta", + type=float, + metavar="FLOAT", + help="The minimum change in the property of interest that is considered significant", ) - - data_args.add_argument( - "--bond_features_size", + interpret_args.add_argument( + "--property_id", type=int, metavar="INT", - help="Size of the extra bond descriptors that will be used as bond features to featurize a\ - given molecule", + help="The index of the property of interest", ) - training_args.add_argument( - "--batch_size", type=int, metavar="INT", default=50, help="Batch size" + # write the argument for rollouts + interpret_args.add_argument( + "--rollout", + type=int, + metavar="INT", + help="The number of rollouts to use for interpretation", ) diff --git a/dfpl/plot.py b/dfpl/plot.py old mode 100644 new mode 100755 diff --git a/dfpl/predictions.py b/dfpl/predictions.py old mode 100644 new mode 100755 diff --git a/dfpl/settings.py b/dfpl/settings.py old mode 100644 new mode 100755 diff --git a/dfpl/single_label_model.py b/dfpl/single_label_model.py old mode 100644 new mode 100755 index 18402f09..d1ee3feb --- a/dfpl/single_label_model.py +++ b/dfpl/single_label_model.py @@ -21,13 +21,13 @@ ) from sklearn.model_selection import StratifiedKFold, train_test_split from tensorflow.keras import metrics, optimizers, regularizers -from tensorflow.keras.layers import AlphaDropout, Dense, Dropout +from tensorflow.keras.layers import AlphaDropout, Dense, Dropout, InputLayer from tensorflow.keras.losses import ( BinaryCrossentropy, BinaryFocalCrossentropy, MeanSquaredError, ) -from tensorflow.keras.models import Model, Sequential +from tensorflow.keras.models import Model, Sequential, load_model from dfpl import callbacks as cb from dfpl import options @@ -180,79 +180,119 @@ def prepare_nn_training_data( # This function defines a feedforward neural network (FNN) with the given input size, options, and output bias def build_fnn_network( - input_size: int, opts: options.Options, output_bias=None + input_size: int, opts: options.Options, output_bias=None, encoder: Model = None ) -> Model: + # Assuming the last layer of the encoder is the input to the FNN + input_size = encoder.layers[-1].output_shape[-1] + # Rename encoder layers for clarity + for i, layer in enumerate(encoder.layers): + layer._name = f'encoder_layer_{i + 1}' + + # Start model with encoder output + x = encoder.output + # Set the output bias if it is provided if output_bias is not None: output_bias = tf.keras.initializers.Constant(output_bias) - # Define the number of hidden layers based on the input size - my_hidden_layers = {"2048": 6, "1024": 5, "999": 5, "512": 4, "256": 3} - if not str(input_size) in my_hidden_layers.keys(): - raise ValueError("Wrong input-size. Must be in {2048, 1024, 999, 512, 256}.") + # Define the number of hidden layers dynamically based on the input size nhl = int(math.log2(input_size) / 2 - 1) + my_hidden_layers = {"2048": 6, "1024": 5, "999": 5, "512": 4, "256": 3} + if str(input_size) not in my_hidden_layers.keys(): + raise ValueError("Input size not supported. Must be in {2048, 1024, 999, 512, 256}.") - # Create a sequential model - model = Sequential() - - # Add the first hidden layer - if opts.activationFunction == "relu": - model.add( - Dense( - units=int(input_size / 2), - input_dim=input_size, - activation="relu", - kernel_regularizer=regularizers.l2(opts.l2reg), - kernel_initializer="he_uniform", - ) - ) - model.add(Dropout(opts.dropout)) - elif opts.activationFunction == "selu": - model.add( - Dense( - units=int(input_size / 2), - input_dim=input_size, - activation="selu", - kernel_initializer="lecun_normal", - ) - ) - model.add(AlphaDropout(opts.dropout)) - else: - logging.error("Only 'relu' and 'selu' activation is supported") - sys.exit(-1) + # Add hidden layers + for i in range(nhl): + units = int(input_size / 2 ** (i + 1)) + dropout_rate = opts.dropout / (2 * i + 1) if i > 0 else opts.dropout - # Add additional hidden layers - for i in range(1, nhl): - factor_units = 2 ** (i + 1) - factor_dropout = 2 * i if opts.activationFunction == "relu": - model.add( - Dense( - units=int(input_size / factor_units), - activation="relu", - kernel_regularizer=regularizers.l2(opts.l2reg), - kernel_initializer="he_uniform", - ) - ) - model.add(Dropout(opts.dropout / factor_dropout)) + x = Dense(units, activation="relu", kernel_regularizer=regularizers.l2(opts.l2reg), + kernel_initializer="he_uniform")(x) + x = Dropout(dropout_rate)(x) elif opts.activationFunction == "selu": - model.add( - Dense( - units=int(input_size / factor_units), - activation="selu", - kernel_initializer="lecun_normal", - ) - ) - model.add(AlphaDropout(opts.dropout / factor_dropout)) + x = Dense(units, activation="selu", kernel_initializer="lecun_normal")(x) + x = AlphaDropout(dropout_rate)(x) else: - logging.error("Only 'relu' and 'selu' activation is supported") + logging.error("Unsupported activation function. Only 'relu' and 'selu' are supported.") sys.exit(-1) - # Add the output layer with a sigmoid activation function and the output bias if provided - model.add(Dense(units=1, activation="sigmoid", bias_initializer=output_bias)) + # Add the output layer + outputs = Dense(units=1, activation="sigmoid", bias_initializer=output_bias)(x) + + # Create the model + model = Model(inputs=encoder.input, outputs=outputs) model.summary() + return model + # # Define the number of hidden layers based on the input size + # my_hidden_layers = {"2048": 6, "1024": 5, "999": 5, "512": 4, "256": 3} + # if not str(input_size) in my_hidden_layers.keys(): + # raise ValueError("Wrong input-size. Must be in {2048, 1024, 999, 512, 256}.") + # nhl = int(math.log2(input_size) / 2 - 1) + # + # # Create a sequential model + # model = Sequential() + # + # # Add the first hidden layer + # if opts.activationFunction == "relu": + # model.add( + # Dense( + # units=int(input_size / 2), + # input_dim=input_size, + # activation="relu", + # kernel_regularizer=regularizers.l2(opts.l2reg), + # kernel_initializer="he_uniform", + # ) + # ) + # model.add(Dropout(opts.dropout)) + # elif opts.activationFunction == "selu": + # model.add( + # Dense( + # units=int(input_size / 2), + # input_dim=input_size, + # activation="selu", + # kernel_initializer="lecun_normal", + # ) + # ) + # model.add(AlphaDropout(opts.dropout)) + # else: + # logging.error("Only 'relu' and 'selu' activation is supported") + # sys.exit(-1) + # + # # Add additional hidden layers + # for i in range(1, nhl): + # factor_units = 2 ** (i + 1) + # factor_dropout = 2 * i + # if opts.activationFunction == "relu": + # model.add( + # Dense( + # units=int(input_size / factor_units), + # activation="relu", + # kernel_regularizer=regularizers.l2(opts.l2reg), + # kernel_initializer="he_uniform", + # ) + # ) + # model.add(Dropout(opts.dropout / factor_dropout)) + # elif opts.activationFunction == "selu": + # model.add( + # Dense( + # units=int(input_size / factor_units), + # activation="selu", + # kernel_initializer="lecun_normal", + # ) + # ) + # model.add(AlphaDropout(opts.dropout / factor_dropout)) + # else: + # logging.error("Only 'relu' and 'selu' activation is supported") + # sys.exit(-1) + # + # # Add the output layer with a sigmoid activation function and the output bias if provided + # model.add(Dense(units=1, activation="sigmoid", bias_initializer=output_bias)) + # model.summary() + # return model + # This function defines a shallow neural network (SNN) with the given input size, options, and output bias def build_snn_network( @@ -333,19 +373,27 @@ def define_single_label_model( else: logging.error(f"Your selected loss is not supported: {opts.lossFunction}.") sys.exit("Unsupported loss function") - + lr_schedule = optimizers.schedules.ExponentialDecay( + opts.learningRate, + decay_steps=1000, + decay_rate=opts.learningRateDecay, + staircase=True, + ) # Set the optimizer according to the option selected if opts.optimizer == "Adam": - my_optimizer = optimizers.Adam(learning_rate=opts.learningRate) + my_optimizer = optimizers.legacy.Adam(learning_rate=lr_schedule) elif opts.optimizer == "SGD": - my_optimizer = optimizers.SGD(lr=opts.learningRate, momentum=0.9) + my_optimizer = optimizers.legacy.SGD(lr=lr_schedule, momentum=0.9) else: logging.error(f"Your selected optimizer is not supported: {opts.optimizer}.") sys.exit("Unsupported optimizer") - + if opts.finetuneEncoder: + encoder = load_model(opts.ecModelDir) # Ensure this loads the correct model + else: + encoder = None # Set the type of neural network according to the option selected if opts.fnnType == "FNN": - model = build_fnn_network(input_size, opts, output_bias) + model = build_fnn_network(input_size, opts, output_bias,encoder=encoder) elif opts.fnnType == "SNN": model = build_snn_network(input_size, opts, output_bias) else: @@ -489,8 +537,10 @@ def fit_and_evaluate_model( y_test: np.ndarray, fold: int, target: str, - opts: options.Options, + opts: options.Options ) -> pd.DataFrame: + + # Print info about training logging.info(f"Training of fold number: {fold}") @@ -544,6 +594,15 @@ def fit_and_evaluate_model( # Evaluate model callback_model = define_single_label_model(input_size=x_train.shape[1], opts=opts) callback_model.load_weights(filepath=checkpoint_model_weights_path) + callback_model.save_weights( + path.join( + opts.outputDir, + f"{target}_single-labeled_Fold-{fold}.model.weights.hdf5", + ) + ) + callback_model.save( + filepath=path.join(opts.outputDir, f"{target}-{fold}_saved_model") + ) performance = evaluate_model( x_test=x_test, y_test=y_test, @@ -553,6 +612,7 @@ def fit_and_evaluate_model( fold=fold, ) + return performance @@ -595,12 +655,7 @@ def train_single_label_models(df: pd.DataFrame, opts: options.Options) -> None: :param df: The dataframe containing x matrix and at least one column for a y target. """ - # find target columns - targets = [ - c - for c in df.columns - if c in ["AR", "ER", "ED", "TR", "GR", "PPARg", "Aromatase"] - ] + targets = [c for c in df.columns if c not in ["smiles", "fp", "fpcompressed"]] if opts.wabTracking and opts.wabTarget != "": # For W&B tracking, we only train one target that's specified as wabTarget "ER". # In case it's not there, we use the first one available @@ -617,7 +672,7 @@ def train_single_label_models(df: pd.DataFrame, opts: options.Options) -> None: # Collect metrics for each fold and target performance_list = [] if opts.split_type == "random": - for target in targets: # [:1]: + for target in targets: # target=targets[1] # --> only for testing the code x, y = prepare_nn_training_data(df, target, opts, return_dataframe=False) if x is None: @@ -630,7 +685,7 @@ def train_single_label_models(df: pd.DataFrame, opts: options.Options) -> None: # for single 'folds' and when sweeping on W&B, we fix the random state if opts.wabTracking and not opts.aeWabTracking: wandb.init( - project=f"FFN_{opts.split_type}", + project=f"May_FFN_{opts.split_type}", group=f"{target}", name=f"{target}_single_fold", ) @@ -642,7 +697,7 @@ def train_single_label_models(df: pd.DataFrame, opts: options.Options) -> None: reinit=True, ) x_train, x_test, y_train, y_test = train_test_split( - x, y, stratify=y, test_size=opts.testSize, random_state=1 + x, y, stratify=y, test_size=opts.testSize, random_state=0 ) logging.info( f"Splitting train/test data with fixed random initializer" @@ -663,32 +718,19 @@ def train_single_label_models(df: pd.DataFrame, opts: options.Options) -> None: ) performance_list.append(performance) # save complete model - trained_model = define_single_label_model( - input_size=len(x[0]), opts=opts - ) - # trained_model.load_weights - # (path.join(opts.outputDir, f"{target}_single-labeled_Fold-0.model.weights.hdf5")) - trained_model.save_weights( - path.join( - opts.outputDir, - f"{target}_single-labeled_Fold-0.model.weights.hdf5", - ) - ) - trained_model.save( - filepath=path.join(opts.outputDir, f"{target}_saved_model") - ) + elif 1 < opts.kFolds < 10: # int(x.shape[0] / 100): # do a kfold cross-validation kfold_c_validator = StratifiedKFold( - n_splits=opts.kFolds, shuffle=True, random_state=42 + n_splits=opts.kFolds, shuffle=True, random_state=0 ) fold_no = 1 # split the data for train, test in kfold_c_validator.split(x, y): if opts.wabTracking and not opts.aeWabTracking: wandb.init( - project=f"FNN_{opts.threshold}_{opts.split_type}", + project=f"May_FNN_{opts.threshold}_{opts.split_type}", group=f"{target}", name=f"{target}-{fold_no}", reinit=True, @@ -720,28 +762,15 @@ def train_single_label_models(df: pd.DataFrame, opts: options.Options) -> None: ) performance_list.append(performance) - # save complete model - trained_model = define_single_label_model( - input_size=len(x[0]), opts=opts - ) - # trained_model.load_weights - # (path.join(opts.outputDir, f"{target}_single-labeled_Fold-0.model.weights.hdf5")) - trained_model.save_weights( - path.join( - opts.outputDir, - f"{target}_single-labeled_Fold-{fold_no}.model.weights.hdf5", - ) - ) - # create output directory and store complete model - trained_model.save( - filepath=path.join( - opts.outputDir, f"{target}-{fold_no}_saved_model" - ) - ) if opts.wabTracking: wandb.finish() - fold_no += 1 + train_indices_path = os.path.join(opts.outputDir, f"fold_{fold_no}_train_indices.csv") + test_indices_path = os.path.join(opts.outputDir, f"fold_{fold_no}_test_indices.csv") + # Save the indices to CSV files + pd.DataFrame(train, columns=['Index']).to_csv(train_indices_path, index=False) + pd.DataFrame(test, columns=['Index']).to_csv(test_indices_path, index=False) + fold_no += 1 # select and copy best model - how to define the best model? best_fold = pd.concat(performance_list, ignore_index=True).sort_values( by=["p_1", "r_1", "MCC"], ascending=False, ignore_index=True @@ -749,11 +778,11 @@ def train_single_label_models(df: pd.DataFrame, opts: options.Options) -> None: # rename the fold to best fold src = os.path.join( opts.outputDir, - f"{target}_single-labeled_Fold-{best_fold}.model.weights.hdf5", + f"{target}_{opts.split_type}_single-labeled_Fold-{best_fold}.model.weights.hdf5", ) dst = os.path.join( opts.outputDir, - f"{target}_single-labeled_Best_Fold-{best_fold}.model.weights.hdf5", + f"{target}_{opts.split_type}_single-labeled_Best_Fold-{best_fold}.model.weights.hdf5", ) os.rename(src, dst) @@ -770,7 +799,6 @@ def train_single_label_models(df: pd.DataFrame, opts: options.Options) -> None: # Rename source directory to destination directory os.rename(src_dir, dst_dir) - # save complete model else: logging.info( "Your selected number of folds for Cross validation is out of range. " @@ -780,13 +808,12 @@ def train_single_label_models(df: pd.DataFrame, opts: options.Options) -> None: ( pd.concat(performance_list, ignore_index=True).to_csv( path_or_buf=path.join( - opts.outputDir, "single_label_random_model.evaluation.csv" + opts.outputDir, f"single_label_{opts.split_type}_model.evaluation.csv" ) ) ) # For each individual target train a model elif opts.split_type == "scaffold_balanced": - # df, irrelevant_columns = preprocess_dataframe(df, opts) for idx, target in enumerate(targets): df = prepare_nn_training_data(df, target, opts, return_dataframe=True) relevant_cols = ["smiles"] + ["fp"] + [target] # list(irrelevant_columns) @@ -808,7 +835,7 @@ def train_single_label_models(df: pd.DataFrame, opts: options.Options) -> None: ) if opts.wabTracking and not opts.aeWabTracking: wandb.init( - project=f"FFN_{opts.split_type}", + project=f"May_FFN_{opts.split_type}", group=f"{target}", name=f"{target}_single_fold", ) @@ -829,20 +856,7 @@ def train_single_label_models(df: pd.DataFrame, opts: options.Options) -> None: opts=opts, ) performance_list.append(performance) - trained_model = define_single_label_model( - input_size=len(x_train[0]), opts=opts - ) - trained_model.save_weights( - path.join( - opts.outputDir, - f"{target}_scaffold_single-labeled_Fold-0.model.weights.hdf5", - ) - ) - trained_model.save( - filepath=path.join( - opts.outputDir, f"{target}_scaffold_saved_model_0" - ) - ) + elif opts.kFolds > 1: for fold_no in range(1, opts.kFolds + 1): print(f"Splitting data with seed {fold_no}") @@ -857,7 +871,7 @@ def train_single_label_models(df: pd.DataFrame, opts: options.Options) -> None: ) if opts.wabTracking and not opts.aeWabTracking: wandb.init( - project=f"FFN_{opts.split_type}", + project=f"May_FFN_{opts.split_type}", group=f"{target}", name=f"{target}-{fold_no}", reinit=True, @@ -880,20 +894,6 @@ def train_single_label_models(df: pd.DataFrame, opts: options.Options) -> None: ) performance_list.append(performance) - trained_model = define_single_label_model( - input_size=len(x_train[0]), opts=opts - ) - trained_model.save_weights( - path.join( - opts.outputDir, - f"{target}_scaffold_single-labeled_Fold-{fold_no}.model.weights.hdf5", - ) - ) - trained_model.save( - filepath=path.join( - opts.outputDir, f"{target}_scaffold_saved_model_{fold_no}" - ) - ) if opts.wabTracking: wandb.finish() fold_no += 1 @@ -904,20 +904,20 @@ def train_single_label_models(df: pd.DataFrame, opts: options.Options) -> None: # rename the fold to best fold src = os.path.join( opts.outputDir, - f"{target}_scaffold_single-labeled_Fold-{best_fold}.model.weights.hdf5", + f"{target}_{opts.split_type}_single-labeled_Fold-{best_fold}.model.weights.hdf5", ) dst = os.path.join( opts.outputDir, - f"{target}_scaffold_single-labeled_BEST_Fold-{best_fold}.model.weights.hdf5", + f"{target}_{opts.split_type}_single-labeled_BEST_Fold-{best_fold}.model.weights.hdf5", ) os.rename(src, dst) src_dir = os.path.join( - opts.outputDir, f"{target}_scaffold_saved_model_{best_fold}" + opts.outputDir, f"{target}_{opts.split_type}_saved_model_{best_fold}" ) dst_dir = os.path.join( opts.outputDir, - f"{target}_scaffold_saved_model_BEST_FOLD_{best_fold}", + f"{target}_{opts.split_type}_saved_model_BEST_FOLD_{best_fold}", ) if path.isdir(dst_dir): @@ -935,7 +935,7 @@ def train_single_label_models(df: pd.DataFrame, opts: options.Options) -> None: ( pd.concat(performance_list, ignore_index=True).to_csv( path_or_buf=path.join( - opts.outputDir, "single_label_scaffold_model.evaluation.csv" + opts.outputDir, f"single_label_{opts.split_type}_model.evaluation.csv" ) ) ) @@ -958,7 +958,7 @@ def train_single_label_models(df: pd.DataFrame, opts: options.Options) -> None: ) if opts.wabTracking and not opts.aeWabTracking: wandb.init( - project=f"FFN_{opts.split_type}AE_{opts.aeSplitType}", + project=f"May_FFN_{opts.split_type}AE_{opts.aeSplitType}", group=f"{target}", name=f"{target}_single_fold", ) @@ -978,18 +978,7 @@ def train_single_label_models(df: pd.DataFrame, opts: options.Options) -> None: opts=opts, ) performance_list.append(performance) - trained_model = define_single_label_model( - input_size=len(x_train[0]), opts=opts - ) - trained_model.save_weights( - path.join( - opts.outputDir, - f"{target}_weight_single-labeled_Fold-0.model.weights.hdf5", - ) - ) - trained_model.save( - filepath=path.join(opts.outputDir, f"{target}_weight_saved_model_0") - ) + elif opts.kFolds > 1: raise Exception( f"Unsupported number of folds: {opts.kFolds} for {opts.split_type} split.\ diff --git a/dfpl/utils.py b/dfpl/utils.py old mode 100644 new mode 100755 index db3d6ec1..78ad47e4 --- a/dfpl/utils.py +++ b/dfpl/utils.py @@ -1,12 +1,16 @@ +import argparse import json import logging import os import pathlib +import sys import warnings from collections import defaultdict +from pathlib import Path from random import Random -from typing import Dict, List, Set, Tuple, Union +from typing import Dict, List, Set, Tuple, Type, TypeVar, Union +import jsonpickle import numpy as np import pandas as pd from rdkit import Chem, RDLogger @@ -14,9 +18,49 @@ from rdkit.Chem.Scaffolds import MurckoScaffold from tqdm import tqdm +# Define a type variable + + RDLogger.DisableLog("rdApp.*") +T = TypeVar("T") +def parseCmdArgs(cls: Type[T], args: argparse.Namespace) -> T: + """ + Parses command-line arguments to create an instance of the given class. + + Args: + cls: The class to create an instance of. + args: argparse.Namespace containing the command-line arguments. + + Returns: + An instance of cls populated with values from the command-line arguments. + """ + # Extract argument flags from sys.argv + arg_flags = {arg.lstrip("-") for arg in sys.argv if arg.startswith("-")} + + # Create the result instance, which will be modified and returned + result = cls() + + # Load JSON file if specified + if hasattr(args, "configFile") and args.configFile: + jsonFile = Path(args.configFile) + if jsonFile.exists() and jsonFile.is_file(): + with jsonFile.open() as f: + content = jsonpickle.decode(f.read()) + for key, value in vars(content).items(): + setattr(result, key, value) + else: + raise ValueError("Could not find JSON input file") + + # Override with user-provided command-line arguments + for key in arg_flags: + if hasattr(args, key): + user_value = getattr(args, key, None) + setattr(result, key, user_value) + + return result + def makePathAbsolute(p: str) -> str: path = pathlib.Path(p) if path.is_absolute(): @@ -30,24 +74,100 @@ def createDirectory(directory: str): if not os.path.exists(path): os.makedirs(path) - -def createArgsFromJson(in_json: str, ignore_elements: list, return_json_object: bool): +def parse_cli_list(value: str): + # Simple parser for lists passed as comma-separated values + return value.split(',') + +def parse_cli_boolean(cli_args, cli_arg_key): + # Determines boolean value based on command line presence + if cli_arg_key in cli_args: + return True # Presence of flag implies True + return False + +# def createArgsFromJson(jsonFile: str): +# arguments = [] +# ignore_elements = ["py/object"] +# cli_args = sys.argv[1:] # Skipping the script name itself +# +# with open(jsonFile, "r") as f: +# data = json.load(f) +# +# processed_cli_keys = [] # To track which CLI keys have been processed +# +# for key, value in data.items(): +# if key not in ignore_elements: +# cli_arg_key = f"--{key}" +# if cli_arg_key in cli_args: +# processed_cli_keys.append(cli_arg_key) +# arg_index = cli_args.index(cli_arg_key) + 1 +# if isinstance(value, bool): +# value = parse_cli_boolean(cli_args, cli_arg_key) +# elif arg_index < len(cli_args): +# cli_value = cli_args[arg_index] +# if isinstance(value, list): +# value = parse_cli_list(cli_value) +# else: +# value = cli_value # Override JSON value with command-line value +# if isinstance(value, bool) and value: +# arguments.append(cli_arg_key) +# elif isinstance(value, list): +# arguments.append(cli_arg_key) +# arguments.extend(map(str, value)) # Ensure all elements are strings +# else: +# arguments.extend([cli_arg_key, str(value)]) +# +# return arguments +def createArgsFromJson(jsonFile: str) -> List[str]: arguments = [] - with open(in_json, "r") as f: + ignore_elements = ["py/object"] + cli_args = sys.argv[1:] # Skipping the script name itself + + with open(jsonFile, "r") as f: data = json.load(f) + + processed_cli_keys = [] # To track which CLI keys have been processed + for key, value in data.items(): if key not in ignore_elements: - if key == "extra_metrics" and isinstance(value, list): - arguments.append("--extra_metrics") - arguments.extend(value) + cli_arg_key = f"--{key}" + if cli_arg_key in cli_args: + processed_cli_keys.append(cli_arg_key) + arg_index = cli_args.index(cli_arg_key) + 1 + if isinstance(value, bool): + value = parse_cli_boolean(cli_args, cli_arg_key) + elif arg_index < len(cli_args) and not cli_args[arg_index].startswith('--'): + cli_value = cli_args[arg_index] + if isinstance(value, list): + value = parse_cli_list(cli_value) + else: + value = cli_value # Override JSON value with command-line value + if isinstance(value, bool): + if value: + arguments.append(cli_arg_key) + elif isinstance(value, list): + arguments.append(cli_arg_key) + arguments.extend(map(str, value)) # Ensure all elements are strings else: - arguments.append("--" + str(key)) - arguments.append(str(value)) - if return_json_object: - return arguments, data - return arguments - + arguments.extend([cli_arg_key, str(value)]) + i = 0 + while i < len(cli_args): + arg = cli_args[i] + if arg.startswith("--"): + key = arg.lstrip("--") + if key not in data: + value = True if i + 1 >= len(cli_args) or cli_args[i + 1].startswith("--") else cli_args[i + 1] + if isinstance(value, bool): + if value: + arguments.append(arg) + else: + arguments.extend([arg, str(value)]) + i += 1 if isinstance(value, bool) else 2 + else: + i += 1 + else: + i += 1 + return arguments def make_mol(s: str, keep_h: bool, add_h: bool, keep_atom_map: bool): """ Builds an RDKit molecule from a SMILES string. @@ -76,49 +196,6 @@ def make_mol(s: str, keep_h: bool, add_h: bool, keep_atom_map: bool): return mol -def generate_scaffold( - mol: Union[str, Chem.Mol, Tuple[Chem.Mol, Chem.Mol]], include_chirality: bool = True -) -> str: - """ - Computes the Bemis-Murcko scaffold for a SMILES string, an RDKit molecule, or an InChI string or InChIKey. - - :param mol: A SMILES, RDKit molecule, InChI string, or InChIKey string. - :param include_chirality: Whether to include chirality in the computed scaffold. - :return: The Bemis-Murcko scaffold for the molecule. - """ - if isinstance(mol, str): - if mol.startswith("InChI="): - mol = inchi_to_mol(mol) - else: - mol = make_mol(mol, keep_h=False, add_h=False, keep_atom_map=False) - elif isinstance(mol, tuple): - mol = mol[0] - scaffold = MurckoScaffold.MurckoScaffoldSmiles( - mol=mol, includeChirality=include_chirality - ) - - return scaffold - - -def scaffold_to_smiles( - mols: List[str], use_indices: bool = False -) -> Dict[str, Union[Set[str], Set[int]]]: - """ - Computes the scaffold for each SMILES and returns a mapping from scaffolds to sets of smiles (or indices). - :param mols: A list of SMILES. - :param use_indices: Whether to map to the SMILES's index in :code:`mols` rather than - mapping to the smiles string itself. This is necessary if there are duplicate smiles. - :return: A dictionary mapping each unique scaffold to all SMILES (or indices) which have that scaffold. - """ - scaffolds = defaultdict(set) - for i, mol in tqdm(enumerate(mols), total=len(mols)): - scaffold = generate_scaffold(mol) - if use_indices: - scaffolds[scaffold].add(i) - else: - scaffolds[scaffold].add(mol) - - return scaffolds # def inchi_to_mol(inchi: str) -> Chem.Mol: @@ -184,7 +261,49 @@ def weight_split( test_df = sorted_data.iloc[test_indices].reset_index(drop=True) return train_df, val_df, test_df +def generate_scaffold( + mol: Union[str, Chem.Mol, Tuple[Chem.Mol, Chem.Mol]], include_chirality: bool = True +) -> str: + """ + Computes the Bemis-Murcko scaffold for a SMILES string, an RDKit molecule, or an InChI string or InChIKey. + + :param mol: A SMILES, RDKit molecule, InChI string, or InChIKey string. + :param include_chirality: Whether to include chirality in the computed scaffold. + :return: The Bemis-Murcko scaffold for the molecule. + """ + if isinstance(mol, str): + if mol.startswith("InChI="): + mol = inchi_to_mol(mol) + else: + mol = make_mol(mol, keep_h=False, add_h=False, keep_atom_map=False) + elif isinstance(mol, tuple): + mol = mol[0] + scaffold = MurckoScaffold.MurckoScaffoldSmiles( + mol=mol, includeChirality=include_chirality + ) + + return scaffold + + +def scaffold_to_smiles( + mols: List[str], use_indices: bool = False +) -> Dict[str, Union[Set[str], Set[int]]]: + """ + Computes the scaffold for each SMILES and returns a mapping from scaffolds to sets of smiles (or indices). + :param mols: A list of SMILES. + :param use_indices: Whether to map to the SMILES's index in :code:`mols` rather than + mapping to the smiles string itself. This is necessary if there are duplicate smiles. + :return: A dictionary mapping each unique scaffold to all SMILES (or indices) which have that scaffold. + """ + scaffolds = defaultdict(set) + for i, mol in tqdm(enumerate(mols), total=len(mols)): + scaffold = generate_scaffold(mol) + if use_indices: + scaffolds[scaffold].add(i) + else: + scaffolds[scaffold].add(mol) + return scaffolds def ae_scaffold_split( data: pd.DataFrame, diff --git a/dfpl/vae.py b/dfpl/vae.py old mode 100644 new mode 100755 index d0a89dbe..4c568dc7 --- a/dfpl/vae.py +++ b/dfpl/vae.py @@ -1,8 +1,6 @@ -import csv import logging import math import os.path -from os.path import basename from typing import Tuple import numpy as np @@ -21,27 +19,31 @@ from dfpl import options, settings from dfpl.utils import ae_scaffold_split, weight_split -disable_eager_execution() - def define_vae_model(opts: options.Options, output_bias=None) -> Tuple[Model, Model]: + disable_eager_execution() + input_size = opts.fpSize - encoding_dim = opts.encFPSize - ac_optimizer = optimizers.Adam( - learning_rate=opts.aeLearningRate, decay=opts.aeLearningRateDecay + encoding_dim = ( + opts.encFPSize + ) # This should be the intended size of your latent space, e.g., 256 + + lr_schedule = optimizers.schedules.ExponentialDecay( + opts.aeLearningRate, + decay_steps=1000, + decay_rate=opts.aeLearningRateDecay, + staircase=True, ) + ac_optimizer = optimizers.legacy.Adam(learning_rate=lr_schedule) if output_bias is not None: output_bias = initializers.Constant(output_bias) - # get the number of meaningful hidden layers (latent space included) hidden_layer_count = round(math.log2(input_size / encoding_dim)) - # the input placeholder input_vec = Input(shape=(input_size,)) - # 1st hidden layer, that receives weights from input layer - # equals bottleneck layer, if hidden_layer_count==1! + # 1st hidden layer if opts.aeActivationFunction != "selu": encoded = Dense( units=int(input_size / 2), activation=opts.aeActivationFunction @@ -53,87 +55,81 @@ def define_vae_model(opts: options.Options, output_bias=None) -> Tuple[Model, Mo kernel_initializer="lecun_normal", )(input_vec) - if hidden_layer_count > 1: - # encoding layers, incl. bottle-neck - for i in range(1, hidden_layer_count): - factor_units = 2 ** (i + 1) - # print(f'{factor_units}: {int(input_size / factor_units)}') - if opts.aeActivationFunction != "selu": - encoded = Dense( - units=int(input_size / factor_units), - activation=opts.aeActivationFunction, - )(encoded) - else: - encoded = Dense( - units=int(input_size / factor_units), - activation=opts.aeActivationFunction, - kernel_initializer="lecun_normal", - )(encoded) - - # latent space layers - factor_units = 2 ** (hidden_layer_count - 1) + # encoding layers + for i in range( + 1, hidden_layer_count - 1 + ): # Adjust the range to stop before the latent space layers + factor_units = 2 ** (i + 1) if opts.aeActivationFunction != "selu": - z_mean = Dense( - units=int(input_size / factor_units), - activation=opts.aeActivationFunction, - )(encoded) - z_log_var = Dense( + encoded = Dense( units=int(input_size / factor_units), activation=opts.aeActivationFunction, )(encoded) else: - z_mean = Dense( + encoded = Dense( units=int(input_size / factor_units), activation=opts.aeActivationFunction, kernel_initializer="lecun_normal", )(encoded) - z_log_var = Dense( + + # latent space layers + if opts.aeActivationFunction != "selu": + z_mean = Dense(units=encoding_dim, activation=opts.aeActivationFunction)( + encoded + ) # Adjusted size to encoding_dim + z_log_var = Dense(units=encoding_dim, activation=opts.aeActivationFunction)( + encoded + ) # Adjusted size to encoding_dim + else: + z_mean = Dense( + units=encoding_dim, + activation=opts.aeActivationFunction, + kernel_initializer="lecun_normal", + )( + encoded + ) # Adjusted size to encoding_dim + z_log_var = Dense( + units=encoding_dim, + activation=opts.aeActivationFunction, + kernel_initializer="lecun_normal", + )( + encoded + ) # Adjusted size to encoding_dim + + # sampling layer + def sampling(args): + z_mean, z_log_var = args + batch = K.shape(z_mean)[0] + dim = K.int_shape(z_mean)[1] + epsilon = K.random_normal(shape=(batch, dim)) + return z_mean + K.exp(0.5 * z_log_var) * epsilon + + z = Lambda(sampling, output_shape=(encoding_dim,))([z_mean, z_log_var]) + decoded = z + + # decoding layers + for i in range(hidden_layer_count - 2, 0, -1): + factor_units = 2**i + if opts.aeActivationFunction != "selu": + decoded = Dense( + units=int(input_size / factor_units), + activation=opts.aeActivationFunction, + )(decoded) + else: + decoded = Dense( units=int(input_size / factor_units), activation=opts.aeActivationFunction, kernel_initializer="lecun_normal", - )(encoded) - - # sampling layer - def sampling(args): - z_mean, z_log_var = args - batch = K.shape(z_mean)[0] - dim = K.int_shape(z_mean)[1] - epsilon = K.random_normal(shape=(batch, dim)) - return z_mean + K.exp(0.5 * z_log_var) * epsilon - - # sample from latent space - z = Lambda(sampling, output_shape=(int(input_size / factor_units),))( - [z_mean, z_log_var] - ) - decoded = z - # decoding layers - for i in range(hidden_layer_count - 2, 0, -1): - factor_units = 2**i - # print(f'{factor_units}: {int(input_size/factor_units)}') - if opts.aeActivationFunction != "selu": - decoded = Dense( - units=int(input_size / factor_units), - activation=opts.aeActivationFunction, - )(decoded) - else: - decoded = Dense( - units=int(input_size / factor_units), - activation=opts.aeActivationFunction, - kernel_initializer="lecun_normal", - )(decoded) - - # output layer - decoded = Dense( - units=input_size, activation="sigmoid", bias_initializer=output_bias - )(decoded) + )(decoded) - else: - # output layer - decoded = Dense( - units=input_size, activation="sigmoid", bias_initializer=output_bias - )(encoded) + # output layer + decoded = Dense( + units=input_size, activation="sigmoid", bias_initializer=output_bias + )(decoded) autoencoder = Model(input_vec, decoded) + encoder = Model(input_vec, z) + autoencoder.summary(print_fn=logging.info) # KL divergence loss def kl_loss(z_mean, z_log_var): @@ -155,9 +151,6 @@ def vae_loss(y_true, y_pred): optimizer=ac_optimizer, loss=vae_loss, metrics=[bce_loss, kl_loss] ) - # build encoder model - encoder = Model(input_vec, z_mean) - return autoencoder, encoder @@ -175,39 +168,9 @@ def train_full_vae(df: pd.DataFrame, opts: options.Options) -> Model: if opts.aeWabTracking and not opts.wabTracking: wandb.init(project=f"VAE_{opts.aeSplitType}") - # Define output files for VAE and encoder weights - if opts.ecWeightsFile == "": - # If no encoder weights file is specified, use the input file name to generate a default file name - logging.info("No VAE encoder weights file specified") - base_file_name = ( - os.path.splitext(basename(opts.inputFile))[0] - + opts.aeType - + opts.aeSplitType - ) - logging.info( - f"(variational) encoder weights will be saved in {base_file_name}.autoencoder.hdf5" - ) - vae_weights_file = os.path.join( - opts.outputDir, base_file_name + ".vae.weights.hdf5" - ) - # ec_weights_file = os.path.join( - # opts.outputDir, base_file_name + ".encoder.weights.hdf5" - # ) - else: - # If an encoder weights file is specified, use it as the encoder weights file name - logging.info(f"VAE encoder will be saved in {opts.ecWeightsFile}") - base_file_name = ( - os.path.splitext(basename(opts.ecWeightsFile))[0] + opts.aeSplitType - ) - vae_weights_file = os.path.join( - opts.outputDir, base_file_name + ".vae.weights.hdf5" - ) - # ec_weights_file = os.path.join(opts.outputDir, opts.ecWeightsFile) - + save_path = os.path.join(opts.ecModelDir, f"{opts.aeSplitType}_split_autoencoder") # Collect the callbacks for training - callback_list = callbacks.autoencoder_callback( - checkpoint_path=vae_weights_file, opts=opts - ) + # Select all fingerprints that are valid and turn them into a numpy array fp_matrix = np.array( df[df["fp"].notnull()]["fp"].to_list(), @@ -219,17 +182,17 @@ def train_full_vae(df: pd.DataFrame, opts: options.Options) -> Model: ) assert 0.0 <= opts.testSize <= 0.5 if opts.aeSplitType == "random": - logging.info("Training VAE using random split") - train_indices = np.arange(fp_matrix.shape[0]) + logging.info("Training autoencoder using random split") + initial_indices = np.arange(fp_matrix.shape[0]) if opts.testSize > 0.0: # Split data into test and training data if opts.aeWabTracking: - x_train, x_test, _, _ = train_test_split( - fp_matrix, train_indices, test_size=opts.testSize, random_state=42 + x_train, x_test, train_indices, test_indices = train_test_split( + fp_matrix, initial_indices, test_size=opts.testSize, random_state=42 ) else: - x_train, x_test, _, _ = train_test_split( - fp_matrix, train_indices, test_size=opts.testSize, random_state=42 + x_train, x_test, train_indices, test_indices = train_test_split( + fp_matrix, initial_indices, test_size=opts.testSize, random_state=42 ) else: x_train = fp_matrix @@ -255,6 +218,12 @@ def train_full_vae(df: pd.DataFrame, opts: options.Options) -> Model: dtype=settings.ac_fp_numpy_type, copy=settings.numpy_copy_values, ) + train_indices = df[ + df.index.isin(train_data[train_data["fp"].notnull()].index) + ].index.to_numpy() + test_indices = df[ + df.index.isin(test_data[test_data["fp"].notnull()].index) + ].index.to_numpy() else: x_train = fp_matrix x_test = None @@ -262,7 +231,6 @@ def train_full_vae(df: pd.DataFrame, opts: options.Options) -> Model: logging.info("Training autoencoder using molecular weight split") train_indices = np.arange(fp_matrix.shape[0]) if opts.testSize > 0.0: - # if opts.aeWabTracking: train_data, val_data, test_data = weight_split( df, sizes=(1 - opts.testSize, 0.0, opts.testSize), bias="small" ) @@ -276,16 +244,21 @@ def train_full_vae(df: pd.DataFrame, opts: options.Options) -> Model: dtype=settings.ac_fp_numpy_type, copy=settings.numpy_copy_values, ) + df_sorted = df.sort_values(by="mol_weight", ascending=True) + # Get the sorted indices from the sorted DataFrame + sorted_indices = df_sorted.index.to_numpy() + + # Find the corresponding indices for train_data, val_data, and test_data in the sorted DataFrame + train_indices = sorted_indices[df.index.isin(train_data.index)] + # val_indices = sorted_indices[df.index.isin(val_data.index)] + test_indices = sorted_indices[df.index.isin(test_data.index)] else: x_train = fp_matrix x_test = None else: raise ValueError(f"Invalid split type: {opts.split_type}") - if opts.testSize > 0.0: - train_indices = train_indices[train_indices < x_train.shape[0]] - test_indices = np.arange(x_train.shape[0], x_train.shape[0] + x_test.shape[0]) - else: - test_indices = None + + # Calculate the initial bias aka the log ratio between 1's and 0'1 in all fingerprints ids, counts = np.unique(x_train.flatten(), return_counts=True) count_dict = dict(zip(ids, counts)) if count_dict[0] == 0: @@ -304,34 +277,34 @@ def train_full_vae(df: pd.DataFrame, opts: options.Options) -> Model: (vae, encoder) = define_vae_model(opts, output_bias=initial_bias) # Train the VAE on the training data + callback_list = callbacks.autoencoder_callback( + checkpoint_path=f"{save_path}.h5", opts=opts + ) + vae_hist = vae.fit( x_train, x_train, epochs=opts.aeEpochs, batch_size=opts.aeBatchSize, verbose=opts.verbose, - callbacks=callback_list, + callbacks=[callback_list], validation_data=(x_test, x_test) if opts.testSize > 0.0 else None, ) # Save the VAE weights - logging.info(f"VAE weights stored in file: {vae_weights_file}") ht.store_and_plot_history( - base_file_name=os.path.join(opts.outputDir, base_file_name + ".VAE"), + base_file_name=save_path, hist=vae_hist, ) - save_path = os.path.join(opts.ecModelDir, f"{opts.aeSplitType}_VAE.h5") - if opts.testSize > 0.0: - (callback_vae, callback_encoder) = define_vae_model(opts) - callback_vae.load_weights(filepath=vae_weights_file) - callback_encoder.save(filepath=save_path) - else: - encoder.save(filepath=save_path) - latent_space = encoder.predict(fp_matrix) - latent_space_file = os.path.join( - opts.outputDir, base_file_name + ".latent_space.csv" - ) - with open(latent_space_file, "w", newline="") as file: - writer = csv.writer(file) - writer.writerows(latent_space) + # Re-define autoencoder and encoder using your function + callback_autoencoder, callback_encoder = define_vae_model(opts) + callback_autoencoder.load_weights(filepath=f"{save_path}.h5") + + for i, layer in enumerate(callback_encoder.layers): + layer.set_weights(callback_autoencoder.layers[i].get_weights()) + + # Save the encoder model + encoder_save_path = f"{save_path}_encoder.h5" + callback_encoder.save_weights(filepath=encoder_save_path) + return encoder, train_indices, test_indices diff --git a/environment.yml b/environment.yml old mode 100644 new mode 100755 index 3c2e7a6c..f008b36b --- a/environment.yml +++ b/environment.yml @@ -1,23 +1,27 @@ -name: dfplenv +name: dfpl_env channels: - conda-forge - defaults dependencies: # dev requirements - - conda-build=3.21.8 - - conda=4.12.0 - - pip=22.0.4 - - pytest=7.1.1 + - conda-build>=3.21.8 + - conda>=4.12.0 + - pip>=22.0.4 + - pytest>=7.1.1 + - python=3.8 # application requirements - - jsonpickle=2.1 - - matplotlib=3.5.1 - - numpy=1.19.5 - - pandas=1.4.2 - - rdkit=2022.03.1 - - scikit-learn=1.0.2 - - seaborn=0.12.2 - - tensorflow-gpu=2.6.0 - - wandb=0.12 - - umap=0.1.1 + - protobuf>=3.21.12 + - jsonpickle>=2.1 + - matplotlib>=3.5.1 + - numpy=1.23.5 + - rdkit>=2023.03.3 + - scikit-learn>=1.0.2 + - seaborn>=0.12.2 + - tensorflow>=2.13 + - wandb>=0.16.2 + - umap-learn=0.5.5 + - numba>=0.51.0 - pip: - - git+https://github.com/soulios/chemprop.git@1d73523e49aa28a90b74edc04aaf45d7e124e338 \ No newline at end of file + - git+https://github.com/soulios/chemprop.git@cfcb67a + + diff --git a/example/interpret.json b/example/interpret.json new file mode 100644 index 00000000..cc0cea00 --- /dev/null +++ b/example/interpret.json @@ -0,0 +1,10 @@ +{ + "data_path": "tests/data/S_dataset.csv", + "checkpoint_path": "dmpnn-tox21-cvnotest8020-30-multilabel-bce/fold_0/model_0/model.pt", + "preds_path": "interpretations.csv", + "max_atoms": 20, + "min_atoms": 8, + "prop_delta": 0.5, + "property_id": [1,2,3,4,5,6,7,8,9,10,11,12], + "rollout": 20 +} \ No newline at end of file diff --git a/example/predict.json b/example/predict.json index 252965e3..c9b9e1f1 100755 --- a/example/predict.json +++ b/example/predict.json @@ -1,12 +1,12 @@ { "py/object": "dfpl.options.Options", - "inputFile": "tests/data/smiles.csv", + "inputFile": "tests/data/S_dataset.csv", "outputDir": "example/results_predict/", "outputFile": "smiles.csv", - "ecModelDir": "example/results_train/random_autoencoder/", + "ecModelDir": "", "ecWeightsFile": "", - "fnnModelDir": "example/results_train/AR_saved_model", - "compressFeatures": true, - "trainAC": false, + "fnnModelDir": "example/results/AR_saved_model", + "aeType": "deterministic", + "compressFeatures": false, "trainFNN": false } diff --git a/example/predictgnn.json b/example/predictgnn.json old mode 100644 new mode 100755 index 157b5e05..e4f58e37 --- a/example/predictgnn.json +++ b/example/predictgnn.json @@ -1,7 +1,19 @@ { "py/object": "dfpl.options.GnnOptions", - "test_path": "tests/data/smiles.csv", - "checkpoint_path": "dmpnn-random/fold_0/model_0/model.pt", - "save_dir": "preds_dmpnn", - "saving_name": "DMPNN_preds.csv" -} \ No newline at end of file + "checkpoint_path": "dmpnntox21-cv-8020/fold_1/model_0/model.pt", + "test_path": "tests/data/tox21.csv", + "preds_path": "example/results_gnn.csv", + "uncertainty_method": "dropout", + "uncertainty_dropout_p": 0.1, + "dropout_sampling_size": 10 +} + + + + + + + + + + diff --git a/example/train.json b/example/train.json index 62f2abb4..824d2418 100755 --- a/example/train.json +++ b/example/train.json @@ -1,23 +1,25 @@ { "py/object": "dfpl.options.Options", - "inputFile": "tests/data/S_dataset.csv", - "outputDir": "example/results_train/", - "ecModelDir": "example/results_train/", - "ecWeightsFile": "random_autoencoder.hdf5", + "inputFile": "tests/data/tox21.csv", + "outputDir": "example/results/", + "ecModelDir": "ae/random_split_autoencoder/encoder_model/", + "ecWeightsFile": "", "verbose": 2, - "trainAC": true, - "compressFeatures": true, + "trainAC": false, + "compressFeatures": false, "encFPSize": 256, "aeSplitType": "random", - "aeEpochs": 2, "aeBatchSize": 351, + "aeEpochs": 2, "aeOptimizer": "Adam", "aeActivationFunction": "relu", "aeLearningRate": 0.001, - "aeLearningRateDecay": 0.0001, + "aeLearningRateDecay": 0.96, "aeType": "deterministic", + "finetuneEncoder": false, + "visualizeLatent": false, "type": "smiles", "fpType": "topological", @@ -35,12 +37,9 @@ "fnnType": "FNN", "optimizer": "Adam", "lossFunction": "bce", - "epochs": 11, - "batchSize": 128, + "epochs": 2, "activationFunction": "selu", - "dropout": 0.0107, - "learningRate": 0.0000022, - "l2reg": 0.001, + "aeWabTracking": false, "wabTracking": false, diff --git a/example/traingnn.json b/example/traingnn.json old mode 100644 new mode 100755 index 7a5a0712..b0e4fa96 --- a/example/traingnn.json +++ b/example/traingnn.json @@ -1,14 +1,13 @@ { "py/object": "dfpl.options.GnnOptions", - "data_path": "tests/data/S_dataset.csv", - "save_dir": "dmpnn-random/", - "epochs": 2, - "num_folds": 2, + "data_path": "tests/data/tox21.csv", + "save_dir": "dmpnntox21-cv-8020/", + "epochs": 30, "metric": "accuracy", "loss_function": "binary_cross_entropy", - "split_type": "random", + "split_type": "cv", "dataset_type": "classification", "smiles_columns": "smiles", - "extra_metrics": ["balanced_accuracy","auc","f1","mcc","recall","specificity","precision"], + "extra_metrics": ["balanced_accuracy"], "hidden_size": 256 } \ No newline at end of file diff --git a/requirements.txt b/requirements.txt old mode 100644 new mode 100755 diff --git a/scripts/run-feasible-MoleculeNet-cases.sh b/scripts/run-feasible-MoleculeNet-cases.sh old mode 100644 new mode 100755 diff --git a/setup.cfg b/setup.cfg old mode 100644 new mode 100755 diff --git a/setup.py b/setup.py old mode 100644 new mode 100755 index 2e76e617..a996f7a3 --- a/setup.py +++ b/setup.py @@ -20,16 +20,16 @@ # all packages need for the final usage # for additional packages during development, use requirements.txt install_requires=[ + "protobuf == 3.20.3", "jsonpickle~=2.1.0", "matplotlib==3.5.1", "numpy==1.22.0", "pandas==1.4.2", "rdkit-pypi==2022.03.1", "scikit-learn==1.0.2", - "keras==2.9.0", - "tensorflow-gpu==2.9.3", - "wandb~=0.12.0", - "umap~=0.1.1", + "tensorflow==2.13", + "wandb~=0.16.2", + "umap-learn~=0.5.3", "seaborn~=0.12.2", "chemprop @ git+https://github.com/soulios/chemprop.git@1d73523e49aa28a90b74edc04aaf45d7e124e338", ], diff --git a/singularity_container/environment.yaml b/singularity_container/environment.yaml old mode 100644 new mode 100755 diff --git a/tests/data/calibra.csv b/tests/data/calibra.csv new file mode 100644 index 00000000..7e2963d5 --- /dev/null +++ b/tests/data/calibra.csv @@ -0,0 +1,26 @@ +smiles,AR,ER,GR,Aromatase,TR,PPARg,ED +CN(C)c1ccc(cc1)C(=O)c2ccc(cc2)N(C)C,1,1,1,1,1,1,1 +CC12CCC3C(CCC4=CC(=O)CCC34C)C1CCC2=O,1,1,1,1,1,0,1 +CC12CCC3C(CCc4cc(O)ccc34)C1CCC2O,1,1,1,0,1,1,1 +Oc1c(Br)cc(cc1Br)C#N,1,1,1,0,1,1,1 +Oc1ccc(C=Cc2cc(O)cc(O)c2)cc1,1,1,1,1,0,1,1 +Oc1ccc(cc1)c2ccccc2,1,1,0,1,1,1,1 +CC(=CC1C(C(=O)OCN2C(=O)C3=C(CCCC3)C2=O)C1(C)C)C,1,1,0,1,0,1,1 +CC(=O)OCC(=O)C1(O)C(CC2C3CCC4=CC(=O)C=CC4(C)C3(F)C(O)CC12C)OC(=O)C,1,1,1,1,0,0,1 +CC(=O)OCC(=O)C12N=C(C)OC1CC3C4CCC5=CC(=O)C=CC5(C)C4C(O)CC23C,1,1,1,1,0,0,1 +CC(C)(C)C(=O)OCOC(=O)C1N2C(CC2=O)S(=O)(=O)C1(C)C,1,1,1,0,0,1,1 +CC(C)(c1cc(Cl)c(O)c(Cl)c1)c2cc(Cl)c(O)c(Cl)c2,0,1,1,0,1,1,1 +CC1(C)OC2CC3C4CC(F)C5=CC(=O)CCC5(C)C4C(O)CC3(C)C2(O1)C(=O)CO,1,1,1,1,0,0,1 +CC1(C)OC2CC3C4CCC5=CC(=O)C=CC5(C)C4(F)C(O)CC3(C)C2(O1)C(=O)CO,1,1,1,1,0,0,1 +CC12CCC(=O)C=C1CCC3C2CCC4(C)C3CCC4(O)C#C,1,1,1,0,1,0,1 +CC1CC2C3CC(F)C4=CC(=O)C=CC4(C)C3(F)C(O)CC2(C)C1(O)C(=O)CO,1,1,1,1,0,0,1 +CC1CC2C3CC(F)C4=CC(=O)C=CC4(C)C3(F)C(O)CC2(C)C1(OC(=O)C)C(=O)COC(=O)C,1,1,1,1,0,0,1 +CC1CC2C3CCC(O)(C(=O)CO)C3(C)CC(O)C2C4(C)C=CC(=O)C=C14,1,1,1,1,0,0,1 +CC1CC2C3CCC4=CC(=O)C=CC4(C)C3(F)C(O)CC2(C)C1(O)C(=O)CO,1,1,1,1,0,0,1 +CCC(=O)OC1(C(C)CC2C3CCC4=CC(=O)C=CC4(C)C3(F)C(O)CC12C)C(=O)CCl,1,1,1,1,0,0,1 +CCCC(=O)OC1(CCC2C3CCC4=CC(=O)CCC4(C)C3C(O)CC12C)C(=O)CO,1,1,1,1,0,0,1 +CCN(CC)c1ccc(C(=O)c2ccccc2C(=O)O)c(O)c1,1,1,0,1,0,1,1 +CC[N+](CC)(CC)CCOc1ccc(C=Cc2ccccc2)cc1,1,1,0,1,1,0,1 +O=C(Oc1ccccc1)c2cccc(c2)C(=O)Oc3ccccc3,1,1,0,1,1,0,1 +SCC(=O)OCCOC(=O)CS,1,0,1,1,1,0,1 +C(CSc1ccccc1)Sc2ccccc2,1,1,0,1,0,0,1 \ No newline at end of file diff --git a/tests/data/inchi.tsv b/tests/data/inchi.tsv deleted file mode 100644 index bfaccf13..00000000 --- a/tests/data/inchi.tsv +++ /dev/null @@ -1,5 +0,0 @@ -DTXSID7020001 InChI=1S/C11H9N3/c12-10-6-5-8-7-3-1-2-4-9(7)13-11(8)14-10/h1-6H,(H3,12,13,14) FJTNLJLPLJDTRM-UHFFFAOYSA-N -DTXSID5039224 InChI=1S/C2H4O/c1-2-3/h2H,1H3 IKHGUXGNUITLKF-UHFFFAOYSA-N -DTXSID50872971 InChI=1S/C4H8N2O/c1-3-5-6(2)4-7/h3-4H,1-2H3/b5-3+ IMAGWKUTFZRWSB-HWKANZROSA-N -DTXSID2020004 InChI=1S/C2H5NO/c1-2-3-4/h2,4H,1H3/b3-2+ FZENGILVLUJGJX-NSCUHMNNSA-N -DTXSID7020005 InChI=1S/C2H5NO/c1-2(3)4/h1H3,(H2,3,4) DLFVBJFMPXGRIB-UHFFFAOYSA-N \ No newline at end of file diff --git a/tests/data/smiles.csv b/tests/data/smiles.csv index 9383afdd..b965e11e 100644 --- a/tests/data/smiles.csv +++ b/tests/data/smiles.csv @@ -1,7 +1,7 @@ -smiles -CN(C)c1ccc(cc1)C(=O)c2ccc(cc2)N(C)C -CC12CCC3C(CCC4=CC(=O)CCC34C)C1CCC2=O -CC12CCC3C(CCc4cc(O)ccc34)C1CCC2O -Oc1c(Br)cc(cc1Br)C#N -Oc1ccc(C=Cc2cc(O)cc(O)c2)cc1 -Oc1ccc(cc1)c2ccccc2 \ No newline at end of file +smiles,ID +CN(C)c1ccc(cc1)C(=O)c2ccc(cc2)N(C)C,2982 +CC12CCC3C(CCC4=CC(=O)CCC34C)C1CCC2=O,4345435 +CC12CCC3C(CCc4cc(O)ccc34)C1CCC2O,42121 +Oc1c(Br)cc(cc1Br)C#N,421421 +Oc1ccc(C=Cc2cc(O)cc(O)c2)cc1,2142143 +Oc1ccc(cc1)c2ccccc2,2413213 \ No newline at end of file diff --git a/tests/generic_encoder/keras_metadata.pb b/tests/generic_encoder/keras_metadata.pb new file mode 100755 index 00000000..16312434 --- /dev/null +++ b/tests/generic_encoder/keras_metadata.pb @@ -0,0 +1,6 @@ + +ß%root"_tf_keras_network*½%{"name": "model_1", "trainable": true, "expects_training_arg": true, "dtype": "float32", "batch_input_shape": null, "must_restore_from_config": false, "class_name": "Functional", "config": {"name": "model_1", "layers": [{"class_name": "InputLayer", "config": {"batch_input_shape": {"class_name": "__tuple__", "items": [null, 2048]}, "dtype": "float32", "sparse": false, "ragged": false, "name": "input_1"}, "name": "input_1", "inbound_nodes": []}, {"class_name": "Dense", "config": {"name": "dense", "trainable": true, "dtype": "float32", "units": 1024, "activation": "selu", "use_bias": true, "kernel_initializer": {"class_name": "LecunNormal", "config": {"seed": null}}, "bias_initializer": {"class_name": "Zeros", "config": {}}, "kernel_regularizer": null, "bias_regularizer": null, "activity_regularizer": null, "kernel_constraint": null, "bias_constraint": null}, "name": "dense", "inbound_nodes": [[["input_1", 0, 0, {}]]]}, {"class_name": "Dense", "config": {"name": "dense_1", "trainable": true, "dtype": "float32", "units": 512, "activation": "selu", "use_bias": true, "kernel_initializer": {"class_name": "LecunNormal", "config": {"seed": null}}, "bias_initializer": {"class_name": "Zeros", "config": {}}, "kernel_regularizer": null, "bias_regularizer": null, "activity_regularizer": null, "kernel_constraint": null, "bias_constraint": null}, "name": "dense_1", "inbound_nodes": [[["dense", 0, 0, {}]]]}, {"class_name": "Dense", "config": {"name": "dense_2", "trainable": true, "dtype": "float32", "units": 256, "activation": "selu", "use_bias": true, "kernel_initializer": {"class_name": "LecunNormal", "config": {"seed": null}}, "bias_initializer": {"class_name": "Zeros", "config": {}}, "kernel_regularizer": null, "bias_regularizer": null, "activity_regularizer": null, "kernel_constraint": null, "bias_constraint": null}, "name": "dense_2", "inbound_nodes": [[["dense_1", 0, 0, {}]]]}], "input_layers": [["input_1", 0, 0]], "output_layers": [["dense_2", 0, 0]]}, "shared_object_id": 10, "input_spec": [{"class_name": "InputSpec", "config": {"dtype": null, "shape": {"class_name": "__tuple__", "items": [null, 2048]}, "ndim": 2, "max_ndim": null, "min_ndim": null, "axes": {}}}], "build_input_shape": {"class_name": "TensorShape", "items": [null, 2048]}, "is_graph_network": true, "full_save_spec": {"class_name": "__tuple__", "items": [[{"class_name": "TypeSpec", "type_spec": "tf.TensorSpec", "serialized": [{"class_name": "TensorShape", "items": [null, 2048]}, "float32", "input_1"]}], {}]}, "save_spec": {"class_name": "TypeSpec", "type_spec": "tf.TensorSpec", "serialized": [{"class_name": "TensorShape", "items": [null, 2048]}, "float32", "input_1"]}, "keras_version": "2.6.0", "backend": "tensorflow", "model_config": {"class_name": "Functional", "config": {"name": "model_1", "layers": [{"class_name": "InputLayer", "config": {"batch_input_shape": {"class_name": "__tuple__", "items": [null, 2048]}, "dtype": "float32", "sparse": false, "ragged": false, "name": "input_1"}, "name": "input_1", "inbound_nodes": [], "shared_object_id": 0}, {"class_name": "Dense", "config": {"name": "dense", "trainable": true, "dtype": "float32", "units": 1024, "activation": "selu", "use_bias": true, "kernel_initializer": {"class_name": "LecunNormal", "config": {"seed": null}, "shared_object_id": 1}, "bias_initializer": {"class_name": "Zeros", "config": {}, "shared_object_id": 2}, "kernel_regularizer": null, "bias_regularizer": null, "activity_regularizer": null, "kernel_constraint": null, "bias_constraint": null}, "name": "dense", "inbound_nodes": [[["input_1", 0, 0, {}]]], "shared_object_id": 3}, {"class_name": "Dense", "config": {"name": "dense_1", "trainable": true, "dtype": "float32", "units": 512, "activation": "selu", "use_bias": true, "kernel_initializer": {"class_name": "LecunNormal", "config": {"seed": null}, "shared_object_id": 4}, "bias_initializer": {"class_name": "Zeros", "config": {}, "shared_object_id": 5}, "kernel_regularizer": null, "bias_regularizer": null, "activity_regularizer": null, "kernel_constraint": null, "bias_constraint": null}, "name": "dense_1", "inbound_nodes": [[["dense", 0, 0, {}]]], "shared_object_id": 6}, {"class_name": "Dense", "config": {"name": "dense_2", "trainable": true, "dtype": "float32", "units": 256, "activation": "selu", "use_bias": true, "kernel_initializer": {"class_name": "LecunNormal", "config": {"seed": null}, "shared_object_id": 7}, "bias_initializer": {"class_name": "Zeros", "config": {}, "shared_object_id": 8}, "kernel_regularizer": null, "bias_regularizer": null, "activity_regularizer": null, "kernel_constraint": null, "bias_constraint": null}, "name": "dense_2", "inbound_nodes": [[["dense_1", 0, 0, {}]]], "shared_object_id": 9}], "input_layers": [["input_1", 0, 0]], "output_layers": [["dense_2", 0, 0]]}}}2 +ü root.layer-0"_tf_keras_input_layer*Ì{"class_name": "InputLayer", "name": "input_1", "dtype": "float32", "sparse": false, "ragged": false, "batch_input_shape": {"class_name": "__tuple__", "items": [null, 2048]}, "config": {"batch_input_shape": {"class_name": "__tuple__", "items": [null, 2048]}, "dtype": "float32", "sparse": false, "ragged": false, "name": "input_1"}}2 +ñroot.layer_with_weights-0"_tf_keras_layer*º{"name": "dense", "trainable": true, "expects_training_arg": false, "dtype": "float32", "batch_input_shape": null, "stateful": false, "must_restore_from_config": false, "class_name": "Dense", "config": {"name": "dense", "trainable": true, "dtype": "float32", "units": 1024, "activation": "selu", "use_bias": true, "kernel_initializer": {"class_name": "LecunNormal", "config": {"seed": null}, "shared_object_id": 1}, "bias_initializer": {"class_name": "Zeros", "config": {}, "shared_object_id": 2}, "kernel_regularizer": null, "bias_regularizer": null, "activity_regularizer": null, "kernel_constraint": null, "bias_constraint": null}, "inbound_nodes": [[["input_1", 0, 0, {}]]], "shared_object_id": 3, "input_spec": {"class_name": "InputSpec", "config": {"dtype": null, "shape": null, "ndim": null, "max_ndim": null, "min_ndim": 2, "axes": {"-1": 2048}}, "shared_object_id": 12}, "build_input_shape": {"class_name": "TensorShape", "items": [null, 2048]}}2 +òroot.layer_with_weights-1"_tf_keras_layer*»{"name": "dense_1", "trainable": true, "expects_training_arg": false, "dtype": "float32", "batch_input_shape": null, "stateful": false, "must_restore_from_config": false, "class_name": "Dense", "config": {"name": "dense_1", "trainable": true, "dtype": "float32", "units": 512, "activation": "selu", "use_bias": true, "kernel_initializer": {"class_name": "LecunNormal", "config": {"seed": null}, "shared_object_id": 4}, "bias_initializer": {"class_name": "Zeros", "config": {}, "shared_object_id": 5}, "kernel_regularizer": null, "bias_regularizer": null, "activity_regularizer": null, "kernel_constraint": null, "bias_constraint": null}, "inbound_nodes": [[["dense", 0, 0, {}]]], "shared_object_id": 6, "input_spec": {"class_name": "InputSpec", "config": {"dtype": null, "shape": null, "ndim": null, "max_ndim": null, "min_ndim": 2, "axes": {"-1": 1024}}, "shared_object_id": 13}, "build_input_shape": {"class_name": "TensorShape", "items": [null, 1024]}}2 +òroot.layer_with_weights-2"_tf_keras_layer*»{"name": "dense_2", "trainable": true, "expects_training_arg": false, "dtype": "float32", "batch_input_shape": null, "stateful": false, "must_restore_from_config": false, "class_name": "Dense", "config": {"name": "dense_2", "trainable": true, "dtype": "float32", "units": 256, "activation": "selu", "use_bias": true, "kernel_initializer": {"class_name": "LecunNormal", "config": {"seed": null}, "shared_object_id": 7}, "bias_initializer": {"class_name": "Zeros", "config": {}, "shared_object_id": 8}, "kernel_regularizer": null, "bias_regularizer": null, "activity_regularizer": null, "kernel_constraint": null, "bias_constraint": null}, "inbound_nodes": [[["dense_1", 0, 0, {}]]], "shared_object_id": 9, "input_spec": {"class_name": "InputSpec", "config": {"dtype": null, "shape": null, "ndim": null, "max_ndim": null, "min_ndim": 2, "axes": {"-1": 512}}, "shared_object_id": 14}, "build_input_shape": {"class_name": "TensorShape", "items": [null, 512]}}2 \ No newline at end of file diff --git a/tests/generic_encoder/saved_model.pb b/tests/generic_encoder/saved_model.pb new file mode 100755 index 00000000..dca9f9b8 Binary files /dev/null and b/tests/generic_encoder/saved_model.pb differ diff --git a/tests/generic_encoder/variables/variables.data-00000-of-00001 b/tests/generic_encoder/variables/variables.data-00000-of-00001 new file mode 100755 index 00000000..db0b5cb2 Binary files /dev/null and b/tests/generic_encoder/variables/variables.data-00000-of-00001 differ diff --git a/tests/generic_encoder/variables/variables.index b/tests/generic_encoder/variables/variables.index new file mode 100755 index 00000000..d14011f8 Binary files /dev/null and b/tests/generic_encoder/variables/variables.index differ diff --git a/tests/run_autoencoder.py b/tests/run_autoencoder.py old mode 100644 new mode 100755 diff --git a/tests/run_fnntraining.py b/tests/run_fnntraining.py old mode 100644 new mode 100755 index 4146ad4a..40f9287a --- a/tests/run_fnntraining.py +++ b/tests/run_fnntraining.py @@ -24,7 +24,7 @@ testSize=0.2, kFolds=1, verbose=2, - trainAC=False, + trainAC=True, trainFNN=True, ) @@ -49,11 +49,7 @@ def run_single_label_training(opts: opt.Options) -> None: if opts.trainAC: logging.info("Training autoencoder") - encoder = ac.train_full_ac(df, opts) - # encoder.save_weights(opts.acFile) - else: - logging.info("Using trained autoencoder") - (_, encoder) = ac.define_ac_model(opts) + encoder, train_indices, test_indices = ac.train_full_ac(df, opts) df = ac.compress_fingerprints(df, encoder) diff --git a/tests/run_predictgnn.py b/tests/run_predictgnn.py old mode 100644 new mode 100755 index 979c2868..addebf52 --- a/tests/run_predictgnn.py +++ b/tests/run_predictgnn.py @@ -1,9 +1,8 @@ import logging -import os import pathlib -import pandas as pd -from chemprop import args, train +import chemprop as cp +from chemprop import train import dfpl.options as opt import dfpl.utils as utils @@ -26,26 +25,10 @@ def test_predictdmpnn(opts: opt.GnnOptions) -> None: ) json_arg_path = utils.makePathAbsolute(f"{example_directory}/predictgnn.json") - ignore_elements = [ - "py/object", - "checkpoint_paths", - "save_dir", - "saving_name", - ] - arguments, data = utils.createArgsFromJson( - json_arg_path, ignore_elements, return_json_object=True - ) - arguments.append("--preds_path") - arguments.append("") - save_dir = data.get("save_dir") - name = data.get("saving_name") - - opts = args.PredictArgs().parse_args(arguments) - opts.preds_path = os.path.join(save_dir, name) - df = pd.read_csv(opts.test_path) - smiles = [[row.smiles] for _, row in df.iterrows()] + arguments = utils.createArgsFromJson(json_arg_path) + opts = cp.args.PredictArgs().parse_args(arguments) - train.make_predictions(args=opts, smiles=smiles) + train.make_predictions(args=opts) print("predictdmpnn test complete.") diff --git a/tests/run_prediction.py b/tests/run_prediction.py old mode 100644 new mode 100755 index cb7d1fea..db111aa0 --- a/tests/run_prediction.py +++ b/tests/run_prediction.py @@ -2,6 +2,8 @@ import pathlib from os import path +from tensorflow.keras.models import load_model + import dfpl.autoencoder as ac import dfpl.fingerprint as fp import dfpl.options as opt @@ -12,14 +14,14 @@ test_predict_args = opt.Options( inputFile=f"{project_directory}/data/smiles.csv", outputDir=f"{project_directory}/preds/", - ecModelDir=utils.makePathAbsolute(f"{project_directory}/output/"), - ecWeightsFile=utils.makePathAbsolute( - f"{project_directory}/output/D_datasetdeterministicrandom.autoencoder.weightsrandom.autoencoder.weights.hdf5" - ), + # ecModelDir=utils.makePathAbsolute( + # f"{project_directory}/data/random_split_autoencoder/encoder_model/" + # ), fnnModelDir=f"{project_directory}/output/fnnTrainingCompressed/AR_saved_model", fpSize=2048, type="smiles", fpType="topological", + compressFeatures=False, ) @@ -36,16 +38,13 @@ def test_predictions(opts: opt.Options): ) # use_compressed = False - if opts.ecWeightsFile: + if opts.ecModelDir: # use_compressed = True # load trained model for autoencoder (autoencoder, encoder) = ac.define_ac_model(opts, output_bias=None) - autoencoder.load_weights(opts.ecWeightsFile) + encoder = load_model(opts.ecModelDir) # compress the fingerprints using the autoencoder df = ac.compress_fingerprints(df, encoder) - # model = tensorflow.keras.models.load_model(opts.fnnModelDir, compile=False) - # model.compile(loss=opts.lossFunction, optimizer=opts.optimizer) - # predict df2 = p.predict_values(df=df, opts=opts) names_columns = [c for c in df2.columns if c not in ["fp", "fpcompressed"]] diff --git a/tests/run_traingnn.py b/tests/run_traingnn.py old mode 100644 new mode 100755 diff --git a/tests/run_vae.py b/tests/run_vae.py old mode 100644 new mode 100755 diff --git a/tests/test_fingerprint.py b/tests/test_fingerprint.py old mode 100644 new mode 100755 diff --git a/tests/test_fractional_sampling.py b/tests/test_fractional_sampling.py old mode 100644 new mode 100755 diff --git a/tests/try_fpcomparison.py b/tests/try_fpcomparison.py old mode 100644 new mode 100755