From 2153cfd5c1df1b3842d9fd3718513dcbc3415ff0 Mon Sep 17 00:00:00 2001 From: MStarmans91 Date: Fri, 15 Jul 2022 13:47:09 +0200 Subject: [PATCH 01/30] Update resample helper function, make error more explainable --- WORC/WORC.py | 2 +- WORC/doc/static/quick_start.rst | 8 +++++++- WORC/processing/helpers.py | 24 +++++++++++++++++++----- 3 files changed, 27 insertions(+), 7 deletions(-) diff --git a/WORC/WORC.py b/WORC/WORC.py index 60cfa116..cc1327ec 100644 --- a/WORC/WORC.py +++ b/WORC/WORC.py @@ -1077,7 +1077,7 @@ def build_training(self): self.links_fingerprinting['classification'].collapse = 'train' else: - raise WORCexceptions.WORCIOError("Please provide labels.") + raise WORCexceptions.WORCIOError("Please provide labels for training, i.e., WORC.labels_train or SimpleWORC.labels_from_this_file.") else: raise WORCexceptions.WORCIOError("Please provide either images or features.") diff --git a/WORC/doc/static/quick_start.rst b/WORC/doc/static/quick_start.rst index 1289e11b..070680ab 100644 --- a/WORC/doc/static/quick_start.rst +++ b/WORC/doc/static/quick_start.rst @@ -187,7 +187,7 @@ After defining the inputs, the following code can be used to run your first expe # Valid quantitative types are ['CT', 'PET', 'Thermography', 'ADC'] # Valid qualitative types are ['MRI', 'DWI', 'US'] experiment.set_image_types(['CT']) - + # Use the standard workflow for your specific modus if modus == 'binary_classification': experiment.binary_classification(coarse=coarse) @@ -225,12 +225,18 @@ named after your experiment name. 'Features', 'features_*.hdf5')) + if len(feature_files) == 0: + raise ValueError('No feature files found: your network has failed.') + feature_files.sort() featurefile_p1 = feature_files[0] features_p1 = pd.read_hdf(featurefile_p1) # Read the overall peformance performance_file = os.path.join(experiment_folder, 'performance_all_0.json') + if not os.path.exists(performance_file): + raise ValueError(f'No performance file {performance_file} found: your network has failed.') + with open(performance_file, 'r') as fp: performance = json.load(fp) diff --git a/WORC/processing/helpers.py b/WORC/processing/helpers.py index 450622bb..ab305206 100644 --- a/WORC/processing/helpers.py +++ b/WORC/processing/helpers.py @@ -19,7 +19,8 @@ import numpy as np -def resample_image(image, new_spacing, interpolator=sitk.sitkBSpline): +def resample_image(image, new_spacing=None, new_size=None, + interpolator=sitk.sitkBSpline): """Resample an image to another spacing. Parameters @@ -34,6 +35,12 @@ def resample_image(image, new_spacing, interpolator=sitk.sitkBSpline): resampled_image : ITK Image Output image. """ + if new_spacing is not None and new_size is not None: + raise ValueError('Either provide resample_spacing OR resample_size as input!') + + if new_spacing is None and new_size is None: + raise ValueError('Either provide resample_spacing OR resample_size as input!') + # Get original settings original_size = image.GetSize() original_spacing = image.GetSpacing() @@ -44,10 +51,17 @@ def resample_image(image, new_spacing, interpolator=sitk.sitkBSpline): if len(original_spacing) == 2: original_spacing = original_spacing + (1.0, ) - # Compute output size - new_size = [int(original_size[0]*original_spacing[0]/new_spacing[0]), - int(original_size[1]*original_spacing[1]/new_spacing[1]), - int(original_size[2]*original_spacing[2]/new_spacing[2])] + if new_size is None + # Compute output size + new_size = [int(original_size[0]*original_spacing[0]/new_spacing[0]), + int(original_size[1]*original_spacing[1]/new_spacing[1]), + int(original_size[2]*original_spacing[2]/new_spacing[2])] + + if new_spacing is None + # Compute output spacing + new_spacing = [original_size[0]*original_spacing[0]/new_size[0], + original_size[1]*original_spacing[1]/new_size[1], + original_size[2]*original_spacing[2]/new_size[2]] # Set and execute the filter ResampleFilter = sitk.ResampleImageFilter() From 8dee09f58559118c435ae90e75fc98d48f954418 Mon Sep 17 00:00:00 2001 From: MStarmans91 Date: Wed, 27 Jul 2022 17:02:22 +0200 Subject: [PATCH 02/30] Bug fix, add question to the FAQ --- WORC/doc/static/faq.rst | 16 +++++++++++++++- WORC/processing/helpers.py | 4 ++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/WORC/doc/static/faq.rst b/WORC/doc/static/faq.rst index 8fe1d720..a8b419fe 100644 --- a/WORC/doc/static/faq.rst +++ b/WORC/doc/static/faq.rst @@ -35,7 +35,7 @@ from your label file occurs in the filename of your inputs. For example, when using the example label file from the `WORC tutorial `_, if your Patient ID is not listed in column 1, this error will occur. -Error: ``File "...\lib\site-packages\numpy\lib\function_base.py", line 4406,`` `` in delete keep[obj,] = False`` ``IndexError: arrays used as indices must be of integer (or boolean) type`` +Error: ``File "...\lib\site-packages\numpy\lib\function_base.py", line 4406, in delete keep[obj,] = False IndexError: arrays used as indices must be of integer (or boolean) type`` ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ This is an error in PyRadiomics 3.0, see also `this issue `_. It has @@ -146,3 +146,17 @@ Altneratively, when using ``BasicWORC``, you can append dictionaries to the the patient names, and as values the paths to the feature files, e.g. ``feature_dict = {'Patient1': '/path/to/featurespatient1.hdf5', 'Patient2': '/path/to/someotherrandandomfolderwith/featurespatient2.hdf5'...}``. + +How to change the temporary and output folders? +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +``WORC`` makes use of the ``fastr`` workflow engine to manage and execute +the experiment, and thus also to manage and produce the output. These folders +can be configured in the ``fastr`` config (https://fastr.readthedocs.io/en/stable/static/file_description.html#config-file). +The ``fastr`` config files can be found in a hidden folder .fastr in your home folder. +``WORC`` adds an additional config file to the config.d folder of ``fastr``: +https://github.com/MStarmans91/WORC/blob/master/WORC/fastrconfig/WORC_config.py. + +The two mounts that determine the temporary and output folders and thus which +you have to change are: +- Temporary output: ``mounts['tmp']`` in the ~/.fastr/config.py file +- Final output: ``mounts['output']`` in the ~/.fastr/config.d/WORC_config.py file diff --git a/WORC/processing/helpers.py b/WORC/processing/helpers.py index ab305206..8354e839 100644 --- a/WORC/processing/helpers.py +++ b/WORC/processing/helpers.py @@ -51,13 +51,13 @@ def resample_image(image, new_spacing=None, new_size=None, if len(original_spacing) == 2: original_spacing = original_spacing + (1.0, ) - if new_size is None + if new_size is None: # Compute output size new_size = [int(original_size[0]*original_spacing[0]/new_spacing[0]), int(original_size[1]*original_spacing[1]/new_spacing[1]), int(original_size[2]*original_spacing[2]/new_spacing[2])] - if new_spacing is None + if new_spacing is None: # Compute output spacing new_spacing = [original_size[0]*original_spacing[0]/new_size[0], original_size[1]*original_spacing[1]/new_size[1], From 3eae155dd9d8c56967b0523c934791e0632778ab Mon Sep 17 00:00:00 2001 From: Martijn Date: Wed, 14 Sep 2022 15:29:06 +0200 Subject: [PATCH 03/30] Fix bug when using elastix in WORC. --- CHANGELOG | 7 ++++++ WORC/WORC.py | 23 ++++++++++---------- WORC/tools/fingerprinting.py | 41 ++++++++++++++++++++---------------- 3 files changed, 42 insertions(+), 29 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index bc2a3531..cb6283fb 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -6,6 +6,13 @@ All notable changes to this project will be documented in this file. The format is based on `Keep a Changelog `_ and this project adheres to `Semantic Versioning `_ +3.6.1 - Unreleased +------------------ + +Fixed +~~~~~ +- Bug when using elastix, dimensionality got wrong name in fastr network. + 3.6.0 - 2022-04-05 ------------------ diff --git a/WORC/WORC.py b/WORC/WORC.py index cc1327ec..61b3fe4b 100644 --- a/WORC/WORC.py +++ b/WORC/WORC.py @@ -662,9 +662,9 @@ def build_training(self): # Optional SMAC output if self.configs[0]['SMAC']['use'] == 'True': - self.sink_smac_results = self.network.create_sink('JsonFile', id='smac_results', - step_id='general_sinks') - self.sink_smac_results.input = self.classify.outputs['smac_results'] + self.sink_smac_results = self.network.create_sink('JsonFile', id='smac_results', + step_id='general_sinks') + self.sink_smac_results.input = self.classify.outputs['smac_results'] if self.TrainTest: # FIXME: the naming here is ugly @@ -942,9 +942,15 @@ def build_training(self): elif self.segmode == 'Register': # --------------------------------------------- # Registration nodes: Align segmentation of first - # modality to others using registration ith Elastix + # modality to others using registration with Elastix self.add_elastix(label, nmod) + # Add to fingerprinting if required + if self.configs[0]['General']['Fingerprint'] == 'True': + # Since there are no segmentations yet of this modality, just use those of the first, provided modality + self.links_fingerprinting[f'{label}_segmentations'] = self.network.create_link(self.converters_seg_train[self.modlabels[0]].outputs['image'], self.node_fingerprinters[label].inputs['segmentations_train']) + self.links_fingerprinting[f'{label}_segmentations'].collapse = 'train' + # ----------------------------------------------------- # Optionally, add segmentix, the in-house segmentation # processor of WORC @@ -1401,7 +1407,7 @@ def add_elastix(self, label, nmod): self.sources_segmentations_train[label] =\ self.network.create_source('ITKImageFile', id='segmentations_train_' + label, - node_group='input', + node_group='train', step_id='train_sources') self.converters_seg_train[label] =\ @@ -1418,7 +1424,7 @@ def add_elastix(self, label, nmod): self.sources_segmentations_test[label] =\ self.network.create_source('ITKImageFile', id='segmentations_test_' + label, - node_group='input', + node_group='test', step_id='test_sources') self.converters_seg_test[label] =\ @@ -1641,11 +1647,6 @@ def add_elastix(self, label, nmod): self.calcfeatures_test[label][i_node].inputs['segmentation'] =\ self.transformix_seg_nodes_test[label].outputs['image'] - # Add to fingerprinting if required - if self.configs[0]['General']['Fingerprint'] == 'True': - self.links_fingerprinting[f'{label}_segmentations'] = self.network.create_link(self.transformix_seg_nodes_train[label].outputs['image'], self.node_fingerprinters[label].inputs['segmentations_train']) - self.links_fingerprinting[f'{label}_segmentations'].collapse = 'train' - # Save outputfor the training set self.sinks_transformations_train[label] =\ self.network.create_sink('ElastixTransformFile', diff --git a/WORC/tools/fingerprinting.py b/WORC/tools/fingerprinting.py index 9b6d694c..b9acec79 100644 --- a/WORC/tools/fingerprinting.py +++ b/WORC/tools/fingerprinting.py @@ -117,7 +117,10 @@ def execute(self): max_num_images = int(config['Fingerprinting']['max_num_image']) if len(self.images) > max_num_images: self.images = self.images[0:max_num_images] - self.segmentations = self.segmentations[0:max_num_images] + # FIXME + if self.segmentations is not None: + print('FIXME: segmentations is None') + self.segmentations = self.segmentations[0:max_num_images] for imagefile in self.images: image = sitk.ReadImage(imagefile) @@ -148,23 +151,25 @@ def execute(self): config['ImageFeatures']['phase'] = 'True' # Check if segmentations are 2D or 3D - num_masked_slices_all = list() - for segmentationfile in self.segmentations: - segmentation = sitk.GetArrayFromImage(sitk.ReadImage(segmentationfile)) - segmentation = segmentation.astype(np.bool) - num_masked_slices = len(np.flatnonzero(np.any(segmentation, axis=(1, 2)))) - num_masked_slices_all.append(num_masked_slices) - - if all(elem == 1 for elem in num_masked_slices_all): - print('All masks only contain one slice, so turn of 3D features.') - # NOTE: PREDICT will mostly switch itself between these features by looking at the masks - config['ImageFeatures']['extraction_mode'] = '2D' - - config['ImageFeatures']['orientation'] = 'False' - config['ImageFeatures']['texture_GLCMMS'] = 'False' - - # For PyRadiomics, only this parameter needs to be changed - config['PyRadiomics']['force2D'] = 'True' + # FIXME + if self.segmentations is not None: + num_masked_slices_all = list() + for segmentationfile in self.segmentations: + segmentation = sitk.GetArrayFromImage(sitk.ReadImage(segmentationfile)) + segmentation = segmentation.astype(np.bool) + num_masked_slices = len(np.flatnonzero(np.any(segmentation, axis=(1, 2)))) + num_masked_slices_all.append(num_masked_slices) + + if all(elem == 1 for elem in num_masked_slices_all): + print('All masks only contain one slice, so turn of 3D features.') + # NOTE: PREDICT will mostly switch itself between these features by looking at the masks + config['ImageFeatures']['extraction_mode'] = '2D' + + config['ImageFeatures']['orientation'] = 'False' + config['ImageFeatures']['texture_GLCMMS'] = 'False' + + # For PyRadiomics, only this parameter needs to be changed + config['PyRadiomics']['force2D'] = 'True' else: raise WORCValueError(f'Type {type} is not valid for fingeprinting. Should be one of ["classification", "images"].') From 1406679fe7cd56c50e1d4adc303e7c57247a749a Mon Sep 17 00:00:00 2001 From: MStarmans91 Date: Mon, 19 Sep 2022 15:06:10 +0200 Subject: [PATCH 04/30] See changelog: major change is added saving of validation workflows, several bugfixes --- .gitignore | 4 + CHANGELOG | 18 + README.md | 6 +- WORC/IOparser/config_io_classifier.py | 11 +- WORC/WORC.py | 4 +- WORC/classification/SearchCV.py | 340 ++++++++++----- WORC/classification/crossval.py | 84 ++-- WORC/classification/fitandscore.py | 388 +++++++++++------- WORC/classification/parameter_optimization.py | 16 +- WORC/doc/generate_config.py | 12 +- WORC/doc/static/faq.rst | 12 + WORC/doc/static/quick_start.rst | 16 +- WORC/facade/basicworc.py | 4 +- WORC/facade/simpleworc.py | 13 + .../fastr_tools/worc/bin/fitandscore_tool.py | 8 +- WORC/tests/test_RSEnsemble.py | 40 ++ WORC/tools/createfixedsplits.py | 103 +++-- WORC/validators/preflightcheck.py | 2 +- 18 files changed, 736 insertions(+), 345 deletions(-) create mode 100644 WORC/tests/test_RSEnsemble.py diff --git a/.gitignore b/.gitignore index c62c35f9..9b27bf7f 100644 --- a/.gitignore +++ b/.gitignore @@ -127,10 +127,14 @@ dmypy.json # Pyre type checker .pyre/ +# Visual studio code config +.vscode + # Example data WORC/exampledata/*.hdf5 WORC/external/* WORC/exampledata/ICCvalues.csv WORC/tests/*.png WORC/tests/*.mat +WORC/tests/performance*.json WORC/tests/WORC_Example_STWStrategyHN_* diff --git a/CHANGELOG b/CHANGELOG index cb6283fb..df00d03e 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -12,6 +12,24 @@ and this project adheres to `Semantic Versioning `_ Fixed ~~~~~ - Bug when using elastix, dimensionality got wrong name in fastr network. +- Bug in BasicWORC when starting from features +- For statistical test thresholding, if refit does not work during ensembling, skip this method + instead of returning NaN. +- Createfixedsplits function was outdated, updates to newest conventions and added + to documentation. + +Changed +~~~~~~~ +- When part of fiting and scoring a workflow fails, during optimization return NaN as performance, + but during refitting skip that step. During optimization, if we skip it, the relation between + the hyperparameter and performance gets disturbed. During refitting, we need to have a model, + so best option is to skip the step. Previously, there was only skipping. + +Added +~~~~~ +- Documentation updates +- Option to save the workflows trained on the train-validation training datasets, besides the option to save workflows + trained on the full training dataset. Not an option for SMAC due to implementation of SMAC. 3.6.0 - 2022-04-05 ------------------ diff --git a/README.md b/README.md index 4a736dfd..c3c2072f 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# WORC v3.6.0 +# WORC v3.6.1 ## Workflow for Optimal Radiomics Classification ## Information @@ -70,14 +70,12 @@ The official documentation can be found at [https://worc.readthedocs.io](https:/ The publicly released WORC database is described in the following paper: ```bibtex -@article {Starmans2021.08.19.21262238, +@article {Starmans2021WORCDatabase, author = {Starmans, Martijn P.A. and Timbergen, Milea J.M. and Vos, Melissa and Padmos, Guillaume A. and Gr{\"u}nhagen, Dirk J. and Verhoef, Cornelis and Sleijfer, Stefan and van Leenders, Geert J.L.H. and Buisman, Florian E. and Willemssen, Francois E.J.A. and Koerkamp, Bas Groot and Angus, Lindsay and van der Veldt, Astrid A.M. and Rajicic, Ana and Odink, Arlette E. and Renckens, Michel and Doukas, Michail and de Man, Rob A. and IJzermans, Jan N.M. and Miclea, Razvan L. and Vermeulen, Peter B. and Thomeer, Maarten G. and Visser, Jacob J. and Niessen, Wiro J. and Klein, Stefan}, title = {The WORC database: MRI and CT scans, segmentations, and clinical labels for 930 patients from six radiomics studies}, elocation-id = {2021.08.19.21262238}, year = {2021}, doi = {10.1101/2021.08.19.21262238}, - publisher = {Cold Spring Harbor Laboratory Press}, - abstract = {The WORC database consists in total of 930 patients composed of six datasets gathered at the Erasmus MC, consisting of patients with: 1) well-differentiated liposarcoma or lipoma (115 patients); 2) desmoid-type fibromatosis or extremity soft-tissue sarcomas (203 patients); 3) primary solid liver tumors, either malignant (hepatocellular carcinoma or intrahepatic cholangiocarcinoma) or benign (hepatocellular adenoma or focal nodular hyperplasia) (186 patients); 4) gastrointestinal stromal tumors (GISTs) and intra-abdominal gastrointestinal tumors radiologically resembling GISTs (246 patients); 5) colorectal liver metastases (77 patients); and 6) lung metastases of metastatic melanoma (103 patients). For each patient, either a magnetic resonance imaging (MRI) or computed tomography (CT) scan, collected from routine clinical care, one or multiple (semi-)automatic lesion segmentations, and ground truth labels from a gold standard (e.g., pathologically proven) are available. All datasets are multicenter imaging datasets, as patients referred to our institute often received imaging at their referring hospital. The dataset can be used to validate or develop radiomics methods, i.e., using machine or deep learning to relate the visual appearance to the ground truth labels, and automatic segmentation methods. See also the research article related to this dataset: Starmans et al., Reproducible radiomics through automated machine learning validated on twelve clinical applications, Submitted.View this table:Competing Interest StatementWiro J. Niessen is founder, scientific lead, and shareholder of Quantib BV. Jacob J. Visser is a medical advisor at Contextflow. Astrid A. M. van der Veldt is a consultant (fees paid to the institute) at BMS, Merck, MSD, Sanofi, Eisai, Pfizer, Roche, Novartis, Pierre Fabre and Ipsen. The other authors do not declare any conflicts of interest.Funding StatementThis research did not receive any specific grant from funding agencies in the public, commercial, or not-for-profit sectors.Author DeclarationsI confirm all relevant ethical guidelines have been followed, and any necessary IRB and/or ethics committee approvals have been obtained.YesThe details of the IRB/oversight body that provided approval or exemption for the research described are given below:The study protocol for the collection of the WORC database conformed to the ethical guidelines of the 1975 Declaration of Helsinki. Approval by the local institutional review board of the Erasmus MC (Rotterdam, the Netherlands) was obtained for collection of the WORC database (MEC-2020-0961), and separately for the six included studies (Lipo: MEC-2016-339, Desmoid: MEC-2016-339, Liver: MEC-2017-1035, GIST: MEC-2017-1187, CRLM: MEC-2017-479, Melanoma: MEC-2019-0693). The need for informed consent was waived due to the use of anonymized, retrospective data.All necessary patient/participant consent has been obtained and the appropriate institutional forms have been archived.YesI understand that all clinical trials and any other prospective interventional studies must be registered with an ICMJE-approved registry, such as ClinicalTrials.gov. I confirm that any such study reported in the manuscript has been registered and the trial registration ID is provided (note: if posting a prospective study registered retrospectively, please provide a statement in the trial ID field explaining why the study was not registered in advance).YesI have followed all appropriate research reporting guidelines and uploaded the relevant EQUATOR Network research reporting checklist(s) and other pertinent material as supplementary files, if applicable.YesThe data referred to in this manuscript is publicly available at https://xnat.bmia.nl/data/projects/worc. The code to download the data and reproduce the experiments from the radiomics study in which this data was presented can be found at https://github.com/MStarmans91/WORCDatabase. https://xnat.bmia.nl/data/projects/worc https://github.com/MStarmans91/WORCDatabase}, URL = {https://www.medrxiv.org/content/early/2021/08/25/2021.08.19.21262238}, eprint = {https://www.medrxiv.org/content/early/2021/08/25/2021.08.19.21262238.full.pdf}, journal = {medRxiv} diff --git a/WORC/IOparser/config_io_classifier.py b/WORC/IOparser/config_io_classifier.py index 024064dc..eb1e948e 100644 --- a/WORC/IOparser/config_io_classifier.py +++ b/WORC/IOparser/config_io_classifier.py @@ -59,6 +59,9 @@ def load_config(config_file_path): settings_dict['General']['tempsave'] =\ settings['General'].getboolean('tempsave') + settings_dict['General']['DoTestNRSNEns'] =\ + settings['General'].getboolean('DoTestNRSNEns') + # Feature Scaling settings_dict['FeatureScaling']['scale_features'] =\ settings['FeatureScaling'].getboolean('scale_features') @@ -392,11 +395,13 @@ def load_config(config_file_path): settings['HyperOptimization'].getint('maxlen') settings_dict['HyperOptimization']['ranking_score'] = \ str(settings['HyperOptimization']['ranking_score']) - settings_dict['HyperOptimization']['refit_workflows'] =\ - settings['HyperOptimization'].getboolean('refit_workflows') + settings_dict['HyperOptimization']['refit_training_workflows'] =\ + settings['HyperOptimization'].getboolean('refit_training_workflows') + settings_dict['HyperOptimization']['refit_validation_workflows'] =\ + settings['HyperOptimization'].getboolean('refit_validation_workflows') settings_dict['HyperOptimization']['memory'] = \ str(settings['HyperOptimization']['memory']) - + # Settings for SMAC settings_dict['SMAC']['use'] =\ settings['SMAC'].getboolean('use') diff --git a/WORC/WORC.py b/WORC/WORC.py index 61b3fe4b..a21e2c6b 100644 --- a/WORC/WORC.py +++ b/WORC/WORC.py @@ -200,6 +200,7 @@ def defaultconfig(self): config['General']['AssumeSameImageAndMaskMetadata'] = 'False' config['General']['ComBat'] = 'False' config['General']['Fingerprint'] = 'True' + config['General']['DoTestNRSNEns'] = 'False' # Fingerprinting config['Fingerprinting'] = dict() @@ -496,7 +497,8 @@ def defaultconfig(self): config['HyperOptimization']['maxlen'] = '100' config['HyperOptimization']['ranking_score'] = 'test_score' config['HyperOptimization']['memory'] = '3G' - config['HyperOptimization']['refit_workflows'] = 'False' + config['HyperOptimization']['refit_training_workflows'] = 'False' + config['HyperOptimization']['refit_validation_workflows'] = 'False' # SMAC options config['SMAC'] = dict() diff --git a/WORC/classification/SearchCV.py b/WORC/classification/SearchCV.py index c623dfee..b191c402 100644 --- a/WORC/classification/SearchCV.py +++ b/WORC/classification/SearchCV.py @@ -1,6 +1,6 @@ #!/usr/bin/env python -# Copyright 2016-2021 Biomedical Imaging Group Rotterdam, Departments of +# Copyright 2016-2022 Biomedical Imaging Group Rotterdam, Departments of # Medical Informatics and Radiology, Erasmus MC, Rotterdam, The Netherlands # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -364,8 +364,8 @@ def __init__(self, param_distributions={}, n_iter=10, scoring=None, refit=True, cv=None, verbose=0, pre_dispatch='2*n_jobs', random_state=None, error_score='raise', return_train_score=True, n_jobspercore=100, maxlen=100, fastr_plugin=None, memory='2G', - ranking_score='test_score', refit_workflows=False, - ensemble_validation_score=None): + ranking_score='test_score', refit_training_workflows=False, + ensemble_validation_score=None, refit_validation_workflows=False): """Initialize SearchCV Object.""" # Added for fastr and joblib executions self.param_distributions = param_distributions @@ -392,7 +392,8 @@ def __init__(self, param_distributions={}, n_iter=10, scoring=None, self.return_train_score = return_train_score self.maxlen = maxlen self.ranking_score = ranking_score - self.refit_workflows = refit_workflows + self.refit_training_workflows = refit_training_workflows + self.refit_validation_workflows = refit_validation_workflows self.fitted_workflows = list() # Only for WORC Paper @@ -639,7 +640,8 @@ def preprocess(self, X, y=None, training=False): def process_fit(self, n_splits, parameters_all, test_sample_counts, test_score_dicts, train_score_dicts, fit_time, score_time, cv_iter, - X, y, fitted_workflows=None, use_smac=False): + X, y, fitted_workflows=None, fitted_validation_workflows=None, + use_smac=False): """Process a fit. Process the outcomes of a SearchCV fit and find the best settings @@ -845,7 +847,7 @@ def _store(key_name, array, weights=None, splits=False, rank=False): self.scorer_ = scorers if self.multimetric_ else scorers['score'] # Refit the top performing workflows on the full training dataset - if self.refit_workflows: + if self.refit_training_workflows: # Select only from one train-val split, as they are identical fitted_workflows = fitted_workflows[:pipelines_per_split] @@ -856,6 +858,17 @@ def _store(key_name, array, weights=None, splits=False, rank=False): fitted_workflows = [f for f in fitted_workflows if f is not None] self.fitted_workflows = fitted_workflows + + if self.refit_validation_workflows: + # Select from all train-val splits the best indices + bestindices_all = list() + for j in range(len(cv_iter)): + bestindices_all.extend([i + n_candidates * j for i in bestindices]) + + fitted_validation_workflows =\ + [fitted_validation_workflows[i] for i in bestindices_all] + + self.fitted_validation_workflows = fitted_validation_workflows return self @@ -916,7 +929,8 @@ def refit_and_score(self, X, y, parameters_all, return_estimator=True, error_score=self.error_score, verbose=verbose, - return_all=True) + return_all=True, + skip=True) # Associate best options with new fits (save_data, GroupSel, VarSel, SelectModel, feature_labels, scalers, @@ -1006,7 +1020,7 @@ def compute_performance(scoring, Y_valid_truth, Y_valid_score): if scoring is None: scoring = self.scoring - # Get settings for best 100 estimators + # Get settings for best estimators parameters_all = self.cv_results_['params'] n_classifiers = len(parameters_all) n_iter = len(self.cv_iter) @@ -1029,6 +1043,9 @@ def compute_performance(scoring, Y_valid_truth, Y_valid_score): # Refit the models and compute the predictions on the validation sets if verbose: print('Precomputing scores on training and validation set for ensembling.') + if self.fitted_validation_workflows: + print('Detected already fitted train-val workflows.') + Y_valid_truth = list() performances = list() all_predictions = list() @@ -1044,68 +1061,80 @@ def compute_performance(scoring, Y_valid_truth, Y_valid_score): if num == 0: Y_valid_truth.append(Y_train[valid]) - new_estimator = clone(base_estimator) - - # Fit the preprocessors of the pipeline - out = fit_and_score(X_train, Y_train, scoring, - train, valid, p_all, - return_all=True) - (save_data, GroupSel, VarSel, SelectModel, feature_labels, scalers, - encoders, Imputers, PCAs, StatisticalSel, ReliefSel, Sampler) = out - new_estimator.best_groupsel = GroupSel - new_estimator.best_scaler = scalers - new_estimator.best_varsel = VarSel - new_estimator.best_modelsel = SelectModel - new_estimator.best_preprocessor = None - new_estimator.best_imputer = Imputers - new_estimator.best_encoder = encoders - new_estimator.best_pca = PCAs - new_estimator.best_featlab = feature_labels - new_estimator.best_statisticalsel = StatisticalSel - new_estimator.best_reliefsel = ReliefSel - new_estimator.best_Sampler = Sampler - - # Use the fitted preprocessors to preprocess the features - X_train_values = np.asarray([x[0] for x in X_train]) - processed_X, processed_y = new_estimator.preprocess(X_train_values[train], - Y_train[train], - training=True) - # Check if there are features left - (patients, features_left) = np.shape(processed_X) - if features_left == 0: - print('no features left' + '\n') - # No features are left; do not consider this pipeline for the ensemble - break + if self.fitted_validation_workflows: + # Use already fitted workflow + estimator = self.fitted_validation_workflows[num + it * self.maxlen] + if estimator is None: + # Estimator is none, so do not consider this pipeline for ensemble + break + + X_train_values = np.asarray([x[0] for x in X_train]) + predictions = estimator.predict_proba(X_train_values[valid]) + else: + new_estimator = clone(base_estimator) + + # Fit the preprocessors of the pipeline + out = fit_and_score(X_train, Y_train, scoring, + train, valid, p_all, + return_all=True) + (save_data, GroupSel, VarSel, SelectModel, feature_labels, scalers, + encoders, Imputers, PCAs, StatisticalSel, ReliefSel, Sampler) = out + new_estimator.best_groupsel = GroupSel + new_estimator.best_scaler = scalers + new_estimator.best_varsel = VarSel + new_estimator.best_modelsel = SelectModel + new_estimator.best_preprocessor = None + new_estimator.best_imputer = Imputers + new_estimator.best_encoder = encoders + new_estimator.best_pca = PCAs + new_estimator.best_featlab = feature_labels + new_estimator.best_statisticalsel = StatisticalSel + new_estimator.best_reliefsel = ReliefSel + new_estimator.best_Sampler = Sampler + + # Use the fitted preprocessors to preprocess the features + X_train_values = np.asarray([x[0] for x in X_train]) + processed_X, processed_y = new_estimator.preprocess(X_train_values[train], + Y_train[train], + training=True) + + # Check if there are features left + (patients, features_left) = np.shape(processed_X) + if features_left == 0: + print('no features left' + '\n') + # No features are left; do not consider this pipeline for the ensemble + break + # Construct and fit the classifier best_estimator = cc.construct_classifier(p_all) best_estimator.fit(processed_X, processed_y) new_estimator.best_estimator_ = best_estimator predictions = new_estimator.predict_proba(X_train_values[valid]) - # Only take the probabilities for the second class - predictions = predictions[:, 1] + # Only take the probabilities for the second class + predictions = predictions[:, 1] - # Store the predictions on this split - #predictions_iter.append(predictions) - predictions_iter[it, :] = predictions + # Store the predictions on this split + #predictions_iter.append(predictions) + predictions_iter[it, :] = predictions - # Compute and store the performance on this split - performances_iter.append(compute_performance(scoring, - Y_train[valid], - predictions)) + # Compute and store the performance on this split + performances_iter.append(compute_performance(scoring, + Y_train[valid], + predictions)) - # print('fitandscore: ' + str(out[0][1]) + ' and computed: ' + - # str(compute_performance(scoring, Y_train[valid], predictions)) + '\n') + # print('fitandscore: ' + str(out[0][1]) + ' and computed: ' + + # str(compute_performance(scoring, Y_train[valid], predictions)) + '\n') - # At the end of the last iteration, store the results of this pipeline - if it == (n_iter - 1): - # Add the pipeline to the list - ensemble_configurations.append(p_all) - # Store the predictions - all_predictions.append(predictions_iter) - # Store the performance - performances.append(np.mean(performances_iter)) + # At the end of the last iteration, store the results of this pipeline + if it == (n_iter - 1): + # Add the pipeline to the list + ensemble_configurations.append(p_all) + # Store the predictions + all_predictions.append(predictions_iter) + # Store the performance + performances.append(np.mean(performances_iter)) # Update the parameters parameters_all = ensemble_configurations @@ -1161,7 +1190,7 @@ def compute_performance(scoring, Y_valid_truth, Y_valid_score): perf = compute_performance(scoring, Y_valid_truth[n_crossval], y_valid_score_new) performances_temp[n_crossval] = perf - # Check which ensemble should be in the ensemble to maximally improve + # Check which estimator should be in the ensemble to maximally improve new_performance = np.mean(performances_temp) performances_n_class.append(new_performance) best_index = sortedindices[iteration] @@ -1173,9 +1202,10 @@ def compute_performance(scoring, Y_valid_truth, Y_valid_score): ensemble = ensemble[0:N_models] best_performance = new_performance - print(f"Ensembling best {scoring}: {best_performance}.") - print(f"Single estimator best {scoring}: {single_estimator_performance}.") - print(f'Ensemble consists of {len(ensemble)} estimators {ensemble}.') + if verbose: + print(f"Ensembling best {scoring}: {best_performance}.") + print(f"Single estimator best {scoring}: {single_estimator_performance}.") + print(f'Ensemble consists of {len(ensemble)} estimators {ensemble}.') elif method == 'ForwardSelection': @@ -1186,6 +1216,7 @@ def compute_performance(scoring, Y_valid_truth, Y_valid_score): Y_valid_score = copy.deepcopy(base_Y_valid_score) if verbose: print(f"Iteration: {iteration}, best {scoring}: {new_performance}.") + best_performance = new_performance if iteration > 1: @@ -1222,10 +1253,11 @@ def compute_performance(scoring, Y_valid_truth, Y_valid_score): best_index = performances_temp.index(new_performance) iteration += 1 - # Print the performance gain - print(f"Ensembling best {scoring}: {best_performance}.") - print(f"Single estimator best {scoring}: {single_estimator_performance}.") - print(f'Ensemble consists of {len(ensemble)} estimators {ensemble}.') + if verbose: + # Print the performance gain + print(f"Ensembling best {scoring}: {best_performance}.") + print(f"Single estimator best {scoring}: {single_estimator_performance}.") + print(f'Ensemble consists of {len(ensemble)} estimators {ensemble}.') elif method == 'Caruana': if verbose: @@ -1279,10 +1311,11 @@ def compute_performance(scoring, Y_valid_truth, Y_valid_score): ensemble = ensemble[0:optimal_N_models] best_performance = optimal_ensemble_performance - # Print the performance gain - print(f"Ensembling best {scoring}: {best_performance}.") - print(f"Single estimator best {scoring}: {single_estimator_performance}.") - print(f'Ensemble consists of {len(ensemble)} estimators {ensemble}.') + if verbose: + # Print the performance gain + print(f"Ensembling best {scoring}: {best_performance}.") + print(f"Single estimator best {scoring}: {single_estimator_performance}.") + print(f'Ensemble consists of {len(ensemble)} estimators {ensemble}.') elif method == 'Bagging': if verbose: @@ -1299,8 +1332,8 @@ def compute_performance(scoring, Y_valid_truth, Y_valid_score): while iteration < 20: Y_valid_score = copy.deepcopy(base_Y_valid_score) - if verbose: - print(f"Iteration: {iteration}, best {scoring}: {new_performance}.") + # if verbose: + # print(f"Iteration: {iteration}, best {scoring}: {new_performance}.") if iteration > 1: for num in range(0, n_iter): @@ -1341,14 +1374,17 @@ def compute_performance(scoring, Y_valid_truth, Y_valid_score): # Select the optimal ensemble size optimal_ensemble_performance = max(best_ensemble_scores) optimal_N_models = best_ensemble_scores.index(optimal_ensemble_performance) + 1 + # Add the best ensemble of this bagging iteration to the final ensemble bag_ensemble = bag_ensemble[0:optimal_N_models] for model in bag_ensemble: ensemble.append(model) + best_performance = optimal_ensemble_performance - # Print the performance gain - print(f'Ensemble consists of {len(ensemble)} estimators {ensemble}.') + if verbose: + # Print the performance gain + print(f'Ensemble consists of {len(ensemble)} estimators {ensemble}.') else: print(f'[WORC WARNING] No valid ensemble method given: {method}. Not ensembling') @@ -1390,7 +1426,8 @@ def compute_performance(scoring, Y_valid_truth, Y_valid_score): validation_score = np.mean(val_split_scores) self.ensemble_validation_score = validation_score - print('Final ensemble validation score: ' + str(self.ensemble_validation_score)) + if verbose: + print('Final ensemble validation score: ' + str(self.ensemble_validation_score)) # Create the ensemble -------------------------------------------------- train = np.arange(0, len(X_train)) @@ -1408,8 +1445,7 @@ def compute_performance(scoring, Y_valid_truth, Y_valid_score): estimator = self.fitted_workflows[i] estimator.refit_and_score(X_train, Y_train, parameters_all[i], - train, train, - verbose=False) + train, train) estimators.append(estimator) else: @@ -1419,7 +1455,8 @@ def compute_performance(scoring, Y_valid_truth, Y_valid_score): nest = len(ensemble) for enum, p_all in enumerate(parameters_all): # Refit a SearchCV object with the provided parameters - print(f"Refitting estimator {enum + 1} / {nest}.") + if verbose: + print(f"Refitting estimator {enum + 1} / {nest}.") base_estimator = clone(base_estimator) # Check if we need to create a multiclass estimator @@ -1604,7 +1641,8 @@ def _fit(self, X, y, groups, parameter_iterable): 'return_n_test_samples', 'return_times', 'return_parameters', 'return_estimator', - 'error_score', 'return_all', 'refit_workflows'] + 'error_score', 'return_all', 'refit_training_workflows', + 'refit_validation_workflows'] verbose = False return_n_test_samples = True @@ -1619,7 +1657,8 @@ def _fit(self, X, y, groups, parameter_iterable): return_parameters, return_estimator, self.error_score, - return_all, self.refit_workflows], + return_all, self.refit_training_workflows, + self.refit_validation_workflows], index=estimator_labels, name='estimator Data') fname = 'estimatordata.hdf5' @@ -1683,25 +1722,53 @@ def _fit(self, X, y, groups, parameter_iterable): # if one choose to see train score, "out" will contain train score info if self.return_train_score: - if self.refit_workflows: - (train_scores, test_scores, test_sample_counts, - fit_time, score_time, parameters_all, fitted_workflows) =\ - zip(*save_data) + if self.refit_training_workflows: + if self.refit_validation_workflows: + (train_scores, test_scores, test_sample_counts, + fit_time, score_time, parameters_all, + fitted_workflows, fitted_validation_workflows) =\ + zip(*save_data) + else: + fitted_validation_workflows = None + (train_scores, test_scores, test_sample_counts, + fit_time, score_time, parameters_all, + fitted_workflows) =\ + zip(*save_data) else: fitted_workflows = None - (train_scores, test_scores, test_sample_counts, - fit_time, score_time, parameters_all) =\ - zip(*save_data) + if self.refit_validation_workflows: + (train_scores, test_scores, test_sample_counts, + fit_time, score_time, parameters_all, + fitted_validation_workflows) =\ + zip(*save_data) + else: + fitted_validation_workflows = None + (train_scores, test_scores, test_sample_counts, + fit_time, score_time, parameters_all) =\ + zip(*save_data) else: - if self.refit_workflows: - (test_scores, test_sample_counts, - fit_time, score_time, parameters_all, fitted_workflows) =\ - zip(*save_data) + if self.refit_training_workflows: + if self.refit_validation_workflows: + (test_scores, test_sample_counts, + fit_time, score_time, parameters_all, fitted_workflows, + fitted_validation_workflows) =\ + zip(*save_data) + else: + fitted_validation_workflows = None + (test_scores, test_sample_counts, + fit_time, score_time, parameters_all, fitted_workflows) =\ + zip(*save_data) else: fitted_workflows = None - (test_scores, test_sample_counts, - fit_time, score_time, parameters_all) =\ - zip(*save_data) + if self.refit_validation_workflows: + (test_scores, test_sample_counts, + fit_time, score_time, parameters_all, + fitted_validation_workflows) =\ + zip(*save_data) + else: + (test_scores, test_sample_counts, + fit_time, score_time, parameters_all) =\ + zip(*save_data) # Remove the temporary folder used if name != 'DEBUG_0': @@ -1718,7 +1785,8 @@ def _fit(self, X, y, groups, parameter_iterable): score_time=score_time, cv_iter=cv_iter, X=X, y=y, - fitted_workflows=fitted_workflows) + fitted_workflows=fitted_workflows, + fitted_validation_workflows=fitted_validation_workflows) class RandomizedSearchCVfastr(BaseSearchCVfastr): @@ -1935,7 +2003,8 @@ def __init__(self, param_distributions={}, n_iter=10, scoring=None, verbose=0, pre_dispatch='2*n_jobs', random_state=None, error_score='raise', return_train_score=True, n_jobspercore=100, fastr_plugin=None, memory='2G', maxlen=100, - ranking_score='test_score', refit_workflows=False): + ranking_score='test_score', refit_training_workflows=False, + refit_validation_workflows=False): super(RandomizedSearchCVfastr, self).__init__( param_distributions=param_distributions, scoring=scoring, fit_params=fit_params, n_iter=n_iter, random_state=random_state, n_jobs=n_jobs, iid=iid, refit=refit, cv=cv, verbose=verbose, @@ -1943,7 +2012,8 @@ def __init__(self, param_distributions={}, n_iter=10, scoring=None, return_train_score=return_train_score, n_jobspercore=n_jobspercore, fastr_plugin=fastr_plugin, memory=memory, maxlen=maxlen, ranking_score=ranking_score, - refit_workflows=refit_workflows) + refit_training_workflows=refit_training_workflows, + refit_validation_workflows=refit_validation_workflows) def fit(self, X, y=None, groups=None): """Randomized model selection and hyperparameter search. @@ -2032,13 +2102,53 @@ def _fit(self, X, y, groups, parameter_iterable): # if one choose to see train score, "out" will contain train score info if self.return_train_score: - (train_scores, test_scores, test_sample_counts, - fit_time, score_time, parameters_all) =\ - save_data + if self.refit_training_workflows: + if self.refit_validation_workflows: + (train_scores, test_scores, test_sample_counts, + fit_time, score_time, parameters_all, + fitted_workflows, fitted_validation_workflows) =\ + zip(*save_data) + else: + fitted_validation_workflows = None + (train_scores, test_scores, test_sample_counts, + fit_time, score_time, parameters_all, + fitted_workflows) =\ + zip(*save_data) + else: + fitted_workflows = None + if self.refit_validation_workflows: + (train_scores, test_scores, test_sample_counts, + fit_time, score_time, parameters_all, + fitted_validation_workflows) =\ + zip(*save_data) + else: + fitted_validation_workflows = None + (train_scores, test_scores, test_sample_counts, + fit_time, score_time, parameters_all) =\ + zip(*save_data) else: - (test_scores, test_sample_counts, - fit_time, score_time, parameters_all) =\ - save_data + if self.refit_training_workflows: + if self.refit_validation_workflows: + (test_scores, test_sample_counts, + fit_time, score_time, parameters_all, fitted_workflows, + fitted_validation_workflows) =\ + zip(*save_data) + else: + fitted_validation_workflows = None + (test_scores, test_sample_counts, + fit_time, score_time, parameters_all, fitted_workflows) =\ + zip(*save_data) + else: + fitted_workflows = None + if self.refit_validation_workflows: + (test_scores, test_sample_counts, + fit_time, score_time, parameters_all, + fitted_validation_workflows) =\ + zip(*save_data) + else: + (test_scores, test_sample_counts, + fit_time, score_time, parameters_all) =\ + zip(*save_data) self.process_fit(n_splits=n_splits, parameters_all=parameters_all, @@ -2048,7 +2158,9 @@ def _fit(self, X, y, groups, parameter_iterable): fit_time=fit_time, score_time=score_time, cv_iter=cv_iter, - X=X, y=y) + X=X, y=y, + fitted_workflows=fitted_workflows, + fitted_validation_workflows=fitted_validation_workflows) return self @@ -3028,15 +3140,15 @@ def _fit(self, groups): # Process the results of the fitting procedure self.process_fit(n_splits=n_splits, - parameters_all=parameters_all, - test_sample_counts=test_sample_counts, - test_score_dicts=test_scores, - train_score_dicts=train_scores, - fit_time=fit_time, - score_time=score_time, - cv_iter=cv_iter, - X=self.features, y=self.labels, - use_smac=True) + parameters_all=parameters_all, + test_sample_counts=test_sample_counts, + test_score_dicts=test_scores, + train_score_dicts=train_scores, + fit_time=fit_time, + score_time=score_time, + cv_iter=cv_iter, + X=self.features, y=self.labels, + use_smac=True) return self diff --git a/WORC/classification/crossval.py b/WORC/classification/crossval.py index 6d906a73..def56571 100644 --- a/WORC/classification/crossval.py +++ b/WORC/classification/crossval.py @@ -1,6 +1,6 @@ #!/usr/bin/env python -# Copyright 2016-2021 Biomedical Imaging Group Rotterdam, Departments of +# Copyright 2016-2022 Biomedical Imaging Group Rotterdam, Departments of # Medical Informatics and Radiology, Erasmus MC, Rotterdam, The Netherlands # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -28,7 +28,7 @@ import glob import random import json -from copy import copy +import copy from sklearn.metrics import f1_score, roc_auc_score @@ -40,7 +40,6 @@ def random_split_cross_validation(image_features, feature_labels, classes, fixedsplits=None, fixed_seed=False, use_fastr=None, fastr_plugin=None, - do_test_RS_Ensemble=False, use_SMAC=False, smac_result_file=None): """Cross-validation in which data is randomly split in each iteration. @@ -239,7 +238,7 @@ def random_split_cross_validation(image_features, feature_labels, classes, save_data.append(temp_save_data) # Test performance for various RS and ensemble sizes - if do_test_RS_Ensemble: + if config['General']['DoTestNRSNEns']: output_json = os.path.join(tempfolder, f'performance_RS_Ens_crossval_{i}.json') test_RS_Ensemble(estimator_input=trained_classifier, X_train=X_train, Y_train=Y_train, @@ -250,7 +249,9 @@ def random_split_cross_validation(image_features, feature_labels, classes, # Save memory delattr(trained_classifier, 'fitted_workflows') trained_classifier.fitted_workflows = list() - + delattr(trained_classifier, 'fitted_validation_workflows') + trained_classifier.fitted_validation_workflows = list() + # Create a temporary save if tempsave: panda_labels = ['trained_classifier', 'X_train', 'X_test', @@ -655,7 +656,7 @@ def crossval(config, label_data, image_features, def nocrossval(config, label_data_train, label_data_test, image_features_train, image_features_test, param_grid=None, use_fastr=False, fastr_plugin=None, ensemble={'Use': False}, - modus='singlelabel', do_test_RS_Ensemble=False): + modus='singlelabel'): """Constructs multiple individual classifiers based on the label settings. Arguments: @@ -787,7 +788,7 @@ def nocrossval(config, label_data_train, label_data_test, image_features_train, classifier_labelss[i_name] = panda_data_temp # Test performance for various RS and ensemble sizes - if do_test_RS_Ensemble: + if config['General']['DoTestNRSNEns']: # FIXME: Use home folder, as this function does not know # Where final or temporary output is located output_json = os.path.join(os.path.expanduser("~"), @@ -802,6 +803,8 @@ def nocrossval(config, label_data_train, label_data_test, image_features_train, # Save memory delattr(trained_classifier, 'fitted_workflows') trained_classifier.fitted_workflows = list() + delattr(trained_classifier, 'fitted_validation_workflows') + trained_classifier.fitted_validation_workflows = list() panda_data = pd.DataFrame(classifier_labelss) @@ -809,7 +812,7 @@ def nocrossval(config, label_data_train, label_data_test, image_features_train, def test_RS_Ensemble(estimator_input, X_train, Y_train, X_test, Y_test, - feature_labels, output_json): + feature_labels, output_json, verbose=False): """Test performance for different random search and ensemble sizes. This function is written for conducting a specific experiment from the @@ -818,14 +821,14 @@ def test_RS_Ensemble(estimator_input, X_train, Y_train, X_test, Y_test, """ # Process some input - estimator_original = copy(estimator_input) + estimator_original = copy.deepcopy(estimator_input) X_train_temp = [(x, feature_labels) for x in X_train] n_workflows = len(estimator_original.fitted_workflows) # Settings - RSs = [10, 50, 100, 1000, 10000] * 10 + [n_workflows] - ensembles = [1, 10, 50, 100] - maxlen = max(ensembles) + RSs = [10, 100, 1000, 10000] * 10 + [n_workflows] + ensembles = [1, 10, 100, 'FitNumber', 'Bagging'] + maxlen = 100 # max ensembles numeric # Loop over the random searches and ensembles keys = list() @@ -842,7 +845,7 @@ def test_RS_Ensemble(estimator_input, X_train, Y_train, X_test, Y_test, # Make a local copy of the estimator and select only subset of workflows print(f'\t Using RS {RS}.') - estimator = copy(estimator_original) + estimator = copy.deepcopy(estimator_original) workflow_num = np.arange(n_workflows).tolist() # Select only a specific set of workflows @@ -855,12 +858,18 @@ def test_RS_Ensemble(estimator_input, X_train, Y_train, X_test, Y_test, workflow_ranking = np.argsort(np.asarray(F1_validation)).tolist()[::-1] # Normally, rank from smallest to largest, so reverse F1_validation = [F1_validation[i] for i in workflow_ranking] - # Only keep the number of RS required and resort based on ensemble + # Only keep the number of RS required and resort based on ranking estimator.fitted_workflows =\ [estimator.fitted_workflows[i] for i in selected_workflows] estimator.fitted_workflows =\ [estimator.fitted_workflows[i] for i in workflow_ranking] - + + # For advanced ensembling methods, keep only the parameters of the selected RS workflows + estimator.cv_results_['params'] =\ + [estimator.cv_results_['params'][i] for i in selected_workflows] + estimator.cv_results_['params'] =\ + [estimator.cv_results_['params'][i] for i in workflow_ranking] + # Store train and validation AUC mean_val_F1 = F1_validation[0:maxlen] F1_training = estimator.cv_results_['mean_train_score'] @@ -872,25 +881,34 @@ def test_RS_Ensemble(estimator_input, X_train, Y_train, X_test, Y_test, performances[f'Mean validation F1-score {key} top {maxlen}'] = mean_val_F1 for ensemble in ensembles: - if ensemble <= RS: - print(f'\t Using ensemble {ensemble}.') + if isinstance(ensemble, int): + if ensemble > RS: + continue + else: + print(f'\t Using ensemble {ensemble}.') + # Create the ensemble + estimator.create_ensemble(X_train_temp, Y_train, method='top_N', + size=ensemble, verbose=verbose) + else: + print(f'\t Using ensemble {ensemble}.') # Create the ensemble - estimator.create_ensemble(X_train_temp, Y_train, method=ensemble) - - # Compute performance - y_prediction = estimator.predict(X_test) - y_score = estimator.predict_proba(X_test)[:, 1] - auc = roc_auc_score(Y_test, y_score) - f1_score_out = f1_score(Y_test, y_prediction, average='weighted') - performances[f'Test F1-score Ensemble {ensemble} {key}'] = f1_score_out - performances[f'Test AUC Ensemble {ensemble} {key}'] = auc - - y_prediction = estimator.predict(X_train) - y_score = estimator.predict_proba(X_train)[:, 1] - auc = roc_auc_score(Y_train, y_score) - f1_score_out = f1_score(Y_train, y_prediction, average='weighted') - performances[f'Train F1-score Ensemble {ensemble} {key}'] = f1_score_out - performances[f'Train AUC Ensemble {ensemble} {key}'] = auc + estimator.create_ensemble(X_train_temp, Y_train, method=ensemble, + verbose=verbose) + + # Compute performance + y_prediction = estimator.predict(X_test) + y_score = estimator.predict_proba(X_test)[:, 1] + auc = roc_auc_score(Y_test, y_score) + f1_score_out = f1_score(Y_test, y_prediction, average='weighted') + performances[f'Test F1-score Ensemble {ensemble} {key}'] = f1_score_out + performances[f'Test AUC Ensemble {ensemble} {key}'] = auc + + y_prediction = estimator.predict(X_train) + y_score = estimator.predict_proba(X_train)[:, 1] + auc = roc_auc_score(Y_train, y_score) + f1_score_out = f1_score(Y_train, y_prediction, average='weighted') + performances[f'Train F1-score Ensemble {ensemble} {key}'] = f1_score_out + performances[f'Train AUC Ensemble {ensemble} {key}'] = auc # Write output with open(output_json, 'w') as fp: diff --git a/WORC/classification/fitandscore.py b/WORC/classification/fitandscore.py index 009b445d..5cdc1b7c 100644 --- a/WORC/classification/fitandscore.py +++ b/WORC/classification/fitandscore.py @@ -60,8 +60,9 @@ def fit_and_score(X, y, scoring, return_times=True, return_parameters=False, return_estimator=False, error_score='raise', verbose=False, - return_all=True, refit_workflows=False, - use_smac=False): + return_all=True, refit_training_workflows=False, + refit_validation_workflows=False, + skip=False): """Fit an estimator to a dataset and score the performance. The following @@ -264,7 +265,10 @@ def fit_and_score(X, y, scoring, # Additional to sklearn defaults: return all parameters and refitted estimator ret.append(parameters) - if refit_workflows: + if refit_training_workflows: + ret.append(None) + + if refit_validation_workflows: ret.append(None) # ------------------------------------------------------------------------ @@ -304,6 +308,7 @@ def fit_and_score(X, y, scoring, imp_type = para_estimator['ImputationMethod'] if verbose: print(f'Imputing NaN with {imp_type}.') + # Only used with KNN in SMAC, otherwise assign default if 'ImputationNeighbours' in para_estimator.keys(): imp_nn = para_estimator['ImputationNeighbours'] @@ -441,7 +446,34 @@ def fit_and_score(X, y, scoring, X_test = VarSel.transform(X_test) except ValueError: if verbose: - print('[WARNING]: No features meet the selected Variance threshold! Skipping selection.') + print('[WARNING]: No features meet the selected variance threshold.') + + VarSel = None + if skip: + if verbose: + print('[WARNING] Refitting, so we need an estimator, thus skipping this step.') + parameters['Featsel_Variance'] = 'False' + else: + if verbose: + print('[WARNING] Returning NaN as performance.') + + # return NaN as performance + para_estimator = delete_nonestimator_parameters(para_estimator) + + # Update the runtime + end_time = time.time() + runtime = end_time - start_time + if return_train_score: + ret[3] = runtime + else: + ret[2] = runtime + if return_all: + return ret, GroupSel, VarSel, SelectModel,\ + feature_labels[0], scaler, encoder, imputer, pca,\ + StatisticalSel, ReliefSel, Sampler + else: + return ret + if verbose: print("\t New Length: " + str(len(X_train[0]))) @@ -451,31 +483,10 @@ def fit_and_score(X, y, scoring, if not return_all: del VarSel - # Check whether there are any features left - if len(X_train[0]) == 0: - # TODO: Make a specific WORC exception for this warning. - if verbose: - print('[WARNING]: No features are selected! Probably your features have too little variance. Parameters:') - print(parameters) - para_estimator = delete_nonestimator_parameters(para_estimator) - - # Update the runtime - end_time = time.time() - runtime = end_time - start_time - if return_train_score: - ret[3] = runtime - else: - ret[2] = runtime - - if return_all: - return ret, GroupSel, VarSel, SelectModel, feature_labels[0], scaler, encoder, imputer, pca, StatisticalSel, ReliefSel, Sampler - else: - return ret - # ------------------------------------------------------------------------ # Feature scaling if verbose and para_estimator['FeatureScaling'] != 'None': - print(f'Fitting scaler and transforming features, method ' + + print('Fitting scaler and transforming features, method ' + f'{para_estimator["FeatureScaling"]}.') scaling_method = para_estimator['FeatureScaling'] @@ -487,7 +498,7 @@ def fit_and_score(X, y, scoring, if n_skip_feat == len(X_train[0]): # Don't need to scale any features if verbose: - print('[WORC Warning] Skipping scaling, only skip features selected.') + print('[WARNING] Skipping scaling, only skip features selected.') scaler = None else: scaler = WORCScaler(method=scaling_method, skip_features=skip_features) @@ -530,12 +541,43 @@ def fit_and_score(X, y, scoring, print("\t Original Length: " + str(len(X_train[0]))) # Transform all objects accordingly - X_train = ReliefSel.transform(X_train) - X_test = ReliefSel.transform(X_test) + X_train_temp = ReliefSel.transform(X_train) + if len(X_train_temp[0]) == 0: + if verbose: + print('[WARNING]: No features are selected! Probably RELIEF could not properly select features.') + + ReliefSel = None + if skip: + if verbose: + print('[WARNING] Refitting, so we need an estimator, thus skipping this step.') + parameters['ReliefUse'] = 'False' + else: + if verbose: + print('[WARNING] Returning NaN as performance.') + + # return NaN as performance + para_estimator = delete_nonestimator_parameters(para_estimator) + + # Update the runtime + end_time = time.time() + runtime = end_time - start_time + if return_train_score: + ret[3] = runtime + else: + ret[2] = runtime + if return_all: + return ret, GroupSel, VarSel, SelectModel,\ + feature_labels[0], scaler, encoder, imputer, pca,\ + StatisticalSel, ReliefSel, Sampler + else: + return ret + else: + X_train = X_train_temp + X_test = ReliefSel.transform(X_test) - if verbose: - print("\t New Length: " + str(len(X_train[0]))) - feature_labels = ReliefSel.transform(feature_labels) + if verbose: + print("\t New Length: " + str(len(X_train[0]))) + feature_labels = ReliefSel.transform(feature_labels) del para_estimator['ReliefUse'] del para_estimator['ReliefNN'] @@ -547,27 +589,6 @@ def fit_and_score(X, y, scoring, if not return_all: del ReliefSel - # Check whether there are any features left - if len(X_train[0]) == 0: - # TODO: Make a specific WORC exception for this warning. - if verbose: - print('[WARNING]: No features are selected! Probably RELIEF could not properly select features. Parameters:') - print(parameters) - para_estimator = delete_nonestimator_parameters(para_estimator) - - # Update the runtime - end_time = time.time() - runtime = end_time - start_time - if return_train_score: - ret[3] = runtime - else: - ret[2] = runtime - - if return_all: - return ret, GroupSel, VarSel, SelectModel, feature_labels[0], scaler, encoder, imputer, pca, StatisticalSel, ReliefSel, Sampler - else: - return ret - # ------------------------------------------------------------------------ # Perform feature selection using a model if 'SelectFromModel' in para_estimator.keys(): @@ -609,9 +630,34 @@ def fit_and_score(X, y, scoring, X_train_temp = SelectModel.transform(X_train) if len(X_train_temp[0]) == 0: if verbose: - print('[WORC WARNING]: No features are selected! Probably your data is too noisy or the selection too strict. Skipping SelectFromModel.') + print('[WARNING]: No features are selected! Probably your data is too noisy or the selection too strict.') + SelectModel = None - parameters['SelectFromModel'] = 'False' + if skip: + if verbose: + print('[WARNING] Refitting, so we need an estimator, thus skipping this step.') + parameters['SelectFromModel'] = 'False' + else: + if verbose: + print('[WARNING] Returning NaN as performance.') + + # return NaN as performance + para_estimator = delete_nonestimator_parameters(para_estimator) + + # Update the runtime + end_time = time.time() + runtime = end_time - start_time + if return_train_score: + ret[3] = runtime + else: + ret[2] = runtime + if return_all: + return ret, GroupSel, VarSel, SelectModel,\ + feature_labels[0], scaler, encoder, imputer, pca,\ + StatisticalSel, ReliefSel, Sampler + else: + return ret + else: X_train = SelectModel.transform(X_train) X_test = SelectModel.transform(X_test) @@ -632,27 +678,6 @@ def fit_and_score(X, y, scoring, if not return_all: del SelectModel - # Check whether there are any features left - if len(X_train[0]) == 0: - # TODO: Make a specific WORC exception for this warning. - if verbose: - print('[WARNING]: No features are selected! Probably SelectFromModel could not properly select features. Parameters:') - print(parameters) - para_estimator = delete_nonestimator_parameters(para_estimator) - - # Update the runtime - end_time = time.time() - runtime = end_time - start_time - if return_train_score: - ret[3] = runtime - else: - ret[2] = runtime - - if return_all: - return ret, GroupSel, VarSel, SelectModel, feature_labels[0], scaler, encoder, imputer, pca, StatisticalSel, ReliefSel, Sampler - else: - return ret - # ---------------------------------------------------------------- # PCA dimensionality reduction # Principle Component Analysis @@ -667,55 +692,78 @@ def fit_and_score(X, y, scoring, pca.fit(X_train) except (ValueError, LinAlgError) as e: if verbose: - print(f'[WARNING]: skipping this setting due to PCA Error: {e}.') + print(f'[WARNING] PCA Error: {e}.') pca = None - - # Update the runtime - end_time = time.time() - runtime = end_time - start_time - if return_train_score: - ret[3] = runtime - else: - ret[2] = runtime - - if return_all: - return ret, GroupSel, VarSel, SelectModel, feature_labels[0], scaler, encoder, imputer, pca, StatisticalSel, ReliefSel, Sampler + if skip: + if verbose: + print('[WARNING] Refitting, so we need an estimator, thus skipping this step.') + parameters['UsePCA'] = 'False' else: - return ret - - evariance = pca.explained_variance_ratio_ - num = 0 - sum = 0 - while sum < 0.95: - sum += evariance[num] - num += 1 - - # Make a PCA based on the determined amound of components - pca = PCA(n_components=num, random_state=random_seed) - try: - pca.fit(X_train) - except (ValueError, LinAlgError) as e: - if verbose: - print(f'[WARNING]: skipping this setting due to PCA Error: {e}.') - - pca = None + if verbose: + print('[WARNING] Returning NaN as performance.') + + # return NaN as performance + para_estimator = delete_nonestimator_parameters(para_estimator) + + # Update the runtime + end_time = time.time() + runtime = end_time - start_time + if return_train_score: + ret[3] = runtime + else: + ret[2] = runtime + if return_all: + return ret, GroupSel, VarSel, SelectModel,\ + feature_labels[0], scaler, encoder, imputer, pca,\ + StatisticalSel, ReliefSel, Sampler + else: + return ret - # Update the runtime - end_time = time.time() - runtime = end_time - start_time - if return_train_score: - ret[3] = runtime - else: - ret[2] = runtime + else: + evariance = pca.explained_variance_ratio_ + num = 0 + sum = 0 + while sum < 0.95: + sum += evariance[num] + num += 1 + + # Make a PCA based on the determined amound of components + pca = PCA(n_components=num, random_state=random_seed) + try: + pca.fit(X_train) + except (ValueError, LinAlgError) as e: + if verbose: + print(f'[WARNING]: PCA Error: {e}.') - if return_all: - return ret, GroupSel, VarSel, SelectModel, feature_labels[0], scaler, encoder, imputer, pca, StatisticalSel, ReliefSel, Sampler + pca = None + if skip: + if verbose: + print('[WARNING] Refitting, so we need an estimator, thus skipping this step.') + parameters['UsePCA'] = 'False' + else: + if verbose: + print('[WARNING] Returning NaN as performance.') + + # return NaN as performance + para_estimator = delete_nonestimator_parameters(para_estimator) + + # Update the runtime + end_time = time.time() + runtime = end_time - start_time + if return_train_score: + ret[3] = runtime + else: + ret[2] = runtime + if return_all: + return ret, GroupSel, VarSel, SelectModel,\ + feature_labels[0], scaler, encoder, imputer, pca,\ + StatisticalSel, ReliefSel, Sampler + else: + return ret else: - return ret - - X_train = pca.transform(X_train) - X_test = pca.transform(X_test) + X_train = pca.transform(X_train) + X_test = pca.transform(X_test) else: # Assume a fixed number of components: cannot be larger than @@ -724,24 +772,43 @@ def fit_and_score(X, y, scoring, if n_components >= len(X_train[0]): if verbose: - print(f"[WORC WARNING] PCA n_components ({n_components})> n_features ({len(X_train[0])}): skipping PCA.") + print(f"[WARNING] PCA n_components ({n_components})> n_features ({len(X_train[0])}): skipping PCA.") else: pca = PCA(n_components=n_components, random_state=random_seed) try: pca.fit(X_train) + X_train = pca.transform(X_train) + X_test = pca.transform(X_test) except (ValueError, LinAlgError) as e: if verbose: - print(f'[WARNING]: skipping this setting due to PCA Error: {e}.') + print(f'[WARNING] PCA Error: {e}.') pca = None - if return_all: - return ret, GroupSel, VarSel, SelectModel, feature_labels[0], scaler, encoder, imputer, pca, StatisticalSel, ReliefSel, Sampler + if skip: + if verbose: + print('[WARNING] Refitting, so we need an estimator, thus skipping this step.') + parameters['UsePCA'] = 'False' else: - return ret - - X_train = pca.transform(X_train) - X_test = pca.transform(X_test) - + if verbose: + print('[WARNING] Returning NaN as performance.') + + # return NaN as performance + para_estimator = delete_nonestimator_parameters(para_estimator) + + # Update the runtime + end_time = time.time() + runtime = end_time - start_time + if return_train_score: + ret[3] = runtime + else: + ret[2] = runtime + if return_all: + return ret, GroupSel, VarSel, SelectModel,\ + feature_labels[0], scaler, encoder, imputer, pca,\ + StatisticalSel, ReliefSel, Sampler + else: + return ret + if verbose: print("\t New Length: " + str(len(X_train[0]))) @@ -769,24 +836,36 @@ def fit_and_score(X, y, scoring, StatisticalSel.fit(X_train, y) X_train_temp = StatisticalSel.transform(X_train) - if len(X_train_temp[0]) == 0: + if len(X_train_temp[0]) == 0: if verbose: - print('[WORC WARNING]: No features are selected! Probably your statistical test feature selection was too strict. Skipping thresholding.') - para_estimator = delete_nonestimator_parameters(para_estimator) - # Update the runtime - end_time = time.time() - runtime = end_time - start_time - if return_train_score: - ret[3] = runtime - else: - ret[2] = runtime - if return_all: - return ret, GroupSel, VarSel, SelectModel,\ - feature_labels[0], scaler, encoder, imputer, pca,\ - StatisticalSel, ReliefSel, Sampler + print('[WARNING] No features are selected! Probably your statistical test feature selection was too strict.') + + StatisticalSel = None + if skip: + if verbose: + print('[WARNING] Refitting, so we need an estimator, thus skipping this step.') + parameters['StatisticalTestUse'] = 'False' else: - return ret - + if verbose: + print('[WARNING] Returning NaN as performance.') + + # return NaN as performance + para_estimator = delete_nonestimator_parameters(para_estimator) + + # Update the runtime + end_time = time.time() + runtime = end_time - start_time + if return_train_score: + ret[3] = runtime + else: + ret[2] = runtime + if return_all: + return ret, GroupSel, VarSel, SelectModel,\ + feature_labels[0], scaler, encoder, imputer, pca,\ + StatisticalSel, ReliefSel, Sampler + else: + return ret + else: X_train = StatisticalSel.transform(X_train) X_test = StatisticalSel.transform(X_test) @@ -849,7 +928,7 @@ def fit_and_score(X, y, scoring, except ae.WORCValueError as e: message = str(e) if verbose: - print('[WORC WARNING] Skipping resampling: ' + message) + print('[WARNING] Skipping resampling: ' + message) Sampler = None parameters['Resampling_Use'] = 'False' @@ -880,7 +959,8 @@ def fit_and_score(X, y, scoring, neg = int(len(y_train_temp) - pos) if pos < 10 or neg < 10: if verbose: - print(f'[WORC WARNING] Skipping resampling: to few objects returned in one or both classes (pos: {pos}, neg: {neg}).') + print(f'[WARNING] Skipping resampling: to few objects returned in one or both classes (pos: {pos}, neg: {neg}).') + Sampler = None parameters['Resampling_Use'] = 'False' else: @@ -989,13 +1069,21 @@ def fit_and_score(X, y, scoring, # Add original parameters to return object ret.append(parameters) - if refit_workflows: + if refit_training_workflows: + # Refit estimator on train-test training dataset indices = np.arange(0, len(y)) estimator = WORC.classification.SearchCV.RandomizedSearchCVfastr() estimator.refit_and_score(X, y, parameters, train=indices, test=indices) ret.append(estimator) - + + if refit_validation_workflows: + # Refit estimator on train-validation training dataset + estimator = WORC.classification.SearchCV.RandomizedSearchCVfastr() + estimator.refit_and_score(X, y, parameters, + train=train, test=test) + ret.append(estimator) + # End the timing and store the fit_time end_time = time.time() runtime = end_time - start_time @@ -1065,9 +1153,9 @@ def replacenan(image_features, verbose=True, feature_labels=None): if np.isnan(value): if verbose: if feature_labels is not None: - print(f"[WORC WARNING] NaN found, patient {pnum}, label {feature_labels[fnum]}. Replacing with zero.") + print(f"[WARNING] NaN found, patient {pnum}, label {feature_labels[fnum]}. Replacing with zero.") else: - print(f"[WORC WARNING] NaN found, patient {pnum}, label {fnum}. Replacing with zero.") + print(f"[WARNING] NaN found, patient {pnum}, label {fnum}. Replacing with zero.") # Note: X is a list of lists, hence we cannot index the element directly image_features_temp[pnum, fnum] = 0 diff --git a/WORC/classification/parameter_optimization.py b/WORC/classification/parameter_optimization.py index 96ce9a78..2eb1144d 100644 --- a/WORC/classification/parameter_optimization.py +++ b/WORC/classification/parameter_optimization.py @@ -27,7 +27,8 @@ def random_search_parameters(features, labels, N_iter, test_size, n_cores=1, fastr_plugin=None, memory='2G', maxlen=100, ranking_score='test_score', random_seed=None, - refit_workflows=False): + refit_training_workflows=False, + refit_validation_workflows=False): """ Train a classifier and simultaneously optimizes hyperparameters using a randomized search. @@ -80,7 +81,8 @@ def random_search_parameters(features, labels, N_iter, test_size, fastr_plugin=fastr_plugin, memory=memory, ranking_score=ranking_score, - refit_workflows=refit_workflows) + refit_training_workflows=refit_training_workflows, + refit_validation_workflows=refit_validation_workflows) else: random_search = RandomizedSearchCVJoblib(param_distributions=param_grid, n_iter=N_iter, @@ -92,7 +94,8 @@ def random_search_parameters(features, labels, N_iter, test_size, fastr_plugin=fastr_plugin, memory=memory, ranking_score=ranking_score, - refit_workflows=refit_workflows) + refit_training_workflows=refit_training_workflows, + refit_validation_workflows=refit_validation_workflows) random_search.fit(features, labels) print("Best found parameters:") for i in random_search.best_params_: @@ -107,7 +110,8 @@ def guided_search_parameters(features, labels, N_iter, test_size, n_jobspercore=200, use_fastr=False, n_cores=1, fastr_plugin=None, memory='2G', maxlen=100, ranking_score='test_score', - random_seed=None, refit_workflows=False, + random_seed=None, refit_training_workflows=False, + refit_validation_workflows=False, smac_result_file=None): """ Train a classifier and simultaneously optimizes hyperparameters using a @@ -161,7 +165,9 @@ def guided_search_parameters(features, labels, N_iter, test_size, ranking_score=ranking_score, features=features, labels=labels, - smac_result_file=smac_result_file) + smac_result_file=smac_result_file, + refit_training_workflows=refit_training_workflows, + refit_validation_workflows=refit_validation_workflows) guided_search.fit(features, labels) print("Best found parameters:") diff --git a/WORC/doc/generate_config.py b/WORC/doc/generate_config.py index 7005ca06..014fc7ab 100644 --- a/WORC/doc/generate_config.py +++ b/WORC/doc/generate_config.py @@ -134,7 +134,8 @@ def generate_config_options(): config['General']['AssumeSameImageAndMaskMetadata'] = 'True, False' config['General']['ComBat'] = 'True, False' config['General']['Fingerprint'] = 'True, False' - + config['General']['DoTestNRSNEns'] = 'Boolean' + # Fingerprinting config['Fingerprinting'] = dict() config['Fingerprinting']['max_num_image'] = 'Integer' @@ -406,7 +407,8 @@ def generate_config_options(): config['HyperOptimization']['maxlen'] = 'Integer' config['HyperOptimization']['ranking_score'] = 'String' config['HyperOptimization']['memory'] = 'String consisting of integer + "G"' - config['HyperOptimization']['refit_workflows'] = 'Boolean' + config['HyperOptimization']['refit_training_workflows'] = 'Boolean' + config['HyperOptimization']['refit_validation_workflows'] = 'Boolean' # Feature scaling options config['FeatureScaling'] = dict() @@ -456,7 +458,8 @@ def generate_config_descriptions(): config['General']['AssumeSameImageAndMaskMetadata'] = 'Make the assumption that the image and mask have the same metadata. If True and there is a mismatch, metadata from the image will be copied to the mask.' config['General']['ComBat'] = 'Whether to use ComBat feature harmonization on your FULL dataset, i.e. not in a train-test setting. See `_ .' config['General']['Fingerprint'] = 'Whether to use Fingerprinting or not.' - + config['General']['DoTestNRSNEns'] = 'If True, repeat the experiments from the WORC paper to check the performance of various N_RS, N_Ens and advanced ensembling combinations.' + # Fingerprinting config['Fingerprinting'] = dict() config['Fingerprinting']['max_num_image'] = 'Maximum number of images and segmentations to evaluate during fingerprinting to limit the workload.' @@ -725,7 +728,8 @@ def generate_config_descriptions(): config['HyperOptimization']['maxlen'] = 'Number of estimators for which the fitted outcomes and parameters are saved. Increasing this number will increase the memory usage.' config['HyperOptimization']['ranking_score'] = 'Score used for ranking the performance of the evaluated workflows.' config['HyperOptimization']['memory'] = 'When using DRMAA plugin, e.g. on BIGR cluster, memory usage of a single optimization job. Should be a string consisting of an integer + "G".' - config['HyperOptimization']['refit_workflows'] = 'If True, refit all workflows in the ensemble automatically during training. This will save time while performing inference, but will take more time during training and make the saved model much larger.' + config['HyperOptimization']['refit_training_workflows'] = 'If True, refit all workflows trained on the full training dataset automatically during training. This will save time while performing inference, but will take more time during training and make the saved model much larger.' + config['HyperOptimization']['refit_validation_workflows'] = 'If True, refit all workflows trained on the train-validation training dataset automatically during training. This will save time while performing validation evaluation, but will take more time during training and make the saved model much larger.' # Feature scaling options config['FeatureScaling'] = dict() diff --git a/WORC/doc/static/faq.rst b/WORC/doc/static/faq.rst index a8b419fe..b7a572cb 100644 --- a/WORC/doc/static/faq.rst +++ b/WORC/doc/static/faq.rst @@ -160,3 +160,15 @@ The two mounts that determine the temporary and output folders and thus which you have to change are: - Temporary output: ``mounts['tmp']`` in the ~/.fastr/config.py file - Final output: ``mounts['output']`` in the ~/.fastr/config.d/WORC_config.py file + +I want a specific cross-validation setup, e.g. specific patients in the train and test set, how can I do that? +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +WIP + +How can I make sure all samples of a patient are either all in the training or all in the test set? +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +WIP + +How can I get the performance on the validation dataset? +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +WIP diff --git a/WORC/doc/static/quick_start.rst b/WORC/doc/static/quick_start.rst index 070680ab..8ce0d689 100644 --- a/WORC/doc/static/quick_start.rst +++ b/WORC/doc/static/quick_start.rst @@ -292,5 +292,19 @@ Some things we would advice to always do: experiment.add_evaluation() -For a complete overview of all functions, please look at the +Changing fields in the configuration can be done with the add_config_overrides function, see below. +We recommend doing this after the modus part, as these also perform config_overrides. +NOTE: all configuration fields have to be provided as strings. + +.. code-block:: python + + overrides = { + 'Classification': { + 'classifiers': 'SVM', + }, + } + + experiment.add_config_overrides(overrides) + +For a complete overview of all configuration functions, please look at the :ref:`Config chapter `. diff --git a/WORC/facade/basicworc.py b/WORC/facade/basicworc.py index 9140db54..b98e3e21 100644 --- a/WORC/facade/basicworc.py +++ b/WORC/facade/basicworc.py @@ -1,6 +1,6 @@ #!/usr/bin/env python -# Copyright 2016-2021 Biomedical Imaging Group Rotterdam, Departments of +# Copyright 2016-2022 Biomedical Imaging Group Rotterdam, Departments of # Medical Informatics and Radiology, Erasmus MC, Rotterdam, The Netherlands # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -158,7 +158,7 @@ def execute(self): if self._worc.images_train: nmod = len(self._worc.images_train) else: - nmod = len(self.features_train) + nmod = len(self._worc.features_train) # Create configuration files self._worc.configs = [self._config_builder.build_config(self._worc.defaultconfig())] * nmod diff --git a/WORC/facade/simpleworc.py b/WORC/facade/simpleworc.py index 72c5265d..f6bc4de4 100644 --- a/WORC/facade/simpleworc.py +++ b/WORC/facade/simpleworc.py @@ -587,6 +587,19 @@ def add_config_overrides(self, config): For a full list of options, see the :ref:`WORC Config chapter ` for allowed options. + Example usage: + + overrides = { + 'Classification': { + 'classifiers': 'SVM', + }, + 'Featsel': { + # Other estimators do not support multiclass + 'SelectFromModel_estimator': 'RF' + } + } + self.add_config_overrides(overrides) + Parameters ---------- config: dictionary diff --git a/WORC/resources/fastr_tools/worc/bin/fitandscore_tool.py b/WORC/resources/fastr_tools/worc/bin/fitandscore_tool.py index 9f4510c5..307d212a 100644 --- a/WORC/resources/fastr_tools/worc/bin/fitandscore_tool.py +++ b/WORC/resources/fastr_tools/worc/bin/fitandscore_tool.py @@ -1,6 +1,6 @@ #!/usr/bin/env python -# Copyright 2016-2020 Biomedical Imaging Group Rotterdam, Departments of +# Copyright 2016-2022 Biomedical Imaging Group Rotterdam, Departments of # Medical Informatics and Radiology, Erasmus MC, Rotterdam, The Netherlands # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -92,7 +92,8 @@ def main(): return_estimator=data['return_estimator'], error_score=data['error_score'], return_all=data['return_all'], - refit_workflows=data['refit_workflows']) + refit_training_workflows=data['refit_training_workflows'], + refit_validation_workflows=data['refit_validation_workflows']) for parameters in para.values()) source_labels = ['RET', 'GroupSel', 'VarSel', 'SelectModel', @@ -124,7 +125,8 @@ def main(): return_estimator=data['return_estimator'], error_score=data['error_score'], return_all=data['return_all'], - refit_workflows=data['refit_workflows']) + refit_training_workflows=data['refit_training_workflows'], + refit_validation_workflows=data['refit_validation_workflows']) for parameters in para.values()) source_labels = ['RET'] diff --git a/WORC/tests/test_RSEnsemble.py b/WORC/tests/test_RSEnsemble.py new file mode 100644 index 00000000..c4a09bc1 --- /dev/null +++ b/WORC/tests/test_RSEnsemble.py @@ -0,0 +1,40 @@ +from WORC.classification.crossval import test_RS_Ensemble +import pandas as pd +import os + +classification_data = r"C:\Users\Martijn Starmans\Documents\GitHub\WORCTutorial\WORC_Example_STWStrategyHN_220915_DoTstNRSNEns\classify\all\tempsave\tempsave_0.hdf5" + +# Read the data and take first predicted label +classification_data = pd.read_hdf(classification_data) +classification_data = classification_data[classification_data.keys()[0]] + +# print(classification_data) + +# Iterate over cross-validation iterations +# for i_cv in range(len(classification_data.classifiers)): +# trained_classifier = classification_data.classifiers[i_cv] +# X_train = classification_data.X_train[i_cv] +# Y_train = classification_data.Y_train[i_cv] +# X_test = classification_data.X_test[i_cv] +# Y_test = classification_data.Y_test[i_cv] +# feature_labels = classification_data.feature_labels +# output_json = os.path.join(os.path.dirname(__file__), f'performance_{i_cv}.json') + +# For tempsave +trained_classifier = classification_data.trained_classifier +X_train = classification_data.X_train +Y_train = classification_data.Y_train +X_test = classification_data.X_test +Y_test = classification_data.Y_test +feature_labels = classification_data.feature_labels +output_json = os.path.join(os.path.dirname(__file__), 'performance_0.json') + +# print(trained_classifier.fitted_workflows) +# print(trained_classifier.fitted_validation_workflows) + +test_RS_Ensemble(estimator_input=trained_classifier, + X_train=X_train, Y_train=Y_train, + X_test=X_test, Y_test=Y_test, + feature_labels=feature_labels, + output_json=output_json, + verbose=True) \ No newline at end of file diff --git a/WORC/tools/createfixedsplits.py b/WORC/tools/createfixedsplits.py index e0db0415..85bf2ecf 100644 --- a/WORC/tools/createfixedsplits.py +++ b/WORC/tools/createfixedsplits.py @@ -1,6 +1,6 @@ #!/usr/bin/env python -# Copyright 2016-2019 Biomedical Imaging Group Rotterdam, Departments of +# Copyright 2016-2022 Biomedical Imaging Group Rotterdam, Departments of # Medical Informatics and Radiology, Erasmus MC, Rotterdam, The Netherlands # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -20,13 +20,41 @@ import WORC.addexceptions as ae from WORC.processing.label_processing import load_labels import pandas as pd +import WORC.processing.label_processing as lp def createfixedsplits(label_file=None, label_type=None, patient_IDs=None, - test_size=0.2, N_iterations=1, regression=False, - stratify=None, modus='singlelabel', output=None): + stratify=True, test_size=0.2, N_iterations=1, + modus='singlelabel', output=None): ''' - Create fixed splits for a cross validation. + Create fixed splits for a random-split cross-validation. + + + Parameters + ---------- + label_file : filepath + CSV file containing the labels of the patients. + label_type: list of strings + labels to extracted from the label file, e.g. ['label1'] + patient_IDs: list of strings + names of patients to take into account. If None, take all + stratify: Boolean + If True, splits are stratified. In this case, you need to provide + label data. + test_size: float + Percentage of patients in test set per iteration. + N_iterations: integer + Number of cross-validation iterations + modus: str + singlelabel or regression. Multilabel not implemented yet. + output: filepath + csv filename to save output to. + + Returns + ------- + df: pandas Dataframe + Fixed splits created. + ''' # Check whether input is valid if patient_IDs is None: @@ -35,27 +63,37 @@ def createfixedsplits(label_file=None, label_type=None, patient_IDs=None, label_data = load_labels(label_file, label_type) patient_IDs = label_data['patient_IDs'] - # Create the stratification object - if modus == 'singlelabel': - stratify = label_data['label'] - elif modus == 'multilabel': - # Create a stratification object from the labels - # Label = 0 means no label equals one - # Other label numbers refer to the label name that is 1 - stratify = list() - labels = label_data['label'] - for pnum in range(0, len(labels[0])): - plabel = 0 - for lnum, slabel in enumerate(labels): - if slabel[pnum] == 1: - plabel = lnum + 1 - stratify.append(plabel) - + else: + raise ae.WORCValueError('Either a label file and label type or patient_IDs need to be provided!') + else: + if stratify is True: + if label_file is not None and label_type is not None: + # Extract data for specific patients only + label_data, _ = lp.findlabeldata(label_file, + label_type, + pids=patient_IDs) else: - raise ae.WORCKeyError('{} is not a valid modus!').format(modus) + raise ae.WORCValueError('A label file and label type needs to be provided for stratified splitting!') + + # Create the stratification object + if stratify: + if modus == 'singlelabel': + stratify = label_data['label'][0].tolist() + elif modus == 'multilabel': + # Create a stratification object from the labels + # Label = 0 means no label equals one + # Other label numbers refer to the label name that is 1 + stratify = list() + labels = label_data['label'] + for pnum in range(0, len(labels[0])): + plabel = 0 + for lnum, slabel in enumerate(labels): + if slabel[pnum] == 1: + plabel = lnum + 1 + stratify.append(plabel) else: - raise ae.WORCIOError('Either a label file and label type or patient_IDs need to be provided!') - + raise ae.WORCKeyError('{} is not a valid modus!').format(modus) + pd_dict = dict() for i in range(N_iterations): print(f'Splitting iteration {i + 1} / {N_iterations}') @@ -65,7 +103,8 @@ def createfixedsplits(label_file=None, label_type=None, patient_IDs=None, # Define stratification unique_patient_IDs, unique_indices =\ np.unique(np.asarray(patient_IDs), return_index=True) - if regression: + + if modus == 'regression' or not stratify: unique_stratify = None else: unique_stratify = [stratify[i] for i in unique_indices] @@ -125,3 +164,19 @@ def createfixedsplits(label_file=None, label_type=None, patient_IDs=None, df.to_csv(output) return df + + +def test(): + patient_IDs = ['HN1004', 'HN1077', 'HN1088', 'HN1146', 'HN1159', 'HN1192', 'HN1259', 'HN1260', + 'HN1323', 'HN1331', 'HN1339', 'HN1342', 'HN1372', 'HN1491', 'HN1501', 'HN1519', + 'HN1524', 'HN1554', 'HN1560', 'HN1748'] + createfixedsplits(label_file=r'C:\Users\Martijn Starmans\Documents\GitHub\WORCTutorial\Data\Examplefiles\pinfo_HN.csv', + patient_IDs=patient_IDs, stratify=True, + label_type=['imaginary_label_1'], N_iterations=3, output='fixedsplits.csv') + + +if __name__ == "__main__": + test() + + + diff --git a/WORC/validators/preflightcheck.py b/WORC/validators/preflightcheck.py index 683ac65d..9ba569b0 100644 --- a/WORC/validators/preflightcheck.py +++ b/WORC/validators/preflightcheck.py @@ -1,6 +1,6 @@ #!/usr/bin/env python -# Copyright 2016-2021 Biomedical Imaging Group Rotterdam, Departments of +# Copyright 2016-2022 Biomedical Imaging Group Rotterdam, Departments of # Medical Informatics and Radiology, Erasmus MC, Rotterdam, The Netherlands # # Licensed under the Apache License, Version 2.0 (the "License"); From 53a70577f885c3b3a06f8aaf0755b93f663f184c Mon Sep 17 00:00:00 2001 From: MStarmans91 Date: Fri, 23 Sep 2022 13:32:03 +0200 Subject: [PATCH 05/30] Added some debugging statments --- WORC/classification/SearchCV.py | 13 +++++----- WORC/classification/crossval.py | 46 ++++++++++++++++++++------------- 2 files changed, 35 insertions(+), 24 deletions(-) diff --git a/WORC/classification/SearchCV.py b/WORC/classification/SearchCV.py index b191c402..955f600d 100644 --- a/WORC/classification/SearchCV.py +++ b/WORC/classification/SearchCV.py @@ -395,6 +395,7 @@ def __init__(self, param_distributions={}, n_iter=10, scoring=None, self.refit_training_workflows = refit_training_workflows self.refit_validation_workflows = refit_validation_workflows self.fitted_workflows = list() + self.fitted_validation_workflows = list() # Only for WORC Paper self.test_RS = True @@ -1042,9 +1043,9 @@ def compute_performance(scoring, Y_valid_truth, Y_valid_score): else: # Refit the models and compute the predictions on the validation sets if verbose: - print('Precomputing scores on training and validation set for ensembling.') + print('\t - Precomputing scores on training and validation set for ensembling.') if self.fitted_validation_workflows: - print('Detected already fitted train-val workflows.') + print('\t - Detected already fitted train-val workflows.') Y_valid_truth = list() performances = list() @@ -1121,8 +1122,8 @@ def compute_performance(scoring, Y_valid_truth, Y_valid_score): # Compute and store the performance on this split performances_iter.append(compute_performance(scoring, - Y_train[valid], - predictions)) + Y_train[valid], + predictions)) # print('fitandscore: ' + str(out[0][1]) + ' and computed: ' + # str(compute_performance(scoring, Y_train[valid], predictions)) + '\n') @@ -1397,7 +1398,7 @@ def compute_performance(scoring, Y_valid_truth, Y_valid_score): if method == 'Single': self.ensemble_validation_score = self.cv_results_['mean_test_score'][0] elif method == 'top_N': - self.ensemble_validation_score = [self.cv_results_['mean_test_score'][i] for i in ensemble] + self.ensemble_validation_score = np.mean([self.cv_results_['mean_test_score'][i] for i in ensemble]) else: selected_params = [parameters_all[i] for i in ensemble] val_split_scores = [] @@ -1433,7 +1434,7 @@ def compute_performance(scoring, Y_valid_truth, Y_valid_score): train = np.arange(0, len(X_train)) if self.fitted_workflows: # Simply select the required estimators - print('\t - Detected already fitted workflows.') + print('\t - Detected already fitted train-test workflows.') estimators = list() for i in ensemble: try: diff --git a/WORC/classification/crossval.py b/WORC/classification/crossval.py index def56571..da67ae37 100644 --- a/WORC/classification/crossval.py +++ b/WORC/classification/crossval.py @@ -236,21 +236,6 @@ def random_split_cross_validation(image_features, feature_labels, classes, Y_test, patient_ID_train, patient_ID_test, random_seed) save_data.append(temp_save_data) - - # Test performance for various RS and ensemble sizes - if config['General']['DoTestNRSNEns']: - output_json = os.path.join(tempfolder, f'performance_RS_Ens_crossval_{i}.json') - test_RS_Ensemble(estimator_input=trained_classifier, - X_train=X_train, Y_train=Y_train, - X_test=X_test, Y_test=Y_test, - feature_labels=feature_labels, - output_json=output_json) - - # Save memory - delattr(trained_classifier, 'fitted_workflows') - trained_classifier.fitted_workflows = list() - delattr(trained_classifier, 'fitted_validation_workflows') - trained_classifier.fitted_validation_workflows = list() # Create a temporary save if tempsave: @@ -276,6 +261,21 @@ def random_split_cross_validation(image_features, feature_labels, classes, panda_data.to_hdf(filename, 'EstimatorData') del panda_data, panda_data_temp + # Test performance for various RS and ensemble sizes + if config['General']['DoTestNRSNEns']: + output_json = os.path.join(tempfolder, f'performance_RS_Ens_crossval_{i}.json') + test_RS_Ensemble(estimator_input=trained_classifier, + X_train=X_train, Y_train=Y_train, + X_test=X_test, Y_test=Y_test, + feature_labels=feature_labels, + output_json=output_json) + + # Save memory + delattr(trained_classifier, 'fitted_workflows') + trained_classifier.fitted_workflows = list() + delattr(trained_classifier, 'fitted_validation_workflows') + trained_classifier.fitted_validation_workflows = list() + # Print elapsed time elapsed = int((time.time() - t) / 60.0) print(f'\t Fitting took {elapsed} minutes.') @@ -824,7 +824,7 @@ def test_RS_Ensemble(estimator_input, X_train, Y_train, X_test, Y_test, estimator_original = copy.deepcopy(estimator_input) X_train_temp = [(x, feature_labels) for x in X_train] n_workflows = len(estimator_original.fitted_workflows) - + # Settings RSs = [10, 100, 1000, 10000] * 10 + [n_workflows] ensembles = [1, 10, 100, 'FitNumber', 'Bagging'] @@ -846,8 +846,9 @@ def test_RS_Ensemble(estimator_input, X_train, Y_train, X_test, Y_test, # Make a local copy of the estimator and select only subset of workflows print(f'\t Using RS {RS}.') estimator = copy.deepcopy(estimator_original) + estimator.maxlen = RS workflow_num = np.arange(n_workflows).tolist() - + # Select only a specific set of workflows random.shuffle(workflow_num) selected_workflows = workflow_num[0:RS] @@ -863,7 +864,16 @@ def test_RS_Ensemble(estimator_input, X_train, Y_train, X_test, Y_test, [estimator.fitted_workflows[i] for i in selected_workflows] estimator.fitted_workflows =\ [estimator.fitted_workflows[i] for i in workflow_ranking] - + + selected_workflows_ranked_all = list() + for j in range(len(estimator.cv_iter)): + selected_workflows_ranked = [i + n_workflows * j for i in selected_workflows] + selected_workflows_ranked = [selected_workflows_ranked[i] for i in workflow_ranking] + selected_workflows_ranked_all.extend(selected_workflows_ranked) + + estimator.fitted_validation_workflows =\ + [estimator.fitted_validation_workflows[i] for i in selected_workflows_ranked_all] + # For advanced ensembling methods, keep only the parameters of the selected RS workflows estimator.cv_results_['params'] =\ [estimator.cv_results_['params'][i] for i in selected_workflows] From 70bfa97566918703a5ea640a5b96bf7dd8c618c0 Mon Sep 17 00:00:00 2001 From: MStarmans91 Date: Sun, 2 Oct 2022 14:20:48 +0200 Subject: [PATCH 06/30] Minor bugfix --- WORC/classification/SearchCV.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/WORC/classification/SearchCV.py b/WORC/classification/SearchCV.py index 955f600d..0562f392 100644 --- a/WORC/classification/SearchCV.py +++ b/WORC/classification/SearchCV.py @@ -821,7 +821,7 @@ def _store(key_name, array, weights=None, splits=False, rank=False): candidate_params_all = np.asarray(candidate_params_all)[bestindices].tolist() for k in results.keys(): results[k] = results[k][bestindices] - n_candidates = len(candidate_params_all) + results['params'] = candidate_params_all # Calculate and store the total_fit_time of this train/test CV @@ -868,7 +868,7 @@ def _store(key_name, array, weights=None, splits=False, rank=False): fitted_validation_workflows =\ [fitted_validation_workflows[i] for i in bestindices_all] - + self.fitted_validation_workflows = fitted_validation_workflows return self From 4dd87f3fff388763a6309dd8919d93496bcad8bb Mon Sep 17 00:00:00 2001 From: Martijn Date: Fri, 7 Oct 2022 11:01:24 +0200 Subject: [PATCH 07/30] Bugfix in how fitted validation workflows are processed. --- WORC/classification/crossval.py | 36 +++++++++++++++--------------- WORC/classification/fitandscore.py | 12 +++++----- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/WORC/classification/crossval.py b/WORC/classification/crossval.py index da67ae37..4b13eb63 100644 --- a/WORC/classification/crossval.py +++ b/WORC/classification/crossval.py @@ -237,6 +237,22 @@ def random_split_cross_validation(image_features, feature_labels, classes, save_data.append(temp_save_data) + # Test performance for various RS and ensemble sizes + if config['General']['DoTestNRSNEns']: + output_json = os.path.join(tempfolder, f'performance_RS_Ens_crossval_{i}.json') + test_RS_Ensemble(estimator_input=trained_classifier, + X_train=X_train, Y_train=Y_train, + X_test=X_test, Y_test=Y_test, + feature_labels=feature_labels, + output_json=output_json) + + # Save memory + delattr(trained_classifier, 'fitted_workflows') + trained_classifier.fitted_workflows = list() + delattr(trained_classifier, 'fitted_validation_workflows') + trained_classifier.fitted_validation_workflows = list() + + # Create a temporary save if tempsave: panda_labels = ['trained_classifier', 'X_train', 'X_test', @@ -261,21 +277,6 @@ def random_split_cross_validation(image_features, feature_labels, classes, panda_data.to_hdf(filename, 'EstimatorData') del panda_data, panda_data_temp - # Test performance for various RS and ensemble sizes - if config['General']['DoTestNRSNEns']: - output_json = os.path.join(tempfolder, f'performance_RS_Ens_crossval_{i}.json') - test_RS_Ensemble(estimator_input=trained_classifier, - X_train=X_train, Y_train=Y_train, - X_test=X_test, Y_test=Y_test, - feature_labels=feature_labels, - output_json=output_json) - - # Save memory - delattr(trained_classifier, 'fitted_workflows') - trained_classifier.fitted_workflows = list() - delattr(trained_classifier, 'fitted_validation_workflows') - trained_classifier.fitted_validation_workflows = list() - # Print elapsed time elapsed = int((time.time() - t) / 60.0) print(f'\t Fitting took {elapsed} minutes.') @@ -821,9 +822,8 @@ def test_RS_Ensemble(estimator_input, X_train, Y_train, X_test, Y_test, """ # Process some input - estimator_original = copy.deepcopy(estimator_input) X_train_temp = [(x, feature_labels) for x in X_train] - n_workflows = len(estimator_original.fitted_workflows) + n_workflows = len(estimator_input.fitted_workflows) # Settings RSs = [10, 100, 1000, 10000] * 10 + [n_workflows] @@ -845,7 +845,7 @@ def test_RS_Ensemble(estimator_input, X_train, Y_train, X_test, Y_test, # Make a local copy of the estimator and select only subset of workflows print(f'\t Using RS {RS}.') - estimator = copy.deepcopy(estimator_original) + estimator = copy.deepcopy(estimator_input) estimator.maxlen = RS workflow_num = np.arange(n_workflows).tolist() diff --git a/WORC/classification/fitandscore.py b/WORC/classification/fitandscore.py index 5cdc1b7c..edc705f0 100644 --- a/WORC/classification/fitandscore.py +++ b/WORC/classification/fitandscore.py @@ -225,8 +225,8 @@ def fit_and_score(X, y, scoring, # Split in train and testing X_train, y_train = _safe_split(estimator, feature_values, y, train) X_test, y_test = _safe_split(estimator, feature_values, y, test, train) - train = np.arange(0, len(y_train)) - test = np.arange(len(y_train), len(y_train) + len(y_test)) + new_train = np.arange(0, len(y_train)) + new_test = np.arange(len(y_train), len(y_train) + len(y_test)) # Set some defaults for if a part fails and we return a dummy fit_time = np.inf @@ -976,8 +976,8 @@ def fit_and_score(X, y, scoring, print(message) # Also reset train and test indices - train = np.arange(0, len(y_train)) - test = np.arange(len(y_train), len(y_train) + len(y_test)) + new_train = np.arange(0, len(y_train)) + new_test = np.arange(len(y_train), len(y_train) + len(y_test)) # Delete the resampling parameters del para_estimator['Resampling_Use'] @@ -1037,8 +1037,8 @@ def fit_and_score(X, y, scoring, try: ret = _fit_and_score(estimator, feature_values, y_all, - scorers, train, - test, verbose, + scorers, new_train, + new_test, verbose, para_estimator, fit_params, return_train_score=return_train_score, return_parameters=return_parameters, From f6b80eaf91914a643c9620d655b20a94d707f988 Mon Sep 17 00:00:00 2001 From: Martijn Date: Fri, 7 Oct 2022 13:47:32 +0200 Subject: [PATCH 08/30] Bugfix in rsens experiment. --- WORC/classification/crossval.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/WORC/classification/crossval.py b/WORC/classification/crossval.py index 4b13eb63..3da5d026 100644 --- a/WORC/classification/crossval.py +++ b/WORC/classification/crossval.py @@ -822,8 +822,9 @@ def test_RS_Ensemble(estimator_input, X_train, Y_train, X_test, Y_test, """ # Process some input + estimator_original = copy.deepcopy(estimator_input) X_train_temp = [(x, feature_labels) for x in X_train] - n_workflows = len(estimator_input.fitted_workflows) + n_workflows = len(estimator_original.fitted_workflows) # Settings RSs = [10, 100, 1000, 10000] * 10 + [n_workflows] @@ -845,7 +846,7 @@ def test_RS_Ensemble(estimator_input, X_train, Y_train, X_test, Y_test, # Make a local copy of the estimator and select only subset of workflows print(f'\t Using RS {RS}.') - estimator = copy.deepcopy(estimator_input) + estimator = copy.deepcopy(estimator_original) estimator.maxlen = RS workflow_num = np.arange(n_workflows).tolist() From ee97ff17c9f15adace29673bd3ce6f76245faf80 Mon Sep 17 00:00:00 2001 From: Martijn Date: Tue, 11 Oct 2022 10:46:42 +0200 Subject: [PATCH 09/30] Bugfix in testrsens --- WORC/classification/SearchCV.py | 46 ++++++++++++++++++++------------- 1 file changed, 28 insertions(+), 18 deletions(-) diff --git a/WORC/classification/SearchCV.py b/WORC/classification/SearchCV.py index 0562f392..46f6f367 100644 --- a/WORC/classification/SearchCV.py +++ b/WORC/classification/SearchCV.py @@ -1046,8 +1046,13 @@ def compute_performance(scoring, Y_valid_truth, Y_valid_score): print('\t - Precomputing scores on training and validation set for ensembling.') if self.fitted_validation_workflows: print('\t - Detected already fitted train-val workflows.') - + + # Create the ground truth Y_valid_truth = list() + for it, (train, valid) in enumerate(self.cv_iter): + Y_valid_truth.append(Y_train[valid]) + + # Precompute the scores of all estimators performances = list() all_predictions = list() ensemble_configurations = list() @@ -1057,22 +1062,7 @@ def compute_performance(scoring, Y_valid_truth, Y_valid_score): predictions_iter = np.zeros((n_iter, prediction_length)) for it, (train, valid) in enumerate(self.cv_iter): - predictions = list() - # Start with storing the ground truth - if num == 0: - Y_valid_truth.append(Y_train[valid]) - - if self.fitted_validation_workflows: - # Use already fitted workflow - estimator = self.fitted_validation_workflows[num + it * self.maxlen] - if estimator is None: - # Estimator is none, so do not consider this pipeline for ensemble - break - - X_train_values = np.asarray([x[0] for x in X_train]) - predictions = estimator.predict_proba(X_train_values[valid]) - - else: + def getpredictions(): new_estimator = clone(base_estimator) # Fit the preprocessors of the pipeline @@ -1105,14 +1095,33 @@ def compute_performance(scoring, Y_valid_truth, Y_valid_score): if features_left == 0: print('no features left' + '\n') # No features are left; do not consider this pipeline for the ensemble - break + return None # Construct and fit the classifier best_estimator = cc.construct_classifier(p_all) best_estimator.fit(processed_X, processed_y) new_estimator.best_estimator_ = best_estimator predictions = new_estimator.predict_proba(X_train_values[valid]) + return predictions + predictions = list() + # Start with storing the ground truth + if self.fitted_validation_workflows: + # Use already fitted workflow + estimator = self.fitted_validation_workflows[num + it * self.maxlen] + if estimator is None: + # Estimator is none, refit and get predictions + predictions = getpredictions() + else: + X_train_values = np.asarray([x[0] for x in X_train]) + predictions = estimator.predict_proba(X_train_values[valid]) + + else: + predictions = getpredictions() + + if predictions is None: + break + # Only take the probabilities for the second class predictions = predictions[:, 1] @@ -1140,6 +1149,7 @@ def compute_performance(scoring, Y_valid_truth, Y_valid_score): # Update the parameters parameters_all = ensemble_configurations n_classifiers = len(ensemble_configurations) + # Construct the array of final predictions base_Y_valid_score = np.zeros((n_iter, n_classifiers, prediction_length)) for iter in range(n_iter): From c0823dc1d4c1bfba64fe283cf728a965347de71a Mon Sep 17 00:00:00 2001 From: MStarmans91 Date: Tue, 11 Oct 2022 10:48:04 +0200 Subject: [PATCH 10/30] Add HDF5 fastr type validator function --- CHANGELOG | 1 + WORC/resources/fastr_types/HDF5.py | 25 +++++++++++++++++++++++-- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index df00d03e..d86c1d31 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -30,6 +30,7 @@ Added - Documentation updates - Option to save the workflows trained on the train-validation training datasets, besides the option to save workflows trained on the full training dataset. Not an option for SMAC due to implementation of SMAC. +- Validate function for fastr HDF5 datatype, as previously fastr deemed empty hdf5 types as valid. 3.6.0 - 2022-04-05 ------------------ diff --git a/WORC/resources/fastr_types/HDF5.py b/WORC/resources/fastr_types/HDF5.py index 0139215f..58a03532 100644 --- a/WORC/resources/fastr_types/HDF5.py +++ b/WORC/resources/fastr_types/HDF5.py @@ -1,4 +1,4 @@ -# Copyright 2011-2014 Biomedical Imaging Group Rotterdam, Departments of +# Copyright 2011-2022 Biomedical Imaging Group Rotterdam, Departments of # Medical Informatics and Radiology, Erasmus MC, Rotterdam, The Netherlands # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -13,10 +13,31 @@ # See the License for the specific language governing permissions and # limitations under the License. - +import os +import pandas as pd +from tables.hdf5extension import HDF5ExtError from fastr.datatypes import URLType class HDF5(URLType): description = 'Pandas HDF5 file' extension = 'hdf5' + + def _validate(self): + # Function to validate the filetype + parsed_value = self.parsed_value + + if self.extension and not parsed_value.endswith(self.extension): + return False + + if not os.path.isfile(parsed_value): + return False + + try: + # Read the file and extract features + data = pd.read_hdf(parsed_value) + + except HDF5ExtError: + # Not a valid hdf5 file + return False + From c34315d28c23f9670b5accddbd0537953a8cc782 Mon Sep 17 00:00:00 2001 From: MStarmans91 Date: Tue, 11 Oct 2022 12:16:52 +0200 Subject: [PATCH 11/30] Bugfix in HDF5 filetype checker --- WORC/classification/crossval.py | 1 - WORC/resources/fastr_types/HDF5.py | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/WORC/classification/crossval.py b/WORC/classification/crossval.py index 3da5d026..86c85375 100644 --- a/WORC/classification/crossval.py +++ b/WORC/classification/crossval.py @@ -252,7 +252,6 @@ def random_split_cross_validation(image_features, feature_labels, classes, delattr(trained_classifier, 'fitted_validation_workflows') trained_classifier.fitted_validation_workflows = list() - # Create a temporary save if tempsave: panda_labels = ['trained_classifier', 'X_train', 'X_test', diff --git a/WORC/resources/fastr_types/HDF5.py b/WORC/resources/fastr_types/HDF5.py index 58a03532..a8745146 100644 --- a/WORC/resources/fastr_types/HDF5.py +++ b/WORC/resources/fastr_types/HDF5.py @@ -36,6 +36,7 @@ def _validate(self): try: # Read the file and extract features data = pd.read_hdf(parsed_value) + return True except HDF5ExtError: # Not a valid hdf5 file From 1d91be33224fbecad0b8c9a5d7e44c79d4ab4eeb Mon Sep 17 00:00:00 2001 From: MStarmans91 Date: Wed, 12 Oct 2022 15:33:03 +0200 Subject: [PATCH 12/30] Remove redundant refitting part, further work on rSENS experiment --- CHANGELOG | 1 + WORC/classification/SearchCV.py | 37 ++++++--------------- WORC/classification/construct_classifier.py | 8 +++-- WORC/classification/crossval.py | 30 ++++++++++++++--- WORC/tests/test_RSEnsemble.py | 1 + 5 files changed, 42 insertions(+), 35 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index d86c1d31..5a392ee9 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -24,6 +24,7 @@ Changed but during refitting skip that step. During optimization, if we skip it, the relation between the hyperparameter and performance gets disturbed. During refitting, we need to have a model, so best option is to skip the step. Previously, there was only skipping. +- Set default of XGB estimator parallelization to single job. Added ~~~~~ diff --git a/WORC/classification/SearchCV.py b/WORC/classification/SearchCV.py index 46f6f367..cc4415b1 100644 --- a/WORC/classification/SearchCV.py +++ b/WORC/classification/SearchCV.py @@ -1212,6 +1212,8 @@ def getpredictions(): N_models = performances_n_class.index(new_performance) + 1 # +1 due to python indexing ensemble = ensemble[0:N_models] best_performance = new_performance + + self.ensemble_validation_score = best_performance if verbose: print(f"Ensembling best {scoring}: {best_performance}.") @@ -1264,6 +1266,8 @@ def getpredictions(): best_index = performances_temp.index(new_performance) iteration += 1 + self.ensemble_validation_score = best_performance + if verbose: # Print the performance gain print(f"Ensembling best {scoring}: {best_performance}.") @@ -1321,6 +1325,7 @@ def getpredictions(): optimal_N_models = best_ensemble_scores.index(optimal_ensemble_performance) + 1 ensemble = ensemble[0:optimal_N_models] best_performance = optimal_ensemble_performance + self.ensemble_validation_score = best_performance if verbose: # Print the performance gain @@ -1393,8 +1398,12 @@ def getpredictions(): best_performance = optimal_ensemble_performance + self.ensemble_validation_score = best_performance + if verbose: # Print the performance gain + print(f"Ensembling best {scoring}: {best_performance}.") + print(f"Single estimator best {scoring}: {single_estimator_performance}.") print(f'Ensemble consists of {len(ensemble)} estimators {ensemble}.') else: @@ -1405,37 +1414,11 @@ def getpredictions(): # First create and score the ensemble on the validation set # If we only want the best solution, we use the score from cv_results_ + # For not Single or Top_N, the score has already been computed during fitting if method == 'Single': self.ensemble_validation_score = self.cv_results_['mean_test_score'][0] elif method == 'top_N': self.ensemble_validation_score = np.mean([self.cv_results_['mean_test_score'][i] for i in ensemble]) - else: - selected_params = [parameters_all[i] for i in ensemble] - val_split_scores = [] - for train, valid in self.cv_iter: - estimators = list() - for enum, p_all in enumerate(selected_params): - new_estimator = clone(base_estimator) - - new_estimator.refit_and_score(X_train, Y_train, p_all, - train, valid, - verbose=False) - - estimators.append(new_estimator) - - new_estimator = clone(base_estimator) - new_estimator.ensemble = Ensemble(estimators) - new_estimator.best_estimator_ = new_estimator.ensemble - # Calculate and store the final performance of the ensemble - # on this validation split - X_train_values = np.asarray([x[0] for x in X_train]) - predictions = new_estimator.predict(X_train_values[valid]) - val_split_scores.append(compute_performance(scoring, - Y_train[valid], - predictions)) - - validation_score = np.mean(val_split_scores) - self.ensemble_validation_score = validation_score if verbose: print('Final ensemble validation score: ' + str(self.ensemble_validation_score)) diff --git a/WORC/classification/construct_classifier.py b/WORC/classification/construct_classifier.py index eb70857e..b35bfb58 100644 --- a/WORC/classification/construct_classifier.py +++ b/WORC/classification/construct_classifier.py @@ -93,10 +93,11 @@ def construct_classifier(config): min_child_weight=min_child_weight, n_estimators=boosting_rounds, colsample_bytree=colsample_bytree, - random_state=config['random_seed']) + random_state=config['random_seed'], + n_jobs=1) elif config['classifiers'] == 'XGBRegressor': - # XGB Classifier + # XGB Regressor max_depth = config['XGB_max_depth'] learning_rate = config['XGB_learning_rate'] gamma = config['XGB_gamma'] @@ -109,7 +110,8 @@ def construct_classifier(config): min_child_weight=min_child_weight, n_estimators=boosting_rounds, colsample_bytree=colsample_bytree, - random_state=config['random_seed']) + random_state=config['random_seed'], + n_jobs=1) elif config['classifiers'] == 'LightGBMClassifier': # LightGBM Classifier diff --git a/WORC/classification/crossval.py b/WORC/classification/crossval.py index 86c85375..86e4ecac 100644 --- a/WORC/classification/crossval.py +++ b/WORC/classification/crossval.py @@ -30,6 +30,7 @@ import json import copy from sklearn.metrics import f1_score, roc_auc_score +from WORC.classification.SearchCV import RandomizedSearchCVfastr def random_split_cross_validation(image_features, feature_labels, classes, @@ -812,12 +813,16 @@ def nocrossval(config, label_data_train, label_data_test, image_features_train, def test_RS_Ensemble(estimator_input, X_train, Y_train, X_test, Y_test, - feature_labels, output_json, verbose=False): + feature_labels, output_json, verbose=False, RSs=None, + ensembles=None, maxlen=100): """Test performance for different random search and ensemble sizes. This function is written for conducting a specific experiment from the WORC paper to test how the performance varies with varying random search and ensemble sizes. We do not recommend usage in general of this part. + + maxlen = 100 # max ensembles numeric + """ # Process some input @@ -826,9 +831,23 @@ def test_RS_Ensemble(estimator_input, X_train, Y_train, X_test, Y_test, n_workflows = len(estimator_original.fitted_workflows) # Settings - RSs = [10, 100, 1000, 10000] * 10 + [n_workflows] - ensembles = [1, 10, 100, 'FitNumber', 'Bagging'] - maxlen = 100 # max ensembles numeric + if RSs is None: + RSs = [10, 100, 1000, 10000] * 10 + [n_workflows] + + if ensembles is None: + ensembles = [1, 10, 100, 'FitNumber', 'Bagging'] + + # Refit validation estimators if required + if not estimator_original.fitted_validation_workflows and estimator_original.refit_validation_workflows: + print('\t Refit all validation workflows so we dont have to do this for every ensembling method.') + for num, parameters in enumerate(estimator_original.cv_results_['params']): + for cvnum, (train, valid) in enumerate(estimator_original.cv_iter): + # if verbose: + # print(f"\t-- Refitting estimator {num} on cross-validation {cvnum}.") + new_estimator = RandomizedSearchCVfastr() + new_estimator.refit_and_score(X_train_temp, Y_train, parameters, + train=train, test=valid) + estimator_original.fitted_validation_workflows.append(new_estimator) # Loop over the random searches and ensembles keys = list() @@ -865,12 +884,13 @@ def test_RS_Ensemble(estimator_input, X_train, Y_train, X_test, Y_test, estimator.fitted_workflows =\ [estimator.fitted_workflows[i] for i in workflow_ranking] + # Select the required already fitted validation workflows selected_workflows_ranked_all = list() for j in range(len(estimator.cv_iter)): selected_workflows_ranked = [i + n_workflows * j for i in selected_workflows] selected_workflows_ranked = [selected_workflows_ranked[i] for i in workflow_ranking] selected_workflows_ranked_all.extend(selected_workflows_ranked) - + estimator.fitted_validation_workflows =\ [estimator.fitted_validation_workflows[i] for i in selected_workflows_ranked_all] diff --git a/WORC/tests/test_RSEnsemble.py b/WORC/tests/test_RSEnsemble.py index c4a09bc1..18c7b260 100644 --- a/WORC/tests/test_RSEnsemble.py +++ b/WORC/tests/test_RSEnsemble.py @@ -22,6 +22,7 @@ # For tempsave trained_classifier = classification_data.trained_classifier +trained_classifier.fitted_validation_workflows = list() X_train = classification_data.X_train Y_train = classification_data.Y_train X_test = classification_data.X_test From ca54b1cf3cd4552778624b8ed376164f42fb9054 Mon Sep 17 00:00:00 2001 From: Martijn Date: Wed, 12 Oct 2022 17:08:34 +0200 Subject: [PATCH 13/30] Some bugfixes in RSENS function. --- WORC/classification/crossval.py | 59 +++++++++++++++++---------------- 1 file changed, 30 insertions(+), 29 deletions(-) diff --git a/WORC/classification/crossval.py b/WORC/classification/crossval.py index 86e4ecac..4e0cbd98 100644 --- a/WORC/classification/crossval.py +++ b/WORC/classification/crossval.py @@ -824,11 +824,10 @@ def test_RS_Ensemble(estimator_input, X_train, Y_train, X_test, Y_test, maxlen = 100 # max ensembles numeric """ - # Process some input estimator_original = copy.deepcopy(estimator_input) X_train_temp = [(x, feature_labels) for x in X_train] - n_workflows = len(estimator_original.fitted_workflows) + n_workflows = len(estimator_original.cv_results_['mean_test_score']) # Settings if RSs is None: @@ -837,18 +836,6 @@ def test_RS_Ensemble(estimator_input, X_train, Y_train, X_test, Y_test, if ensembles is None: ensembles = [1, 10, 100, 'FitNumber', 'Bagging'] - # Refit validation estimators if required - if not estimator_original.fitted_validation_workflows and estimator_original.refit_validation_workflows: - print('\t Refit all validation workflows so we dont have to do this for every ensembling method.') - for num, parameters in enumerate(estimator_original.cv_results_['params']): - for cvnum, (train, valid) in enumerate(estimator_original.cv_iter): - # if verbose: - # print(f"\t-- Refitting estimator {num} on cross-validation {cvnum}.") - new_estimator = RandomizedSearchCVfastr() - new_estimator.refit_and_score(X_train_temp, Y_train, parameters, - train=train, test=valid) - estimator_original.fitted_validation_workflows.append(new_estimator) - # Loop over the random searches and ensembles keys = list() performances = dict() @@ -879,27 +866,41 @@ def test_RS_Ensemble(estimator_input, X_train, Y_train, X_test, Y_test, F1_validation = [F1_validation[i] for i in workflow_ranking] # Only keep the number of RS required and resort based on ranking - estimator.fitted_workflows =\ - [estimator.fitted_workflows[i] for i in selected_workflows] - estimator.fitted_workflows =\ - [estimator.fitted_workflows[i] for i in workflow_ranking] - - # Select the required already fitted validation workflows - selected_workflows_ranked_all = list() - for j in range(len(estimator.cv_iter)): - selected_workflows_ranked = [i + n_workflows * j for i in selected_workflows] - selected_workflows_ranked = [selected_workflows_ranked[i] for i in workflow_ranking] - selected_workflows_ranked_all.extend(selected_workflows_ranked) - - estimator.fitted_validation_workflows =\ - [estimator.fitted_validation_workflows[i] for i in selected_workflows_ranked_all] - + if estimator.fitted_workflows: + estimator.fitted_workflows =\ + [estimator.fitted_workflows[i] for i in selected_workflows] + estimator.fitted_workflows =\ + [estimator.fitted_workflows[i] for i in workflow_ranking] + # For advanced ensembling methods, keep only the parameters of the selected RS workflows estimator.cv_results_['params'] =\ [estimator.cv_results_['params'][i] for i in selected_workflows] estimator.cv_results_['params'] =\ [estimator.cv_results_['params'][i] for i in workflow_ranking] + # Refit validation estimators if required + if not estimator.fitted_validation_workflows and estimator.refit_validation_workflows: + print('\t Refit all validation workflows so we dont have to do this for every ensembling method.') + for num, parameters in enumerate(estimator.cv_results_['params']): + for cvnum, (train, valid) in enumerate(estimator.cv_iter): + if verbose: + print(f"\t -- Refitting estimator {num} on cross-validation {cvnum}.") + new_estimator = RandomizedSearchCVfastr() + new_estimator.refit_and_score(X_train_temp, Y_train, parameters, + train=train, test=valid) + estimator.fitted_validation_workflows.append(new_estimator) + + elif estimator.fitted_validation_workflows: + # Select the required already fitted validation workflows + selected_workflows_ranked_all = list() + for j in range(len(estimator.cv_iter)): + selected_workflows_ranked = [i + n_workflows * j for i in selected_workflows] + selected_workflows_ranked = [selected_workflows_ranked[i] for i in workflow_ranking] + selected_workflows_ranked_all.extend(selected_workflows_ranked) + + estimator.fitted_validation_workflows =\ + [estimator.fitted_validation_workflows[i] for i in selected_workflows_ranked_all] + # Store train and validation AUC mean_val_F1 = F1_validation[0:maxlen] F1_training = estimator.cv_results_['mean_train_score'] From f934ceadbc16d6ca580e4f4ce6a29c2dad467084 Mon Sep 17 00:00:00 2001 From: MStarmans91 Date: Wed, 12 Oct 2022 18:02:35 +0200 Subject: [PATCH 14/30] Use joblib for parallelization of rsens --- WORC/classification/crossval.py | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/WORC/classification/crossval.py b/WORC/classification/crossval.py index 4e0cbd98..261b9395 100644 --- a/WORC/classification/crossval.py +++ b/WORC/classification/crossval.py @@ -21,15 +21,16 @@ import os import time from time import gmtime, strftime -from sklearn.model_selection import train_test_split, LeaveOneOut -from .parameter_optimization import random_search_parameters, guided_search_parameters -import WORC.addexceptions as ae -from WORC.classification.regressors import regressors import glob import random import json import copy from sklearn.metrics import f1_score, roc_auc_score +from sklearn.model_selection import train_test_split, LeaveOneOut +from joblib import Parallel, delayed +import WORC.addexceptions as ae +from .parameter_optimization import random_search_parameters, guided_search_parameters +from WORC.classification.regressors import regressors from WORC.classification.SearchCV import RandomizedSearchCVfastr @@ -881,14 +882,21 @@ def test_RS_Ensemble(estimator_input, X_train, Y_train, X_test, Y_test, # Refit validation estimators if required if not estimator.fitted_validation_workflows and estimator.refit_validation_workflows: print('\t Refit all validation workflows so we dont have to do this for every ensembling method.') - for num, parameters in enumerate(estimator.cv_results_['params']): - for cvnum, (train, valid) in enumerate(estimator.cv_iter): - if verbose: - print(f"\t -- Refitting estimator {num} on cross-validation {cvnum}.") - new_estimator = RandomizedSearchCVfastr() - new_estimator.refit_and_score(X_train_temp, Y_train, parameters, - train=train, test=valid) - estimator.fitted_validation_workflows.append(new_estimator) + + # Define function to fit a single estimator + def fitvalidationestimator(parameters, train, test): + new_estimator = RandomizedSearchCVfastr() + new_estimator.refit_and_score(X_train_temp, Y_train, parameters, + train=train, test=test) + return new_estimator + + # Use joblib to parallelize fitting + estimators =\ + Parallel(n_jobs=-1)(delayed(fitvalidationestimator)( + parameters, train, test) + for parameters in estimator.cv_results_['params'] + for train, test in estimator.cv_iter) + estimator.fitted_validation_workflows = estimators elif estimator.fitted_validation_workflows: # Select the required already fitted validation workflows From 6f0472371c805d86dbcba8af2ac6be3f9fff53c6 Mon Sep 17 00:00:00 2001 From: Martijn Date: Tue, 18 Oct 2022 09:19:54 +0200 Subject: [PATCH 15/30] Add option to eliminate all NaN features during elimination. Fix bug in segmentix when using Dummy's. --- CHANGELOG | 4 ++++ WORC/IOparser/config_io_classifier.py | 3 +++ WORC/WORC.py | 1 + WORC/classification/fitandscore.py | 7 ++++++- WORC/classification/trainclassifier.py | 1 + .../segmentix/bin/segmentix_tool.py | 19 +++++++++++++++---- 6 files changed, 30 insertions(+), 5 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 5a392ee9..603fc38e 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -17,6 +17,8 @@ Fixed instead of returning NaN. - Createfixedsplits function was outdated, updates to newest conventions and added to documentation. +- When using Dummy's, segmentix now still copies metadata information from image + to segmentation when required. Changed ~~~~~~~ @@ -32,6 +34,8 @@ Added - Option to save the workflows trained on the train-validation training datasets, besides the option to save workflows trained on the full training dataset. Not an option for SMAC due to implementation of SMAC. - Validate function for fastr HDF5 datatype, as previously fastr deemed empty hdf5 types as valid. +- Option to eliminate features which are all NaN during imputation. + 3.6.0 - 2022-04-05 ------------------ diff --git a/WORC/IOparser/config_io_classifier.py b/WORC/IOparser/config_io_classifier.py index eb1e948e..a8c2d8c1 100644 --- a/WORC/IOparser/config_io_classifier.py +++ b/WORC/IOparser/config_io_classifier.py @@ -157,6 +157,9 @@ def load_config(config_file_path): [int(str(item).strip()) for item in settings['Imputation']['n_neighbors'].split(',')] + settings_dict['Imputation']['skipallNaN'] =\ + [str(settings['Imputation']['skipallNaN'])] + # OneHotEncoding settings_dict['OneHotEncoding']['Use'] =\ [str(item).strip() for item in diff --git a/WORC/WORC.py b/WORC/WORC.py index a21e2c6b..08f66fe1 100644 --- a/WORC/WORC.py +++ b/WORC/WORC.py @@ -353,6 +353,7 @@ def defaultconfig(self): config['Imputation']['use'] = 'True' config['Imputation']['strategy'] = 'mean, median, most_frequent, constant, knn' config['Imputation']['n_neighbors'] = '5, 5' + config['Imputation']['skipallNaN'] = 'True' # Feature scaling options config['FeatureScaling'] = dict() diff --git a/WORC/classification/fitandscore.py b/WORC/classification/fitandscore.py index edc705f0..527cff4f 100644 --- a/WORC/classification/fitandscore.py +++ b/WORC/classification/fitandscore.py @@ -326,8 +326,12 @@ def fit_and_score(X, y, scoring, if original_shape != imputed_shape: removed_features = original_shape[1] - imputed_shape[1] - raise ae.WORCValueError(f'Several features ({removed_features}) were np.NaN for all objects. Hence, imputation was not possible. Either make sure this is correct and turn of imputation, or correct the feature.') + if para_estimator['ImputationSkipAllNaN'] == 'True': + print(f"[WARNING]: Several features ({removed_features}) were np.NaN for all objects. config['Imputation']['skipallNaN'] set to True, so simply eliminate these features.") + else: + raise ae.WORCValueError(f'Several features ({removed_features}) were np.NaN for all objects. Hence, imputation was not possible. Either make sure this is correct and turn of imputation, or correct the feature.') + del para_estimator['ImputationSkipAllNaN'] del para_estimator['Imputation'] del para_estimator['ImputationMethod'] if 'ImputationNeighbours' in para_estimator.keys(): @@ -1116,6 +1120,7 @@ def delete_nonestimator_parameters(parameters): 'Imputation', 'ImputationMethod', 'ImputationNeighbours', + 'ImputationSkipAllNaN', 'SelectFromModel', 'SelectFromModel_lasso_alpha', 'SelectFromModel_estimator', diff --git a/WORC/classification/trainclassifier.py b/WORC/classification/trainclassifier.py index 3b366721..6dc4bfc7 100644 --- a/WORC/classification/trainclassifier.py +++ b/WORC/classification/trainclassifier.py @@ -226,6 +226,7 @@ def add_parameters_to_grid(param_grid, config): param_grid['Imputation'] = config['Imputation']['use'] param_grid['ImputationMethod'] = config['Imputation']['strategy'] + param_grid['ImputationSkipAllNaN'] = config['Imputation']['skipallNaN'] param_grid['ImputationNeighbours'] =\ discrete_uniform(loc=config['Imputation']['n_neighbors'][0], scale=config['Imputation']['n_neighbors'][1]) diff --git a/WORC/resources/fastr_tools/segmentix/bin/segmentix_tool.py b/WORC/resources/fastr_tools/segmentix/bin/segmentix_tool.py index 23dcb74c..918a3b42 100644 --- a/WORC/resources/fastr_tools/segmentix/bin/segmentix_tool.py +++ b/WORC/resources/fastr_tools/segmentix/bin/segmentix_tool.py @@ -1,6 +1,6 @@ #!/usr/bin/env python -# Copyright 2017-2018 Biomedical Imaging Group Rotterdam, Departments of +# Copyright 2017-2022 Biomedical Imaging Group Rotterdam, Departments of # Medical Informatics and Radiology, Erasmus MC, Rotterdam, The Netherlands # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,8 +16,10 @@ # limitations under the License. import argparse -from WORC.processing.segmentix import segmentix from shutil import copyfile +import SimpleITK as sitk +from WORC.processing.segmentix import segmentix +import WORC.IOparser.config_segmentix as config_io def main(): @@ -63,9 +65,18 @@ def main(): if 'Dummy' in str(args.im): # Image is a dummy, so we do not do anything with the segmentation but - # simply copy the input to the output + # simply copy the input to the output, except copy information if args.out is not None: - copyfile(str(args.seg), str(args.out)) + config = config_io.load_config(args.para) + if config['Segmentix']['AssumeSameImageAndMaskMetadata']: + print('[Segmentix] Copy metadata information from image to mask.') + image = sitk.ReadImage(args.im) + seg = sitk.ReadImage(args.seg) + seg.CopyInformation(image) + sitk.WriteImage(seg, args.out) + else: + copyfile(str(args.seg), str(args.out)) + else: segmentix(image=args.im, segmentation=args.seg, parameters=args.para, output=args.out, metadata_file=args.md, mask=args.mask) From 570bb47ed4f2c6317b5dfc0bd5be56946aec1da7 Mon Sep 17 00:00:00 2001 From: MStarmans91 Date: Wed, 19 Oct 2022 09:59:27 +0200 Subject: [PATCH 16/30] Documentation updates --- WORC/classification/SearchCV.py | 3 - WORC/doc/static/developerdocumentation.rst | 2 +- WORC/doc/static/quick_start.rst | 37 +++--- WORC/doc/static/user_manual.rst | 127 ++++++++++++++++----- 4 files changed, 117 insertions(+), 52 deletions(-) diff --git a/WORC/classification/SearchCV.py b/WORC/classification/SearchCV.py index cc4415b1..3ffc84c4 100644 --- a/WORC/classification/SearchCV.py +++ b/WORC/classification/SearchCV.py @@ -855,9 +855,6 @@ def _store(key_name, array, weights=None, splits=False, rank=False): # Sort according to best indices fitted_workflows = [fitted_workflows[i] for i in bestindices] - # Remove None workflows - fitted_workflows = [f for f in fitted_workflows if f is not None] - self.fitted_workflows = fitted_workflows if self.refit_validation_workflows: diff --git a/WORC/doc/static/developerdocumentation.rst b/WORC/doc/static/developerdocumentation.rst index 959f0410..70df0e0a 100644 --- a/WORC/doc/static/developerdocumentation.rst +++ b/WORC/doc/static/developerdocumentation.rst @@ -65,7 +65,7 @@ Adding methods to hyperoptimization add a part to that function to embed your method in the workflow. We advice you to embed your method in a sklearn compatible class, having init, fit and transform functions. See for example - py:mod:`WORC.featureprocessing.Preprocessor.Preprocessor`. + :py:mod:`WORC.featureprocessing.Preprocessor.Preprocessor`. In :py:mod:`WORC.classification.fitandscore.fit_and_score`, make sure diff --git a/WORC/doc/static/quick_start.rst b/WORC/doc/static/quick_start.rst index 8ce0d689..a3a7625e 100644 --- a/WORC/doc/static/quick_start.rst +++ b/WORC/doc/static/quick_start.rst @@ -252,7 +252,7 @@ named after your experiment name. for k, v in stats.items(): print(f"\t {k} {v}.") -.. note:: the performance is probably horrible, which is expected as we ran the experiment on coarse settings. These settings are recommended to only use for testing: see also below. +.. note:: The performance is probably horrible, which is expected as we ran the experiment on coarse settings. These settings are recommended to only use for testing: see also below. Tips and Tricks @@ -269,10 +269,10 @@ Some things we would advice to always do: * Run actual experiments on the full settings (coarse=False): -.. code-block:: python + .. code-block:: python - coarse = False - experiment.binary_classification(coarse=coarse) + coarse = False + experiment.binary_classification(coarse=coarse) .. note:: This will result in more computation time. We therefore recommmend to run this script on either a cluster or high performance PC. If so, @@ -281,30 +281,33 @@ Some things we would advice to always do: .. code-block:: python - experiment.set_multicore_execution() + experiment.set_multicore_execution() This is not required when running WORC on the BIGR or SURFSara Cartesius cluster, as automatic detectors for these clusters have been built into SimpleWORC and BasicWORC. * Add extensive evaluation: ``experiment.add_evaluation()`` before ``experiment.execute()``: -.. code-block:: python + .. code-block:: python - experiment.add_evaluation() + experiment.add_evaluation() -Changing fields in the configuration can be done with the add_config_overrides function, see below. -We recommend doing this after the modus part, as these also perform config_overrides. -NOTE: all configuration fields have to be provided as strings. + See the "Evaluation of your network" section in the :ref:`User Manual ` + chapter for more details on the evaluation outputs. -.. code-block:: python +* Changing fields in the configuration can be done with the add_config_overrides function, see below. + We recommend doing this after the modus part, as these also perform config_overrides. + NOTE: all configuration fields have to be provided as strings. + + .. code-block:: python overrides = { - 'Classification': { - 'classifiers': 'SVM', - }, - } + 'Classification': { + 'classifiers': 'SVM', + }, + } experiment.add_config_overrides(overrides) -For a complete overview of all configuration functions, please look at the -:ref:`Config chapter `. + For a complete overview of all configuration functions, please look at the + :ref:`Config chapter `. diff --git a/WORC/doc/static/user_manual.rst b/WORC/doc/static/user_manual.rst index a8cb3dae..6bba8918 100644 --- a/WORC/doc/static/user_manual.rst +++ b/WORC/doc/static/user_manual.rst @@ -120,7 +120,6 @@ in the classification. Images and segmentations ^^^^^^^^^^^^^^^^^^^^^^^^ - The minimal input for a Radiomics pipeline consists of either images plus segmentations, or features, plus a label file (and a configuration, but you can just use the default one). @@ -172,7 +171,6 @@ your segmentation, e.g. using dilation, then use a mask to make sure it is still valid. See the :ref:`config chapter ` for all segmentix options. - Features ^^^^^^^^ If you already computed your features, e.g. from a previous run, you can @@ -190,7 +188,6 @@ Check the PREDICT.imagefeatures.patient_feature module for the currently implemented tags. - Elastix_Para ^^^^^^^^^^^^ If you have multiple images for each patient, e.g. T1 and T2, but only a @@ -250,29 +247,28 @@ assuming you have created the relevant objects as listed above: network.set() network.execute() +.. _um-evaluation: -Evaluation of your network --------------------------- - -In WORC, there are two options for testing your fitted models: +Outputs and evaluation of your network +--------------------------------------- -1. Single dataset: cross-validation (currently only random-split) -2. Separate train and test dataset: bootstrapping on test dataset +The following outputs and evaluation methods are always generated: -Within these evaluation settings, the following performance evaluation methods are used: - -1. Confidence intervals on several metrics: +1. Performance of your model (main output). + Stored in file ``performance_all_{num}.json``. If you created multiple models to predict multiple labels, or did multilabel classification, the ``{num}`` corresponds + to the label. Contains 95% Confidence intervals on several metrics. + For classification: a. Area under the curve (AUC) of the receiver operating characteristic (ROC) curve. In a multiclass setting, weuse the multiclass AUC from the `TADPOLE Challenge `_. b. Accuracy. - c. Balanced Classification Accuracy (BCA) as defined by the `TADPOLE Challenge `_. + c. Balanced Classification Accuracy (BCA), based on Balanced Classification Rate by `Tharwat, A., 2021. Classification assessment methods. Applied Computing and Informatics 17, 168–192.`. d. F1-score - e. Sensitivity, aka recall or true positive rate - f. Specificity, aka true negative rate + e. Sensitivity or recall or true positive rate + f. Specificity or true negative rate g. Negative predictive value (NPV) - h. Precision, aka Positive predictive value (PPV) + h. Precision or Positive predictive value (PPV) For regression: @@ -294,36 +290,103 @@ Within these evaluation settings, the following performance evaluation methods a In bootstrapping, 95% confidence intervals are created using the ''standard'' method according to a normal distribution: see Table 6, method 1 in `Efron B., Tibshirani R. Bootstrap Methods for Standard Errors, Confidence Intervals, and Other Measures of Statistical Accuracy, Statistical Science Vol.1, No,1, 54-77, 1986`. -2. ROC and PRC curves with 95% confidence intervals using the fixed-width bands method, see `Macskassy S. A., Provost F., Rosset S. ROC Confidence Bands: An Empirical Evaluation. In: Proceedings of the 22nd international conference on Machine learning. 2005.` +2. The configuration used by WORC. + + Stored in files ``config_{type}_{num}.ini``. These are the result of the fingerprinting of your dataset. The ``config_all_{num}.ini`` config is used in classification, the other types + are used for feature extraction and are named after the image types you provided. For example, if you provided two image types, ``['MRI', 'CT']``, you will get + ``config_MRI_0.ini`` and ``config_CT_0.ini``. If you provide multiple of the same types, the numbers will change. + +3. The fitted models. + + Stored in files ``estimator_all_{num}.hdf5``. + +4. The extracted features. + + Stored in the ``Features`` folder, in the files ``features_{featuretoolboxname}_{image_type}_{num}_{sample_id}.hdf5``. + +.. note:: For every output file, fastr generates a provenance file (``...prov.json``) stating how a file was generated, see https://fastr.readthedocs.io/en/stable/static/user_manual.html#provenance. + +The following outputs and evaluation methods are only created when ``WORC.add_evaluation()`` is used (similar for ``SimpleWORC`` and ``BasicWORC``), +and are stored in the ``Evaluation`` in the output folder of your experiment. + +1. Receiver Operating Characteristic (ROC) and Precision-Recall (PR) curves. + + Stored in files ``ROC_all_{num}.{ext}`` and ``PRC_all_{num}.{ext}``. For each curve, a ``.png`` is generated for previewing, a ``.tex`` with tikzplotlib + which can be used to plot the figure in LateX in high quality, and a ``.csv`` with the confidence intervals so you can easily check these. + + 95% confidence bands are constructured using the fixed-width bands method from `Macskassy S. A., Provost F., Rosset S. ROC Confidence Bands: An Empirical Evaluation. In: Proceedings of the 22nd international conference on Machine learning. 2005.` + +2. Univariate statistical testing of the features. -3. Univariate statistical testing of the features using: + Stored in files ``StatisticalTestFeatures_all_{num}.{ext}``. A ``.png`` is generated for previewing, a ``.tex`` with tikzplotlib + which can be used to plot the figure in LateX in high quality, and a ``.csv`` with the p-values. + + The following statistical tests are used: a. A student t-test b. A Welch test c. A Wilcoxon test d. A Mann-Whitney U test - The uncorrected p-values for all these tests are reported in a single excel sheet. Pick the right test and significance - level based on your assumptions. Normally, we make use of the Mann-Whitney U test, as our features do not have to be normally - distributed, it's nonparametric, and assumes independent samples. + The uncorrected p-values for all these tests are reported in a the .csv. Pick the right test and significance + level based on your assumptions. + + Normally, we make use of the Mann-Whitney U test, as our features do not have to be normally + distributed, it's nonparametric, and assumes independent samples. Additionally, generally correction should be done + for multiple testing, which we always do with Bonferonni correction. Hence, .png and .tex files contain the + p-values of the Mann-Whitney U; the p-value of the magenta statistical significance has been corrected with + Bonferonni correction. + +3. Overview of hyperparameters used in the top ranked models. + + Stored in file ``Hyperparameters_all_{num}.csv``. + + Each row corresponds with the hyperparameters of one workflow. The following information is displayed in the respective columns: + + A. The cross-validation iteration. + B. The rank of that workflow in that cross-validation. + C. The metric on which the ranking in column B was based. + D. The mean score on the validation datasets in the nested cross-validation of the metric in column C. + E. The mean score on the training datasets in the nested cross-validation of the metric in column C. + F. The mean time it took to fit that workflow in the validation datasets. + G. and further: the actual hyperparameters. + + For how many of the top ranked workflows the hyperparameters are included in this file depends on the ``config["Ensemble"]["Size"]``, see :ref:`configuration chapter `. + +4. Boxplots of the features. + + Stored in ``BoxplotsFeatures_all_{num}.zip``. The .zip files contains multiple .png files, each with maximum 25 boxplots of features. + + For the full **training** dataset (i.e., if a separate test-set is provided, this is not included in these plots.), per features, one boxplot + is generated depicting the distribution of features for all samples (blue), and for binary classification, also only for the samples + with label 0 (green) and for the samples with label 1 (red). Hence, this gives an impression whether some features show major differences + in the distribution among the different classes, and thus could be useful in the classification to separate them. -4. Ranking patients from typical to atypical as determined by the model, based on either: +5. Ranking patients from typical to atypical as determined by the model. + + Stored in files ``RankedPosteriors_all_{num}.{ext}`` and ``RankedPercentages_all_{num}.{ext}``. + + Two types of rankings are done: a. The percentage of times a patient was classified correctly when occuring in the test set. Patients always correctly classified can be seen as typical examples; patients always classified incorrectly as atypical. b. The mean posterior of the patient when occuring in the test set. - These measures can only be used in classification. Besides an Excel with the rankings, snapshots of the middle slice - of the image + segmentation are saved with the ground truth label and the percentage/posterior in the filename. In - this way, one can scroll through the patients from typical to atypical to distinguish a pattern. + These measures can only be used in classification. Besides a .csv with the rankings, snapshots of the middle slice + of the image + segmentation are saved with the ground truth label and the percentage/posterior in the filename in + a .zip file. In this way, one can scroll through the patients from typical to atypical to distinguish a pattern. + +6. A barchart of how often certain features groups or feature selection groups were selected in the optimal methods. + + Stored in files ``Barchart_all_{num}.{ext}``. A ``.png`` is generated for previewing, a ``.tex`` with tikzplotlib + which can be used to plot the figure in LateX in high quality. -5. A barchart of how often certain features groups were selected in the optimal methods. Only useful when using - groupwise feature selection. + Gives an idea of which features are most relevant for the predictions of the model, and which feature methods are often succesful. + The overview of the hyperparameters, see above, is more quantitative and useful however. - By default, only the first evaluation method, e.g. metric computation, is used. The other methods can simply be added - to WORC by using the ``add_evaluation()`` function, either directly in WORC or through the facade: +7. Decomposition of your feature space. -6. Decomposition of your feature space. + Stored in file ``Decomposition_all_{num}.png``. The following decompositions are performed: @@ -338,6 +401,7 @@ Within these evaluation settings, the following performance evaluation methods a regular PCA shows good separation of your classes, your classes can be split using linear combinations of your features. + To add the evaluation workflow, simply use the ``add_evaluation`` function: .. code-block:: python @@ -348,9 +412,10 @@ To add the evaluation workflow, simply use the ``add_evaluation`` function: ... experiment.add_evaluation(label_type) +Or in the ``SimpleWORC`` or ``BasicWORC`` facades: + .. code-block:: python - import WORC from WORC import SimpleWORC experiment = SimpleWORC('somename') ... From 7286f682554fcb1f186297dc26585665c1e02d6a Mon Sep 17 00:00:00 2001 From: MStarmans91 Date: Wed, 19 Oct 2022 10:04:45 +0200 Subject: [PATCH 17/30] Minor documentation update --- WORC/doc/static/user_manual.rst | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/WORC/doc/static/user_manual.rst b/WORC/doc/static/user_manual.rst index 6bba8918..ae1f2a36 100644 --- a/WORC/doc/static/user_manual.rst +++ b/WORC/doc/static/user_manual.rst @@ -254,10 +254,12 @@ Outputs and evaluation of your network The following outputs and evaluation methods are always generated: -1. Performance of your model (main output). +1. Performance of your models (main output). Stored in file ``performance_all_{num}.json``. If you created multiple models to predict multiple labels, or did multilabel classification, the ``{num}`` corresponds - to the label. Contains 95% Confidence intervals on several metrics. + to the label. The file consists of three parts. + + **Mean and 95% confidence intervals of several performance metrics.** For classification: @@ -290,6 +292,14 @@ The following outputs and evaluation methods are always generated: In bootstrapping, 95% confidence intervals are created using the ''standard'' method according to a normal distribution: see Table 6, method 1 in `Efron B., Tibshirani R. Bootstrap Methods for Standard Errors, Confidence Intervals, and Other Measures of Statistical Accuracy, Statistical Science Vol.1, No,1, 54-77, 1986`. + **Rankings of your samples** + In thid dictionary, the "Percentages" part shows how often a sample was classified correctly + when that sample was in the test set. The number of times the sample was in in the test set is also listed. + Those samples that were always classified correctly or always classified incorrecty are also named, including their ground truth label. + + **The metric values for each train-test cross-validation iteration** + These are where the confidence intervals are based upon. + 2. The configuration used by WORC. Stored in files ``config_{type}_{num}.ini``. These are the result of the fingerprinting of your dataset. The ``config_all_{num}.ini`` config is used in classification, the other types From 354d6c9459265a0ab0b4a5a5470612a39b9d4476 Mon Sep 17 00:00:00 2001 From: MStarmans91 Date: Wed, 19 Oct 2022 10:23:54 +0200 Subject: [PATCH 18/30] Documentation update --- WORC/doc/static/user_manual.rst | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/WORC/doc/static/user_manual.rst b/WORC/doc/static/user_manual.rst index ae1f2a36..fe39c284 100644 --- a/WORC/doc/static/user_manual.rst +++ b/WORC/doc/static/user_manual.rst @@ -252,7 +252,10 @@ assuming you have created the relevant objects as listed above: Outputs and evaluation of your network --------------------------------------- -The following outputs and evaluation methods are always generated: +General remark: when we talk about a sample, we mean one sample that has a set of features associated with it and is thus used as such in the model training or evaluation. +A sample can correspond with a single patient, but if you have multiple tumors per patient for which features are separately extracted per tumor, these can be treated as separate sample. + +The following outputs and evaluation methods are always generated. 1. Performance of your models (main output). @@ -304,15 +307,32 @@ The following outputs and evaluation methods are always generated: Stored in files ``config_{type}_{num}.ini``. These are the result of the fingerprinting of your dataset. The ``config_all_{num}.ini`` config is used in classification, the other types are used for feature extraction and are named after the image types you provided. For example, if you provided two image types, ``['MRI', 'CT']``, you will get - ``config_MRI_0.ini`` and ``config_CT_0.ini``. If you provide multiple of the same types, the numbers will change. + ``config_MRI_0.ini`` and ``config_CT_0.ini``. If you provide multiple of the same types, the numbers will change. The fields correspond with those from :ref:`configuration chapter `. 3. The fitted models. - Stored in files ``estimator_all_{num}.hdf5``. + Stored in file ``estimator_all_{num}.hdf5``. Contains a pandas dataframe, with inside a pandas series per label for which WORC fitted a model, commonly just one. + The series contains the following attributes: + + - classifiers: a list with per train-test cross-validation, the fitted model on the training set. These are thus the actually fitted models. + - X_train: a list with per train-test cross-validation, a list with for each sample in the training set all feature values. These can be used in re-fitting. + - Y_train: a list with per train-test cross-validation, a list with for each sample in the training set the ground truth labels. These can be used in re-fitting. + - patient_ID_train: a list with per train-test cross-validation, a list with the labels of all samples included in the training set. + - X_test: a list with per train-test cross-validation, a list with for each sample in the test set all feature values. These can be used in re-fitting. + - X_test: a list with per train-test cross-validation, a list with for each sample in the test set the ground truth labels. These can be used in re-fitting. + - patient_ID_test: a list with per train-test cross-validation, a list with the labels of all samples included in the test set. + - config: the WORC config used. Corresponds to the ``config_all_{num}.ini`` file mentioned above. + - random-seed: a list with per train-test cross-validation, the random seed used in splitting the train and test dataset. + - feature_labels: the names of the features. As these are the same for all samples, only one set is provided. 4. The extracted features. - Stored in the ``Features`` folder, in the files ``features_{featuretoolboxname}_{image_type}_{num}_{sample_id}.hdf5``. + Stored in the ``Features`` folder, in the files ``features_{featuretoolboxname}_{image_type}_{num}_{sample_id}.hdf5``. Contains a panas series wih the following attributes: + + - feature_labels: the labels or names of the features. + - feature_values: the value of the features. Each element corresponds with the same element from the feature_labels attribute. + - parameters: the parameters used in the feature extraction. Originate from the WORC config. + - image_type: the type of the image that was used, which you as user provided. Used in the feature labels to distinguish between features extracted from different images. .. note:: For every output file, fastr generates a provenance file (``...prov.json``) stating how a file was generated, see https://fastr.readthedocs.io/en/stable/static/user_manual.html#provenance. From 1b37bd07bd5b4afacb18c7c634d2c5ee79d000aa Mon Sep 17 00:00:00 2001 From: MStarmans91 Date: Wed, 19 Oct 2022 10:27:55 +0200 Subject: [PATCH 19/30] Update user_manual.rst --- WORC/doc/static/user_manual.rst | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/WORC/doc/static/user_manual.rst b/WORC/doc/static/user_manual.rst index fe39c284..74fc49ed 100644 --- a/WORC/doc/static/user_manual.rst +++ b/WORC/doc/static/user_manual.rst @@ -255,7 +255,9 @@ Outputs and evaluation of your network General remark: when we talk about a sample, we mean one sample that has a set of features associated with it and is thus used as such in the model training or evaluation. A sample can correspond with a single patient, but if you have multiple tumors per patient for which features are separately extracted per tumor, these can be treated as separate sample. -The following outputs and evaluation methods are always generated. +The following outputs and evaluation methods are always generated: + +.. note:: For every output file, fastr generates a provenance file (``...prov.json``) stating how a file was generated, see https://fastr.readthedocs.io/en/stable/static/user_manual.html#provenance. 1. Performance of your models (main output). @@ -334,8 +336,6 @@ The following outputs and evaluation methods are always generated. - parameters: the parameters used in the feature extraction. Originate from the WORC config. - image_type: the type of the image that was used, which you as user provided. Used in the feature labels to distinguish between features extracted from different images. -.. note:: For every output file, fastr generates a provenance file (``...prov.json``) stating how a file was generated, see https://fastr.readthedocs.io/en/stable/static/user_manual.html#provenance. - The following outputs and evaluation methods are only created when ``WORC.add_evaluation()`` is used (similar for ``SimpleWORC`` and ``BasicWORC``), and are stored in the ``Evaluation`` in the output folder of your experiment. @@ -451,6 +451,16 @@ Or in the ``SimpleWORC`` or ``BasicWORC`` facades: ... experiment.add_evaluation() +The following outputs are only generated if certain configuration settings are used: + +1. Adjusted segmentations. + + Stored in the ``Segmentations`` folder, in the files ``seg__{image_type}_{num}_{howsegmentationwasgenerated}_{sample_id}.hdf5``. + Only generated when the original segmentations were modified, e.g. using WORC's internal program segmentix + (see relevant section of the :ref:`configuration chapter `) or when registration was + performed to warp the segmentations from one sequence to another. + + Debugging --------- From 8c573d419fa675048990c1ffa787d21aafe2b729 Mon Sep 17 00:00:00 2001 From: Martijn Date: Fri, 28 Oct 2022 17:38:19 +0200 Subject: [PATCH 20/30] Fix bug when estimators are None. --- WORC/classification/SearchCV.py | 82 ++++++++++++++++----------------- WORC/classification/crossval.py | 2 +- 2 files changed, 42 insertions(+), 42 deletions(-) diff --git a/WORC/classification/SearchCV.py b/WORC/classification/SearchCV.py index cc4415b1..71594f17 100644 --- a/WORC/classification/SearchCV.py +++ b/WORC/classification/SearchCV.py @@ -855,9 +855,6 @@ def _store(key_name, array, weights=None, splits=False, rank=False): # Sort according to best indices fitted_workflows = [fitted_workflows[i] for i in bestindices] - # Remove None workflows - fitted_workflows = [f for f in fitted_workflows if f is not None] - self.fitted_workflows = fitted_workflows if self.refit_validation_workflows: @@ -1429,16 +1426,20 @@ def getpredictions(): # Simply select the required estimators print('\t - Detected already fitted train-test workflows.') estimators = list() - for i in ensemble: + for enum in ensemble: try: # Try a prediction to see if estimator is truly fitted - self.fitted_workflows[i].predict(np.asarray([X_train[0][0], X_train[1][0]])) - estimators.append(self.fitted_workflows[i]) - except (NotFittedError, ValueError): - print(f'\t\t - Estimator {i} not fitted (correctly) yet, refit.') - estimator = self.fitted_workflows[i] + self.fitted_workflows[enum].predict(np.asarray([X_train[0][0], X_train[1][0]])) + estimators.append(self.fitted_workflows[enum]) + except (NotFittedError, ValueError, AttributeError): + print(f'\t\t - Estimator {enum} not fitted (correctly) yet, refit.') + if self.fitted_workflows[enum] is not None: + estimator = self.fitted_workflows[enum] + else: + estimator = clone(base_estimator) + estimator.refit_and_score(X_train, Y_train, - parameters_all[i], + parameters_all[enum], train, train) estimators.append(estimator) @@ -1467,33 +1468,32 @@ def getpredictions(): estimators.append(base_estimator) except (NotFittedError, ValueError): print(f'\t\t - Estimator {enum} could not be fitted (correctly), do not include in ensemble.') - if enum + 1 == nest and not estimators: - print(f'\t\t - Reached end of ensemble ({enum + 1}), but ensemble is empty, thus go on untill we find an estimator that works') - while not estimators: - # We cannot have an empy ensemble, thus go on untill we find an estimator that works - enum += 1 - p_all = self.cv_results_['params'][enum] - - # Refit a SearchCV object with the provided parameters - base_estimator = clone(base_estimator) - - # Check if we need to create a multiclass estimator - base_estimator.refit_and_score(X_train, Y_train, p_all, - train, train, - verbose=False) - - # Determine whether to overfit the feature scaling on the test set - base_estimator.overfit_scaler = overfit_scaler - - try: - # Try a prediction to see if estimator is truly fitted - base_estimator.predict(np.asarray([X_train[0][0], X_train[1][0]])) - estimators.append(base_estimator) - except (NotFittedError, ValueError): - pass - print(f'\t\t - Needed estimator {enum}.') - else: - pass + + if not estimators: + print(f'\t\t - Ensemble is empty, thus go on untill we find an estimator that works and that is the final ensemble.') + while not estimators: + # We cannot have an empy ensemble, thus go on untill we find an estimator that works + enum += 1 + p_all = self.cv_results_['params'][enum] + + # Refit a SearchCV object with the provided parameters + base_estimator = clone(base_estimator) + + # Check if we need to create a multiclass estimator + base_estimator.refit_and_score(X_train, Y_train, p_all, + train, train, + verbose=False) + + # Determine whether to overfit the feature scaling on the test set + base_estimator.overfit_scaler = overfit_scaler + + try: + # Try a prediction to see if estimator is truly fitted + base_estimator.predict(np.asarray([X_train[0][0], X_train[1][0]])) + estimators.append(base_estimator) + except (NotFittedError, ValueError): + pass + print(f'\t\t - Needed estimator {enum}.') self.ensemble = Ensemble(estimators) self.best_estimator_ = self.ensemble @@ -1663,10 +1663,10 @@ def _fit(self, X, y, groups, parameter_iterable): # Create the fastr network network = fastr.create_network('WORC_GridSearch_' + name) - estimator_data = network.create_source('HDF5', id='estimator_source') - traintest_data = network.create_source('HDF5', id='traintest') - parameter_data = network.create_source('JsonFile', id='parameters') - sink_output = network.create_sink('HDF5', id='output') + estimator_data = network.create_source('HDF5', id='estimator_source', resources=ResourceLimit(memory='4G')) + traintest_data = network.create_source('HDF5', id='traintest', resources=ResourceLimit(memory='4G')) + parameter_data = network.create_source('JsonFile', id='parameters', resources=ResourceLimit(memory='4G')) + sink_output = network.create_sink('HDF5', id='output', resources=ResourceLimit(memory='6G')) fitandscore =\ network.create_node('worc/fitandscore:1.0', diff --git a/WORC/classification/crossval.py b/WORC/classification/crossval.py index 261b9395..3a78765f 100644 --- a/WORC/classification/crossval.py +++ b/WORC/classification/crossval.py @@ -29,7 +29,7 @@ from sklearn.model_selection import train_test_split, LeaveOneOut from joblib import Parallel, delayed import WORC.addexceptions as ae -from .parameter_optimization import random_search_parameters, guided_search_parameters +from WORC.classification.parameter_optimization import random_search_parameters, guided_search_parameters from WORC.classification.regressors import regressors from WORC.classification.SearchCV import RandomizedSearchCVfastr From 1c5233f439aa862b1179987c9ec0fc310ac2d119 Mon Sep 17 00:00:00 2001 From: MStarmans91 Date: Wed, 2 Nov 2022 16:54:06 +0100 Subject: [PATCH 21/30] Bugfixes and documentation updates --- WORC/classification/SearchCV.py | 8 +- WORC/classification/fitandscore.py | 22 +- WORC/doc/static/faq.rst | 30 ++- WORC/doc/static/quick_start.rst | 8 +- WORC/doc/static/user_manual.rst | 332 +++++++++++++++++++---------- setup.py | 4 +- version | 2 +- 7 files changed, 274 insertions(+), 132 deletions(-) diff --git a/WORC/classification/SearchCV.py b/WORC/classification/SearchCV.py index 71594f17..91d2f5c1 100644 --- a/WORC/classification/SearchCV.py +++ b/WORC/classification/SearchCV.py @@ -1442,7 +1442,13 @@ def getpredictions(): parameters_all[enum], train, train) - estimators.append(estimator) + try: + # Try a prediction to see if estimator is truly fitted + estimator.predict(np.asarray([X_train[0][0], X_train[1][0]])) + estimators.append(estimator) + except (NotFittedError, ValueError): + print(f'\t\t - Estimator {enum} could not be fitted (correctly), do not include in ensemble.') + else: # Create the ensemble trained on the full training set parameters_all = [parameters_all[i] for i in ensemble] diff --git a/WORC/classification/fitandscore.py b/WORC/classification/fitandscore.py index 527cff4f..fb929867 100644 --- a/WORC/classification/fitandscore.py +++ b/WORC/classification/fitandscore.py @@ -320,16 +320,30 @@ def fit_and_score(X, y, scoring, imputer.fit(X_train) original_shape = X_train.shape - X_train = imputer.transform(X_train) - imputed_shape = X_train.shape - X_test = imputer.transform(X_test) - + imputed_shape = imputer.transform(X_train).shape + if original_shape != imputed_shape: removed_features = original_shape[1] - imputed_shape[1] if para_estimator['ImputationSkipAllNaN'] == 'True': print(f"[WARNING]: Several features ({removed_features}) were np.NaN for all objects. config['Imputation']['skipallNaN'] set to True, so simply eliminate these features.") + if hasattr(imputer.Imputer, 'statistics_'): + X_train = imputer.transform(X_train) + X_test = imputer.transform(X_test) + feature_labels_zero = [fl for fnum, fl in enumerate(feature_labels[0]) if not np.isnan(imputer.Imputer.statistics_[fnum])] + feature_labels = [feature_labels_zero for i in X_train] + else: + # Fit a mean imputer to transform the labels + temp_imputer = Imputer(missing_values=np.nan, strategy='mean') + temp_imputer.fit(X_train) + X_train = imputer.transform(X_train) + X_test = imputer.transform(X_test) + feature_labels_zero = [fl for fnum, fl in enumerate(feature_labels[0]) if not np.isnan(temp_imputer.Imputer.statistics_[fnum])] + feature_labels = [feature_labels_zero for i in X_train] else: raise ae.WORCValueError(f'Several features ({removed_features}) were np.NaN for all objects. Hence, imputation was not possible. Either make sure this is correct and turn of imputation, or correct the feature.') + else: + X_train = imputer.transform(X_train) + X_test = imputer.transform(X_test) del para_estimator['ImputationSkipAllNaN'] del para_estimator['Imputation'] diff --git a/WORC/doc/static/faq.rst b/WORC/doc/static/faq.rst index b7a572cb..16ff8a75 100644 --- a/WORC/doc/static/faq.rst +++ b/WORC/doc/static/faq.rst @@ -20,6 +20,16 @@ The ``fastr`` toolbox has a method to trace back errors. For more details, see the `fastr documentation `_. +Error: ``File "H5FDsec2.c", line 941, in H5FD_sec2_lock unable to lock file, errno = 37, error message = 'No locks available'`` +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Known HDF5 error, see also https://github.com/h5py/h5py/issues/1101. +Can be solved by setting the HDF5_USE_FILE_LOCKING environment variable to 'FALSE', +e.g. adding export HDF5_USE_FILE_LOCKING='FALSE' to your ~..bashrc on Linux. + +Error: ``Failed building wheel for cryptography`` +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +This bug can be caused when using pyOpenSSL 22.1.0, we know version 20.0.1 at least works. + Error: ``WORC.addexceptions.WORCValueError: First column in the file`` ``given to SimpleWORC().labels_from_this_file(**) needs to be named Patient.`` ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ This means that your label file, i.e. in which the label to be predicted for @@ -161,14 +171,16 @@ you have to change are: - Temporary output: ``mounts['tmp']`` in the ~/.fastr/config.py file - Final output: ``mounts['output']`` in the ~/.fastr/config.d/WORC_config.py file -I want a specific cross-validation setup, e.g. specific patients in the train and test set, how can I do that? -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -WIP - -How can I make sure all samples of a patient are either all in the training or all in the test set? -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -WIP - How can I get the performance on the validation dataset? ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -WIP +The performance of the top 1 workflow is stored in the fitted estimators in the estimator_all_0.hdf5 file: + +.. code-block:: python + + data = pd.read_hdf("estimator_all_0.hdf5") + data = data[list(data.keys())[0]] + + validation_performance = list() + # Iterate over all train-test cross validations + for clf in data.classifiers: + validation_performance.append(clf.best_score_) diff --git a/WORC/doc/static/quick_start.rst b/WORC/doc/static/quick_start.rst index a3a7625e..dd6ae5a0 100644 --- a/WORC/doc/static/quick_start.rst +++ b/WORC/doc/static/quick_start.rst @@ -292,12 +292,12 @@ Some things we would advice to always do: experiment.add_evaluation() - See the "Evaluation of your network" section in the :ref:`User Manual ` + See the "Outputs and evaluation of your network" section in the :ref:`User Manual ` chapter for more details on the evaluation outputs. -* Changing fields in the configuration can be done with the add_config_overrides function, see below. - We recommend doing this after the modus part, as these also perform config_overrides. - NOTE: all configuration fields have to be provided as strings. +Changing fields in the configuration can be done with the add_config_overrides function, see below. +We recommend doing this after the modus part, as these also perform config_overrides. +NOTE: all configuration fields have to be provided as strings. .. code-block:: python diff --git a/WORC/doc/static/user_manual.rst b/WORC/doc/static/user_manual.rst index 74fc49ed..a5ebb511 100644 --- a/WORC/doc/static/user_manual.rst +++ b/WORC/doc/static/user_manual.rst @@ -9,70 +9,169 @@ and describe the more advanced features. .. _tools: -Interacting with WORC ---------------------- -The WORC toolbox is build around of one main object, the WORC object. This object provides all functionality +WORC object and facades +------------------------ + +The WORC toolbox is build around of one main object, the ``WORC`` object. This object provides all functionality of the toolbox. However, to make certain functionalities easier to use and limit the complexity, -we have constructed two facades. The ``SimpleWORC`` facade is the simplest to interact with and provides -all functionality required for conducting basic experiments. The ``BasicWORC`` object is based on the ``SimpleWORC`` -object and provides several more advances functions. The specific functionalities of these two facades and the -``WORC`` object itself can be found in this section. +we have constructed two facades: ``SimpleWORC`` and ``BasicWORC``. We advice new users to start with ``SimpleWORC``, +more advanced users ``BasicWORC``, and only use ``WORC`` for development purposes. Additionally, we advice you to take a look at the :ref:`configuration chapter ` +for all the settings that can be adjusted in WORC. -For documentation on ``SimpleWORC`` and ``BasicWORC``, please look at the documentation -within those modules: :py:mod:`WORC.facade.simpleworc` and :py:mod:`WORC.facade.basicworc`. Many of the functions are actually wrappers to interact with the WORC -object, and therefore use the functionality described below. For basic usage, only using -``SimpleWORC``, it's respective documentation and the -`WORCTutorial Github `_ should be sufficient. +The specific functionalities of these two facades and the ``WORC`` object itself can be found in this section. -Additionally, we advice you to take a look at the :ref:`configuration chapter ` -for all the settings that can be adjusted in ``WORC``. +SimpleWORC +~~~~~~~~~~~~~~~~ +The ``SimpleWORC`` facade is the simplest to interact with and provides +all functionality required for conducting basic experiments. +Much of the documentation of ``SimpleWORC`` can be found in its tutorial (https://github.com/MStarmans91/WORCtutorial and +:ref:`the quick start `) and the docstrings of the functions in the object (:py:mod:`WORC.facade.simpleworc`). +Many of the functions are wrappers to interact with the ``WORC`` object, and therefore in the background use the functionality described below. -The WORC Object +BasicWORC ~~~~~~~~~~~~~~~~ +The ``BasicWORC`` object is based on the ``SimpleWORC`` object, and thus provides exactly the same functionality, +plus several more advances functions. Much of the documentation of ``BasicWORC`` can be found in its tutorial (WIP) +and the docstrings of the functions in the object (:py:mod:`WORC.facade.basicworc`). + +One of the functionalities that ``BasicWORC`` provides over ``SimpleWORC`` is that you can also directly provide +your data to ``WORC`` (e.g. ``images_train``) instead of using one of the wrapping functions of +``SimpleWORC`` (e.g. ``images_from_this_directory) + +.. _WORC: + +WORC +~~~~~~~~~~~~~~~ +The ``WORC`` object can directly be assessed in the following way: .. code-block:: python import WORC network = WORC.WORC('somename') It's attributes are split in a couple of categories. We will not discuss -the WORC.defaultconfig() function here, which generates the default +the ``WORC.defaultconfig()`` function here, which generates the default configuration, as it is listed in a separate page, see the :ref:`Config chapter `. More detailed documentation of the various functions can be found in the docstrings of :py:mod:`WORC.WORC`: we will mostly focus on the attributes, inputs, outputs and workflows here. +There are numerous ``WORC`` attributes which serve as source nodes (i.e. inputs) for the +FASTR network. These are: -Input file definitions ----------------------- +- ``images_train`` and ``images_test`` +- ``segmentations_train`` and ``segmentations_test`` +- ``semantics_train`` and ``semantics_test`` +- ``labels_train`` and ``labels_test`` +- ``masks_train`` and ``masks_test`` +- ``features_train`` and ``features_test`` +- ``metadata_train`` and ``metadata_test`` +- ``Elastix_Para`` +- ``fastrconfigs`` -Attributes: Sources -~~~~~~~~~~~~~~~~~~~ +These directly correspond to the :ref:`input file definitions discussed below ` +How to provide your data to ``WORC`` is also described in this section. -There are numerous WORC attributes which serve as source nodes for the -FASTR network. These are: +After supplying your sources as described above, you need to build the FASTR network. This +can be done through the ``WORC.build()`` command. Depending on your sources, +several nodes will be added and linked. This creates the ``WORC.network`` +object, which is a ``fastr.network`` object. You can edit this network +freely, e.g. add another source or node. You can print the network with +the ``WORC.network.draw_network`` command. +Next, we have to tell the network which sources should be used in the +source nodes. This can be done through the ``WORC.set()`` function. This will +put your supplied sources into the source nodes and also creates the +needed sink nodes. You can check these by looking at the created +``WORC.source_data`` and ``WORC.sink_data`` objects. -- images_train and images_test -- segmentations_train and segmentations_test -- semantics_train and semantics_test -- labels_train and labels_test -- masks_train and masks_test -- features_train and features_test -- metadata_train and metadata_test -- Elastix_Para -- fastrconfigs +Finally, after completing above steps, you can execute the network +through the ``WORC.execute()`` command. +Thus a typical experiment in ``WORC`` would follow the following structure, +assuming you have created the relevant objects as listed above: -When using a single dataset for both training and evaluation, you should -supply all sources in train objects. By default, performance on a single -dataset will be evaluated using cross-validation. Optionally, you can supply -a separate training and test set. +.. code-block:: python + + import WORC + + # Create object + experiment = WORC.WORC('name') + + # Append sources + experiment.images_train.append(images_train) + experiment.segmentations_train.append(segmentations_train) + experiment.labels_train.append(labels_train) + + # Create a configuration + config = experiment.defaultconfig() + experiment.configs.append(config) + + # Build, set, and execute + network.build() + network.set() + network.execute() + + +.. _inputs: + +Input file definitions and how to provide them to WORC +------------------------------------------------------- + +Providing your inputs to WORC and data flows +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Let's first start on how to provide any of the below mentioned types of input data to ``WORC``. +``WORC`` facilitates different data flows (or networks or pipelines), which are automatically +constructed based on the inputs and configuration you provide. We here +discuss how the data can be set in ``BasicWORC`` and ``WORC``: +``SimpleWORC`` provides several wrappers to more easily provide data, which interact with +thee objects. + +As an example, we here show how to provide images and segmentations to ``BasicWORC`` and ``WORC``. + +.. code-block:: python + + images1 = {'patient1': '/data/Patient1/image_MR.nii.gz', 'patient2': '/data/Patient2/image_MR.nii.gz'} + segmentations1 = {'patient1': '/data/Patient1/seg_tumor_MR.nii.gz', 'patient2': '/data/Patient2/seg_tumor_MR.nii.gz'} + + network.images_train.append(images1) + network.segmentations_train.append(segmentations1) -Each source should be given as a dictionary of strings corresponding to -the source filenames. Each element should correspond to a single object for the classification, -e.g. a patient. The keys are used to match the features to the -label and semantics sources, so make sure these correspond to the label -file. +Here ``network`` can be a ``BasicWORC`` or ``WORC`` object. Each source is a list, to which you can provide +dictionaries containing the actual sources. In these dictionaries, each element should correspond to a single +object for classification, e.g., a patient or a lesions. The keys indicate +the ID of the element, e.g. the patient name, while the values should be strings corresponding to +the source filenames. The keys are used to match the images and segmentations to the +label and semantics sources, so make sure these correspond to the label file. +.. note:: You have to make sure the images and segmentation (and other) sources match in size, + i.e., that the same keys are present. + +.. note:: You have to supply a configuration file for each image or feature source you append. + Thus, in the first example above, you need to append two configurations! + +Using multiple sources per patient +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +If you want to provide multiple sources, e.g. images, per patient, simply append another dictionary +to the source list, e.g.: + +.. code-block:: python + + images1 = {'patient1': '/data/Patient1/image_MR.nii.gz', 'patient2': '/data/Patient2/image_MR.nii.gz'} + images2 = {'patient1': '/data/Patient1/image_CT.nii.gz', 'patient2': '/data/Patient2/image_CT.nii.gz'} + segmentations1 = {'patient1': '/data/Patient1/seg_tumor_MR.nii.gz', 'patient2': '/data/Patient2/seg_tumor_MR.nii.gz'} + segmentations2 = {'patient1': '/data/Patient1/seg_tumor_CT.nii.gz', 'patient2': '/data/Patient2/seg_tumor_CT.nii.gz'} + + network.images_train.append(images1) + network.images_train.append(images2) + + network.segmentations_train.append(segmentations1) + network.segmentations_train.append(segmentations2) + + +``WORC`` will use the keys of the dictionaries to match the features from the same object or patient and combine +them for the machine learning part. + +Mutiple ROIs or segmentations per object/patient +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ You can off course have multiple images or ROIs per object, e.g. a liver ROI and a tumor ROI. This can be easily done by appending to the sources. For example: @@ -89,8 +188,8 @@ sources. For example: network.segmentations_train.append(segmentations1) network.segmentations_train.append(segmentations2) -When using multiple sequences per patients (e.g. T1 and T2), the same -appending procedure can be used. +``WORC`` will use the keys of the dictionaries to match the features from the same object or patient and combine +them for the machine learning part. If you want to use multiple ROIs independently per patient, e.g. multiple tumors, you can do so by simply adding them to the dictionary. To make sure the data is still split per patient in the @@ -104,23 +203,79 @@ cross-validation, please add a sample number after an underscore to the key, e.g If your label file (see below) contains the label ''patient1'', both samples will get this label in the classification. -.. note:: You have to make sure the images and segmentation sources match in size. +.. note:: ``WORC`` will automatically group all samples from a patient either all in the training + or all in the test set. -.. note:: You have to supply a configuration file for each image or feature source you append. - Thus, in the first example above, you need to append two configurations! +Training and test sets +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +When using a single dataset for both training and evaluation, you should +only supply "training" datasets. By default, performance on a single +dataset will be evaluated using cross-validation (default random split, but leave-one-out can also be configured). +Alternatively, you can supply a separate training and test set, by which you tell +``WORC`` to use this single train-test split. To distinguish between these, for every source, we have a +train and test object which you can set: + +.. code-block:: python + + images_train = {'patient1': '/data/Patient1/image_MR.nii.gz', 'patient2': '/data/Patient2/image_MR.nii.gz'} + segmentations_train = {'patient1': '/data/Patient1/seg_tumor_MR.nii.gz', 'patient2': '/data/Patient2/seg_tumor_MR.nii.gz'} + + network.images_train.append(images_train) + network.segmentations_train.append(segmentations_train) + + images_test = {'patient3': '/data/Patient3/image_MR.nii.gz', 'patient4': '/data/Patient4/image_MR.nii.gz'} + segmentations_test = {'patient3': '/data/Patient3/seg_tumor_MR.nii.gz', 'patient4': '/data/Patient4/seg_tumor_MR.nii.gz'} + + network.images_test.append(images_test) + network.segmentations_test.append(segmentations_test) + +Another alternative is to only provide training objects, but also a .csv defining fixed training and test splits to be used for the +evaluation, e.g. ``network.fixed_splits = '/data/fixedsplits.csv``. See the https://github.com/MStarmans91/WORCtutorial repository for an example. ``SimpleWORC`` has the ``set_fixed_splits`` to set this object. + +Missing data and dummy's +^^^^^^^^^^^^^^^^^^^^^^^^^^ +Suppose you are missing a specific image for a specific patient. ``WORC`` can impute the features of this patient. +The underlying package we use for workflow execution (fastr) can however handle missing data. Therefore, to tell ``WORC`` to +do so, you still have to provide a source but can add ''Dummy'' to the key: -.. note:: When you use - multiple image sequences, you can supply a ROI for each sequence by - appending to to segmentations object. Alternatively, when you do not - supply a segmentation for a specific sequence, WORC will use Elastix to - align this sequence to another through image registration. It will then - warp the segmentation from this sequence to the sequence for which you - did not supply a segmentation. **WORC will always align these sequences with no segmentations to the first sequence, i.e. the first object in the images_train list.** - Hence make sure you supply the sequence for which you have a ROI as the first object. +.. code-block:: python + + images1 = {'patient1': '/data/Patientc/image_MR.nii.gz', 'patient2_Dummy': '/data/Patient1/image_MR.nii.gz'} + segmentations1 = {'patient1': '/data/Patient1/seg_tumor_MR.nii.gz', 'patient2_Dummy': '/data/Patient1/seg_tumor_MR.nii.gz'} + + network.images_train.append(images1) + network.segmentations_train.append(segmentations1) + +``WORC`` will process the sources normally up till the imputation part, so you have to provide valid data. As you see in the example above, +we simply provided data from another patient. + +Segmentation on the first image, but not on the others +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +When you use multiple image sequences, you can supply a ROI for each sequence by +appending to to segmentations object as above. Alternatively, when you do not +supply a segmentation for a specific sequence, ``WORC`` will use Elastix to +align this sequence to another through image registration. It will then +warp the segmentation from this sequence to the sequence for which you +did not supply a segmentation. **WORC will always align these sequences with no segmentations to the first sequence, i.e. the first object in the images_train list.** +Hence make sure you supply the sequence for which you have a ROI as the first object: + +.. code-block:: python + + images1 = {'patient1': '/data/Patient1/image_MR.nii.gz', 'patient2': '/data/Patient2/image_MR.nii.gz'} + images2 = {'patient1': '/data/Patient1/image_CT.nii.gz', 'patient2': '/data/Patient2/image_CT.nii.gz'} + segmentations1 = {'patient1': '/data/Patient1/seg_tumor_MR.nii.gz', 'patient2': '/data/Patient2/seg_tumor_MR.nii.gz'} + + network.images_train.append(images1) + network.images_train.append(images2) + + network.segmentations_train.append(segmentations1) + +When providing only a segmentation for the first image in this way, ``WORC`` will automatically +recognize that it needs to use registration. Images and segmentations -^^^^^^^^^^^^^^^^^^^^^^^^ -The minimal input for a Radiomics pipeline consists of either images +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +The minimal input for a radiomics pipeline consists of either images plus segmentations, or features, plus a label file (and a configuration, but you can just use the default one). @@ -131,7 +286,7 @@ image formats such as DICOM, NIFTI, TIFF, NRRD and MHD. .. _um-labels: Labels -^^^^^^ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The labels are predicted in the classification: should be a .txt or .csv file. The first column should head ``Patient`` and contain the patient ID. The next columns can contain labels you want to predict, e.g. tumor type, risk, genetics. For example: @@ -150,21 +305,22 @@ can contain labels you want to predict, e.g. tumor type, risk, genetics. For exa These labels are matched to the correct image/features by the sample names of the image/features. So in this case, your sources should look as following: - .. code-block:: python images_train = {'patient1': ..., 'patient2': ..., ...} segmentations_train = {'patient1': ..., 'patient2': ..., ...} -Semantics -^^^^^^^^^ -Semantic features are non-computational features and are extracted using PREDICT. Examples include +.. note:: ``WORC`` will automatically group all samples from a patient either all in the training + or all in the test set. + +Semantics or non-radiomics features +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Semantic features are non-computational features, thus features that you supply instead of extract. Examples include using the age and sex of the patients in the classification. You can supply these as a .csv listing your features per patient, similar to the :ref:`label file ` - Masks -^^^^^ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ WORC contains a segmentation preprocessing tool, called segmentix. The idea is that you can manipulate your segmentation, e.g. using dilation, then use a mask to make sure it @@ -172,7 +328,7 @@ is still valid. See the :ref:`config chapter ` for all segmentix Features -^^^^^^^^ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ If you already computed your features, e.g. from a previous run, you can directly supply the features instead of the images and segmentations and skip the feature computation step. These should be stored in .hdf5 files @@ -180,7 +336,7 @@ matching the WORC format. Metadata -^^^^^^^^ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ This source can be used if you want to use tags from the DICOM header as features, e.g. patient age and sex. In this case, this source should contain a single DICOM per patient from which the tags that are read. @@ -189,7 +345,7 @@ implemented tags. Elastix_Para -^^^^^^^^^^^^ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ If you have multiple images for each patient, e.g. T1 and T2, but only a single segmentation, you can use image registration to align and transform the segmentation to the other modality. This is done in WORC @@ -202,56 +358,10 @@ map and pass this object to ``WORC``. ``WORC.images_train`` (or test) source you supply. The segmentation will be alligned to all other image sources. - - -Construction and execution commands -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -After supplying your sources as described above, you need to build the FASTR network. This -can be done through the ``WORC.build()`` command. Depending on your sources, -several nodes will be added and linked. This creates the ``WORC.network`` -object, which is a ``fastr.network`` object. You can edit this network -freely, e.g. add another source or node. You can print the network with -the ``WORC.network.draw_network`` command. - - -Next, we have to tell the network which sources should be used in the -source nodes. This can be done through the ``WORC.set()`` function. This will -put your supplied sources into the source nodes and also creates the -needed sink nodes. You can check these by looking at the created -``WORC.source_data`` and ``WORC.sink_data`` objects. - -Finally, after completing above steps, you can execute the network -through the ``WORC.execute()`` command. - -Thus a typical experiment in ``WORC`` would follow the following structure, -assuming you have created the relevant objects as listed above: - -.. code-block:: python - - import WORC - - # Create object - experiment = WORC.WORC('name') - - # Append sources - experiment.images_train.append(images_train) - experiment.segmentations_train.append(segmentations_train) - experiment.labels_train.append(labels_train) - - # Create a configuration - config = experiment.defaultconfig() - experiment.configs.append(config) - - # Build, set, and execute - network.build() - network.set() - network.execute() - .. _um-evaluation: Outputs and evaluation of your network --------------------------------------- - General remark: when we talk about a sample, we mean one sample that has a set of features associated with it and is thus used as such in the model training or evaluation. A sample can correspond with a single patient, but if you have multiple tumors per patient for which features are separately extracted per tumor, these can be treated as separate sample. diff --git a/setup.py b/setup.py index ec2ba860..6dbfa934 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,4 @@ -# Copyright 2016 - 2021 Biomedical Imaging Group Rotterdam, Departments of +# Copyright 2016 - 2022 Biomedical Imaging Group Rotterdam, Departments of # Medical Informatics and Radiology, Erasmus MC, Rotterdam, The Netherlands # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -93,7 +93,7 @@ def run_tests(self): setup( name='WORC', - version='3.6.0', + version='3.6.1', description='Workflow for Optimal Radiomics Classification.', long_description=_description, url='https://github.com/MStarmans91/WORC', diff --git a/version b/version index 40c341bd..9575d51b 100644 --- a/version +++ b/version @@ -1 +1 @@ -3.6.0 +3.6.1 From f28a23f85340bd6872c80685cc9fdcb17e578cf1 Mon Sep 17 00:00:00 2001 From: MStarmans91 Date: Wed, 2 Nov 2022 17:24:49 +0100 Subject: [PATCH 22/30] Print validation score in RSENS experiment --- WORC/classification/crossval.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/WORC/classification/crossval.py b/WORC/classification/crossval.py index 3a78765f..1ea62008 100644 --- a/WORC/classification/crossval.py +++ b/WORC/classification/crossval.py @@ -934,6 +934,8 @@ def fitvalidationestimator(parameters, train, test): estimator.create_ensemble(X_train_temp, Y_train, method=ensemble, verbose=verbose) + performances[f'Validation F1-score Ensemble {ensemble} {key}'] = estimator.ensemble_validation_score + # Compute performance y_prediction = estimator.predict(X_test) y_score = estimator.predict_proba(X_test)[:, 1] From 968f14a8a81ef81488b04fdf99fa013d9c05cf26 Mon Sep 17 00:00:00 2001 From: MStarmans91 Date: Wed, 2 Nov 2022 17:47:17 +0100 Subject: [PATCH 23/30] Prepare for 3.6.1 release. --- CHANGELOG | 2 +- README.rst | 64 +- .../autogen/WORC.classification.doctree | Bin 760170 -> 763343 bytes .../doctrees/autogen/WORC.facade.doctree | Bin 119589 -> 123559 bytes .../doctrees/autogen/WORC.processing.doctree | Bin 95538 -> 95700 bytes .../doctrees/autogen/WORC.tools.doctree | Bin 95437 -> 102772 bytes WORC/doc/_build/doctrees/environment.pickle | Bin 3003419 -> 3032869 bytes .../_build/doctrees/static/changelog.doctree | Bin 159680 -> 167039 bytes .../doctrees/static/configuration.doctree | Bin 395823 -> 399891 bytes .../doctrees/static/quick_start.doctree | Bin 43621 -> 46568 bytes .../doctrees/static/user_manual.doctree | Bin 71808 -> 138201 bytes WORC/doc/_build/html/.buildinfo | 2 +- .../_modules/WORC/IOparser/config_WORC.html | 4 +- .../WORC/IOparser/config_io_classifier.html | 18 +- .../WORC/IOparser/config_preprocessing.html | 4 +- .../WORC/IOparser/config_segmentix.html | 4 +- .../html/_modules/WORC/IOparser/file_io.html | 4 +- WORC/doc/_build/html/_modules/WORC/WORC.html | 34 +- .../html/_modules/WORC/addexceptions.html | 4 +- .../WORC/classification/AdvancedSampler.html | 4 +- .../WORC/classification/ObjectSampler.html | 4 +- .../WORC/classification/RankedSVM.html | 4 +- .../WORC/classification/SearchCV.html | 512 +++++++++------- .../classification/construct_classifier.html | 12 +- .../classification/createfixedsplits.html | 4 +- .../WORC/classification/crossval.html | 155 +++-- .../WORC/classification/estimators.html | 4 +- .../WORC/classification/fitandscore.html | 433 +++++++++----- .../_modules/WORC/classification/metrics.html | 4 +- .../parameter_optimization.html | 20 +- .../WORC/classification/trainclassifier.html | 5 +- .../_modules/WORC/detectors/detectors.html | 4 +- .../WORC/exampledata/datadownloader.html | 4 +- .../WORC/featureprocessing/Imputer.html | 4 +- .../WORC/featureprocessing/Relief.html | 4 +- .../WORC/featureprocessing/SelectGroups.html | 4 +- .../featureprocessing/SelectIndividuals.html | 4 +- .../StatisticalTestFeatures.html | 4 +- .../StatisticalTestThreshold.html | 4 +- .../featureprocessing/VarianceThreshold.html | 4 +- .../_modules/WORC/plotting/compute_CI.html | 4 +- .../_modules/WORC/plotting/linstretch.html | 4 +- .../html/_modules/WORC/plotting/plot_ROC.html | 4 +- .../_modules/WORC/plotting/plot_barchart.html | 4 +- .../_modules/WORC/plotting/plot_images.html | 4 +- .../WORC/plotting/plot_ranked_scores.html | 4 +- .../_modules/WORC/plotting/scatterplot.html | 4 +- .../processing/ExtractNLargestBlobsn.html | 4 +- .../_modules/WORC/processing/classes.html | 4 +- .../WORC/processing/label_processing.html | 4 +- .../fastr_tests/CalcFeatures_test.html | 4 +- .../resources/fastr_tests/elastix_test.html | 4 +- .../resources/fastr_tests/segmentix_test.html | 4 +- .../html/_modules/WORC/tools/Elastix.html | 4 +- .../html/_modules/WORC/tools/Evaluate.html | 4 +- .../html/_modules/WORC/tools/Slicer.html | 4 +- .../html/_modules/WORC/tools/Transformix.html | 4 +- .../WORC/tools/createfixedsplits.html | 107 +++- WORC/doc/_build/html/_modules/index.html | 4 +- .../html/_sources/static/quick_start.rst.txt | 43 +- .../html/_sources/static/user_manual.rst.txt | 495 ++++++++++----- .../html/_static/documentation_options.js | 2 +- .../_build/html/autogen/WORC.IOparser.html | 4 +- .../html/autogen/WORC.classification.html | 33 +- WORC/doc/_build/html/autogen/WORC.config.html | 4 +- .../_build/html/autogen/WORC.detectors.html | 4 +- .../_build/html/autogen/WORC.exampledata.html | 4 +- WORC/doc/_build/html/autogen/WORC.facade.html | 21 +- .../html/autogen/WORC.featureprocessing.html | 4 +- WORC/doc/_build/html/autogen/WORC.html | 5 +- .../_build/html/autogen/WORC.plotting.html | 4 +- .../_build/html/autogen/WORC.processing.html | 6 +- .../autogen/WORC.resources.fastr_tests.html | 4 +- .../autogen/WORC.resources.fastr_tools.html | 4 +- .../_build/html/autogen/WORC.resources.html | 4 +- WORC/doc/_build/html/autogen/WORC.tools.html | 42 +- WORC/doc/_build/html/genindex.html | 6 +- WORC/doc/_build/html/index.html | 141 +++-- WORC/doc/_build/html/objects.inv | Bin 6400 -> 6471 bytes WORC/doc/_build/html/py-modindex.html | 4 +- WORC/doc/_build/html/search.html | 4 +- WORC/doc/_build/html/searchindex.js | 2 +- WORC/doc/_build/html/static/changelog.html | 566 ++++++++++-------- .../doc/_build/html/static/configuration.html | 39 +- .../_build/html/static/file_description.html | 4 +- WORC/doc/_build/html/static/introduction.html | 4 +- WORC/doc/_build/html/static/quick_start.html | 52 +- WORC/doc/_build/html/static/user_manual.html | 522 +++++++++++----- WORC/doc/autogen/WORC.tests.rst | 9 + .../config/WORC.config_General_defopts.rst | 1 + .../WORC.config_General_description.rst | 1 + .../WORC.config_HyperOptimization_defopts.rst | 27 +- ...C.config_HyperOptimization_description.rst | 27 +- .../config/WORC.config_Imputation_defopts.rst | 1 + .../WORC.config_Imputation_description.rst | 1 + WORC/doc/generate_config.py | 6 +- 96 files changed, 2318 insertions(+), 1298 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 603fc38e..205f12fe 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -6,7 +6,7 @@ All notable changes to this project will be documented in this file. The format is based on `Keep a Changelog `_ and this project adheres to `Semantic Versioning `_ -3.6.1 - Unreleased +3.6.1 - 2022-11-02 ------------------ Fixed diff --git a/README.rst b/README.rst index a7e5a9d4..a8c50a63 100644 --- a/README.rst +++ b/README.rst @@ -1,4 +1,4 @@ -WORC v3.6.0 +WORC v3.6.1 =========== Workflow for Optimal Radiomics Classification @@ -16,8 +16,8 @@ Information Introduction ============ -WORC is an open-source python package for the easy execution of full -radiomics pipelines. +WORC is an open-source python package for the easy execution and fully +automatic construction and optimization of radiomics workflows. We aim to establish a general radiomics platform supporting easy integration of other tools. With our modular build and support of @@ -33,9 +33,28 @@ License This package is covered by the open source `APACHE 2.0 License `__. -When using WORC, please cite this repository as following: - -``Martijn P.A. Starmans, Sebastian R. van der Voort, Thomas Phil and Stefan Klein. Workflow for Optimal Radiomics Classification (WORC). Zenodo (2018). Available from: https://github.com/MStarmans91/WORC. DOI: http://doi.org/10.5281/zenodo.3840534.`` +When using WORC, please cite this repository and the paper describing +WORC as as follows: + +.. code:: bibtex + + @article{starmans2021reproducible, + title={Reproducible radiomics through automated machine learning validated on twelve clinical applications}, + author={Martijn P. A. Starmans and Sebastian R. van der Voort and Thomas Phil and Milea J. M. Timbergen and Melissa Vos and Guillaume A. Padmos and Wouter Kessels and David Hanff and Dirk J. Grunhagen and Cornelis Verhoef and Stefan Sleijfer and Martin J. van den Bent and Marion Smits and Roy S. Dwarkasing and Christopher J. Els and Federico Fiduzi and Geert J. L. H. van Leenders and Anela Blazevic and Johannes Hofland and Tessa Brabander and Renza A. H. van Gils and Gaston J. H. Franssen and Richard A. Feelders and Wouter W. de Herder and Florian E. Buisman and Francois E. J. A. Willemssen and Bas Groot Koerkamp and Lindsay Angus and Astrid A. M. van der Veldt and Ana Rajicic and Arlette E. Odink and Mitchell Deen and Jose M. Castillo T. and Jifke Veenland and Ivo Schoots and Michel Renckens and Michail Doukas and Rob A. de Man and Jan N. M. IJzermans and Razvan L. Miclea and Peter B. Vermeulen and Esther E. Bron and Maarten G. Thomeer and Jacob J. Visser and Wiro J. Niessen and Stefan Klein}, + year={2021}, + eprint={2108.08618}, + archivePrefix={arXiv}, + primaryClass={eess.IV} + } + + @software{starmans2018worc, + author = {Martijn P. A. Starmans and Thomas Phil and Sebastian R. van der Voort and Stefan Klein}, + title = {Workflow for Optimal Radiomics Classification (WORC)}, + year = {2018}, + publisher = {Zenodo}, + doi = {10.5281/zenodo.3840534}, + url = {https://github.com/MStarmans91/WORC} + } For the DOI, visit |image4|. @@ -48,14 +67,32 @@ occur. Please contact us through the channels below if you find any and we will try to fix them as soon as possible, or create an issue on this Github. -Tutorial and Documentation --------------------------- +Tutorial, documentation and dataset +----------------------------------- -The WORC tutorial is hosted in a `separate -repository `__. +The WORC tutorial is hosted at +https://github.com/MStarmans91/WORCTutorial. The official documentation can be found at https://worc.readthedocs.io. +The publicly released WORC database is described in the following paper: + +.. code:: bibtex + + @article {Starmans2021WORCDatabase, + author = {Starmans, Martijn P.A. and Timbergen, Milea J.M. and Vos, Melissa and Padmos, Guillaume A. and Gr{\"u}nhagen, Dirk J. and Verhoef, Cornelis and Sleijfer, Stefan and van Leenders, Geert J.L.H. and Buisman, Florian E. and Willemssen, Francois E.J.A. and Koerkamp, Bas Groot and Angus, Lindsay and van der Veldt, Astrid A.M. and Rajicic, Ana and Odink, Arlette E. and Renckens, Michel and Doukas, Michail and de Man, Rob A. and IJzermans, Jan N.M. and Miclea, Razvan L. and Vermeulen, Peter B. and Thomeer, Maarten G. and Visser, Jacob J. and Niessen, Wiro J. and Klein, Stefan}, + title = {The WORC database: MRI and CT scans, segmentations, and clinical labels for 930 patients from six radiomics studies}, + elocation-id = {2021.08.19.21262238}, + year = {2021}, + doi = {10.1101/2021.08.19.21262238}, + URL = {https://www.medrxiv.org/content/early/2021/08/25/2021.08.19.21262238}, + eprint = {https://www.medrxiv.org/content/early/2021/08/25/2021.08.19.21262238.full.pdf}, + journal = {medRxiv} + } + +The code to download the WORC database and reproduce our experiments can +be found at https://github.com/MStarmans91/WORCDatabase. + Installation ------------ @@ -109,13 +146,6 @@ Tutorial `__. Besides a Jupyter notebook with instructions, we provide there also an example script for you to get started with. -WIP ---- - -- We are writing the paper on WORC. -- We are expanding the example experiments of WORC with open source - datasets. - Contact ------- diff --git a/WORC/doc/_build/doctrees/autogen/WORC.classification.doctree b/WORC/doc/_build/doctrees/autogen/WORC.classification.doctree index d2f87b8dc3a0fcd2bb2b6cfd65e04f22fd05727f..d97352e358bb60105f232cfd957788ab72ebf122 100644 GIT binary patch delta 106206 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

)N> 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 diff --git a/WORC/doc/_build/doctrees/autogen/WORC.facade.doctree b/WORC/doc/_build/doctrees/autogen/WORC.facade.doctree index bab24cad66e034612e9b57d0a72d802f38056ff3..6248df527eeecaf5f7cbcec9b67ccd01252f08f3 100644 GIT binary patch delta 16122 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 delta 13727 zcma)Cd3+RA@~0liOeZswnPg^?%;X>lYQjx8@!U zPk%uU6;PCw5Mn?@E>V_45OHNu5K!@lvP4-9N2P9ds6v*fzw;L-1TBt`ydKBS4ST(YVX+9N}~G(Xc{jgyBJ z=iOUTeD%E*v1*qLEP?Ks!d!%-oMoUKu91OYTiTco1Upuu@ zq`Kuc0bzaU6Qi|jc({ogF7#YYw^>E?aYm{hnl=}@=&wxcqjh1bZf<3(lxWPLy9((# zx%G2n8fSN8l6LgNIx3Z%!}N1~TF8iM$#jum1lHuc0$;6A!ud zzs=~WJ;b~oPUPi6UJw2^yhfJ{5+!bZdr5Dtgn2!g$cx-&`hUZ#taPzf$xL2IWI~>` zNHIyff}!-%vc~#`Cj-H@G~HocbFI!k`J||E%l+dZQ!k&{S8R3bU(9SKw!8Ih4|Nmo zx%D=69mHO@UNY+rudz7ED6__`e?99)nprBK&_0st)~g2R>kDR|(SBgib#4w9W4a%w zpK+^jIq}pUt-hxG2($zjAhJK)95YCcF=I-hk<+Aez9*F=7+!(Ow(vTA_1uQ;qNT?1 zbm>%&>LJfO4B{q@;t6_G|2C%^h&wb6r9qnXgkiWCVcjneLr>i`E2tkYkBUJWLdcP= z(;>y%gCMiiftrf9=QI^>b2N%Kec$&UJ$XS`KQcE>JgsrG_0TZdA}Ki23brm92iq!q zY6e?32X6|t1M{Y8@6gjoY?H>p_SF2B#9JELIV$Hhgp^uW6s`sBVT>MK@t9@Fd)rMd68M9i;8+t>A*Wf`R zgmX^hz>PQ;4bfO{)W~HYJ*vrja$%1 z4gc*Z?oL9_ZRJpgc;Gxz272k^7ZrF67&OpB0Y9)f8Cv4d)%wso<7(0{qHL7{VY$31 zyybp|Q9gYcN*_|rR4C<`TVa~}f0*!j#T$)c3QR$|MR9>M69p2RUFbIZT6o{Gyy8Q2 zn(Lss*Xbza={u9GoukdNw?TZ7q;`(ON&2Q`EA>4qx_A#0687BhlE`!6SCpToABbe* zvahIUq&FHgLT~(XTlk6Y6a_T98(%XdF@aEF43zICEc@9@e* z_jO2x+FYYoz0%Heh{&>v03TefpL?aH*Ex?=>6uKbM8%;byfI|J1Gp0>=S%-w zND;Ru>(f?$>b;$~vNj`=RUAH&tm1HDvWmkouU#v~C3F1Y6jGuuPO+5eo6Jhgvnesa z!4wKcxnH3tk=RQF-rJvU82qwiwcSFXPxT|vI{z^y}$JiK$k-ue(!-g@g^H?kTc^m<8H&^t->eX5|q(b$&cqk(k zdXEqxo8@|s9K9M^c%5@e-60-c)g#H({c4VZ808`VbQcpVTtfmkzTZ>?ubDUT+3J4a6=v9rn$ZS~U4uZc6B zTC9W8L96FgV}BS%x=5s41fB74spQ`FdUc;6;MgUfxJ^yp(tCQ=`>G-%kZ?5KkW9pz7Uf=j=B|!S=dh?!{F!`zai|txDiAfp9*ehp?`cZO+WVWVA0+2WZtG`qL1y# zZQ>rj6D@pbGNV3;_&A`@QJeczUYzVxd2xbI-HnB4!5)ncj7@c#8YwoZ)|df%j5_Wq?l&YY!(8wl~9 zbDTVXsn7EKb4QkG|6opsd|cTWoXGPJphkG^`ss72eal4O_)Pyd$jJXs`q=-Goazo6 zgMGg0W=GxNzA8m2QeQvuNt)jO`E-+pocWTkx@u=KKu`_(8>M>;1{4DU^>F#5baBbS zoF?w`Sy2!!LLnEP?o`!Pk1dYh_|OHU#hdfka6{mj=Z z-On=RGl|kDES`!>o8*v2T=t=dVF|O8mWMa%H_^SQgmJnQ?GSIJaA;gQRSsfriYi}r zrRcxUc}_p_=aAg}5R8Q{l`N{sKTpwd570ZD@auJ-G*D$aK8lCIE&|LFkEEzDaQRgj zH1Hc`dV2wW4g(ZI<-rb|Vuitu6b^$%ek%-iI+#)zd~;@n)*dNP)k42npU)<|NfpMH z*xJw8e(JwZi5vZN*t$>j@yk}@@eJS3PqjA`>8Ha=2h$I zXFqY;oc!cFzw#5+B-#GU9MJ2|Hqd{(uf6!)FDrvES8sJbqPO~WF{J6^f5d}GRSK3x zKmYmw6g?GLn=f{P0d&Vj z*6*kfy3{W?({ia_P$lZGj0;#Ub-}@#TCxxG}Kk=)rpG zuh@lsr%-thS=A7F%4Tjza{rrooIxH19s>gLBv&kV+P*Ru#?AL|7ptxYvM%gf@j~D#Q9wD`kCWIQ0!9#neflea4+T^#me5*oEG`IeEIOT)VsUe7JQiz@r$PycPf}Il-k(~F zvDgE|sZ>6)oJ>_c7J0;jX?I2#as@u2E(-`bJ^(6C@ewL(6f*k>Jj*|ks-iB72>EoA z$#XdJys}RkcCfJHf~Rxh#WA=)RmI?LWHE zo8xZ&*FU&{>UG$N?ntwBWOgvSZI|gsIejzfXLMu^8oC`dboUr!UuyIN@xi z+rhYQCTtqrPxA|4^A#Zz0YbLgGZ{g(c41kU2L+~ys47bhs!T+!87-fQt__-5!7u<- zc1?nu6_B37&ln*NcciKCxS5Q??l~ll!^7!E6dtz+t?>BzI;{0XEU3a`WRM#A7sa$7 zN5_<)I?#_KM9lQDpsHGAP*p7z2%DSvEyaQ$mn|cM%D;!<9Lv9l1=+vX;Zw7EAMW5v z{w-QUXZc$TDAIP)SR}S1$UDv2sk7y~%^}6yZng$#y9w16wt_W6JH*5X6>*{WW4h&i zWNQ$EBEPm@Tj&7p(@gd)8{eQJ(|x6kUwgU`c7paRQ=N}n-yo#5{MoQRP9W>sSR6dGIbwQZWGz$M z5HHh<_|#mcb`G|*Obfd~fn1jfF8Z&Xww?$gv2`J}RuXOiDb|LpZ1nyO&;+zyO!YyC zx54-5mQ&I8kTVs17UEQNGQ^$WuS2R6d_1K3{{IeH8OUHl83-q-R`6#bGzhnXZKH=m zGSmxpYTmGdnjAJm%Ld~R&!pWP5z+eE2DgN2u@&4Hv}Vj9KkQJ0oTF(to^jBN2lRw~ zaI;*|6U*sa!b*W&VWq%;@MQ|n^v+>cpmR7;0UGTX=D0^8He@uiC1m9nt%T7{ccLR7 ze_WwcySxa2E{^r z66XCkEN|!o8|2x3@RB%%+akR(JQA}Q~aLFU?I}YVJd=!-@3InW75RfjKj+1 z3(}R#Uz@HH(`$pEr)ZMS9=|`VJpS)E$MX2UhuPzMrCT1q*TI!Me&6j->b?_6&<;Z+ zu57;AHS+HRh{%PD}$;P=zHj{RpMXR22JVQ_xm!I`ElOqZ>P z!kO@SWc;5>Sde{&L1(jsJ)iCZRfEi@SS7k^ZPgbafX(jyOw{)>F;uV?Sz>Swib*oW zAQw6G3&@kpOCecIU@g`LT>_m5PeRRAXWr?A6jY@d6jV{MCrq9B+PM$HU$98cpt$nr z1VjZwc_p>wWtUNqDfc`CV_*%z;5v3yhB{7ebUcX%lf@&pGDB6i2Z@j?+xIG3 z#Zv9}W8fr!SAH}O{t6kg<#=c*BAF_ehBH+zZIT)9O?ZeA@A_WZVggJR-b_AM+D3lM zh;K_WI1x5#x3c)&nH)6+DV-s2%2dnTdJ%vCQx$z55+cs6|IAdm^?atv ztv_eVswZHB_yK?Eqm2&{1U416BfYi!+Ytvf6#@}!`QLYNq`caDI!x8BK@!xZNrYYC zkLmED$cs?nCO?>g2cE7x-dUDDf|1yk?y`#LYm5#NwM1)6puM{f6BQ8=2fRT@*|ax5 zrGUpdv`jWGg|f61`Q{M=R<~`0ZRo^HA^2Ft+Si_a67Wlyxe*nAbKh?^5pCZCN` zEtAhi*ktlU_|%NJr{lOt0-1d5(=c2+O^+k7Zw-@s%V3H4noWMF4A#3Z@%Tj&S>XMd z?%3oP*yP_mgX`vxDki?D85IT{Sv)z)QGxViZX1XHV{?CP0sGl7w-YP5Y!}()Nw~Y# zR}P0k42+Vcp|5N-7fyNml5k$Gp;0v5UFh2MdC};o+V~sKhmHahJvk^&Suh_W{@ysn zvR3aXTk9EoYFewv!IZ4kbpcvyDLszFmPE-x3%$3}E${j-MEM+YaRIaw+8a!_#z{we zKGTJ3=U2c=(B5XcjZQlH_U45s#xuW}bz&iFd_vZU){otaL5%qy>yY^%zEf5ar=oOr z_)c7matvRHsu(^+lGsY;$x3&M^ei$H1Q%Drn-GxeD)D4=itTe8i7opaH|*0o%d*c` z4t8Xpg-c+Ob~BP7_+D9TpOZwz_UVzuRnb!~!dGHImV9_A#KhfMcvi}D6?eip`0UbB z$f7aT12pE6&3a_TZQ+zlfmo8od*Mm8Mf>GwS0RxxewC(Y$=8-crCj$CJSx^;cZQPM zsw{cu%W#X>Xx&qnM|=>!M@KbUJUZ#aR$G~+tae;kO&rZa=bA4^tpK-Nwi1GHL>Ukz zQaGkOhx050J}3Q4phxBfoBifH7#pvy<)T$EPxECf8+x*p4L$RSl5METQLCY)*EyGL zaBVi*U|yC=SF+8kP$b%C>us6_<-%8CzjhPzyixIN*07kW`0lduHCQd~&z3i>fuVuH zL=r77Kv|7(bYQluS%cZMKhNx5`@p=F?FGj8e-}~Z}em8La(!9>O!yfV(LO~H)85SP3f4rP-`rvF4RPbsSBM` zW9mZnd+hJwd+Xj+9WeRDTLRqE|mML^)3Rzi{yP`Hz{KIY8VHKT_`3;$GV&H=x1l%dm^fn>ks4j+ReUpuRs3T7 ziIvkuEib+HjTO-v|B#w(TpXvAj8&Y*x|3U$ zd*yfUz_-G{eePYD0m}%tSdxQVUaow1Gqe>Kvt`|82$@u8-iKS@9YwV<2UB>itUH1! zeCvDA3`zAjgJD-u0|bjq$KReSQ$B?v_)L)>!V}~c7%9Fb=GXYYLEE6OAEKXVY=Mv; zAAweK%vNZUYlK0Gz#-l{7SO+024h*Z6;?xiIes&Ci|f%(u6%hLWaQN#FUyFO_f3}< zY)$#aHazs%q2(wp$ZY>UwxT)B0%x@!6O+oL3{dU z(-hypCf@O-q!OR#Y};A<7N__Fmx(6=6px?rkoaCs<5NO5#V6auJHG!>;*(K(O7aee z|DG}YuM@+C%PCg4I%)1{dffcv^AIjQfvS& z-y>2G0Br#d03!_m!<_&oDF7)({-HqxFhsp{l4p-VYJw-ydm$5q-4h*ehm;i(JWI=vS$r6<`;V1GM8FG{kClAK{EHX7R3n9_OuKXAyV zL6nW%XEJ|+Z0gyYvhA8X+SXUp{DdL?{ Q#gk0Yz$TZ|PeT6x16)`2PXGV_ diff --git a/WORC/doc/_build/doctrees/autogen/WORC.processing.doctree b/WORC/doc/_build/doctrees/autogen/WORC.processing.doctree index 2e34a3c3dc2e19cc0d3f827fe3423184955f2be7..df93595791bab04341c420d2571a9271105ef76a 100644 GIT binary patch delta 12149 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%0AMn3j!H^#zp>al45r@SisN?foaov zvj*i)(d~X(J7BM?Pq$YPFZl!g^siNG4_qZo67Pw(r)*K z=#4@^+s%`48cd50uX&}sjdI8ZKaA3qck})4N8nZR`AlAldWwm+4Pi=}L zo*k&)Jcu=L_HzBkp{%xXU)u!a1y9cd^71+?jRSRf!J|#O;PG8RUhqUCATPMs z58RD^6t1%{wCQ-kB`95RITDZ;eD(z71;={;FSA#u(@LjKm^HU@!pxdq)U<-W{CXto zu@L*!Kkx_m7^W&v0$(;&$&OXYp7oasFJ}~1u**lGUzH9aFql8vk~m7wyl_Duxr|xV%8mgMUP_RTnjL< z__0o4KHka3rhLAl-703OpmcN;Yv-56z>k=y82Ir)U_X{cSKl8a)yuC!G(ZvgaRPDU z1gOGP#b5&~ONigmQ1$TR3#uM|bU@X^*TPgie0@jN!}lmuJ=_=t_F^4%*u4o7c9;$; zH%`Cl4ZhDdB|8;i1$s>3M*N<>#-#8XC z0vqK&matUVm)YC_9Sxf;Xzu*LIFPM~oLMn@#`p;}@?$~+-2Oko?FgPEuJmiBttex? z;W3Qe9wec+3 zi;jNU9%Fcq?B5B+;BLF!(m#BUZUloLnHkB|U%VR~!eFkFvnyf2sO*GRP{tCy=<26E zU^w^4L&HypgYQ>=EFArntSKDsPu3Pa$uhVnnG$RyCm0X#N^F#1Rf%vUVlgzAE-O;7am`?6IcPA(2*gkA=Lju=pSS^pG>}$vScs` z(mQ2x7`?F>=nbzv^ugrr5k_xw_sm?pzk7}ldJUo5t95kfjy7d1>#J>@5*7ezKmc&O z_frAj(VdYC^WZk0!QnX}d>jx8;E3M+gO=`hnihtR&HsONk16-}qkBT=*LC#28R%bC zumReU3PN`oa|<>D`Wkr}gfrQF$;U&DAM3m2Xd}GuG0i0H6-P6@^b&o*^sv5TGoZsZ z_@^?D`}7|}w``)r(erflju!MQGs4iZ8PH)L^i$}#QTs9U9p+tBIC?3e+sEkWLE_Dv j6_$L|fB>>_iI^gj-yw|oxSTTrP)wz_Eb5+{%^vwL-QmI< diff --git a/WORC/doc/_build/doctrees/autogen/WORC.tools.doctree b/WORC/doc/_build/doctrees/autogen/WORC.tools.doctree index 6ba829239619a313fb3ee46236c27a35a01f73ea..27120bc0fcc69cbbe5b49b3de2736ed7ca4d6865 100644 GIT binary patch delta 7185 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 lp7q%!$UJ{~nUNROtdc6sa{{zdD33mVh delta 2777 zcma)8T~J&_7UnbrGUAOxk>K2W2L_CU%p}V&A<;wwkq{s*1|hB_B!$aQ28JsFnIGJw zHW~xCV|I*EJ?2K8t#nt^&F{c8XO4XhK+Dr>T55|03`AK4Y2uVZA*!5uLJHbhfSDO4z>7h zIc&kBV&-!$M@)ut+LNI*iT`up=jE{1quJ_72QOOrc>8LM`US)mKkUFS{Q_sA99Ch< z4}Q3gEv1mpJFc6!rUFiZ6X&gFSJ{8WxGXY5Q3-7Fg z4fw<+FpTc-U?fsJ+}YoENKi94P@XPB`LmTD&u@YPN0lohBV<^2yLeu-@^mHSvaK%O z67^xxZfIhyE%0r64?%fRC_D6{*p!hG3wc8c!PA8s z3*i@E2(f&fx7@dl73tWV2_7iMtM!r-2PSCkrnQHArgYGG$(}&&ExI^Ln@q|fHy@|= zK@##2YGEz^=-C`?JNey>Ai$hDCicUaqmE35RjUh)HsO73@EmjMyz`|Ry!`+?PIF@M z!voO5dUaxjb?eyF1<$AE4#Jm=4eOFxe>de(EXE{CQ>-f(Dm<24AM%JpKO}5R6g;F8 z1%4(d>;E;BY8^Lj=F_crFS7(Dcd|1&wTmbSuN8?FAL)R{Xdy~@jaRRBV|6d&;m4h@ zlTE5RC#3VA+Qd@wXDb~gf$(nr^RW)HFEo?~A8V2FFiZmboYY>{*Wm($F=><@ni@+cF z>`1kiwg|%iZpbeA9IrMmbjb`&AEbN!3+xJm4?ZPBF>k))WYdOhlS?Aw60MiiA=7R0 zgK(zXf|3X_g96f@oZ|w|I%cNE9)rKZ&_Ko42{LDosf|c zw^IK&4h*WPiY5sjw+OvSg2p9aT!O_VP+Wqsl5jd5$U0bkak3vt{_PbFPcWxAprfoH2%GNUmoq3WR`fyrp#D+?rDHeEg z6p9I=;7KUWlYDhhM3k6Iheb^p);cSULUamP5c?IHihCC8QVi3lQ87%YNjG{e@kk(zxHLco6MrtT}tGOPwS9*e^vcEHUq)~$2ycT3a;`Z29sED3|RhpV-PKrzpEn-p7hcI~d*XWnhvz+wH`PaoF`g=c70T diff --git a/WORC/doc/_build/doctrees/environment.pickle b/WORC/doc/_build/doctrees/environment.pickle index b4825bb7ac7d2c369e425edc0156e45c129e1e72..cf6267104e182709e0460270e5dac253ee16f6de 100644 GIT binary patch literal 3032869 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

7Re6e||>){G9#60|D0zw>R#i*%}u7f(`uD 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

  • test_combat() (in module WORC.tests.test_combat) diff --git a/WORC/doc/_build/html/index.html b/WORC/doc/_build/html/index.html index 24807176..5599c09b 100644 --- a/WORC/doc/_build/html/index.html +++ b/WORC/doc/_build/html/index.html @@ -8,7 +8,7 @@ - WORC: Workflow for Optimal Radiomics Classification — WORC 3.6.0 documentation + WORC: Workflow for Optimal Radiomics Classification — WORC 3.6.1 documentation @@ -63,7 +63,7 @@
    - 3.6.0 + 3.6.1
    @@ -244,16 +244,24 @@

    WORC DocumentationUser Manual