From 5409c32466cfac7e82769d2bf4f587dcd3fbb5e9 Mon Sep 17 00:00:00 2001 From: FelixSteinbauer <43598998+FelixSteinbauer@users.noreply.github.com> Date: Tue, 18 Jul 2023 15:36:56 +0200 Subject: [PATCH 01/76] Used proper torchmetric parameter order and replaced own PSNR implementation with torchmetric PSNR - I replaced the self-implemented PSNR computation with the one provided by torchmetric. - The ordering of torchmetric function call arguments is actually predictions ("preds") and then target ("target"), not the other way around. --- GANDLF/metrics/synthesis.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/GANDLF/metrics/synthesis.py b/GANDLF/metrics/synthesis.py index 3321b121a..258a63c42 100644 --- a/GANDLF/metrics/synthesis.py +++ b/GANDLF/metrics/synthesis.py @@ -8,6 +8,7 @@ MeanSquaredError, MeanSquaredLogError, MeanAbsoluteError, + PeakSignalNoiseRatio, ) from GANDLF.utils import get_image_from_tensor @@ -25,7 +26,7 @@ def structural_similarity_index(target, prediction, mask=None) -> torch.Tensor: torch.Tensor: The structural similarity index. """ ssim = StructuralSimilarityIndexMeasure(return_full_image=True) - _, ssim_idx_full_image = ssim(target, prediction) + _, ssim_idx_full_image = ssim(preds=prediction, target=target) mask = torch.ones_like(ssim_idx_full_image) if mask is None else mask try: ssim_idx = ssim_idx_full_image[mask] @@ -45,7 +46,7 @@ def mean_squared_error(target, prediction) -> torch.Tensor: prediction (torch.Tensor): The prediction tensor. """ mse = MeanSquaredError() - return mse(target, prediction) + return mse(preds=prediction, target=target) def peak_signal_noise_ratio(target, prediction) -> torch.Tensor: @@ -56,12 +57,8 @@ def peak_signal_noise_ratio(target, prediction) -> torch.Tensor: target (torch.Tensor): The target tensor. prediction (torch.Tensor): The prediction tensor. """ - mse = mean_squared_error(target, prediction) - return ( - 10.0 - * torch.log10((torch.max(target) - torch.min(target)) ** 2) - / (mse + sys.float_info.epsilon) - ) + psnr = PeakSignalNoiseRatio() + return psnr(preds=prediction, target=target) def mean_squared_log_error(target, prediction) -> torch.Tensor: @@ -73,7 +70,7 @@ def mean_squared_log_error(target, prediction) -> torch.Tensor: prediction (torch.Tensor): The prediction tensor. """ mle = MeanSquaredLogError() - return mle(target, prediction) + return mle(preds=prediction, target=target) def mean_absolute_error(target, prediction) -> torch.Tensor: @@ -85,7 +82,7 @@ def mean_absolute_error(target, prediction) -> torch.Tensor: prediction (torch.Tensor): The prediction tensor. """ mae = MeanAbsoluteError() - return mae(target, prediction) + return mae(preds=prediction, target=target) def _get_ncc_image(target, prediction) -> sitk.Image: From 00ea9c5368bed035fc53709758d620b80bee91ba Mon Sep 17 00:00:00 2001 From: FelixSteinbauer <43598998+FelixSteinbauer@users.noreply.github.com> Date: Fri, 21 Jul 2023 20:43:05 +0200 Subject: [PATCH 02/76] Added peak_signal_noise_ratio_eps as dicussed peak_signal_noise_ratio_eps with the initial PSNR implementation (using range and epsilon) --- GANDLF/metrics/synthesis.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/GANDLF/metrics/synthesis.py b/GANDLF/metrics/synthesis.py index 258a63c42..3ded3867a 100644 --- a/GANDLF/metrics/synthesis.py +++ b/GANDLF/metrics/synthesis.py @@ -61,6 +61,24 @@ def peak_signal_noise_ratio(target, prediction) -> torch.Tensor: return psnr(preds=prediction, target=target) +def peak_signal_noise_ratio_eps(target, prediction) -> torch.Tensor: + """ + Computes the peak signal to noise ratio between the target and prediction. Uses a small epsilon in the denominator + of the fraction to avoid infinity as output. Also, operates on the data range instead of the maximum. This version + of PSNR is potentially more robust than the original formular. + + Args: + target (torch.Tensor): The target tensor. + prediction (torch.Tensor): The prediction tensor. + """ + mse = mean_squared_error(target, prediction) + return ( + 10.0 + * torch.log10((torch.max(target) - torch.min(target)) ** 2) + / (mse + sys.float_info.epsilon) + ) + + def mean_squared_log_error(target, prediction) -> torch.Tensor: """ Computes the mean squared log error between the target and prediction. From 960e47a316c623120c0168a93aeb8849d384f46f Mon Sep 17 00:00:00 2001 From: FelixSteinbauer <43598998+FelixSteinbauer@users.noreply.github.com> Date: Fri, 21 Jul 2023 20:47:08 +0200 Subject: [PATCH 03/76] Added peak_signal_noise_ratio_eps call for synthesis case. Additionally to the vanilla PSNR, also the PSNR based on value range and with epsilon in the denominator is now added to the overall_stats_dict as "psnr_range_eps" --- GANDLF/cli/generate_metrics.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/GANDLF/cli/generate_metrics.py b/GANDLF/cli/generate_metrics.py index cf64f991f..56a3ff633 100644 --- a/GANDLF/cli/generate_metrics.py +++ b/GANDLF/cli/generate_metrics.py @@ -14,6 +14,7 @@ structural_similarity_index, mean_squared_error, peak_signal_noise_ratio, + peak_signal_noise_ratio_eps, mean_squared_log_error, mean_absolute_error, ncc_mean, @@ -258,10 +259,13 @@ def __fix_2d_tensor(input_tensor): gt_image_infill, output_infill ).item() - # PSNR - similar to pytorch PeakSignalNoiseRatio until 4 digits after decimal point overall_stats_dict[current_subject_id]["psnr"] = peak_signal_noise_ratio( gt_image_infill, output_infill ).item() + + overall_stats_dict[current_subject_id]["psnr_range_eps"] = peak_signal_noise_ratio_eps( + gt_image_infill, output_infill + ).item() pprint(overall_stats_dict) if outputfile is not None: From e3e36d75d66353476e8ee692486532cf288a55a2 Mon Sep 17 00:00:00 2001 From: FelixSteinbauer <43598998+FelixSteinbauer@users.noreply.github.com> Date: Fri, 21 Jul 2023 20:52:04 +0200 Subject: [PATCH 04/76] Added peak_signal_noise_ratio_eps also the the metrics init file --- GANDLF/metrics/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/GANDLF/metrics/__init__.py b/GANDLF/metrics/__init__.py index b8de47cf1..55feddd80 100644 --- a/GANDLF/metrics/__init__.py +++ b/GANDLF/metrics/__init__.py @@ -31,6 +31,7 @@ structural_similarity_index, mean_squared_error, peak_signal_noise_ratio, + peak_signal_noise_ratio_eps, mean_squared_log_error, mean_absolute_error, ncc_mean, From 0e2d72e97c75e8e3b51a352914ca0d0ba317a748 Mon Sep 17 00:00:00 2001 From: FelixSteinbauer <43598998+FelixSteinbauer@users.noreply.github.com> Date: Fri, 21 Jul 2023 21:39:33 +0200 Subject: [PATCH 05/76] Removed Trailing whitespace (I think) --- GANDLF/cli/generate_metrics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GANDLF/cli/generate_metrics.py b/GANDLF/cli/generate_metrics.py index 56a3ff633..ff9d2b069 100644 --- a/GANDLF/cli/generate_metrics.py +++ b/GANDLF/cli/generate_metrics.py @@ -262,7 +262,7 @@ def __fix_2d_tensor(input_tensor): overall_stats_dict[current_subject_id]["psnr"] = peak_signal_noise_ratio( gt_image_infill, output_infill ).item() - + overall_stats_dict[current_subject_id]["psnr_range_eps"] = peak_signal_noise_ratio_eps( gt_image_infill, output_infill ).item() From 9c9d9e63a4556422d5374aacb23318ed324d2d3a Mon Sep 17 00:00:00 2001 From: Geeks-sid Date: Fri, 28 Jul 2023 12:17:34 -0400 Subject: [PATCH 06/76] upgrade openvino version --- .devcontainer/onCreateCommand.sh | 2 +- .github/workflows/python-test.yml | 2 +- Dockerfile-CPU | 2 +- Dockerfile-CUDA11.6 | 2 +- Dockerfile-ROCm | 2 +- GANDLF/utils/modelio.py | 78 ++++++++++++++----------------- docs/setup.md | 2 +- 7 files changed, 42 insertions(+), 48 deletions(-) diff --git a/.devcontainer/onCreateCommand.sh b/.devcontainer/onCreateCommand.sh index c4bf0ea5c..bd69500a8 100755 --- a/.devcontainer/onCreateCommand.sh +++ b/.devcontainer/onCreateCommand.sh @@ -2,7 +2,7 @@ pip install --upgrade pip pip install wheel -pip install openvino-dev==2022.1.0 # [OPTIONAL] to generate optimized models for inference +pip install openvino-dev==2023.0.1 # [OPTIONAL] to generate optimized models for inference pip install mlcube_docker # [OPTIONAL] to deploy GaNDLF models as MLCube-compliant Docker containers pip install medmnist==2.1.0 pip install torch==1.13.1+cpu torchvision==0.14.1+cpu torchaudio==0.13.1 --extra-index-url https://download.pytorch.org/whl/cpu diff --git a/.github/workflows/python-test.yml b/.github/workflows/python-test.yml index 592c8e342..5364dc8df 100644 --- a/.github/workflows/python-test.yml +++ b/.github/workflows/python-test.yml @@ -72,7 +72,7 @@ jobs: sudo apt-get install libvips libvips-tools -y python -m pip install --upgrade pip python -m pip install wheel - python -m pip install openvino-dev==2022.1.0 mlcube_docker + python -m pip install openvino-dev==2023.0.1 mlcube_docker pip install torch==1.13.1+cpu torchvision==0.14.1+cpu torchaudio==0.13.1 --extra-index-url https://download.pytorch.org/whl/cpu pip install -e . - name: Run generic unit tests diff --git a/Dockerfile-CPU b/Dockerfile-CPU index 127ffda89..0b5221fa7 100644 --- a/Dockerfile-CPU +++ b/Dockerfile-CPU @@ -8,7 +8,7 @@ RUN apt-get update && apt-get install -y python3.8 python3-pip libjpeg8-dev zlib RUN python3.8 -m pip install --upgrade pip # EXPLICITLY install cpu versions of torch/torchvision (not all versions have +cpu modes on PyPI...) RUN python3.8 -m pip install torch==1.13.1+cpu torchvision==0.14.1+cpu torchaudio==0.13.1 --extra-index-url https://download.pytorch.org/whl/cpu -RUN python3.8 -m pip install openvino-dev==2022.1.0 opencv-python-headless mlcube_docker +RUN python3.8 -m pip install openvino-dev==2023.0.1 opencv-python-headless mlcube_docker # Do some dependency installation separately here to make layer caching more efficient COPY ./setup.py ./setup.py diff --git a/Dockerfile-CUDA11.6 b/Dockerfile-CUDA11.6 index 05409bb47..0b8ed9c42 100644 --- a/Dockerfile-CUDA11.6 +++ b/Dockerfile-CUDA11.6 @@ -10,7 +10,7 @@ LABEL version=1.0 RUN apt-get update && apt-get install -y python3.8 python3-pip libjpeg8-dev zlib1g-dev python3-dev libpython3.8-dev libffi-dev libgl1 RUN python3.8 -m pip install --upgrade pip RUN python3.8 -m pip install torch==1.13.1+cu116 torchvision==0.14.1+cu116 torchaudio==0.13.1 --extra-index-url https://download.pytorch.org/whl/cu116 -RUN python3.8 -m pip install openvino-dev==2022.1.0 opencv-python-headless mlcube_docker +RUN python3.8 -m pip install openvino-dev==2023.0.1 opencv-python-headless mlcube_docker # Do some dependency installation separately here to make layer caching more efficient COPY ./setup.py ./setup.py diff --git a/Dockerfile-ROCm b/Dockerfile-ROCm index 9cf8053fc..62e34cb81 100644 --- a/Dockerfile-ROCm +++ b/Dockerfile-ROCm @@ -8,7 +8,7 @@ LABEL version=1.0 # The base image contains ROCm, python 3.8 and pytorch already, no need to install those RUN python3 -m pip install --upgrade pip RUN python3.8 -m pip install torch==1.13.1+rocm5.2 torchvision==0.14.1+rocm5.2 torchaudio==0.13.1 --extra-index-url https://download.pytorch.org/whl/rocm5.2 -RUN python3 -m pip install --upgrade pip && python3 -m pip install openvino-dev==2022.1.0 opencv-python-headless mlcube_docker +RUN python3 -m pip install --upgrade pip && python3 -m pip install openvino-dev==2023.0.1 opencv-python-headless mlcube_docker RUN apt-get update && apt-get install -y libgl1 # Do some dependency installation separately here to make layer caching more efficient diff --git a/GANDLF/utils/modelio.py b/GANDLF/utils/modelio.py index fa1004852..8143b4f5b 100644 --- a/GANDLF/utils/modelio.py +++ b/GANDLF/utils/modelio.py @@ -26,40 +26,48 @@ initial_model_path_end = "_initial.pth.tar" +import os +import torch +import subprocess + + def optimize_and_save_model(model, params, path, onnx_export=True): """ Perform post-training optimization and save it to a file. Args: - model (torch model): Trained torch model. + model (torch.nn.Module): Trained torch model. params (dict): The parameter dictionary. path (str): The path to save the model dictionary to. onnx_export (bool): Whether to export to ONNX and OpenVINO. """ + # Check if ONNX export is enabled in the parameter dictionary onnx_export = params["model"].get("onnx_export", onnx_export) - # check for incompatible topologies and disable onnx export - # customized imagenet_vgg no longer supported for onnx export: https://github.com/pytorch/pytorch/issues/42653 + + # Check for incompatible topologies and disable ONNX export + # Customized imagenet_vgg no longer supported for ONNX export if onnx_export: - if (params["model"]["architecture"] in ["sdnet", "brain_age"]) or ( - "imagenet_vgg" in params["model"]["architecture"] - ): + architecture = params["model"]["architecture"] + if architecture in ["sdnet", "brain_age"] or "imagenet_vgg" in architecture: onnx_export = False - if not (onnx_export): + if not onnx_export: + # Print a warning if ONNX export is disabled and not already warned if "onnx_print" not in params: print("WARNING: Current model is not supported by ONNX/OpenVINO!") params["onnx_print"] = True return else: try: - print("Optimizing best model.") + print("Optimizing the best model.") num_channel = params["model"]["num_channels"] model_dimension = params["model"]["dimension"] ov_output_data_type = params["model"].get("data_type", "FP32") input_shape = params["patch_size"] onnx_path = path - if not (onnx_path.endswith(".onnx")): + if not onnx_path.endswith(".onnx"): onnx_path = onnx_path.replace("pth.tar", "onnx") + if model_dimension == 2: dummy_input = torch.randn( (1, num_channel, input_shape[0], input_shape[1]) @@ -69,6 +77,7 @@ def optimize_and_save_model(model, params, path, onnx_export=True): (1, num_channel, input_shape[0], input_shape[1], input_shape[2]) ) + # Export the model to ONNX format with torch.no_grad(): torch.onnx.export( model.to("cpu"), @@ -86,52 +95,37 @@ def optimize_and_save_model(model, params, path, onnx_export=True): print("WARNING: Cannot export to ONNX model.") return - # https://github.com/mlcommons/GaNDLF/issues/605 + # Check if OpenVINO is present and try to convert the ONNX model openvino_present = False try: - import openvino + import openvino as ov + from openvino.tools.mo import convert_model openvino_present = True except ImportError: print("WARNING: OpenVINO is not present.") if openvino_present: + xml_path = onnx_path.replace("onnx", "xml") + bin_path = onnx_path.replace("onnx", "bin") try: if model_dimension == 2: - subprocess.call( - [ - "mo", - "--input_model", - "{0}".format(onnx_path), - "--input_shape", - "[1,{0},{1},{2}]".format( - num_channel, input_shape[0], input_shape[1] - ), - "--data_type", - "{0}".format(ov_output_data_type), - "--output_dir", - "{0}".format(ov_output_dir), - ], + ov_model = convert_model( + onnx_path, + input_shape=(1, num_channel, input_shape[0], input_shape[1]), ) else: - subprocess.call( - [ - "mo", - "--input_model", - "{0}".format(onnx_path), - "--input_shape", - "[1,{0},{1},{2},{3}]".format( - num_channel, - input_shape[0], - input_shape[1], - input_shape[2], - ), - "--data_type", - "{0}".format(ov_output_data_type), - "--output_dir", - "{0}".format(ov_output_dir), - ], + ov_model = convert_model( + onnx_path, + input_shape=( + 1, + num_channel, + input_shape[0], + input_shape[1], + input_shape[2], + ), ) + ov.runtime.serialize(ov_model, xml_path=xml_path, bin_path=bin_path) except subprocess.CalledProcessError: print("WARNING: OpenVINO Model Optimizer IR conversion failed.") diff --git a/docs/setup.md b/docs/setup.md index b3b9eed36..ed15f014d 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -42,7 +42,7 @@ GaNDLF's primary computational foundation is built on PyTorch, and as such it su The following dependencies are optional, and are needed for specific features of GaNDLF. ```bash -(venv_gandlf) $> pip install openvino-dev==2022.1.0 # [OPTIONAL] to generate post-training optimized models for inference +(venv_gandlf) $> pip install openvino-dev==2023.0.1 # [OPTIONAL] to generate post-training optimized models for inference (venv_gandlf) $> pip install mlcube_docker # [OPTIONAL] to deploy GaNDLF models as MLCube-compliant Docker containers ``` From 183697215225e62d4ead0cc6ab073aa280023e1d Mon Sep 17 00:00:00 2001 From: Geeks-sid Date: Fri, 28 Jul 2023 12:19:29 -0400 Subject: [PATCH 07/76] udpate to base function --- .../cli/post_training_model_optimization.py | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/GANDLF/cli/post_training_model_optimization.py b/GANDLF/cli/post_training_model_optimization.py index 65a3147b2..a5be38b73 100644 --- a/GANDLF/cli/post_training_model_optimization.py +++ b/GANDLF/cli/post_training_model_optimization.py @@ -4,7 +4,7 @@ from GANDLF.utils import version_check, load_model, optimize_and_save_model -def post_training_model_optimization(model_path, config_path): +def post_training_model_optimization(model_path: str, config_path: str) -> bool: """ CLI function to optimize a model for deployment. @@ -15,29 +15,29 @@ def post_training_model_optimization(model_path, config_path): Returns: bool: True if successful, False otherwise. """ - + # Load the model and its parameters from the given paths main_dict = load_model(model_path, "cpu") parameters = main_dict.get("parameters", None) - parameters = ( - parseConfig(config_path, version_check_flag=False) - if parameters is None - else parameters - ) - ( - model, - _, - _, - _, - _, - parameters, - ) = create_pytorch_objects(parameters, device="cpu") + + # If parameters are not available in the model file, parse them from the config file + if parameters is None: + parameters = parseConfig(config_path, version_check_flag=False) + + # Create PyTorch objects and set onnx_export to True for optimization + model, _, _, _, _, parameters = create_pytorch_objects(parameters, device="cpu") parameters["model"]["onnx_export"] = True + # Perform version check and load the model's state dictionary version_check(parameters["version"], version_to_check=main_dict["version"]) model.load_state_dict(main_dict["model_state_dict"]) + + # Optimize the model and save it to an ONNX file optimize_and_save_model(model, parameters, model_path, onnx_export=True) + + # Check if the optimized model file exists optimized_model_path = model_path.replace("pth.tar", "onnx") if not os.path.exists(optimized_model_path): - print("Error while optimizing model.") + print("Error while optimizing the model.") return False + return True From f5d2488b96ff57e230b562a79b95168fbb09836e Mon Sep 17 00:00:00 2001 From: Geeks-sid Date: Fri, 28 Jul 2023 12:22:08 -0400 Subject: [PATCH 08/76] update to modelio --- GANDLF/utils/modelio.py | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/GANDLF/utils/modelio.py b/GANDLF/utils/modelio.py index 8143b4f5b..4512f4cfc 100644 --- a/GANDLF/utils/modelio.py +++ b/GANDLF/utils/modelio.py @@ -3,6 +3,7 @@ from .generic import get_unique_timestamp from ..version import __version__ +from typing import Dict, Any # these are the base keys for the model dictionary to save model_dict_full = { @@ -130,13 +131,19 @@ def optimize_and_save_model(model, params, path, onnx_export=True): print("WARNING: OpenVINO Model Optimizer IR conversion failed.") -def save_model(model_dict, model, params, path, onnx_export=True): +def save_model( + model_dict: Dict[str, Any], + model: torch.nn.Module, + params: Dict[str, Any], + path: str, + onnx_export: bool = True, +): """ Save the model dictionary to a file. Args: model_dict (dict): Model dictionary to save. - model (torch model): Trained torch model. + model (torch.nn.Module): Trained torch model. params (dict): The parameter dictionary. path (str): The path to save the model dictionary to. onnx_export (bool): Whether to export to ONNX and OpenVINO. @@ -147,6 +154,7 @@ def save_model(model_dict, model, params, path, onnx_export=True): ).hexdigest() model_dict["version"] = __version__ model_dict["parameters"] = params + try: model_dict["git_hash"] = ( subprocess.check_output(["git", "rev-parse", "HEAD"]) @@ -155,20 +163,23 @@ def save_model(model_dict, model, params, path, onnx_export=True): ) except subprocess.CalledProcessError: model_dict["git_hash"] = None + torch.save(model_dict, path) # post-training optimization optimize_and_save_model(model, params, path, onnx_export=onnx_export) -def load_model(path, device, full_sanity_check=True): +def load_model( + path: str, device: torch.device, full_sanity_check: bool = True +) -> Dict[str, Any]: """ Load a model dictionary from a file. Args: path (str): The path to save the model dictionary to. device (torch.device): The device to run the model on. - full_sanity_check (bool): Whether to run full sanity checking on model. + full_sanity_check (bool): Whether to run full sanity checking on the model. Returns: dict: Model dictionary containing model parameters and metadata. @@ -187,8 +198,9 @@ def load_model(path, device, full_sanity_check=True): ) # check if required keys are absent, and if so raise an error + model_dict_required = {"version", "parameters", "timestamp", "timestamp_hash"} incomplete_required_keys = [ - key for key in model_dict_required.keys() if key not in model_dict.keys() + key for key in model_dict_required if key not in model_dict.keys() ] if len(incomplete_required_keys) > 0: raise KeyError( @@ -199,7 +211,7 @@ def load_model(path, device, full_sanity_check=True): return model_dict -def load_ov_model(path, device="CPU"): +def load_ov_model(path: str, device: str = "CPU"): """ Load an OpenVINO IR model from an .xml file. From 130c99d68658cd4955e40ead966ba3dcc7f9d86d Mon Sep 17 00:00:00 2001 From: Geeks-sid Date: Fri, 28 Jul 2023 12:39:37 -0400 Subject: [PATCH 09/76] removed hardcoded required keys --- GANDLF/utils/modelio.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/GANDLF/utils/modelio.py b/GANDLF/utils/modelio.py index 4512f4cfc..1111255a2 100644 --- a/GANDLF/utils/modelio.py +++ b/GANDLF/utils/modelio.py @@ -198,9 +198,8 @@ def load_model( ) # check if required keys are absent, and if so raise an error - model_dict_required = {"version", "parameters", "timestamp", "timestamp_hash"} incomplete_required_keys = [ - key for key in model_dict_required if key not in model_dict.keys() + key for key in model_dict_required.keys() if key not in model_dict.keys() ] if len(incomplete_required_keys) > 0: raise KeyError( From d98472ca4d90ea7e4f491895218a875acb754943 Mon Sep 17 00:00:00 2001 From: Geeks-sid Date: Fri, 28 Jul 2023 12:48:30 -0400 Subject: [PATCH 10/76] updated imports --- GANDLF/utils/modelio.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/GANDLF/utils/modelio.py b/GANDLF/utils/modelio.py index 1111255a2..0deead23f 100644 --- a/GANDLF/utils/modelio.py +++ b/GANDLF/utils/modelio.py @@ -1,9 +1,12 @@ -import os, hashlib, pkg_resources, subprocess +import hashlib +import os +import subprocess +from typing import Any, Dict + import torch -from .generic import get_unique_timestamp from ..version import __version__ -from typing import Dict, Any +from .generic import get_unique_timestamp # these are the base keys for the model dictionary to save model_dict_full = { @@ -27,11 +30,6 @@ initial_model_path_end = "_initial.pth.tar" -import os -import torch -import subprocess - - def optimize_and_save_model(model, params, path, onnx_export=True): """ Perform post-training optimization and save it to a file. From 0452c2679a5387678e010b34400cfd8f0047fe99 Mon Sep 17 00:00:00 2001 From: sarthakpati Date: Fri, 28 Jul 2023 19:17:03 -0400 Subject: [PATCH 11/76] updated default weight decay --- GANDLF/optimizers/wrap_torch.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/GANDLF/optimizers/wrap_torch.py b/GANDLF/optimizers/wrap_torch.py index fe91be096..9a0455ec5 100644 --- a/GANDLF/optimizers/wrap_torch.py +++ b/GANDLF/optimizers/wrap_torch.py @@ -28,10 +28,10 @@ def sgd(parameters): optimizer = SGD( parameters["model_parameters"], lr=parameters.get("learning_rate"), - momentum=parameters["optimizer"].get("momentum", 0.9), - weight_decay=parameters["optimizer"].get("weight_decay", 0), + momentum=parameters["optimizer"].get("momentum", 0.99), + weight_decay=parameters["optimizer"].get("weight_decay", 3e-05), dampening=parameters["optimizer"].get("dampening", 0), - nesterov=parameters["optimizer"].get("Nesterov", False), + nesterov=parameters["optimizer"].get("nesterov", True), ) return optimizer @@ -55,7 +55,7 @@ def asgd(parameters): alpha=parameters["optimizer"].get("alpha", 0.75), t0=parameters["optimizer"].get("t0", 1e6), lambd=parameters["optimizer"].get("lambd", 1e-4), - weight_decay=parameters["optimizer"].get("weight_decay", 0), + weight_decay=parameters["optimizer"].get("weight_decay", 3e-05), ) @@ -177,7 +177,7 @@ def adadelta(parameters): lr=parameters.get("learning_rate"), rho=parameters["optimizer"].get("rho", 0.9), eps=parameters["optimizer"].get("eps", 1e-6), - weight_decay=parameters["optimizer"].get("weight_decay", 0), + weight_decay=parameters["optimizer"].get("weight_decay", 3e-05), ) @@ -199,7 +199,7 @@ def adagrad(parameters): lr=parameters.get("learning_rate"), lr_decay=parameters["optimizer"].get("lr_decay", 0), eps=parameters["optimizer"].get("eps", 1e-6), - weight_decay=parameters["optimizer"].get("weight_decay", 0), + weight_decay=parameters["optimizer"].get("weight_decay", 3e-05), ) @@ -222,7 +222,7 @@ def rmsprop(parameters): eps=parameters["optimizer"].get("eps", 1e-8), centered=parameters["optimizer"].get("centered", False), momentum=parameters["optimizer"].get("momentum", 0), - weight_decay=parameters["optimizer"].get("weight_decay", 0), + weight_decay=parameters["optimizer"].get("weight_decay", 3e-05), ) @@ -242,6 +242,6 @@ def radam(parameters): lr=parameters.get("learning_rate"), betas=parameters["optimizer"].get("betas", (0.9, 0.999)), eps=parameters["optimizer"].get("eps", 1e-8), - weight_decay=parameters["optimizer"].get("weight_decay", 0), + weight_decay=parameters["optimizer"].get("weight_decay", 3e-05), foreach=parameters["optimizer"].get("foreach", None), ) From 08aac5fe0518bbc2d84bb0aab1c89d3f68b1eaea Mon Sep 17 00:00:00 2001 From: FelixSteinbauer <43598998+FelixSteinbauer@users.noreply.github.com> Date: Sat, 29 Jul 2023 10:21:10 +0200 Subject: [PATCH 12/76] Added epsilon and data_range as parameters to PSNR --- GANDLF/metrics/synthesis.py | 41 +++++++++++++++++-------------------- 1 file changed, 19 insertions(+), 22 deletions(-) diff --git a/GANDLF/metrics/synthesis.py b/GANDLF/metrics/synthesis.py index 3ded3867a..af5eef9f5 100644 --- a/GANDLF/metrics/synthesis.py +++ b/GANDLF/metrics/synthesis.py @@ -49,34 +49,31 @@ def mean_squared_error(target, prediction) -> torch.Tensor: return mse(preds=prediction, target=target) -def peak_signal_noise_ratio(target, prediction) -> torch.Tensor: +def peak_signal_noise_ratio(target, prediction, data_range=None, epsilon=None) -> torch.Tensor: """ Computes the peak signal to noise ratio between the target and prediction. Args: target (torch.Tensor): The target tensor. prediction (torch.Tensor): The prediction tensor. - """ - psnr = PeakSignalNoiseRatio() - return psnr(preds=prediction, target=target) - - -def peak_signal_noise_ratio_eps(target, prediction) -> torch.Tensor: - """ - Computes the peak signal to noise ratio between the target and prediction. Uses a small epsilon in the denominator - of the fraction to avoid infinity as output. Also, operates on the data range instead of the maximum. This version - of PSNR is potentially more robust than the original formular. - - Args: - target (torch.Tensor): The target tensor. - prediction (torch.Tensor): The prediction tensor. - """ - mse = mean_squared_error(target, prediction) - return ( - 10.0 - * torch.log10((torch.max(target) - torch.min(target)) ** 2) - / (mse + sys.float_info.epsilon) - ) + data_range (float, optional): If not None, this data range is used as enumerator instead of computing it from the given data. Defaults to None. + epsilon (float, optional): If not None, this epsilon is added to the denominator of the fraction to avoid infinity as output. Defaults to None. + """ + + if epsilon == None: + psnr = PeakSignalNoiseRatio(data_range=data_range) + return psnr(preds=prediction, target=target) + else: #reimplement torchmetrics RSNR but with epsilon + mse = mean_squared_error(target, prediction) + if data_range == None: #compute data_range like torchmetrics if not given + min_v = 0 if torch.min(target) > 0 else torch.min(target) #look at this line + max_v = torch.max(target) + data_range = max_v - min_v + return ( + 10.0 + * torch.log10(data_range) ** 2) + / (mse + epsilon) + ) def mean_squared_log_error(target, prediction) -> torch.Tensor: From 3ffbaa791320bd601a4ef3c308561e685e4d27ca Mon Sep 17 00:00:00 2001 From: FelixSteinbauer <43598998+FelixSteinbauer@users.noreply.github.com> Date: Sat, 29 Jul 2023 10:21:58 +0200 Subject: [PATCH 13/76] Unified PSNR versions --- GANDLF/metrics/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/GANDLF/metrics/__init__.py b/GANDLF/metrics/__init__.py index 55feddd80..b8de47cf1 100644 --- a/GANDLF/metrics/__init__.py +++ b/GANDLF/metrics/__init__.py @@ -31,7 +31,6 @@ structural_similarity_index, mean_squared_error, peak_signal_noise_ratio, - peak_signal_noise_ratio_eps, mean_squared_log_error, mean_absolute_error, ncc_mean, From af8c12dcb3327e8a1995d19ba0ba3b28a89d116a Mon Sep 17 00:00:00 2001 From: FelixSteinbauer <43598998+FelixSteinbauer@users.noreply.github.com> Date: Sat, 29 Jul 2023 10:23:44 +0200 Subject: [PATCH 14/76] Update generate_metrics.py for usage of the unified PSNR signature --- GANDLF/cli/generate_metrics.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/GANDLF/cli/generate_metrics.py b/GANDLF/cli/generate_metrics.py index ff9d2b069..75800ccea 100644 --- a/GANDLF/cli/generate_metrics.py +++ b/GANDLF/cli/generate_metrics.py @@ -14,7 +14,6 @@ structural_similarity_index, mean_squared_error, peak_signal_noise_ratio, - peak_signal_noise_ratio_eps, mean_squared_log_error, mean_absolute_error, ncc_mean, @@ -263,8 +262,8 @@ def __fix_2d_tensor(input_tensor): gt_image_infill, output_infill ).item() - overall_stats_dict[current_subject_id]["psnr_range_eps"] = peak_signal_noise_ratio_eps( - gt_image_infill, output_infill + overall_stats_dict[current_subject_id]["psnr_eps"] = peak_signal_noise_ratio( + gt_image_infill, output_infill, epsilon=sys.float_info.epsilon ).item() pprint(overall_stats_dict) From b42ca4e6b801eb1f4d379c741db49cdb30395b97 Mon Sep 17 00:00:00 2001 From: FelixSteinbauer <43598998+FelixSteinbauer@users.noreply.github.com> Date: Sat, 29 Jul 2023 10:40:55 +0200 Subject: [PATCH 15/76] Fixed IndentationError? --- GANDLF/metrics/synthesis.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/GANDLF/metrics/synthesis.py b/GANDLF/metrics/synthesis.py index af5eef9f5..6db0c74e4 100644 --- a/GANDLF/metrics/synthesis.py +++ b/GANDLF/metrics/synthesis.py @@ -69,11 +69,7 @@ def peak_signal_noise_ratio(target, prediction, data_range=None, epsilon=None) - min_v = 0 if torch.min(target) > 0 else torch.min(target) #look at this line max_v = torch.max(target) data_range = max_v - min_v - return ( - 10.0 - * torch.log10(data_range) ** 2) - / (mse + epsilon) - ) + return 10.0 * ( torch.log10(data_range) ** 2) / (mse + epsilon) ) def mean_squared_log_error(target, prediction) -> torch.Tensor: From fdea5185761578c0b162db77340c9e9484af0a67 Mon Sep 17 00:00:00 2001 From: FelixSteinbauer <43598998+FelixSteinbauer@users.noreply.github.com> Date: Sat, 29 Jul 2023 10:56:43 +0200 Subject: [PATCH 16/76] Fixed: unmatched ")" --- GANDLF/metrics/synthesis.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GANDLF/metrics/synthesis.py b/GANDLF/metrics/synthesis.py index 6db0c74e4..017a6cc27 100644 --- a/GANDLF/metrics/synthesis.py +++ b/GANDLF/metrics/synthesis.py @@ -69,7 +69,7 @@ def peak_signal_noise_ratio(target, prediction, data_range=None, epsilon=None) - min_v = 0 if torch.min(target) > 0 else torch.min(target) #look at this line max_v = torch.max(target) data_range = max_v - min_v - return 10.0 * ( torch.log10(data_range) ** 2) / (mse + epsilon) ) + return 10.0 * (( torch.log10(data_range) ** 2) / (mse + epsilon) ) def mean_squared_log_error(target, prediction) -> torch.Tensor: From d2bf1274ca56ce3795fdc18e1b590642cf225609 Mon Sep 17 00:00:00 2001 From: FelixSteinbauer <43598998+FelixSteinbauer@users.noreply.github.com> Date: Sat, 29 Jul 2023 11:58:26 +0200 Subject: [PATCH 17/76] Removed unused sys package --- GANDLF/metrics/synthesis.py | 1 - 1 file changed, 1 deletion(-) diff --git a/GANDLF/metrics/synthesis.py b/GANDLF/metrics/synthesis.py index 017a6cc27..12550b0a6 100644 --- a/GANDLF/metrics/synthesis.py +++ b/GANDLF/metrics/synthesis.py @@ -1,4 +1,3 @@ -import sys import SimpleITK as sitk import PIL.Image import numpy as np From acef5d8dd3504fd27fe69c82a3b0ea722c045903 Mon Sep 17 00:00:00 2001 From: FelixSteinbauer <43598998+FelixSteinbauer@users.noreply.github.com> Date: Sat, 29 Jul 2023 11:58:50 +0200 Subject: [PATCH 18/76] Added required sys package --- GANDLF/cli/generate_metrics.py | 1 + 1 file changed, 1 insertion(+) diff --git a/GANDLF/cli/generate_metrics.py b/GANDLF/cli/generate_metrics.py index 75800ccea..7a0e2c5ca 100644 --- a/GANDLF/cli/generate_metrics.py +++ b/GANDLF/cli/generate_metrics.py @@ -1,3 +1,4 @@ +import sys import yaml from pprint import pprint import pandas as pd From 941c85bdc0cef7866984771e62e3ee8be261f255 Mon Sep 17 00:00:00 2001 From: FelixSteinbauer <43598998+FelixSteinbauer@users.noreply.github.com> Date: Sat, 29 Jul 2023 13:14:24 +0200 Subject: [PATCH 19/76] Introduced percentile normalization for synthesis challenge metrics --- GANDLF/cli/generate_metrics.py | 33 ++++++++++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/GANDLF/cli/generate_metrics.py b/GANDLF/cli/generate_metrics.py index cf64f991f..058eb83c3 100644 --- a/GANDLF/cli/generate_metrics.py +++ b/GANDLF/cli/generate_metrics.py @@ -193,6 +193,32 @@ def __fix_2d_tensor(input_tensor): else: return input_tensor + def __percentile_clip(input_tensor, reference_tensor=None, p_min=0.5, p_max=99.5, strictlyPositive=True): + """Normalizes a tensor based on percentiles. Clips values below and above the percentile. + Percentiles for normalization can come from another tensor. + + Args: + input_tensor (torch.Tensor): Tensor to be normalized based on the data from the reference_tensor. + If reference_tensor is None, the percentiles from this tensor will be used. + reference_tensor (torch.Tensor, optional): The tensor used for obtaining the percentiles. + p_min (float, optional): Lower end percentile. Defaults to 0.5. + p_max (float, optional): Upper end percentile. Defaults to 99.5. + strictlyPositive (bool, optional): Ensures that really all values are above 0 before normalization. Defaults to True. + + Returns: + torch.Tensor: The input_tensor normalized based on the percentiles of the reference tensor. + """ + if(reference_tensor == None): + reference_tensor = input_tensor + v_min, v_max = np.percentile(reference_tensor, [p_min,p_max]) #get p_min percentile and p_max percentile + + if( v_min < 0 and strictlyPositive): #set lower bound to be 0 if it would be below + v_min = 0 + output_tensor = np.clip(input_tensor,v_min,v_max) #clip values to percentiles from reference_tensor + output_tensor = (output_tensor - v_min)/(v_max-v_min) #normalizes values to [0;1] + + return output_tensor + for _, row in tqdm(input_df.iterrows(), total=input_df.shape[0]): current_subject_id = row[headers["subjectid"]] overall_stats_dict[current_subject_id] = {} @@ -219,9 +245,9 @@ def __fix_2d_tensor(input_tensor): # Normalize to [0;1] based on GT (otherwise MSE will depend on the image intensity range) normalize = parameters.get("normalize", True) if normalize: - v_max = gt_image_infill.max() - output_infill /= v_max - gt_image_infill /= v_max + reference_tensor = gt_image * ~mask #use all the tissue that is not masked for normalization + gt_image_infill = __percentile_clip(gt_image_infill, reference_tensor=reference_tensor, p_min=0.5, p_max=99.5, strictlyPositive=True) + prediction_infill = __percentile_clip(prediction_infill, reference_tensor=reference_tensor, p_min=0.5, p_max=99.5, strictlyPositive=True) overall_stats_dict[current_subject_id][ "ssim" @@ -258,6 +284,7 @@ def __fix_2d_tensor(input_tensor): gt_image_infill, output_infill ).item() + #TODO: use data_range=1.0 as parameter for PSNR when the Pull request is accepted that introduces the data_range parameter! # PSNR - similar to pytorch PeakSignalNoiseRatio until 4 digits after decimal point overall_stats_dict[current_subject_id]["psnr"] = peak_signal_noise_ratio( gt_image_infill, output_infill From 6d1b3fbf290b3b81cdd9e16c1e5f28c51f3e7519 Mon Sep 17 00:00:00 2001 From: FelixSteinbauer <43598998+FelixSteinbauer@users.noreply.github.com> Date: Sat, 29 Jul 2023 13:27:59 +0200 Subject: [PATCH 20/76] Adapted different naming, removed tailing whitespaces --- GANDLF/cli/generate_metrics.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/GANDLF/cli/generate_metrics.py b/GANDLF/cli/generate_metrics.py index 058eb83c3..f4f970319 100644 --- a/GANDLF/cli/generate_metrics.py +++ b/GANDLF/cli/generate_metrics.py @@ -196,7 +196,7 @@ def __fix_2d_tensor(input_tensor): def __percentile_clip(input_tensor, reference_tensor=None, p_min=0.5, p_max=99.5, strictlyPositive=True): """Normalizes a tensor based on percentiles. Clips values below and above the percentile. Percentiles for normalization can come from another tensor. - + Args: input_tensor (torch.Tensor): Tensor to be normalized based on the data from the reference_tensor. If reference_tensor is None, the percentiles from this tensor will be used. @@ -204,19 +204,18 @@ def __percentile_clip(input_tensor, reference_tensor=None, p_min=0.5, p_max=99.5 p_min (float, optional): Lower end percentile. Defaults to 0.5. p_max (float, optional): Upper end percentile. Defaults to 99.5. strictlyPositive (bool, optional): Ensures that really all values are above 0 before normalization. Defaults to True. - + Returns: torch.Tensor: The input_tensor normalized based on the percentiles of the reference tensor. """ if(reference_tensor == None): reference_tensor = input_tensor v_min, v_max = np.percentile(reference_tensor, [p_min,p_max]) #get p_min percentile and p_max percentile - + if( v_min < 0 and strictlyPositive): #set lower bound to be 0 if it would be below v_min = 0 output_tensor = np.clip(input_tensor,v_min,v_max) #clip values to percentiles from reference_tensor output_tensor = (output_tensor - v_min)/(v_max-v_min) #normalizes values to [0;1] - return output_tensor for _, row in tqdm(input_df.iterrows(), total=input_df.shape[0]): @@ -245,9 +244,9 @@ def __percentile_clip(input_tensor, reference_tensor=None, p_min=0.5, p_max=99.5 # Normalize to [0;1] based on GT (otherwise MSE will depend on the image intensity range) normalize = parameters.get("normalize", True) if normalize: - reference_tensor = gt_image * ~mask #use all the tissue that is not masked for normalization + reference_tensor = target_image * ~mask #use all the tissue that is not masked for normalization gt_image_infill = __percentile_clip(gt_image_infill, reference_tensor=reference_tensor, p_min=0.5, p_max=99.5, strictlyPositive=True) - prediction_infill = __percentile_clip(prediction_infill, reference_tensor=reference_tensor, p_min=0.5, p_max=99.5, strictlyPositive=True) + output_infill = __percentile_clip(output_infill, reference_tensor=reference_tensor, p_min=0.5, p_max=99.5, strictlyPositive=True) overall_stats_dict[current_subject_id][ "ssim" From ee6079c74e8387c0602795ed5f588a63061088e3 Mon Sep 17 00:00:00 2001 From: Sarthak Pati Date: Sun, 30 Jul 2023 09:21:47 -0400 Subject: [PATCH 21/76] reverted to in-line if for coverage --- GANDLF/cli/post_training_model_optimization.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/GANDLF/cli/post_training_model_optimization.py b/GANDLF/cli/post_training_model_optimization.py index a5be38b73..46a5ea6ff 100644 --- a/GANDLF/cli/post_training_model_optimization.py +++ b/GANDLF/cli/post_training_model_optimization.py @@ -20,8 +20,11 @@ def post_training_model_optimization(model_path: str, config_path: str) -> bool: parameters = main_dict.get("parameters", None) # If parameters are not available in the model file, parse them from the config file - if parameters is None: - parameters = parseConfig(config_path, version_check_flag=False) + parameters = ( + parseConfig(config_path, version_check_flag=False) + if parameters is None + else parameters + ) # Create PyTorch objects and set onnx_export to True for optimization model, _, _, _, _, parameters = create_pytorch_objects(parameters, device="cpu") From 9472f068dead62e7308ef985a915e415c82dddd7 Mon Sep 17 00:00:00 2001 From: FelixSteinbauer <43598998+FelixSteinbauer@users.noreply.github.com> Date: Mon, 31 Jul 2023 17:06:16 +0200 Subject: [PATCH 22/76] Different description (comment) for non-torchmetrics PSNR Co-authored-by: Sarthak Pati --- GANDLF/metrics/synthesis.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/GANDLF/metrics/synthesis.py b/GANDLF/metrics/synthesis.py index 12550b0a6..4d70e284b 100644 --- a/GANDLF/metrics/synthesis.py +++ b/GANDLF/metrics/synthesis.py @@ -62,7 +62,8 @@ def peak_signal_noise_ratio(target, prediction, data_range=None, epsilon=None) - if epsilon == None: psnr = PeakSignalNoiseRatio(data_range=data_range) return psnr(preds=prediction, target=target) - else: #reimplement torchmetrics RSNR but with epsilon + else: + # implementation of PSNR that does not give `inf`/`nan` when `mse==0` mse = mean_squared_error(target, prediction) if data_range == None: #compute data_range like torchmetrics if not given min_v = 0 if torch.min(target) > 0 else torch.min(target) #look at this line From 5be2fcaebbe37febbf55b750efb4f5a928f8cd37 Mon Sep 17 00:00:00 2001 From: FelixSteinbauer <43598998+FelixSteinbauer@users.noreply.github.com> Date: Mon, 31 Jul 2023 17:09:31 +0200 Subject: [PATCH 23/76] Fixed wrong parenthesis in PSR definition Co-authored-by: Sarthak Pati --- GANDLF/metrics/synthesis.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GANDLF/metrics/synthesis.py b/GANDLF/metrics/synthesis.py index 4d70e284b..a10ba7d96 100644 --- a/GANDLF/metrics/synthesis.py +++ b/GANDLF/metrics/synthesis.py @@ -69,7 +69,7 @@ def peak_signal_noise_ratio(target, prediction, data_range=None, epsilon=None) - min_v = 0 if torch.min(target) > 0 else torch.min(target) #look at this line max_v = torch.max(target) data_range = max_v - min_v - return 10.0 * (( torch.log10(data_range) ** 2) / (mse + epsilon) ) + return 10.0 * torch.log10((data_range ** 2) / (mse + epsilon)) def mean_squared_log_error(target, prediction) -> torch.Tensor: From 70f8ff5a9bb18f4a91c3bdadeaa3eb1bd2db97bf Mon Sep 17 00:00:00 2001 From: FelixSteinbauer <43598998+FelixSteinbauer@users.noreply.github.com> Date: Mon, 31 Jul 2023 17:30:35 +0200 Subject: [PATCH 24/76] Trying different quotation marks I don't know why changing this comment broke the testing pipeline. Maybe it was not the comment. I changed the quotation marks hoping that would help (probably does not though...) --- GANDLF/metrics/synthesis.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GANDLF/metrics/synthesis.py b/GANDLF/metrics/synthesis.py index a10ba7d96..de04700eb 100644 --- a/GANDLF/metrics/synthesis.py +++ b/GANDLF/metrics/synthesis.py @@ -63,7 +63,7 @@ def peak_signal_noise_ratio(target, prediction, data_range=None, epsilon=None) - psnr = PeakSignalNoiseRatio(data_range=data_range) return psnr(preds=prediction, target=target) else: - # implementation of PSNR that does not give `inf`/`nan` when `mse==0` + # implementation of PSNR that does not give 'inf'/'nan' when 'mse==0' mse = mean_squared_error(target, prediction) if data_range == None: #compute data_range like torchmetrics if not given min_v = 0 if torch.min(target) > 0 else torch.min(target) #look at this line From dfa16657ab95c10b15c590198f52ba733bf41793 Mon Sep 17 00:00:00 2001 From: FelixSteinbauer <43598998+FelixSteinbauer@users.noreply.github.com> Date: Tue, 1 Aug 2023 20:51:11 +0200 Subject: [PATCH 25/76] Tried removing comment --- GANDLF/metrics/synthesis.py | 1 - 1 file changed, 1 deletion(-) diff --git a/GANDLF/metrics/synthesis.py b/GANDLF/metrics/synthesis.py index de04700eb..21ce07c7f 100644 --- a/GANDLF/metrics/synthesis.py +++ b/GANDLF/metrics/synthesis.py @@ -63,7 +63,6 @@ def peak_signal_noise_ratio(target, prediction, data_range=None, epsilon=None) - psnr = PeakSignalNoiseRatio(data_range=data_range) return psnr(preds=prediction, target=target) else: - # implementation of PSNR that does not give 'inf'/'nan' when 'mse==0' mse = mean_squared_error(target, prediction) if data_range == None: #compute data_range like torchmetrics if not given min_v = 0 if torch.min(target) > 0 else torch.min(target) #look at this line From 621b8891955b1be767df2b00fd3ca64b31ea7ae2 Mon Sep 17 00:00:00 2001 From: Sarthak Pati Date: Tue, 1 Aug 2023 15:04:55 -0400 Subject: [PATCH 26/76] added a version check --- GANDLF/utils/modelio.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/GANDLF/utils/modelio.py b/GANDLF/utils/modelio.py index 0deead23f..40b785be9 100644 --- a/GANDLF/utils/modelio.py +++ b/GANDLF/utils/modelio.py @@ -99,8 +99,10 @@ def optimize_and_save_model(model, params, path, onnx_export=True): try: import openvino as ov from openvino.tools.mo import convert_model - - openvino_present = True + from openvino.runtime import get_version + openvino_present = False + if "2023" in get_version(): + openvino_present = True except ImportError: print("WARNING: OpenVINO is not present.") From 5f584af9197ae77dccdedd770e114fedcdef11f5 Mon Sep 17 00:00:00 2001 From: FelixSteinbauer <43598998+FelixSteinbauer@users.noreply.github.com> Date: Tue, 1 Aug 2023 21:08:50 +0200 Subject: [PATCH 27/76] Trying to revert parenthesis fix Now the code should be in the state when it last worked --- GANDLF/metrics/synthesis.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/GANDLF/metrics/synthesis.py b/GANDLF/metrics/synthesis.py index 21ce07c7f..1e22fe21e 100644 --- a/GANDLF/metrics/synthesis.py +++ b/GANDLF/metrics/synthesis.py @@ -68,7 +68,8 @@ def peak_signal_noise_ratio(target, prediction, data_range=None, epsilon=None) - min_v = 0 if torch.min(target) > 0 else torch.min(target) #look at this line max_v = torch.max(target) data_range = max_v - min_v - return 10.0 * torch.log10((data_range ** 2) / (mse + epsilon)) + return 10.0 * (( torch.log10(data_range) ** 2) / (mse + epsilon) ) + #return 10.0 * torch.log10((data_range ** 2) / (mse + epsilon)) def mean_squared_log_error(target, prediction) -> torch.Tensor: From d8b1f8d33c6be488d3a70eea1fe572d04dace53a Mon Sep 17 00:00:00 2001 From: FelixSteinbauer <43598998+FelixSteinbauer@users.noreply.github.com> Date: Tue, 1 Aug 2023 21:16:33 +0200 Subject: [PATCH 28/76] Returned to current state of code I do not know why the pipeline fails To me it seems unrelated to the changes that were made since the last successful run. --- GANDLF/metrics/synthesis.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/GANDLF/metrics/synthesis.py b/GANDLF/metrics/synthesis.py index 1e22fe21e..fd3590a58 100644 --- a/GANDLF/metrics/synthesis.py +++ b/GANDLF/metrics/synthesis.py @@ -62,14 +62,13 @@ def peak_signal_noise_ratio(target, prediction, data_range=None, epsilon=None) - if epsilon == None: psnr = PeakSignalNoiseRatio(data_range=data_range) return psnr(preds=prediction, target=target) - else: + else: # implementation of PSNR that does not give 'inf'/'nan' when 'mse==0' mse = mean_squared_error(target, prediction) if data_range == None: #compute data_range like torchmetrics if not given min_v = 0 if torch.min(target) > 0 else torch.min(target) #look at this line max_v = torch.max(target) data_range = max_v - min_v - return 10.0 * (( torch.log10(data_range) ** 2) / (mse + epsilon) ) - #return 10.0 * torch.log10((data_range ** 2) / (mse + epsilon)) + return 10.0 * torch.log10((data_range ** 2) / (mse + epsilon)) def mean_squared_log_error(target, prediction) -> torch.Tensor: From 67b9d720b466b84b192e566035f2a721f2f6a9d6 Mon Sep 17 00:00:00 2001 From: FelixSteinbauer <43598998+FelixSteinbauer@users.noreply.github.com> Date: Tue, 1 Aug 2023 21:32:44 +0200 Subject: [PATCH 29/76] Sarthaks suggestion for code coverage Co-authored-by: Sarthak Pati --- GANDLF/cli/generate_metrics.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/GANDLF/cli/generate_metrics.py b/GANDLF/cli/generate_metrics.py index f4f970319..162ab1f1b 100644 --- a/GANDLF/cli/generate_metrics.py +++ b/GANDLF/cli/generate_metrics.py @@ -208,8 +208,7 @@ def __percentile_clip(input_tensor, reference_tensor=None, p_min=0.5, p_max=99.5 Returns: torch.Tensor: The input_tensor normalized based on the percentiles of the reference tensor. """ - if(reference_tensor == None): - reference_tensor = input_tensor + reference_tensor = input_tensor if reference_tensor is None else reference_tensor v_min, v_max = np.percentile(reference_tensor, [p_min,p_max]) #get p_min percentile and p_max percentile if( v_min < 0 and strictlyPositive): #set lower bound to be 0 if it would be below From e9a91b1bf99752667ce81a1a40d1eccd327768cd Mon Sep 17 00:00:00 2001 From: FelixSteinbauer <43598998+FelixSteinbauer@users.noreply.github.com> Date: Tue, 1 Aug 2023 21:33:49 +0200 Subject: [PATCH 30/76] Sarthaks suggestion to increase code coverage Nr. 2 Co-authored-by: Sarthak Pati --- GANDLF/cli/generate_metrics.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/GANDLF/cli/generate_metrics.py b/GANDLF/cli/generate_metrics.py index 162ab1f1b..f0bc15590 100644 --- a/GANDLF/cli/generate_metrics.py +++ b/GANDLF/cli/generate_metrics.py @@ -211,8 +211,8 @@ def __percentile_clip(input_tensor, reference_tensor=None, p_min=0.5, p_max=99.5 reference_tensor = input_tensor if reference_tensor is None else reference_tensor v_min, v_max = np.percentile(reference_tensor, [p_min,p_max]) #get p_min percentile and p_max percentile - if( v_min < 0 and strictlyPositive): #set lower bound to be 0 if it would be below - v_min = 0 + # set lower bound to be 0 if strictlyPositive is enabled + v_min = max(v_min, 0.0) if strictlyPositive else v_min output_tensor = np.clip(input_tensor,v_min,v_max) #clip values to percentiles from reference_tensor output_tensor = (output_tensor - v_min)/(v_max-v_min) #normalizes values to [0;1] return output_tensor From 0cac8276f5fb3a77e758be02dcfd0b3734f333c9 Mon Sep 17 00:00:00 2001 From: Sarthak Pati Date: Tue, 1 Aug 2023 16:56:27 -0400 Subject: [PATCH 31/76] better check + comment --- GANDLF/utils/modelio.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/GANDLF/utils/modelio.py b/GANDLF/utils/modelio.py index 40b785be9..af4f77d84 100644 --- a/GANDLF/utils/modelio.py +++ b/GANDLF/utils/modelio.py @@ -101,7 +101,8 @@ def optimize_and_save_model(model, params, path, onnx_export=True): from openvino.tools.mo import convert_model from openvino.runtime import get_version openvino_present = False - if "2023" in get_version(): + # check for the correct openvino version to prevent inadvertent api breaks + if "2023.0.1" in get_version(): openvino_present = True except ImportError: print("WARNING: OpenVINO is not present.") From 813e8979f31a48c98e848b721bde064f2889d818 Mon Sep 17 00:00:00 2001 From: Sarthak Pati Date: Tue, 1 Aug 2023 17:05:43 -0400 Subject: [PATCH 32/76] checking a fix for openfl-CI --- .github/workflows/openfl-test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/openfl-test.yml b/.github/workflows/openfl-test.yml index 43883cff0..f65ecd929 100644 --- a/.github/workflows/openfl-test.yml +++ b/.github/workflows/openfl-test.yml @@ -90,6 +90,7 @@ jobs: cd openfl git fetch --tags git checkout $(git describe --tags --abbrev=0) + pip install tensorboardX<=2.6.1 # https://github.com/lanpa/tensorboardX/issues/716 pip install -e . cd .. echo "Copying files to appropriate directories" From 3f300c8a0c886d718a8e5594df0dc50842adcd26 Mon Sep 17 00:00:00 2001 From: Sarthak Pati Date: Tue, 1 Aug 2023 17:22:35 -0400 Subject: [PATCH 33/76] updated syntax --- .github/workflows/openfl-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/openfl-test.yml b/.github/workflows/openfl-test.yml index f65ecd929..c512768aa 100644 --- a/.github/workflows/openfl-test.yml +++ b/.github/workflows/openfl-test.yml @@ -90,7 +90,7 @@ jobs: cd openfl git fetch --tags git checkout $(git describe --tags --abbrev=0) - pip install tensorboardX<=2.6.1 # https://github.com/lanpa/tensorboardX/issues/716 + pip install "tensorboardX!=2.6.2" # https://github.com/lanpa/tensorboardX/issues/716 pip install -e . cd .. echo "Copying files to appropriate directories" From f714ef0bd69af15d6ab766d62e714805292b1424 Mon Sep 17 00:00:00 2001 From: Sarthak Pati Date: Tue, 1 Aug 2023 19:41:13 -0400 Subject: [PATCH 34/76] trying sed to replace protobuf version requirement --- .github/workflows/openfl-test.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/openfl-test.yml b/.github/workflows/openfl-test.yml index c512768aa..c33f10c79 100644 --- a/.github/workflows/openfl-test.yml +++ b/.github/workflows/openfl-test.yml @@ -90,7 +90,8 @@ jobs: cd openfl git fetch --tags git checkout $(git describe --tags --abbrev=0) - pip install "tensorboardX!=2.6.2" # https://github.com/lanpa/tensorboardX/issues/716 + # pip install "tensorboardX!=2.6.2" # https://github.com/lanpa/tensorboardX/issues/716 + sed -i -e 's/protobuf==3.19.6/protobuf/g' setup.py ## this should NOT be there pip install -e . cd .. echo "Copying files to appropriate directories" From da780850a34b8be9ad1d325625920ebfbd0628c6 Mon Sep 17 00:00:00 2001 From: Sarthak Pati Date: Wed, 2 Aug 2023 08:40:03 -0400 Subject: [PATCH 35/76] removed unwanted variable --- GANDLF/utils/modelio.py | 1 - 1 file changed, 1 deletion(-) diff --git a/GANDLF/utils/modelio.py b/GANDLF/utils/modelio.py index af4f77d84..a01857dc6 100644 --- a/GANDLF/utils/modelio.py +++ b/GANDLF/utils/modelio.py @@ -61,7 +61,6 @@ def optimize_and_save_model(model, params, path, onnx_export=True): print("Optimizing the best model.") num_channel = params["model"]["num_channels"] model_dimension = params["model"]["dimension"] - ov_output_data_type = params["model"].get("data_type", "FP32") input_shape = params["patch_size"] onnx_path = path if not onnx_path.endswith(".onnx"): From 285aefd5868002cdceb4e669eb8e7e54e1fe367e Mon Sep 17 00:00:00 2001 From: Siddhesh Thakur Date: Wed, 2 Aug 2023 09:40:12 -0400 Subject: [PATCH 36/76] Update GANDLF/utils/modelio.py Co-authored-by: Sarthak Pati --- GANDLF/utils/modelio.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/GANDLF/utils/modelio.py b/GANDLF/utils/modelio.py index a01857dc6..e49c59cee 100644 --- a/GANDLF/utils/modelio.py +++ b/GANDLF/utils/modelio.py @@ -127,8 +127,8 @@ def optimize_and_save_model(model, params, path, onnx_export=True): ), ) ov.runtime.serialize(ov_model, xml_path=xml_path, bin_path=bin_path) - except subprocess.CalledProcessError: - print("WARNING: OpenVINO Model Optimizer IR conversion failed.") + except Exception as e: + print("WARNING: OpenVINO Model Optimizer IR conversion failed: " + e) def save_model( From e6580768f848f2a7bdf36165ef9fe5356a999006 Mon Sep 17 00:00:00 2001 From: FelixSteinbauer <43598998+FelixSteinbauer@users.noreply.github.com> Date: Wed, 2 Aug 2023 16:02:55 +0200 Subject: [PATCH 37/76] Additional PSNR versions/columns for the normalized synthesis case As we normalize the data to [0;1], we have top-down knowledge now about the data range. For this knowledge I added two cases/columns of PSNR evaluations. --- GANDLF/cli/generate_metrics.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/GANDLF/cli/generate_metrics.py b/GANDLF/cli/generate_metrics.py index 9d3b2d8c0..4dc6eaa74 100644 --- a/GANDLF/cli/generate_metrics.py +++ b/GANDLF/cli/generate_metrics.py @@ -283,16 +283,23 @@ def __percentile_clip(input_tensor, reference_tensor=None, p_min=0.5, p_max=99.5 gt_image_infill, output_infill ).item() - #TODO: use data_range=1.0 as parameter for PSNR when the Pull request is accepted that introduces the data_range parameter! - # PSNR - similar to pytorch PeakSignalNoiseRatio until 4 digits after decimal point - overall_stats_dict[current_subject_id]["psnr"] = peak_signal_noise_ratio( - gt_image_infill, output_infill + overall_stats_dict[current_subject_id]["psnr"] = peak_signal_noise_ratio( #torchmetrics PSNR using "max" + gt_image_infill, output_infill ).item() - overall_stats_dict[current_subject_id]["psnr_eps"] = peak_signal_noise_ratio( + overall_stats_dict[current_subject_id]["psnr_eps"] = peak_signal_noise_ratio( #same as above but with epsilon for robustness gt_image_infill, output_infill, epsilon=sys.float_info.epsilon ).item() + if normalize : #only use fix data range to [0;1] if the data was normalized before + overall_stats_dict[current_subject_id]["psnr_01"] = peak_signal_noise_ratio( #torchmetrics PSNR but with fixed data range of 0 to 1 + gt_image_infill, output_infill, data_range=1.0 + ).item() + + overall_stats_dict[current_subject_id]["psnr_01_eps"] = peak_signal_noise_ratio( #same as above but with epsilon for robustness + gt_image_infill, output_infill, epsilon=sys.float_info.epsilon + ).item() + pprint(overall_stats_dict) if outputfile is not None: with open(outputfile, "w") as outfile: From 31605a349d20a60e9cacaa8095c3cd9d76620429 Mon Sep 17 00:00:00 2001 From: FelixSteinbauer <43598998+FelixSteinbauer@users.noreply.github.com> Date: Wed, 2 Aug 2023 18:05:35 +0200 Subject: [PATCH 38/76] Comment formatting, running black --- GANDLF/cli/generate_metrics.py | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/GANDLF/cli/generate_metrics.py b/GANDLF/cli/generate_metrics.py index 4dc6eaa74..c43a1f10e 100644 --- a/GANDLF/cli/generate_metrics.py +++ b/GANDLF/cli/generate_metrics.py @@ -283,20 +283,31 @@ def __percentile_clip(input_tensor, reference_tensor=None, p_min=0.5, p_max=99.5 gt_image_infill, output_infill ).item() - overall_stats_dict[current_subject_id]["psnr"] = peak_signal_noise_ratio( #torchmetrics PSNR using "max" - gt_image_infill, output_infill + # torchmetrics PSNR using "max" + overall_stats_dict[current_subject_id]["psnr"] = peak_signal_noise_ratio( + gt_image_infill, output_infill ).item() - overall_stats_dict[current_subject_id]["psnr_eps"] = peak_signal_noise_ratio( #same as above but with epsilon for robustness + # same as above but with epsilon for robustness + overall_stats_dict[current_subject_id][ + "psnr_eps" + ] = peak_signal_noise_ratio( gt_image_infill, output_infill, epsilon=sys.float_info.epsilon ).item() - if normalize : #only use fix data range to [0;1] if the data was normalized before - overall_stats_dict[current_subject_id]["psnr_01"] = peak_signal_noise_ratio( #torchmetrics PSNR but with fixed data range of 0 to 1 + # only use fix data range to [0;1] if the data was normalized before + if normalize: + # torchmetrics PSNR but with fixed data range of 0 to 1 + overall_stats_dict[current_subject_id][ + "psnr_01" + ] = peak_signal_noise_ratio( gt_image_infill, output_infill, data_range=1.0 ).item() - - overall_stats_dict[current_subject_id]["psnr_01_eps"] = peak_signal_noise_ratio( #same as above but with epsilon for robustness + + # same as above but with epsilon for robustness + overall_stats_dict[current_subject_id][ + "psnr_01_eps" + ] = peak_signal_noise_ratio( gt_image_infill, output_infill, epsilon=sys.float_info.epsilon ).item() From b66a7c61de075198dba6b98d8d931f89f0ad06f8 Mon Sep 17 00:00:00 2001 From: Sarthak Pati Date: Wed, 2 Aug 2023 12:40:49 -0400 Subject: [PATCH 39/76] removed spaces --- GANDLF/cli/generate_metrics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GANDLF/cli/generate_metrics.py b/GANDLF/cli/generate_metrics.py index c43a1f10e..07423a3fc 100644 --- a/GANDLF/cli/generate_metrics.py +++ b/GANDLF/cli/generate_metrics.py @@ -283,7 +283,7 @@ def __percentile_clip(input_tensor, reference_tensor=None, p_min=0.5, p_max=99.5 gt_image_infill, output_infill ).item() - # torchmetrics PSNR using "max" + # torchmetrics PSNR using "max" overall_stats_dict[current_subject_id]["psnr"] = peak_signal_noise_ratio( gt_image_infill, output_infill ).item() From 855d297783bfa6ed51484e1fcacb29e7aa01fd77 Mon Sep 17 00:00:00 2001 From: sarthakpati Date: Wed, 2 Aug 2023 15:22:43 -0400 Subject: [PATCH 40/76] added print of weights --- GANDLF/compute/generic.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/GANDLF/compute/generic.py b/GANDLF/compute/generic.py index 517db518f..d3674f42e 100644 --- a/GANDLF/compute/generic.py +++ b/GANDLF/compute/generic.py @@ -97,6 +97,9 @@ def create_pytorch_objects(parameters, train_csv=None, val_csv=None, device="cpu parameters["class_weights"], ) = get_class_imbalance_weights(parameters["training_data"], parameters) + print("Class weights : ", parameters["class_weights"]) + print("Penalty weights: ", parameters["weights"]) + else: scheduler = None From 6d476a238e459410c9ce11f676410b617cef356a Mon Sep 17 00:00:00 2001 From: sarthakpati Date: Sat, 5 Aug 2023 12:53:08 -0400 Subject: [PATCH 41/76] added comments and expected data types --- GANDLF/losses/segmentation.py | 35 ++++++++++++++++++++++++++--------- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/GANDLF/losses/segmentation.py b/GANDLF/losses/segmentation.py index 5591e2c1e..399ddd710 100644 --- a/GANDLF/losses/segmentation.py +++ b/GANDLF/losses/segmentation.py @@ -3,13 +3,13 @@ # Dice scores and dice losses -def dice(predicted, target) -> torch.Tensor: +def dice(predicted: torch.Tensor, target: torch.Tensor) -> torch.Tensor: """ This function computes a dice score between two tensors. Args: - predicted (_type_): Predicted value by the network. - target (_type_): Required target label to match the predicted with + predicted (torch.Tensor): Predicted value by the network. + target (torch.Tensor): Required target label to match the predicted with Returns: torch.Tensor: The computed dice score. @@ -25,7 +25,14 @@ def dice(predicted, target) -> torch.Tensor: return dice_score -def MCD(predicted, target, num_class, weights=None, ignore_class=None, loss_type=0): +def MCD( + predicted: torch.Tensor, + target: torch.Tensor, + num_class: int, + weights: list = None, + ignore_class: int = None, + loss_type: int = 0, +) -> torch.Tensor: """ This function computes the mean class dice score between two tensors @@ -66,7 +73,9 @@ def MCD(predicted, target, num_class, weights=None, ignore_class=None, loss_type return acc_dice -def MCD_loss(predicted, target, params): +def MCD_loss( + predicted: torch.Tensor, target: torch.Tensor, params: dict +) -> torch.Tensor: """ These weights should be the penalty weights, not dice weights """ @@ -80,7 +89,9 @@ def MCD_loss(predicted, target, params): ) -def MCD_log_loss(predicted, target, params): +def MCD_log_loss( + predicted: torch.Tensor, target: torch.Tensor, params: dict +) -> torch.Tensor: """ These weights should be the penalty weights, not dice weights """ @@ -94,7 +105,9 @@ def MCD_log_loss(predicted, target, params): ) -def tversky_loss(predicted, target, alpha=0.5, beta=0.5): +def tversky_loss( + predicted: torch.Tensor, target: torch.Tensor, alpha: float = 0.5, beta: float = 0.5 +) -> torch.Tensor: """ This function calculates the Tversky loss between two tensors. @@ -127,7 +140,9 @@ def tversky_loss(predicted, target, alpha=0.5, beta=0.5): return loss -def MCT_loss(predicted, target, params=None): +def MCT_loss( + predicted: torch.Tensor, target: torch.Tensor, params: dict = None +) -> torch.Tensor: """ This function calculates the Multi-Class Tversky loss between two tensors. @@ -171,7 +186,9 @@ def KullbackLeiblerDivergence(mu, logvar, params=None): return loss.mean() -def FocalLoss(predicted, target, params=None): +def FocalLoss( + predicted: torch.Tensor, target: torch.Tensor, params: dict = None +) -> torch.Tensor: """ This function calculates the Focal loss between two tensors. From 44ac9a9005899c1d8c868cd6fcab61f5a69a75ef Mon Sep 17 00:00:00 2001 From: sarthakpati Date: Sat, 5 Aug 2023 12:55:41 -0400 Subject: [PATCH 42/76] added mcc function --- GANDLF/losses/segmentation.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/GANDLF/losses/segmentation.py b/GANDLF/losses/segmentation.py index 399ddd710..9aa9e5b3c 100644 --- a/GANDLF/losses/segmentation.py +++ b/GANDLF/losses/segmentation.py @@ -25,6 +25,37 @@ def dice(predicted: torch.Tensor, target: torch.Tensor) -> torch.Tensor: return dice_score +def mcc(predictions: torch.Tensor, targets: torch.Tensor) -> torch.Tensor: + """ + This function computes the Matthews Correlation Coefficient (MCC) between two tensors. Adapted from https://github.com/kakumarabhishek/MCC-Loss/blob/main/loss.py. + + Args: + predictions (torch.Tensor): The predicted value by the network. + targets (torch.Tensor): Required target label to match the predicted with + + Returns: + torch.Tensor: The computed MCC score. + """ + tp = torch.sum(torch.mul(predictions, targets)) + tn = torch.sum(torch.mul((1 - predictions), (1 - targets))) + fp = torch.sum(torch.mul(predictions, (1 - targets))) + fn = torch.sum(torch.mul((1 - predictions), targets)) + + numerator = torch.mul(tp, tn) - torch.mul(fp, fn) + # Adding epsilon to the denominator to avoid divide-by-zero errors. + denominator = ( + torch.sqrt( + torch.add(tp, 1, fp) + * torch.add(tp, 1, fn) + * torch.add(tn, 1, fp) + * torch.add(tn, 1, fn) + ) + + sys.float_info.epsilon + ) + + return torch.div(numerator.sum(), denominator.sum()) + + def MCD( predicted: torch.Tensor, target: torch.Tensor, From 7bca7d1a60dd7a4ec715aa196704e8df23f72285 Mon Sep 17 00:00:00 2001 From: sarthakpati Date: Sat, 5 Aug 2023 12:56:26 -0400 Subject: [PATCH 43/76] using torch epsilon --- GANDLF/losses/segmentation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GANDLF/losses/segmentation.py b/GANDLF/losses/segmentation.py index 9aa9e5b3c..f6b42ead6 100644 --- a/GANDLF/losses/segmentation.py +++ b/GANDLF/losses/segmentation.py @@ -50,7 +50,7 @@ def mcc(predictions: torch.Tensor, targets: torch.Tensor) -> torch.Tensor: * torch.add(tn, 1, fp) * torch.add(tn, 1, fn) ) - + sys.float_info.epsilon + + torch.finfo(torch.float32).eps ) return torch.div(numerator.sum(), denominator.sum()) From cc67266363226ba1e5c812e8f96b3418249ac54b Mon Sep 17 00:00:00 2001 From: sarthakpati Date: Sat, 5 Aug 2023 13:01:33 -0400 Subject: [PATCH 44/76] added 2 types of mcc loss --- GANDLF/losses/segmentation.py | 83 ++++++++++++++++++++++++++++++++--- 1 file changed, 76 insertions(+), 7 deletions(-) diff --git a/GANDLF/losses/segmentation.py b/GANDLF/losses/segmentation.py index f6b42ead6..bbef68cdd 100644 --- a/GANDLF/losses/segmentation.py +++ b/GANDLF/losses/segmentation.py @@ -56,10 +56,11 @@ def mcc(predictions: torch.Tensor, targets: torch.Tensor) -> torch.Tensor: return torch.div(numerator.sum(), denominator.sum()) -def MCD( +def generic_loss_calculator( predicted: torch.Tensor, target: torch.Tensor, num_class: int, + loss_criteria: function, weights: list = None, ignore_class: int = None, loss_type: int = 0, @@ -71,6 +72,7 @@ def MCD( predicted (torch.Tensor): Predicted generally by the network target (torch.Tensor): Required target label to match the predicted with num_class (int): Number of classes (including the background class) + loss_criteria (function): Loss function to use weights (list, optional): Dice weights for each class (excluding the background class), defaults to None ignore_class (int, optional): Class to ignore, defaults to None loss_type (int, optional): Type of loss to compute, defaults to 0 @@ -81,11 +83,10 @@ def MCD( Returns: torch.Tensor: Mean Class Dice score """ - acc_dice = 0 for i in range(num_class): # 0 is background - currentDice = dice(predicted[:, i, ...], target[:, i, ...]) + currentDice = loss_criteria(predicted[:, i, ...], target[:, i, ...]) if loss_type == 1: currentDice = 1 - currentDice # subtract from 1 because this is a loss @@ -108,12 +109,21 @@ def MCD_loss( predicted: torch.Tensor, target: torch.Tensor, params: dict ) -> torch.Tensor: """ - These weights should be the penalty weights, not dice weights + This function computes the Dice loss between two tensors. These weights should be the penalty weights, not dice weights. + + Args: + predicted (torch.Tensor): The predicted value by the network. + target (torch.Tensor): Required target label to match the predicted with + params (dict): Dictionary of parameters + + Returns: + torch.Tensor: The computed MCC loss. """ - return MCD( + return generic_loss_calculator( predicted, target, len(params["model"]["class_list"]), + dice, params["weights"], None, 1, @@ -124,12 +134,71 @@ def MCD_log_loss( predicted: torch.Tensor, target: torch.Tensor, params: dict ) -> torch.Tensor: """ - These weights should be the penalty weights, not dice weights + This function computes the Dice loss between two tensors with log. These weights should be the penalty weights, not dice weights. + + Args: + predicted (torch.Tensor): The predicted value by the network. + target (torch.Tensor): Required target label to match the predicted with + params (dict): Dictionary of parameters + + Returns: + torch.Tensor: The computed MCC loss. + """ + return generic_loss_calculator( + predicted, + target, + len(params["model"]["class_list"]), + dice, + params["weights"], + None, + 2, + ) + + +def MCD_loss( + predicted: torch.Tensor, target: torch.Tensor, params: dict +) -> torch.Tensor: + """ + This function computes the Matthews Correlation Coefficient (MCC) loss between two tensors. These weights should be the penalty weights, not dice weights. + + Args: + predicted (torch.Tensor): The predicted value by the network. + target (torch.Tensor): Required target label to match the predicted with + params (dict): Dictionary of parameters + + Returns: + torch.Tensor: The computed MCC loss. + """ + return generic_loss_calculator( + predicted, + target, + len(params["model"]["class_list"]), + mcc, + params["weights"], + None, + 1, + ) + + +def MCC_log_loss( + predicted: torch.Tensor, target: torch.Tensor, params: dict +) -> torch.Tensor: + """ + This function computes the Matthews Correlation Coefficient (MCC) loss between two tensors with log. These weights should be the penalty weights, not dice weights. + + Args: + predicted (torch.Tensor): The predicted value by the network. + target (torch.Tensor): Required target label to match the predicted with + params (dict): Dictionary of parameters + + Returns: + torch.Tensor: The computed MCC loss. """ - return MCD( + return generic_loss_calculator( predicted, target, len(params["model"]["class_list"]), + mcc, params["weights"], None, 2, From 2649ce1e854c181df2cf849e357bc58417402c53 Mon Sep 17 00:00:00 2001 From: sarthakpati Date: Sat, 5 Aug 2023 13:02:45 -0400 Subject: [PATCH 45/76] updated global dict --- GANDLF/losses/__init__.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/GANDLF/losses/__init__.py b/GANDLF/losses/__init__.py index 4e6d04a86..4779bb87c 100644 --- a/GANDLF/losses/__init__.py +++ b/GANDLF/losses/__init__.py @@ -7,6 +7,8 @@ MCT_loss, KullbackLeiblerDivergence, FocalLoss, + MCC_loss, + MCC_log_loss, ) from .regression import CE, CEL, MSE_loss, L1_loss from .hybrid import DCCE, DCCE_Logits, DC_Focal @@ -17,7 +19,14 @@ "dc": MCD_loss, "dice": MCD_loss, "dc_log": MCD_log_loss, + "dclog": MCD_log_loss, "dice_log": MCD_log_loss, + "dicelog": MCD_log_loss, + "mcc": MCC_loss, + "mcc_log": MCC_log_loss, + "mcclog": MCC_log_loss, + "mathews": MCC_loss, + "mathews_log": MCC_log_loss, "dcce": DCCE, "dcce_logits": DCCE_Logits, "ce": CE, From 5d4eeb6fae8e4eeb109242e96af4c8b0f7ba464a Mon Sep 17 00:00:00 2001 From: sarthakpati Date: Sat, 5 Aug 2023 13:02:54 -0400 Subject: [PATCH 46/76] renamed --- GANDLF/losses/segmentation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GANDLF/losses/segmentation.py b/GANDLF/losses/segmentation.py index bbef68cdd..1992a2310 100644 --- a/GANDLF/losses/segmentation.py +++ b/GANDLF/losses/segmentation.py @@ -155,7 +155,7 @@ def MCD_log_loss( ) -def MCD_loss( +def MCC_loss( predicted: torch.Tensor, target: torch.Tensor, params: dict ) -> torch.Tensor: """ From 79c0aba0f999e6a3dfc4346996b10da4d19afa8f Mon Sep 17 00:00:00 2001 From: sarthakpati Date: Sat, 5 Aug 2023 13:03:53 -0400 Subject: [PATCH 47/76] typo fix --- GANDLF/losses/segmentation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GANDLF/losses/segmentation.py b/GANDLF/losses/segmentation.py index 1992a2310..2b2a0238a 100644 --- a/GANDLF/losses/segmentation.py +++ b/GANDLF/losses/segmentation.py @@ -308,7 +308,7 @@ def FocalLoss( def _focal_loss(preds, target, gamma, size_average=True): """ - Internal helper function to calcualte focal loss for a single class. + Internal helper function to calculate focal loss for a single class. Args: preds (torch.Tensor): predicted generally by the network From 5efb2407954211c2c71f042327229f692066777d Mon Sep 17 00:00:00 2001 From: sarthakpati Date: Sat, 5 Aug 2023 13:09:19 -0400 Subject: [PATCH 48/76] updated variable names and comments --- GANDLF/losses/segmentation.py | 35 +++++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/GANDLF/losses/segmentation.py b/GANDLF/losses/segmentation.py index 2b2a0238a..25ddbfc60 100644 --- a/GANDLF/losses/segmentation.py +++ b/GANDLF/losses/segmentation.py @@ -75,7 +75,7 @@ def generic_loss_calculator( loss_criteria (function): Loss function to use weights (list, optional): Dice weights for each class (excluding the background class), defaults to None ignore_class (int, optional): Class to ignore, defaults to None - loss_type (int, optional): Type of loss to compute, defaults to 0 + loss_type (int, optional): Type of loss to compute, defaults to 0. The options are: 0: no loss, normal dice calculation 1: dice loss, (1-dice) 2: log dice, -log(dice) @@ -83,26 +83,33 @@ def generic_loss_calculator( Returns: torch.Tensor: Mean Class Dice score """ - acc_dice = 0 + accumulated_loss = 0 + # default to a ridiculous value so that it is ignored by default + ignore_class = -1e10 if ignore_class is None else ignore_class - for i in range(num_class): # 0 is background - currentDice = loss_criteria(predicted[:, i, ...], target[:, i, ...]) + for class_index in range(num_class): + if class_index != ignore_class: + current_loss = loss_criteria( + predicted[:, class_index, ...], target[:, class_index, ...] + ) - if loss_type == 1: - currentDice = 1 - currentDice # subtract from 1 because this is a loss - elif loss_type == 2: - # negative because we want positive losses - currentDice = -torch.log(currentDice + torch.finfo(torch.float32).eps) + if loss_type == 1: + # subtract from 1 because this is a loss + current_loss = 1 - current_loss + elif loss_type == 2: + # negative because we want positive losses, and add epsilon to avoid infinities + current_loss = -torch.log(current_loss + torch.finfo(torch.float32).eps) - if weights is not None: - currentDice = currentDice * weights[i] # multiply by weight + # multiply by appropriate weight if provided + if weights is not None: + current_loss = current_loss * weights[class_index] - acc_dice += currentDice + accumulated_loss += current_loss if weights is None: - acc_dice /= num_class # we should not be considering 0 + accumulated_loss /= num_class - return acc_dice + return accumulated_loss def MCD_loss( From c8bd0bd18255243bd5789fa3763f386249778e16 Mon Sep 17 00:00:00 2001 From: sarthakpati Date: Sat, 5 Aug 2023 13:10:48 -0400 Subject: [PATCH 49/76] initialized a default loss --- GANDLF/losses/segmentation.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/GANDLF/losses/segmentation.py b/GANDLF/losses/segmentation.py index 25ddbfc60..36e9a3797 100644 --- a/GANDLF/losses/segmentation.py +++ b/GANDLF/losses/segmentation.py @@ -93,12 +93,13 @@ def generic_loss_calculator( predicted[:, class_index, ...], target[:, class_index, ...] ) - if loss_type == 1: - # subtract from 1 because this is a loss - current_loss = 1 - current_loss - elif loss_type == 2: + # subtract from 1 because this is supposed to be a loss + default_loss = 1 - current_loss + if loss_type == 2 or loss_type == "log": # negative because we want positive losses, and add epsilon to avoid infinities current_loss = -torch.log(current_loss + torch.finfo(torch.float32).eps) + else: + current_loss = default_loss # multiply by appropriate weight if provided if weights is not None: From 99d5c7ffdd38c686e8f5af90b6c9f8752b890f45 Mon Sep 17 00:00:00 2001 From: sarthakpati Date: Sat, 5 Aug 2023 13:17:55 -0400 Subject: [PATCH 50/76] black . --- GANDLF/utils/modelio.py | 1 + 1 file changed, 1 insertion(+) diff --git a/GANDLF/utils/modelio.py b/GANDLF/utils/modelio.py index e49c59cee..60a873006 100644 --- a/GANDLF/utils/modelio.py +++ b/GANDLF/utils/modelio.py @@ -99,6 +99,7 @@ def optimize_and_save_model(model, params, path, onnx_export=True): import openvino as ov from openvino.tools.mo import convert_model from openvino.runtime import get_version + openvino_present = False # check for the correct openvino version to prevent inadvertent api breaks if "2023.0.1" in get_version(): From 1513cca7213ac1799b71dd0c424c59d2529319b1 Mon Sep 17 00:00:00 2001 From: sarthakpati Date: Sat, 5 Aug 2023 13:18:07 -0400 Subject: [PATCH 51/76] this is causing problems --- GANDLF/losses/segmentation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GANDLF/losses/segmentation.py b/GANDLF/losses/segmentation.py index 36e9a3797..6b4369df6 100644 --- a/GANDLF/losses/segmentation.py +++ b/GANDLF/losses/segmentation.py @@ -60,7 +60,7 @@ def generic_loss_calculator( predicted: torch.Tensor, target: torch.Tensor, num_class: int, - loss_criteria: function, + loss_criteria, weights: list = None, ignore_class: int = None, loss_type: int = 0, From 113f8a3cdeaaae6db8d494fdc1a9c754d9b9d185 Mon Sep 17 00:00:00 2001 From: sarthakpati Date: Sat, 5 Aug 2023 13:18:22 -0400 Subject: [PATCH 52/76] added new losses to test --- testing/test_full.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/testing/test_full.py b/testing/test_full.py index 73ad8af65..28e2f00df 100644 --- a/testing/test_full.py +++ b/testing/test_full.py @@ -1250,7 +1250,9 @@ def get_parameters_after_alteration(loss_type: str) -> dict: "dcce_logits", "tversky", "focal", - "dc_focal"]: + "dc_focal", + "mcc" + "mcc_log"]: parameters, training_data = get_parameters_after_alteration(loss_type) sanitize_outputDir() TrainingManager( From 58aea3e7094e240ca5f809bc49499fe6d31a3274 Mon Sep 17 00:00:00 2001 From: sarthakpati Date: Sat, 5 Aug 2023 13:18:46 -0400 Subject: [PATCH 53/76] no need for this variable --- GANDLF/utils/modelio.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/GANDLF/utils/modelio.py b/GANDLF/utils/modelio.py index 60a873006..92f4a404f 100644 --- a/GANDLF/utils/modelio.py +++ b/GANDLF/utils/modelio.py @@ -87,8 +87,6 @@ def optimize_and_save_model(model, params, path, onnx_export=True): input_names=["input"], output_names=["output"], ) - - ov_output_dir = os.path.dirname(os.path.abspath(path)) except RuntimeWarning: print("WARNING: Cannot export to ONNX model.") return From 2852131cc83a9d91a4addf3782b3161f67dd9681 Mon Sep 17 00:00:00 2001 From: sarthakpati Date: Sat, 5 Aug 2023 13:19:46 -0400 Subject: [PATCH 54/76] added doc --- docs/customize.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/customize.md b/docs/customize.md index 24347f4cf..ff4b71f2c 100644 --- a/docs/customize.md +++ b/docs/customize.md @@ -39,7 +39,7 @@ This file contains mid-level information regarding various parameters that can b - Defined in the `loss_function` parameter of the model configuration. - By passing `weighted_loss: True`, the loss function will be weighted by the inverse of the class frequency. - This parameter controls the function which the model is trained. All options can be found [here](https://github.com/mlcommons/GaNDLF/blob/master/GANDLF/losses/__init__.py). Some examples are: - - Segmentation: dice (`dice` or `dc`), dice and cross entropy (`dcce`), focal loss (`focal`), dice and focal (`dc_focal`) + - Segmentation: dice (`dice` or `dc`), dice and cross entropy (`dcce`), focal loss (`focal`), dice and focal (`dc_focal`), mathews (`mcc`) - Classification/regression: mean squared error (`mse`) - And many more. From 8c5808b20edb22e6dad5db8dffff47bc6d1c30fe Mon Sep 17 00:00:00 2001 From: sarthakpati Date: Sat, 5 Aug 2023 13:20:12 -0400 Subject: [PATCH 55/76] updated doc --- docs/customize.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/customize.md b/docs/customize.md index ff4b71f2c..f6b9df16e 100644 --- a/docs/customize.md +++ b/docs/customize.md @@ -39,7 +39,7 @@ This file contains mid-level information regarding various parameters that can b - Defined in the `loss_function` parameter of the model configuration. - By passing `weighted_loss: True`, the loss function will be weighted by the inverse of the class frequency. - This parameter controls the function which the model is trained. All options can be found [here](https://github.com/mlcommons/GaNDLF/blob/master/GANDLF/losses/__init__.py). Some examples are: - - Segmentation: dice (`dice` or `dc`), dice and cross entropy (`dcce`), focal loss (`focal`), dice and focal (`dc_focal`), mathews (`mcc`) + - Segmentation: dice (`dice` or `dc`), dice and cross entropy (`dcce`), focal loss (`focal`), dice and focal (`dc_focal`), matthews (`mcc`) - Classification/regression: mean squared error (`mse`) - And many more. From b26360d1ea10e2f7581e54d7c20e393b16476a5c Mon Sep 17 00:00:00 2001 From: sarthakpati Date: Sat, 5 Aug 2023 13:21:04 -0400 Subject: [PATCH 56/76] updated all options --- samples/config_all_options.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/config_all_options.yaml b/samples/config_all_options.yaml index 0c0d405db..0627b45af 100644 --- a/samples/config_all_options.yaml +++ b/samples/config_all_options.yaml @@ -110,7 +110,7 @@ scheduler: max_lr: 1, } # Set which loss function you want to use - options : 'dc' - for dice only, 'dcce' - for sum of dice and CE and you can guess the next (only lower-case please) -# options: dc (dice only), dc_log (-log of dice), ce (), dcce (sum of dice and ce), mse () ... +# options: dc (dice only), dc_log (-log of dice), ce (), dcce (sum of dice and ce), focal/dc_focal, mcc/mcc_log, mse () ... # mse is the MSE defined by torch and can define a variable 'reduction'; see https://pytorch.org/docs/stable/generated/torch.nn.MSELoss.html#torch.nn.MSELoss # focal is the focal loss and can define 2 variables: gamma and size_average # use mse_torch for regression/classification problems and dice for segmentation From d68062dbfb76a5135a29c9b9e11933c012df1a49 Mon Sep 17 00:00:00 2001 From: Sarthak Pati Date: Sat, 5 Aug 2023 20:02:14 -0400 Subject: [PATCH 57/76] better formatting --- docs/usage.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/usage.md b/docs/usage.md index ac047d07c..26bf777d7 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -172,6 +172,7 @@ GaNDLF requires a YAML-based configuration that controls various aspects of the - [Classification example](https://github.com/mlcommons/GaNDLF/blob/master/samples/config_classification.yaml) **Notes**: + - More details on the configuration options are available in the [customization page](customize.md). - Ensure that the configuration has valid syntax by checking the file using any YAML validator such as [yamlchecker.com](https://yamlchecker.com/) or [yamlvalidator.com](https://yamlvalidator.com/) **before** trying to train. From f493d66d5b95f62f2cb7201878beb46e288c1c52 Mon Sep 17 00:00:00 2001 From: sarthakpati Date: Sun, 6 Aug 2023 10:01:32 -0400 Subject: [PATCH 58/76] updated checkout and renamed job names for clarity --- .github/workflows/openfl-test.yml | 5 ++--- .github/workflows/python-test.yml | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/.github/workflows/openfl-test.yml b/.github/workflows/openfl-test.yml index c33f10c79..cd3ed6628 100644 --- a/.github/workflows/openfl-test.yml +++ b/.github/workflows/openfl-test.yml @@ -11,7 +11,7 @@ on: jobs: - build: + openfl-test: runs-on: ubuntu-latest @@ -25,7 +25,7 @@ jobs: sudo rm -rf "$ANDROID_SDK_ROOT" df -h - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3 # Use changed-files-specific action to collect file changes. # The following commented condition applied to a step will run that step only if non-docs files have changed. @@ -90,7 +90,6 @@ jobs: cd openfl git fetch --tags git checkout $(git describe --tags --abbrev=0) - # pip install "tensorboardX!=2.6.2" # https://github.com/lanpa/tensorboardX/issues/716 sed -i -e 's/protobuf==3.19.6/protobuf/g' setup.py ## this should NOT be there pip install -e . cd .. diff --git a/.github/workflows/python-test.yml b/.github/workflows/python-test.yml index 5364dc8df..baefad34e 100644 --- a/.github/workflows/python-test.yml +++ b/.github/workflows/python-test.yml @@ -11,7 +11,7 @@ on: jobs: - build: + full-test: runs-on: ubuntu-latest @@ -25,7 +25,7 @@ jobs: sudo rm -rf "$ANDROID_SDK_ROOT" df -h - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3 # Use changed-files-specific action to collect file changes. # The following commented condition applied to a step will run that step only if non-docs files have changed. From a8308f543a56866c1c4284547b9b6996b1610292 Mon Sep 17 00:00:00 2001 From: Sarthak Pati Date: Sun, 6 Aug 2023 10:05:25 -0400 Subject: [PATCH 59/76] updated checkout version and test name for clarity --- .github/workflows/openfl-test.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/openfl-test.yml b/.github/workflows/openfl-test.yml index c33f10c79..cd3ed6628 100644 --- a/.github/workflows/openfl-test.yml +++ b/.github/workflows/openfl-test.yml @@ -11,7 +11,7 @@ on: jobs: - build: + openfl-test: runs-on: ubuntu-latest @@ -25,7 +25,7 @@ jobs: sudo rm -rf "$ANDROID_SDK_ROOT" df -h - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3 # Use changed-files-specific action to collect file changes. # The following commented condition applied to a step will run that step only if non-docs files have changed. @@ -90,7 +90,6 @@ jobs: cd openfl git fetch --tags git checkout $(git describe --tags --abbrev=0) - # pip install "tensorboardX!=2.6.2" # https://github.com/lanpa/tensorboardX/issues/716 sed -i -e 's/protobuf==3.19.6/protobuf/g' setup.py ## this should NOT be there pip install -e . cd .. From 66627d23dfb769b5ac63d03f5e8d9c3b6930e197 Mon Sep 17 00:00:00 2001 From: Sarthak Pati Date: Sun, 6 Aug 2023 10:05:54 -0400 Subject: [PATCH 60/76] updated checkout version and test name for clarity --- .github/workflows/python-test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/python-test.yml b/.github/workflows/python-test.yml index 5364dc8df..baefad34e 100644 --- a/.github/workflows/python-test.yml +++ b/.github/workflows/python-test.yml @@ -11,7 +11,7 @@ on: jobs: - build: + full-test: runs-on: ubuntu-latest @@ -25,7 +25,7 @@ jobs: sudo rm -rf "$ANDROID_SDK_ROOT" df -h - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3 # Use changed-files-specific action to collect file changes. # The following commented condition applied to a step will run that step only if non-docs files have changed. From bd8d9845c6351d882b85137466a96d9086d2885a Mon Sep 17 00:00:00 2001 From: Sarthak Pati Date: Sun, 6 Aug 2023 10:08:22 -0400 Subject: [PATCH 61/76] updated defaults for sgd --- GANDLF/optimizers/wrap_torch.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/GANDLF/optimizers/wrap_torch.py b/GANDLF/optimizers/wrap_torch.py index fe91be096..9a0455ec5 100644 --- a/GANDLF/optimizers/wrap_torch.py +++ b/GANDLF/optimizers/wrap_torch.py @@ -28,10 +28,10 @@ def sgd(parameters): optimizer = SGD( parameters["model_parameters"], lr=parameters.get("learning_rate"), - momentum=parameters["optimizer"].get("momentum", 0.9), - weight_decay=parameters["optimizer"].get("weight_decay", 0), + momentum=parameters["optimizer"].get("momentum", 0.99), + weight_decay=parameters["optimizer"].get("weight_decay", 3e-05), dampening=parameters["optimizer"].get("dampening", 0), - nesterov=parameters["optimizer"].get("Nesterov", False), + nesterov=parameters["optimizer"].get("nesterov", True), ) return optimizer @@ -55,7 +55,7 @@ def asgd(parameters): alpha=parameters["optimizer"].get("alpha", 0.75), t0=parameters["optimizer"].get("t0", 1e6), lambd=parameters["optimizer"].get("lambd", 1e-4), - weight_decay=parameters["optimizer"].get("weight_decay", 0), + weight_decay=parameters["optimizer"].get("weight_decay", 3e-05), ) @@ -177,7 +177,7 @@ def adadelta(parameters): lr=parameters.get("learning_rate"), rho=parameters["optimizer"].get("rho", 0.9), eps=parameters["optimizer"].get("eps", 1e-6), - weight_decay=parameters["optimizer"].get("weight_decay", 0), + weight_decay=parameters["optimizer"].get("weight_decay", 3e-05), ) @@ -199,7 +199,7 @@ def adagrad(parameters): lr=parameters.get("learning_rate"), lr_decay=parameters["optimizer"].get("lr_decay", 0), eps=parameters["optimizer"].get("eps", 1e-6), - weight_decay=parameters["optimizer"].get("weight_decay", 0), + weight_decay=parameters["optimizer"].get("weight_decay", 3e-05), ) @@ -222,7 +222,7 @@ def rmsprop(parameters): eps=parameters["optimizer"].get("eps", 1e-8), centered=parameters["optimizer"].get("centered", False), momentum=parameters["optimizer"].get("momentum", 0), - weight_decay=parameters["optimizer"].get("weight_decay", 0), + weight_decay=parameters["optimizer"].get("weight_decay", 3e-05), ) @@ -242,6 +242,6 @@ def radam(parameters): lr=parameters.get("learning_rate"), betas=parameters["optimizer"].get("betas", (0.9, 0.999)), eps=parameters["optimizer"].get("eps", 1e-8), - weight_decay=parameters["optimizer"].get("weight_decay", 0), + weight_decay=parameters["optimizer"].get("weight_decay", 3e-05), foreach=parameters["optimizer"].get("foreach", None), ) From 5fb7794b06f531df16c1b93324f0d7f7718958b7 Mon Sep 17 00:00:00 2001 From: Sarthak Pati Date: Sun, 6 Aug 2023 12:00:10 -0400 Subject: [PATCH 62/76] formatting update --- docs/usage.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/usage.md b/docs/usage.md index 26bf777d7..701e899f9 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -157,6 +157,7 @@ The following command shows how the script works: ``` **Notes**: + - For classification/regression, add a column called `ValueToPredict`. Currently, we are supporting only a single value prediction per model. - `SubjectID` or `PatientName` is used to ensure that the randomized split is done per-subject rather than per-image. - For data arrangement different to what is described above, a customized script will need to be written to generate the CSV, or you can enter the data manually into the CSV. From 34738842fffe1002f93c36433d840d52cfbb6b63 Mon Sep 17 00:00:00 2001 From: Sarthak Pati Date: Mon, 7 Aug 2023 11:53:59 -0400 Subject: [PATCH 63/76] trying update --- .github/workflows/openfl-test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/openfl-test.yml b/.github/workflows/openfl-test.yml index cd3ed6628..0b59413f2 100644 --- a/.github/workflows/openfl-test.yml +++ b/.github/workflows/openfl-test.yml @@ -69,6 +69,7 @@ jobs: - name: Install dependencies and package if: steps.changed-files-specific.outputs.only_modified == 'false' # Run on any non-docs change run: | + sudo apt-get update sudo apt-get install libvips libvips-tools -y python -m pip install --upgrade pip python -m pip install wheel From e95f6b17d31cabb8a2caa49efe2350f19284b1be Mon Sep 17 00:00:00 2001 From: Sarthak Pati Date: Mon, 7 Aug 2023 11:56:57 -0400 Subject: [PATCH 64/76] apparently manual update is needed now --- .github/workflows/python-test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/python-test.yml b/.github/workflows/python-test.yml index baefad34e..112bcce1b 100644 --- a/.github/workflows/python-test.yml +++ b/.github/workflows/python-test.yml @@ -69,6 +69,7 @@ jobs: - name: Install dependencies and package if: steps.changed-files-specific.outputs.only_modified == 'false' # Run on any non-docs change run: | + sudo apt-get update sudo apt-get install libvips libvips-tools -y python -m pip install --upgrade pip python -m pip install wheel From b68b0ee7267f5e90df01ce6f4ee5b304e8e926f6 Mon Sep 17 00:00:00 2001 From: sarthakpati Date: Mon, 7 Aug 2023 15:40:56 -0400 Subject: [PATCH 65/76] was missing a comma --- testing/test_full.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/testing/test_full.py b/testing/test_full.py index 28e2f00df..3fc1e28f2 100644 --- a/testing/test_full.py +++ b/testing/test_full.py @@ -1213,6 +1213,7 @@ def test_train_metrics_regression_rad_2d(device): def test_train_losses_segmentation_rad_2d(device): print("23: Starting 2D Rad segmentation tests for losses") + # healper function to read and parse yaml and return parameters def get_parameters_after_alteration(loss_type: str) -> dict: parameters = parseConfig( @@ -1242,17 +1243,19 @@ def get_parameters_after_alteration(loss_type: str) -> dict: parameters["model"]["print_summary"] = False parameters = populate_header_in_parameters(parameters, parameters["headers"]) return parameters, training_data + # loop through selected models and train for single epoch for loss_type in [ - "dc", - "dc_log", - "dcce", - "dcce_logits", + "dc", + "dc_log", + "dcce", + "dcce_logits", "tversky", - "focal", + "focal", "dc_focal", - "mcc" - "mcc_log"]: + "mcc", + "mcc_log", + ]: parameters, training_data = get_parameters_after_alteration(loss_type) sanitize_outputDir() TrainingManager( From 7964b08b5a79a8e066fd5ffdbd6f3482413367ca Mon Sep 17 00:00:00 2001 From: hasan7n Date: Wed, 9 Aug 2023 14:54:42 +0200 Subject: [PATCH 66/76] allow custom entrypoint script for model deploy --- GANDLF/cli/deploy.py | 16 ++++++++++++++-- gandlf_deploy | 4 ++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/GANDLF/cli/deploy.py b/GANDLF/cli/deploy.py index 9997289f8..498806fd5 100644 --- a/GANDLF/cli/deploy.py +++ b/GANDLF/cli/deploy.py @@ -35,6 +35,7 @@ def run_deployment( outputdir (str): The path to the output directory. target (str): The target to deploy to. mlcube_type (str): Either 'model' or 'metrics' + entrypoint_script (str): The path of entrypoint script. Only used for metrics and inference configfile (str, Optional): The path to the configuration file. Required for models modeldir (str, Optional): The path to the model directory. Required for models requires_gpu (str, Optional): Whether the model requires GPU. Required for models @@ -98,6 +99,7 @@ def deploy_docker_mlcube( Args: mlcubedir (str): The path to the mlcube directory. outputdir (str): The path to the output directory. + entrypoint_script (str): The path of entrypoint script. Only used for metrics and inference config (str, Optional): The path to the configuration file. Required for models modeldir (str, Optional): The path to the model directory. Required for models requires_gpu (str, Optional): Whether the model requires GPU. Required for models @@ -124,7 +126,9 @@ def deploy_docker_mlcube( # First grab the existing the mlcube config if modeldir is not None: # we can use that as an indicator if we are doing model or metrics deployment - mlcube_config = get_model_mlcube_config(mlcube_config_file, requires_gpu) + mlcube_config = get_model_mlcube_config( + mlcube_config_file, requires_gpu, entrypoint_script + ) else: mlcube_config = get_metrics_mlcube_config(mlcube_config_file, entrypoint_script) @@ -252,13 +256,14 @@ def get_metrics_mlcube_config(mlcube_config_file, entrypoint_script): return mlcube_config -def get_model_mlcube_config(mlcube_config_file, requires_gpu): +def get_model_mlcube_config(mlcube_config_file, requires_gpu, entrypoint_script): """ This function returns the mlcube config for the model. Args: mlcube_config_file (str): Path to mlcube config file. requires_gpu (bool): Whether the model requires GPU. + entrypoint_script (str): The path of entrypoint script. Only used for infer task """ mlcube_config = None with open(mlcube_config_file, "r") as f: @@ -305,6 +310,13 @@ def get_model_mlcube_config(mlcube_config_file, requires_gpu): "--device cpu", "--device cuda" ) + if entrypoint_script: + # modify the infer entrypoint to run a custom script. + device = "cuda" if requires_gpu else "cpu" + mlcube_config["tasks"]["infer"][ + "entrypoint" + ] = f"python3.8 /entrypoint.py --device {device}" + return mlcube_config # Duplicate training task into one from reset (must be explicit) and one that resumes with new data # In either case, the embedded model will not change persistently. diff --git a/gandlf_deploy b/gandlf_deploy index 6495c7005..cebb6c001 100644 --- a/gandlf_deploy +++ b/gandlf_deploy @@ -51,7 +51,7 @@ if __name__ == "__main__": "--mlcube-type", metavar="", type=str, - help="The mlcube type. Valid inputs are: " + ", ".join(deploy_targets) + " .", + help="The mlcube type. Valid inputs are: " + ", ".join(mlcube_types) + " .", required=True, ) parser.add_argument( @@ -83,7 +83,7 @@ if __name__ == "__main__": "--entrypoint", metavar="", type=str, - help="An optional custom python entrypoint script to use instead of the default specified in mlcube.yaml.", + help="An optional custom python entrypoint script to use instead of the default specified in mlcube.yaml. (Only for inference and metrics)", default=None, ) From 8c1543c44427390e5254104afa1c39c3f65a872d Mon Sep 17 00:00:00 2001 From: hasan7n Date: Wed, 9 Aug 2023 14:55:29 +0200 Subject: [PATCH 67/76] modify templates of mlcube.yaml --- mlcube/metrics_mlcube/mlcube_medperf.yaml | 2 +- mlcube/model_mlcube/mlcube_medperf.yaml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/mlcube/metrics_mlcube/mlcube_medperf.yaml b/mlcube/metrics_mlcube/mlcube_medperf.yaml index 4aa9b7cd7..29cc0cf02 100644 --- a/mlcube/metrics_mlcube/mlcube_medperf.yaml +++ b/mlcube/metrics_mlcube/mlcube_medperf.yaml @@ -21,7 +21,7 @@ platform: docker: # The image tag that will be built/pulled/used. Change to suit your organization/model:version. - image: mlcommons/gandlf:0.0.1 + image: mlcommons/gandlf-metrics:0.0.1 ## Generally, these build options will only be needed by GaNDLF maintainers. # Docker build context relative to $MLCUBE_ROOT. (gandlf_deploy can handle this automatically.) diff --git a/mlcube/model_mlcube/mlcube_medperf.yaml b/mlcube/model_mlcube/mlcube_medperf.yaml index ce7e02dc1..c4d94cbf8 100644 --- a/mlcube/model_mlcube/mlcube_medperf.yaml +++ b/mlcube/model_mlcube/mlcube_medperf.yaml @@ -24,13 +24,13 @@ platform: docker: # The image tag that will be built/pulled/used. Change to suit your organization/model:version. - image: mlcommons/gandlf:0.0.1 + image: mlcommons/gandlf-model:0.0.1 ## Generally, these build options will only be needed by GaNDLF maintainers. # Docker build context relative to $MLCUBE_ROOT. (gandlf_deploy can handle this automatically.) build_context: "../" # Docker file name within docker build context. Any "Dockerfile-*" in the GaNDLF source repo is valid. - build_file: "Dockerfile-CUDA11.6" + build_file: "Dockerfile-CPU" # These settings should usually be set by global MLCube configuration, generally not per-deployment. # However, some sane defaults (for Docker >19.03) are here: From 73bdb11f48f799dcffd26f3ae377d22e434ca3c9 Mon Sep 17 00:00:00 2001 From: hasan7n Date: Wed, 9 Aug 2023 14:55:45 +0200 Subject: [PATCH 68/76] add custom entrypoint example --- .../getting_started_3d_rad_seg.py | 28 ++++------ .../getting_started_3d_rad_seg.py | 56 +++++++++++++++++++ .../example_custom_entrypoint/template.py | 47 ++++++++++++++++ 3 files changed, 115 insertions(+), 16 deletions(-) create mode 100644 mlcube/model_mlcube/example_custom_entrypoint/getting_started_3d_rad_seg.py create mode 100644 mlcube/model_mlcube/example_custom_entrypoint/template.py diff --git a/mlcube/metrics_mlcube/example_custom_entrypoint/getting_started_3d_rad_seg.py b/mlcube/metrics_mlcube/example_custom_entrypoint/getting_started_3d_rad_seg.py index f631f2091..26f5083a2 100644 --- a/mlcube/metrics_mlcube/example_custom_entrypoint/getting_started_3d_rad_seg.py +++ b/mlcube/metrics_mlcube/example_custom_entrypoint/getting_started_3d_rad_seg.py @@ -7,21 +7,18 @@ def create_csv(predictions, labels): """This function expects `predictions` to be structured in a certain way: it should contain the following paths: `testing//_seg.nii.gz` - `labels` is expected to have a `data.csv` file containing relative paths of - labels""" + where is an integer not padded with zeros.`labels` is expected to have + a list of folders, each containing a 'mask.nii.gz'. Names of the folders are subject + IDs (integers padded with zeros to have three digit places).""" - # read and parse expected labels dict - labels_data_csv = os.path.join(labels, "data.csv") - labels_df = pd.read_csv(labels_data_csv) - for col in labels_df: - if col.lower() == "subjectid": - subjectid_header = col - if col.lower() == "label": - label_header = col - labels_records = labels_df.to_dict("records") - labels_dict = { - int(rec[subjectid_header]): rec[label_header] for rec in labels_records - } + # read and parse expected labels + labels_dict = {} + for subjectID in os.listdir(labels): + folder_path = os.path.join(labels, subjectID) + if not os.path.isdir(folder_path): + continue + label_path = os.path.join(folder_path, "mask.nii.gz") + labels_dict[int(subjectID)] = label_path # generate data input dict input_data = [] @@ -31,7 +28,6 @@ def create_csv(predictions, labels): ) pred_path = os.path.abspath(pred_path) label_path = labels_dict[int(subjectID)] - label_path = os.path.join(labels, label_path) label_path = os.path.abspath(label_path) prediction_record = { "SubjectID": subjectID, @@ -41,7 +37,7 @@ def create_csv(predictions, labels): input_data.append(prediction_record) input_data_df = pd.DataFrame(input_data) - input_data_df.to_csv("./data.csv") + input_data_df.to_csv("./data.csv", index=False) def run_gandlf(output_path, parameters_file): diff --git a/mlcube/model_mlcube/example_custom_entrypoint/getting_started_3d_rad_seg.py b/mlcube/model_mlcube/example_custom_entrypoint/getting_started_3d_rad_seg.py new file mode 100644 index 000000000..182489219 --- /dev/null +++ b/mlcube/model_mlcube/example_custom_entrypoint/getting_started_3d_rad_seg.py @@ -0,0 +1,56 @@ +import os +import argparse +import sys +import pandas as pd + + +def create_csv(data_path): + """This function expects `data_path` to be structured in a certain way: + it should contain a list of folders: each one contains a 'image.nii.gz'. + """ + input_data = [] + for subjectID in os.listdir(data_path): + folder_path = os.path.join(data_path, subjectID) + if not os.path.isdir(folder_path): + continue + image_path = os.path.join(folder_path, "image.nii.gz") + image_path = os.path.abspath(image_path) + record = { + "SubjectID": subjectID, + "Channel_0": image_path, + } + input_data.append(record) + + input_data_df = pd.DataFrame(input_data) + input_data_df.to_csv("./data.csv", index=False) + + +def run_gandlf(output_path, device): + """ + A function that calls GaNDLF's run command with the previously created csv. + + Args: + output_path (str): The path to the output file/folder + device (str): device to run on (i.e. CPU or GPU) + """ + exit_status = os.system( + "python3.8 gandlf_run --train False " + f"--device {device} --config /embedded_config.yml " + f"--modeldir /embedded_model/ -i ./data.csv -o {output_path}" + ) + exit_code = os.WEXITSTATUS(exit_status) + sys.exit(exit_code) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("--data_path", metavar="", type=str, required=True) + parser.add_argument("--output_path", metavar="", type=str, default=None) + parser.add_argument( + "--device", metavar="", type=str, required=True, choices=["cpu", "cuda"] + ) + + args = parser.parse_args() + + create_csv(args.data_path) + run_gandlf(args.output_path, args.device) diff --git a/mlcube/model_mlcube/example_custom_entrypoint/template.py b/mlcube/model_mlcube/example_custom_entrypoint/template.py new file mode 100644 index 000000000..b0a676039 --- /dev/null +++ b/mlcube/model_mlcube/example_custom_entrypoint/template.py @@ -0,0 +1,47 @@ +"""If input folder to the MLCube does not contain a csv file that defines the data cases, +a custom entrypoint is needed to create a temporary csv file before calling GaNDLF's run command. +This script should expect the same arguments passed to the command `mlcube run --task infer`, +i.e. it should expect the inputs and outputs defined in `mlcube.yaml` in the `infer` task. +Note that the device argument will be set by gandlf_deploy (gandlf_deploy will run the entrypoint +with --device).""" + +import os +import argparse +import sys + + +def create_csv(data_path): + """A function that creates a ./data.csv file from input folder.""" + # Add your logic here + raise NotImplementedError + + +def run_gandlf(output_path, device): + """ + A function that calls GaNDLF's generate metrics command with the previously created csv. + + Args: + output_path (str): The path to the output file/folder + parameters_file (str): The path to the parameters file + """ + exit_status = os.system( + "python3.8 gandlf_run --train False " + f"--device {device} --config /embedded_config.yml " + f"--modeldir /embedded_model/ -i ./data.csv -o {output_path}" + ) + exit_code = os.WEXITSTATUS(exit_status) + sys.exit(exit_code) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("--data_path", metavar="", type=str, required=True) + parser.add_argument("--output_path", metavar="", type=str, default=None) + parser.add_argument( + "--device", metavar="", type=str, required=True, choices=["cpu", "cuda"] + ) + + args = parser.parse_args() + + create_csv(args.data_path) + run_gandlf(args.output_path, args.device) From 30fa96cd3a07c1270bc8e7557d59712c9ff75d12 Mon Sep 17 00:00:00 2001 From: hasan7n Date: Wed, 9 Aug 2023 14:55:55 +0200 Subject: [PATCH 69/76] add tests --- .github/workflows/mlcube-test.yml | 82 ++++++++++++++++++++++ testing/test_deploy.sh | 112 ++++++++++++++++++++++++++++++ 2 files changed, 194 insertions(+) create mode 100644 .github/workflows/mlcube-test.yml create mode 100644 testing/test_deploy.sh diff --git a/.github/workflows/mlcube-test.yml b/.github/workflows/mlcube-test.yml new file mode 100644 index 000000000..8306011f8 --- /dev/null +++ b/.github/workflows/mlcube-test.yml @@ -0,0 +1,82 @@ +# This workflow will test gandlf_deploy for model and metrics MLCubes + +name: CI-PyTest + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + + +jobs: + test-deploy: + + runs-on: ubuntu-latest + + steps: + - name: Free space + run: | + df -h + sudo rm -rf /opt/ghc + sudo rm -rf "/usr/local/share/boost" + sudo rm -rf "$AGENT_TOOLSDIRECTORY" + sudo rm -rf "$ANDROID_SDK_ROOT" + df -h + - name: Checkout + uses: actions/checkout@v3 + + # Use changed-files-specific action to collect file changes. + # The following commented condition applied to a step will run that step only if non-docs files have changed. + # It should be applied to all functionality-related steps. + # if: steps.changed-files-specific.outputs.only_modified == 'false' + - name: Detect and screen file changes + id: changed-files-specific + uses: tj-actions/changed-files@v34 + with: + files: | + .github/*.md + .github/ISSUE_TEMPLATE/*.md + .github/workflows/devcontainer.yml + .github/workflows/docker-image.yml + .devcontainer/** + docs/** + mlcube/** + *.md + LICENSE + Dockerfile-* + + - name: Summarize docs and non-docs modifications + run: | + echo "List of docs files that have changed: ${{ steps.changed-files-specific.outputs.all_modified_files }}" + echo "Changed non-docs files: ${{ steps.changed-files-specific.outputs.other_modified_files }}" + + # This second step is unnecessary but highly recommended because + # It will cache database and saves time re-downloading it if database isn't stale. + - name: Cache pip + uses: actions/cache@v3 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }} + restore-keys: | + ${{ runner.os }}-pip- + - name: Set up Python 3.8 + if: steps.changed-files-specific.outputs.only_modified == 'false' # Run on any non-docs change + uses: actions/setup-python@v4 + with: + python-version: 3.8 + - name: Install dependencies and package + if: steps.changed-files-specific.outputs.only_modified == 'false' # Run on any non-docs change + run: | + sudo apt-get update + sudo apt-get install libvips libvips-tools -y + python -m pip install --upgrade pip + python -m pip install wheel + python -m pip install openvino-dev==2023.0.1 mlcube_docker + pip install torch==1.13.1+cpu torchvision==0.14.1+cpu torchaudio==0.13.1 --extra-index-url https://download.pytorch.org/whl/cpu + pip install -e . + - name: Run mlcube deploy tests + working-directory: ./testing + if: steps.changed-files-specific.outputs.only_modified == 'false' # Run on any non-docs change + run: | + sh test_deploy.sh diff --git a/testing/test_deploy.sh b/testing/test_deploy.sh new file mode 100644 index 000000000..43ff7d531 --- /dev/null +++ b/testing/test_deploy.sh @@ -0,0 +1,112 @@ +############### +#### Setup #### +############### + +MODEL_MLCUBE_TEMPLATE=../../mlcube/model_mlcube/mlcube_medperf.yaml +METRICS_MLCUBE_TEMPLATE=../../mlcube/metrics_mlcube/mlcube_medperf.yaml + +MODEL_MLCUBE_ENTRYPOINT=../../mlcube/model_mlcube/example_custom_entrypoint/getting_started_3d_rad_seg.py +METRICS_MLCUBE_ENTRYPOINT=../../mlcube/metrics_mlcube/example_custom_entrypoint/getting_started_3d_rad_seg.py + +# Create a workspace +mkdir -p test_deploy +cd test_deploy + +# Download the data +FILENAME=y8162xkq1zz5555ye3pwadry2m2e39bs.zip +wget https://upenn.box.com/shared/static/$FILENAME +unzip $FILENAME +mv data/3d_rad_segmentation . +rm $FILENAME +rm -rf data + +# Setup the data CSV for training +echo "SubjectID,Channel_0,Label +001,3d_rad_segmentation/001/image.nii.gz,3d_rad_segmentation/001/mask.nii.gz +002,3d_rad_segmentation/002/image.nii.gz,3d_rad_segmentation/002/mask.nii.gz +003,3d_rad_segmentation/003/image.nii.gz,3d_rad_segmentation/003/mask.nii.gz +004,3d_rad_segmentation/004/image.nii.gz,3d_rad_segmentation/004/mask.nii.gz +005,3d_rad_segmentation/005/image.nii.gz,3d_rad_segmentation/005/mask.nii.gz +006,3d_rad_segmentation/006/image.nii.gz,3d_rad_segmentation/006/mask.nii.gz +007,3d_rad_segmentation/007/image.nii.gz,3d_rad_segmentation/007/mask.nii.gz +008,3d_rad_segmentation/008/image.nii.gz,3d_rad_segmentation/008/mask.nii.gz +009,3d_rad_segmentation/009/image.nii.gz,3d_rad_segmentation/009/mask.nii.gz +010,3d_rad_segmentation/010/image.nii.gz,3d_rad_segmentation/010/mask.nii.gz" >> data.csv + +# Setup config file +cp ../../samples/config_getting_started_segmentation_rad3d.yaml . + +################## +#### Training #### +################## + +gandlf_run \ + -c ./config_getting_started_segmentation_rad3d.yaml \ + -i ./data.csv \ + -m ./trained_model_output \ + -t True \ + -d cpu + +# remove data.csv to assume that we need a custom script with gandlf deploy +rm data.csv + +################ +#### deploy #### +################ + +# deploy model +mkdir model_mlcube +cp $MODEL_MLCUBE_TEMPLATE model_mlcube/mlcube.yaml + +gandlf_deploy \ + -c ./config_getting_started_segmentation_rad3d.yaml \ + -m ./trained_model_output \ + --target docker \ + --mlcube-root ./model_mlcube \ + -o ./built_model_mlcube \ + --mlcube-type model \ + -g False \ + --entrypoint $MODEL_MLCUBE_ENTRYPOINT + +# deploy metrics +mkdir metrics_mlcube +cp $METRICS_MLCUBE_TEMPLATE metrics_mlcube/mlcube.yaml + +gandlf_deploy \ + --target docker \ + --mlcube-root ./metrics_mlcube \ + -o ./built_metrics_mlcube \ + --mlcube-type metrics \ + --entrypoint $METRICS_MLCUBE_ENTRYPOINT + +###################### +#### run pipeline #### +###################### + +mlcube run \ + --mlcube ./built_model_mlcube \ + --task infer \ + data_path=../../3d_rad_segmentation \ + output_path=../../predictions + +mlcube run \ + --mlcube ./built_metrics_mlcube \ + --task evaluate \ + predictions=../../predictions \ + labels=../../3d_rad_segmentation \ + output_path=../../results.yaml \ + parameters_file=../../config_getting_started_segmentation_rad3d.yaml + + +############### +#### check #### +############### + +if [ -f "results.yaml" ]; then + echo "Success" + cd .. + sudo rm -rf ./test_deploy/ +else + echo "Failure" + exit 1 +fi From e61329b70db4a3e9df80dcc4a010f3258ced79b8 Mon Sep 17 00:00:00 2001 From: hasan7n Date: Wed, 9 Aug 2023 15:18:56 +0200 Subject: [PATCH 70/76] add shebang per codacy suggestion --- testing/test_deploy.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/testing/test_deploy.sh b/testing/test_deploy.sh index 43ff7d531..bb5d1dfaa 100644 --- a/testing/test_deploy.sh +++ b/testing/test_deploy.sh @@ -1,3 +1,5 @@ +#! /bin/sh + ############### #### Setup #### ############### From 2577803312259e93c393a67347060fa988ef749c Mon Sep 17 00:00:00 2001 From: hasan7n Date: Wed, 9 Aug 2023 15:19:11 +0200 Subject: [PATCH 71/76] fix mlcube ci test workflow name --- .github/workflows/mlcube-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/mlcube-test.yml b/.github/workflows/mlcube-test.yml index 8306011f8..0897fb573 100644 --- a/.github/workflows/mlcube-test.yml +++ b/.github/workflows/mlcube-test.yml @@ -1,6 +1,6 @@ # This workflow will test gandlf_deploy for model and metrics MLCubes -name: CI-PyTest +name: MLCube-Test on: push: From 1d492aa87a9cd284e87764577c15226e20563225 Mon Sep 17 00:00:00 2001 From: FelixSteinbauer <43598998+FelixSteinbauer@users.noreply.github.com> Date: Thu, 10 Aug 2023 16:21:29 +0200 Subject: [PATCH 72/76] Use tuple for data_range in PSNR instead of range size --- GANDLF/metrics/synthesis.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/GANDLF/metrics/synthesis.py b/GANDLF/metrics/synthesis.py index fd3590a58..3b56e61b0 100644 --- a/GANDLF/metrics/synthesis.py +++ b/GANDLF/metrics/synthesis.py @@ -55,7 +55,7 @@ def peak_signal_noise_ratio(target, prediction, data_range=None, epsilon=None) - Args: target (torch.Tensor): The target tensor. prediction (torch.Tensor): The prediction tensor. - data_range (float, optional): If not None, this data range is used as enumerator instead of computing it from the given data. Defaults to None. + data_range (tuple, optional): If not None, this data range (min, max) is used as enumerator instead of computing it from the given data. Defaults to None. epsilon (float, optional): If not None, this epsilon is added to the denominator of the fraction to avoid infinity as output. Defaults to None. """ @@ -67,8 +67,9 @@ def peak_signal_noise_ratio(target, prediction, data_range=None, epsilon=None) - if data_range == None: #compute data_range like torchmetrics if not given min_v = 0 if torch.min(target) > 0 else torch.min(target) #look at this line max_v = torch.max(target) - data_range = max_v - min_v - return 10.0 * torch.log10((data_range ** 2) / (mse + epsilon)) + else: + min_v, max_v = data_range + return 10.0 * torch.log10(((max_v-min_v) ** 2) / (mse + epsilon)) def mean_squared_log_error(target, prediction) -> torch.Tensor: From 5690df784311a619b900418d14b1d1e8941dcf2e Mon Sep 17 00:00:00 2001 From: FelixSteinbauer <43598998+FelixSteinbauer@users.noreply.github.com> Date: Thu, 10 Aug 2023 16:31:00 +0200 Subject: [PATCH 73/76] Use tuple for datat range & fixed the missing range in the psnr_01_eps case --- GANDLF/cli/generate_metrics.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/GANDLF/cli/generate_metrics.py b/GANDLF/cli/generate_metrics.py index 07423a3fc..aa47e5f69 100644 --- a/GANDLF/cli/generate_metrics.py +++ b/GANDLF/cli/generate_metrics.py @@ -301,14 +301,14 @@ def __percentile_clip(input_tensor, reference_tensor=None, p_min=0.5, p_max=99.5 overall_stats_dict[current_subject_id][ "psnr_01" ] = peak_signal_noise_ratio( - gt_image_infill, output_infill, data_range=1.0 + gt_image_infill, output_infill, data_range=(0,1) ).item() # same as above but with epsilon for robustness overall_stats_dict[current_subject_id][ "psnr_01_eps" ] = peak_signal_noise_ratio( - gt_image_infill, output_infill, epsilon=sys.float_info.epsilon + gt_image_infill, output_infill, data_range=(0,1), epsilon=sys.float_info.epsilon ).item() pprint(overall_stats_dict) From 24d175f10cb95f87470bff8791325c7a261f246d Mon Sep 17 00:00:00 2001 From: FelixSteinbauer <43598998+FelixSteinbauer@users.noreply.github.com> Date: Thu, 10 Aug 2023 17:22:00 +0200 Subject: [PATCH 74/76] Added range computation for the torchmetrics call --- GANDLF/metrics/synthesis.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GANDLF/metrics/synthesis.py b/GANDLF/metrics/synthesis.py index 3b56e61b0..9aa4da466 100644 --- a/GANDLF/metrics/synthesis.py +++ b/GANDLF/metrics/synthesis.py @@ -60,7 +60,7 @@ def peak_signal_noise_ratio(target, prediction, data_range=None, epsilon=None) - """ if epsilon == None: - psnr = PeakSignalNoiseRatio(data_range=data_range) + psnr = PeakSignalNoiseRatio() if data_range == None else PeakSignalNoiseRatio(data_range=data_range[1]-data_range[0]) return psnr(preds=prediction, target=target) else: # implementation of PSNR that does not give 'inf'/'nan' when 'mse==0' mse = mean_squared_error(target, prediction) From 60f35d0fbcc12edda4d7f01a2d4069467ed647d1 Mon Sep 17 00:00:00 2001 From: hasan7n Date: Mon, 14 Aug 2023 03:35:26 +0200 Subject: [PATCH 75/76] add unittest for custom entrypoint --- testing/test_full.py | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/testing/test_full.py b/testing/test_full.py index 3fc1e28f2..2e0ad0bae 100644 --- a/testing/test_full.py +++ b/testing/test_full.py @@ -2941,17 +2941,25 @@ def test_generic_deploy_docker(): reset=True, ) - result = run_deployment( - os.path.join(gandlfRootDir, "mlcube/model_mlcube/"), - deploymentOutputDir, - "docker", - "model", - configfile=testingDir + "/config_segmentation.yaml", - modeldir=outputDir, - requires_gpu=True, - ) - - assert result, "run_deployment returned false" + custom_entrypoint = os.path.join( + gandlfRootDir, + "mlcube/model_mlcube/example_custom_entrypoint/getting_started_3d_rad_seg.py", + ) + for entrypoint_script in [None, custom_entrypoint]: + result = run_deployment( + os.path.join(gandlfRootDir, "mlcube/model_mlcube/"), + deploymentOutputDir, + "docker", + "model", + entrypoint_script=entrypoint_script, + configfile=testingDir + "/config_segmentation.yaml", + modeldir=outputDir, + requires_gpu=True, + ) + msg = "run_deployment returned false" + if entrypoint_script: + msg += " with custom entrypoint script" + assert result, msg sanitize_outputDir() print("passed") From bc0d3fa6c25b75728fbd9796380d9b82c5a2583f Mon Sep 17 00:00:00 2001 From: FelixSteinbauer <43598998+FelixSteinbauer@users.noreply.github.com> Date: Thu, 24 Aug 2023 18:28:25 +0200 Subject: [PATCH 76/76] Added parameter to toggle NCC computation Introduced a parameter (compute_ncc, defaut=True) that allows to turn off the (time-consuming) computation of the 4 NCC metrics. --- GANDLF/cli/generate_metrics.py | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/GANDLF/cli/generate_metrics.py b/GANDLF/cli/generate_metrics.py index aa47e5f69..eaef3f77f 100644 --- a/GANDLF/cli/generate_metrics.py +++ b/GANDLF/cli/generate_metrics.py @@ -253,18 +253,20 @@ def __percentile_clip(input_tensor, reference_tensor=None, p_min=0.5, p_max=99.5 ] = structural_similarity_index(gt_image_infill, output_infill, mask).item() # ncc metrics - overall_stats_dict[current_subject_id]["ncc_mean"] = ncc_mean( - gt_image_infill, output_infill - ) - overall_stats_dict[current_subject_id]["ncc_std"] = ncc_std( - gt_image_infill, output_infill - ) - overall_stats_dict[current_subject_id]["ncc_max"] = ncc_max( - gt_image_infill, output_infill - ) - overall_stats_dict[current_subject_id]["ncc_min"] = ncc_min( - gt_image_infill, output_infill - ) + compute_ncc = parameters.get("compute_ncc", True) + if compute_ncc: + overall_stats_dict[current_subject_id]["ncc_mean"] = ncc_mean( + gt_image_infill, output_infill + ) + overall_stats_dict[current_subject_id]["ncc_std"] = ncc_std( + gt_image_infill, output_infill + ) + overall_stats_dict[current_subject_id]["ncc_max"] = ncc_max( + gt_image_infill, output_infill + ) + overall_stats_dict[current_subject_id]["ncc_min"] = ncc_min( + gt_image_infill, output_infill + ) # only voxels that are to be inferred (-> flat array) # these are required for mse, psnr, etc.