diff --git a/CHANGELOG.md b/CHANGELOG.md index caa8bb67e..0f8560b7e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,7 @@ # Change Log ## v0.11.0dev -[Full Changelog](https://github.com/SeldonIO/alibi-detect/compare/v0.10.3...master) +[Full Changelog](https://github.com/SeldonIO/alibi-detect/compare/v0.10.5...master) ### Added - **New feature** MMD drift detector has been extended with a [KeOps](https://www.kernel-operations.io/keops/index.html) backend to scale and speed up the detector. @@ -26,6 +26,15 @@ See the [documentation](https://docs.seldon.io/projects/alibi-detect/en/latest/c - UTF-8 decoding is enforced when `README.md` is opened by `setup.py`. This is to prevent pip install errors on systems with `PYTHONIOENCODING` set to use other encoders ([#605](https://github.com/SeldonIO/alibi-detect/pull/605)). - Skip specific save/load tests that require downloading remote artefacts if the relevant URI(s) is/are down ([#607](https://github.com/SeldonIO/alibi-detect/pull/607)). +## v0.10.5 +## [v0.10.5](https://github.com/SeldonIO/alibi-detect/tree/v0.10.5) (2023-01-26) +[Full Changelog](https://github.com/SeldonIO/alibi-detect/compare/v0.10.4...v0.10.5) + +### Fixed +- Fixed two bugs preventing backward compatibility when loading detectors saved with `=v0.10.0` did not properly obey the legacy file format. The `config.toml` file format used by default in `>=v0.10.0` is unaffected. + ## v0.10.4 ## [v0.10.4](https://github.com/SeldonIO/alibi-detect/tree/v0.10.4) (2022-10-21) [Full Changelog](https://github.com/SeldonIO/alibi-detect/compare/v0.10.3...v0.10.4) diff --git a/CITATION.cff b/CITATION.cff index e162f25b3..2534e463a 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -19,6 +19,6 @@ authors: - family-names: "Athorne" given-names: "Alex" title: "Alibi Detect: Algorithms for outlier, adversarial and drift detection" -version: 0.10.4 -date-released: 2022-10-21 +version: 0.10.5 +date-released: 2023-01-26 url: "https://github.com/SeldonIO/alibi-detect" diff --git a/README.md b/README.md index edd68d3c3..18612b729 100644 --- a/README.md +++ b/README.md @@ -407,8 +407,8 @@ BibTeX entry: title = {Alibi Detect: Algorithms for outlier, adversarial and drift detection}, author = {Van Looveren, Arnaud and Klaise, Janis and Vacanti, Giovanni and Cobb, Oliver and Scillitoe, Ashley and Samoilescu, Robert and Athorne, Alex}, url = {https://github.com/SeldonIO/alibi-detect}, - version = {0.10.4}, - date = {2022-10-21}, + version = {0.10.5}, + date = {2023-01-26}, year = {2019} } ``` diff --git a/alibi_detect/saving/_tensorflow/loading.py b/alibi_detect/saving/_tensorflow/loading.py index cb9974907..ba6a33d23 100644 --- a/alibi_detect/saving/_tensorflow/loading.py +++ b/alibi_detect/saving/_tensorflow/loading.py @@ -36,7 +36,7 @@ def load_model(filepath: Union[str, os.PathLike], - load_dir: str = 'model', + filename: str = 'model', custom_objects: dict = None, layer: Optional[int] = None, ) -> tf.keras.Model: @@ -47,8 +47,8 @@ def load_model(filepath: Union[str, os.PathLike], ---------- filepath Saved model directory. - load_dir - Name of saved model folder within the filepath directory. + filename + Name of saved model within the filepath directory. custom_objects Optional custom objects when loading the TensorFlow model. layer @@ -60,11 +60,12 @@ def load_model(filepath: Union[str, os.PathLike], Loaded model. """ # TODO - update this to accept tf format - later PR. - model_dir = Path(filepath).joinpath(load_dir) + model_dir = Path(filepath) + model_name = filename + '.h5' # Check if model exists - if 'model.h5' not in [f.name for f in model_dir.glob('[!.]*.h5')]: - raise FileNotFoundError(f'No .h5 file found in {model_dir}.') - model = tf.keras.models.load_model(model_dir.joinpath('model.h5'), custom_objects=custom_objects) + if model_name not in [f.name for f in model_dir.glob('[!.]*.h5')]: + raise FileNotFoundError(f'{model_name} not found in {model_dir.resolve()}.') + model = tf.keras.models.load_model(model_dir.joinpath(model_name), custom_objects=custom_objects) # Optionally extract hidden layer if isinstance(layer, int): model = HiddenOutput(model, layer=layer) @@ -233,7 +234,17 @@ def load_detector_legacy(filepath: Union[str, os.PathLike], suffix: str, **kwarg # load outlier detector specific parameters state_dict = dill.load(open(filepath.joinpath(detector_name + suffix), 'rb')) + # Update the drift detector preprocess kwargs if state_dict is from an old alibi-detect version (=v0.10 + # Set x_ref_preprocessed to True + state_dict['kwargs']['x_ref_preprocessed'] = True + # Move `preprocess_x_ref` from `other` to `kwargs` + state_dict['kwargs']['preprocess_x_ref'] = state_dict['other']['preprocess_x_ref'] + # initialize detector + model_dir = filepath.joinpath('model') detector: Optional[Detector] = None # to avoid mypy errors if detector_name == 'OutlierAE': ae = load_tf_ae(filepath) @@ -254,13 +265,13 @@ def load_detector_legacy(filepath: Union[str, os.PathLike], suffix: str, **kwarg elif detector_name == 'AdversarialAE': ae = load_tf_ae(filepath) custom_objects = kwargs['custom_objects'] if 'custom_objects' in k else None - model = load_model(filepath, custom_objects=custom_objects) + model = load_model(model_dir, custom_objects=custom_objects) model_hl = load_tf_hl(filepath, model, state_dict) detector = init_ad_ae(state_dict, ae, model, model_hl) elif detector_name == 'ModelDistillation': - md = load_model(filepath, load_dir='distilled_model') + md = load_model(model_dir, filename='distilled_model') custom_objects = kwargs['custom_objects'] if 'custom_objects' in k else None - model = load_model(filepath, custom_objects=custom_objects) + model = load_model(model_dir, custom_objects=custom_objects) detector = init_ad_md(state_dict, md, model) elif detector_name == 'OutlierProphet': detector = init_od_prophet(state_dict) # type: ignore[assignment] @@ -274,8 +285,9 @@ def load_detector_legacy(filepath: Union[str, os.PathLike], suffix: str, **kwarg if state_dict['other']['load_text_embedding']: emb, tokenizer = load_text_embed(filepath) try: # legacy load_model behaviour was to return None if not found. Now it raises error, hence need try-except. - model = load_model(filepath, load_dir='encoder') + model = load_model(model_dir, filename='encoder') except FileNotFoundError: + logger.warning('No model found in {}, setting `model` to `None`.'.format(model_dir)) model = None if detector_name == 'KSDrift': load_fn = init_cd_ksdrift # type: ignore[assignment] @@ -287,7 +299,7 @@ def load_detector_legacy(filepath: Union[str, os.PathLike], suffix: str, **kwarg load_fn = init_cd_tabulardrift # type: ignore[assignment] elif detector_name == 'ClassifierDriftTF': # Don't need try-except here since model is not optional for ClassifierDrift - clf_drift = load_model(filepath, load_dir='clf_drift') + clf_drift = load_model(model_dir, filename='clf_drift') load_fn = partial(init_cd_classifierdrift, clf_drift) # type: ignore[assignment] else: raise NotImplementedError diff --git a/alibi_detect/saving/_tensorflow/saving.py b/alibi_detect/saving/_tensorflow/saving.py index bdf625967..faebc541a 100644 --- a/alibi_detect/saving/_tensorflow/saving.py +++ b/alibi_detect/saving/_tensorflow/saving.py @@ -81,7 +81,7 @@ def save_model_config(model: Callable, if model is not None: filepath = base_path.joinpath(local_path) - save_model(model, filepath=filepath, save_dir='model') + save_model(model, filepath=filepath.joinpath('model')) cfg_model = { 'flavour': Framework.TENSORFLOW.value, 'src': local_path.joinpath('model') @@ -91,7 +91,7 @@ def save_model_config(model: Callable, def save_model(model: tf.keras.Model, filepath: Union[str, os.PathLike], - save_dir: Union[str, os.PathLike] = 'model', + filename: str = 'model', save_format: Literal['tf', 'h5'] = 'h5') -> None: # TODO - change to tf, later PR """ Save TensorFlow model. @@ -102,20 +102,20 @@ def save_model(model: tf.keras.Model, The tf.keras.Model to save. filepath Save directory. - save_dir - Name of folder to save to within the filepath directory. + filename + Name of file to save to within the filepath directory. save_format The format to save to. 'tf' to save to the newer SavedModel format, 'h5' to save to the lighter-weight legacy hdf5 format. """ # create folder to save model in - model_path = Path(filepath).joinpath(save_dir) + model_path = Path(filepath) if not model_path.is_dir(): logger.warning('Directory {} does not exist and is now created.'.format(model_path)) model_path.mkdir(parents=True, exist_ok=True) # save model - model_path = model_path.joinpath('model.h5') if save_format == 'h5' else model_path + model_path = model_path.joinpath(filename + '.h5') if save_format == 'h5' else model_path if isinstance(model, tf.keras.Model): model.save(model_path, save_format=save_format) @@ -254,30 +254,31 @@ def save_detector_legacy(detector, filepath): dill.dump(state_dict, f) # save detector specific TensorFlow models + model_dir = filepath.joinpath('model') if isinstance(detector, OutlierAE): save_tf_ae(detector, filepath) elif isinstance(detector, OutlierVAE): save_tf_vae(detector, filepath) elif isinstance(detector, (ChiSquareDrift, ClassifierDrift, KSDrift, MMDDrift, TabularDrift)): if model is not None: - save_model(model, filepath, save_dir='encoder') + save_model(model, model_dir, filename='encoder') if embed is not None: save_embedding_legacy(embed, embed_args, filepath) if tokenizer is not None: tokenizer.save_pretrained(filepath.joinpath('model')) if detector_name == 'ClassifierDriftTF': - save_model(clf_drift, filepath, save_dir='clf_drift') + save_model(clf_drift, model_dir, filename='clf_drift') elif isinstance(detector, OutlierAEGMM): save_tf_aegmm(detector, filepath) elif isinstance(detector, OutlierVAEGMM): save_tf_vaegmm(detector, filepath) elif isinstance(detector, AdversarialAE): save_tf_ae(detector, filepath) - save_model(detector.model, filepath) + save_model(detector.model, model_dir) save_tf_hl(detector.model_hl, filepath) elif isinstance(detector, ModelDistillation): - save_model(detector.distilled_model, filepath, save_dir='distilled_model') - save_model(detector.model, filepath, save_dir='model') + save_model(detector.distilled_model, model_dir, filename='distilled_model') + save_model(detector.model, model_dir, filename='model') elif isinstance(detector, OutlierSeq2Seq): save_tf_s2s(detector, filepath) elif isinstance(detector, LLR): diff --git a/alibi_detect/saving/loading.py b/alibi_detect/saving/loading.py index b96e18ec2..e2fab8f62 100644 --- a/alibi_detect/saving/loading.py +++ b/alibi_detect/saving/loading.py @@ -281,7 +281,7 @@ def _load_model_config(cfg: dict) -> Callable: "a compatible model.") if flavour == Framework.TENSORFLOW: - model = load_model_tf(src, load_dir='.', custom_objects=custom_obj, layer=layer) + model = load_model_tf(src, custom_objects=custom_obj, layer=layer) elif flavour == Framework.PYTORCH: model = load_model_pt(src, layer=layer) elif flavour == Framework.SKLEARN: diff --git a/alibi_detect/utils/tests/test_saving_legacy.py b/alibi_detect/utils/tests/test_saving_legacy.py index 51b34a090..d991af5cc 100644 --- a/alibi_detect/utils/tests/test_saving_legacy.py +++ b/alibi_detect/utils/tests/test_saving_legacy.py @@ -1,6 +1,6 @@ """ Tests for saving/loading of detectors with legacy .dill state_dict. As legacy save/load functionality becomes -deprecated, these tests will be removed, and more tests will be added to test_savin.py. +deprecated, these tests will be removed, and more tests will be added to test_saving.py. """ from alibi_detect.utils.missing_optional_dependency import MissingDependency from functools import partial diff --git a/licenses/license.txt b/licenses/license.txt index 5888d1d2b..6c6fc50e0 100644 --- a/licenses/license.txt +++ b/licenses/license.txt @@ -1,5 +1,5 @@ Pillow -9.3.0 +9.4.0 Historical Permission Notice and Disclaimer (HPND) The Python Imaging Library (PIL) is @@ -8,7 +8,7 @@ The Python Imaging Library (PIL) is Pillow is the friendly PIL fork. It is - Copyright © 2010-2022 by Alex Clark and contributors + Copyright © 2010-2023 by Alex Clark and contributors Like PIL, Pillow is licensed under the open source HPND License: @@ -1034,7 +1034,7 @@ one at http://mozilla.org/MPL/2.0/. charset-normalizer -2.1.1 +3.0.1 MIT License MIT License @@ -1059,11 +1059,11 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. contourpy -1.0.6 +1.0.7 BSD License BSD 3-Clause License -Copyright (c) 2021-2022, ContourPy Developers. +Copyright (c) 2021-2023, ContourPy Developers. All rights reserved. Redistribution and use in source and binary forms, with or without @@ -1169,7 +1169,7 @@ ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. filelock -3.8.2 +3.9.0 The Unlicense (Unlicense) This is free and unencumbered software released into the public domain. @@ -1224,7 +1224,7 @@ SOFTWARE. fsspec -2022.11.0 +2023.1.0 BSD License BSD 3-Clause License @@ -1258,7 +1258,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. huggingface-hub -0.11.1 +0.12.0 Apache Software License Apache License Version 2.0, January 2004 @@ -1525,7 +1525,7 @@ THE SOFTWARE. imageio -2.22.4 +2.25.0 BSD License Copyright (c) 2014-2022, imageio developers All rights reserved. @@ -1554,7 +1554,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. importlib-metadata -5.1.0 +6.0.0 Apache Software License Apache License @@ -1927,7 +1927,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. matplotlib -3.6.2 +3.6.3 Python Software Foundation License License agreement for matplotlib versions 1.3.0 and later ========================================================= @@ -2030,13 +2030,13 @@ Licensee agrees to be bound by the terms and conditions of this License Agreement. networkx -2.8.8 +3.0 BSD License NetworkX is distributed with the 3-clause BSD license. :: - Copyright (C) 2004-2022, NetworkX Developers + Copyright (C) 2004-2023, NetworkX Developers Aric Hagberg Dan Schult Pieter Swart @@ -3016,7 +3016,7 @@ Public License instead of this License. But first, please read opencv-python -4.6.0.66 +4.7.0.68 MIT License OpenCV library is redistributed within opencv-python package. This license applies to OpenCV binary in the directory cv2/. @@ -5458,7 +5458,7 @@ used in advertising or otherwise to promote the sale, use or other dealings in this Software without prior written authorization from The Open Group. packaging -22.0 +23.0 Apache Software License; BSD License This software is made available under the terms of *either* of the licenses found in LICENSE.APACHE or LICENSE.BSD. Contributions to this software is made @@ -5466,7 +5466,7 @@ under the terms of *both* these licenses. pandas -1.5.2 +1.5.3 BSD License BSD 3-Clause License @@ -5535,7 +5535,7 @@ THE POSSIBILITY OF SUCH DAMAGE. pydantic -1.10.2 +1.10.4 MIT License The MIT License (MIT) @@ -5642,7 +5642,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. The above BSD License Applies to all code, even that also covered by Apache 2.0. pytz -2022.6 +2022.7.1 MIT License Copyright (c) 2003-2019 Stuart Bishop @@ -5879,7 +5879,7 @@ All additions and alterations are licensed under the Apache 2.0 License. requests -2.28.1 +2.28.2 Apache Software License Apache License @@ -6145,7 +6145,7 @@ skimage/_shared/version_requirements.py:is_installed: scikit-learn -1.2.0 +1.2.1 BSD License BSD 3-Clause License @@ -6179,7 +6179,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. scipy -1.9.3 +1.10.0 BSD License Copyright (c) 2001-2002 Enthought, Inc. 2003-2022, SciPy Developers. All rights reserved. @@ -7145,11 +7145,11 @@ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. tifffile -2022.10.10 +2023.1.23.1 BSD License BSD 3-Clause License -Copyright (c) 2008-2022, Christoph Gohlke +Copyright (c) 2008-2023, Christoph Gohlke All rights reserved. Redistribution and use in source and binary forms, with or without @@ -7303,7 +7303,7 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. transformers -4.25.1 +4.26.0 Apache Software License Copyright 2018- The Hugging Face team. All rights reserved. @@ -7770,7 +7770,7 @@ OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. urllib3 -1.26.13 +1.26.14 MIT License MIT License diff --git a/licenses/license_info.csv b/licenses/license_info.csv index 2f1976cda..de792f2b8 100644 --- a/licenses/license_info.csv +++ b/licenses/license_info.csv @@ -1,52 +1,52 @@ "Name","Version","License" -"Pillow","9.3.0","Historical Permission Notice and Disclaimer (HPND)" +"Pillow","9.4.0","Historical Permission Notice and Disclaimer (HPND)" "PyWavelets","1.4.1","MIT License" "PyYAML","6.0","MIT License" "alibi-detect","0.11.0.dev0","Apache Software License" "catalogue","2.0.8","MIT License" "certifi","2022.12.7","Mozilla Public License 2.0 (MPL 2.0)" -"charset-normalizer","2.1.1","MIT License" -"contourpy","1.0.6","BSD License" +"charset-normalizer","3.0.1","MIT License" +"contourpy","1.0.7","BSD License" "cycler","0.11.0","BSD License" "dataclasses","0.6","Apache Software License" "dill","0.3.6","BSD License" -"filelock","3.8.2","The Unlicense (Unlicense)" +"filelock","3.9.0","The Unlicense (Unlicense)" "fonttools","4.38.0","MIT License" -"fsspec","2022.11.0","BSD License" -"huggingface-hub","0.11.1","Apache Software License" +"fsspec","2023.1.0","BSD License" +"huggingface-hub","0.12.0","Apache Software License" "idna","3.4","BSD License" "idna-ssl","1.1.0","MIT License" -"imageio","2.22.4","BSD License" -"importlib-metadata","5.1.0","Apache Software License" +"imageio","2.25.0","BSD License" +"importlib-metadata","6.0.0","Apache Software License" "joblib","1.2.0","BSD License" "kiwisolver","1.4.4","BSD License" "llvmlite","0.39.1","BSD" "locket","1.0.0","BSD License" -"matplotlib","3.6.2","Python Software Foundation License" -"networkx","2.8.8","BSD License" +"matplotlib","3.6.3","Python Software Foundation License" +"networkx","3.0","BSD License" "numba","0.56.4","BSD License" "numpy","1.23.5","BSD License" -"opencv-python","4.6.0.66","MIT License" -"packaging","22.0","Apache Software License; BSD License" -"pandas","1.5.2","BSD License" +"opencv-python","4.7.0.68","MIT License" +"packaging","23.0","Apache Software License; BSD License" +"pandas","1.5.3","BSD License" "partd","1.3.0","BSD" -"pydantic","1.10.2","MIT License" +"pydantic","1.10.4","MIT License" "pyparsing","3.0.9","MIT License" "python-dateutil","2.8.2","Apache Software License; BSD License" -"pytz","2022.6","MIT License" +"pytz","2022.7.1","MIT License" "regex","2022.10.31","Apache Software License" -"requests","2.28.1","Apache Software License" +"requests","2.28.2","Apache Software License" "scikit-image","0.19.3","BSD License" -"scikit-learn","1.2.0","BSD License" -"scipy","1.9.3","BSD License" +"scikit-learn","1.2.1","BSD License" +"scipy","1.10.0","BSD License" "six","1.16.0","MIT License" "threadpoolctl","3.1.0","BSD License" -"tifffile","2022.10.10","BSD License" +"tifffile","2023.1.23.1","BSD License" "tokenizers","0.13.2","Apache Software License" "toml","0.10.2","MIT License" "toolz","0.12.0","BSD License" "tqdm","4.64.1","MIT License; Mozilla Public License 2.0 (MPL 2.0)" -"transformers","4.25.1","Apache Software License" +"transformers","4.26.0","Apache Software License" "typing_extensions","4.4.0","Python Software Foundation License" -"urllib3","1.26.13","MIT License" +"urllib3","1.26.14","MIT License" "zipp","3.11.0","MIT License" \ No newline at end of file